| 1 | package instanceexec |
| 2 | |
| 3 | import ( |
| 4 | "go/ast" |
| 5 | "go/build" |
| 6 | "go/parser" |
| 7 | "go/token" |
| 8 | "path" |
| 9 | "path/filepath" |
| 10 | "runtime" |
| 11 | "sort" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | "testing" |
| 15 | ) |
| 16 | |
| 17 | // Every carlos process inside an instance unit (the probe, the trampoline |
| 18 | // and its exit hook, for all four templates) runs under |
| 19 | // RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX. A socket of any other |
| 20 | // family is refused, so code that asks for one fails on every box: the |
| 21 | // probe's net.InterfaceAddrs, which is netlink, kept every box on v0.25.0 |
| 22 | // from ever being ready (#126). This walks the platform packages this one |
| 23 | // builds from, as linux sees them, and refuses the calls that open one. |
| 24 | // Third-party code is outside the walk; at the time of writing its one |
| 25 | // such call is google/uuid's net.Interfaces, reached only for v1 and v6 |
| 26 | // UUIDs, which nothing here makes. |
| 27 | func TestInstanceCodeOpensOnlyTheUnitsAddressFamilies(t *testing.T) { |
| 28 | const module = "github.com/carlosframework/platform" |
| 29 | root, err := filepath.Abs(filepath.Join("..", "..")) |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | ctx := build.Default |
| 34 | ctx.GOOS, ctx.GOARCH, ctx.CgoEnabled = "linux", runtime.GOARCH, false |
| 35 | |
| 36 | refused := map[string]map[string]bool{ |
| 37 | "net": {"Interfaces": true, "InterfaceAddrs": true, "InterfaceByName": true, "InterfaceByIndex": true}, |
| 38 | "syscall": {"NetlinkRIB": true}, |
| 39 | "golang.org/x/sys/unix": {}, |
| 40 | } |
| 41 | allowedAF := map[string]bool{"AF_INET": true, "AF_INET6": true, "AF_UNIX": true, "AF_UNSPEC": true} |
| 42 | |
| 43 | var found []string |
| 44 | seen := map[string]bool{} |
| 45 | queue := []string{module + "/internal/instanceexec"} |
| 46 | for len(queue) > 0 { |
| 47 | ip := queue[0] |
| 48 | queue = queue[1:] |
| 49 | if seen[ip] { |
| 50 | continue |
| 51 | } |
| 52 | seen[ip] = true |
| 53 | dir := filepath.Join(root, strings.TrimPrefix(ip, module)) |
| 54 | pkg, err := ctx.ImportDir(dir, 0) |
| 55 | if err != nil { |
| 56 | t.Fatalf("%s: %v", ip, err) |
| 57 | } |
| 58 | for _, imp := range pkg.Imports { |
| 59 | if strings.HasPrefix(imp, module+"/") { |
| 60 | queue = append(queue, imp) |
| 61 | } |
| 62 | } |
| 63 | for _, name := range pkg.GoFiles { |
| 64 | file := filepath.Join(dir, name) |
| 65 | f, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.SkipObjectResolution) |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | local := map[string]string{} |
| 70 | for _, is := range f.Imports { |
| 71 | p, _ := strconv.Unquote(is.Path.Value) |
| 72 | n := path.Base(p) |
| 73 | if is.Name != nil { |
| 74 | n = is.Name.Name |
| 75 | } |
| 76 | local[n] = p |
| 77 | } |
| 78 | ast.Inspect(f, func(n ast.Node) bool { |
| 79 | sel, ok := n.(*ast.SelectorExpr) |
| 80 | if !ok { |
| 81 | return true |
| 82 | } |
| 83 | x, ok := sel.X.(*ast.Ident) |
| 84 | if !ok { |
| 85 | return true |
| 86 | } |
| 87 | p := local[x.Name] |
| 88 | names, watched := refused[p] |
| 89 | if !watched { |
| 90 | return true |
| 91 | } |
| 92 | af := strings.HasPrefix(sel.Sel.Name, "AF_") && !allowedAF[sel.Sel.Name] |
| 93 | if names[sel.Sel.Name] || af { |
| 94 | found = append(found, strings.TrimPrefix(file, root+string(filepath.Separator))+": "+p+"."+sel.Sel.Name) |
| 95 | } |
| 96 | return true |
| 97 | }) |
| 98 | } |
| 99 | } |
| 100 | sort.Strings(found) |
| 101 | for _, f := range found { |
| 102 | t.Errorf("%s needs an address family the instance units refuse", f) |
| 103 | } |
| 104 | if !seen[module+"/internal/instancectl"] { |
| 105 | t.Errorf("the walk never reached instancectl: %v", seen) |
| 106 | } |
| 107 | } |
| 108 | |