| 1 | package instancectl |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "bytes" |
| 6 | "encoding/hex" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io/fs" |
| 10 | "net/netip" |
| 11 | "os" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | ) |
| 15 | |
| 16 | // The box's own addresses, for the resolver allow (#214 item 3, #126). |
| 17 | // An allowed resolver must not be one of them: the allow opens an address |
| 18 | // on every port, and on the box's own address that is every listener |
| 19 | // bound to all addresses. |
| 20 | // |
| 21 | // They are read from procfs, not asked of the kernel over netlink, which |
| 22 | // is what net.InterfaceAddrs does: this service runs with |
| 23 | // RestrictAddressFamilies=AF_UNIX, and the probe unit, which used to make |
| 24 | // this check, with AF_INET AF_INET6 AF_UNIX, so netlink is refused in |
| 25 | // both (the v0.25.0 roll: "netlinkrib: address family not supported by |
| 26 | // protocol" on every box, and no box ever ready). A file read needs no |
| 27 | // socket at all, and widening either unit's families for one listing was |
| 28 | // the alternative. |
| 29 | |
| 30 | // Where the box's addresses are read; tests point them at fixtures. |
| 31 | const ( |
| 32 | fibTriePath = "/proc/net/fib_trie" // IPv4: the routing tables, LOCAL entries |
| 33 | ifInet6Path = "/proc/net/if_inet6" // IPv6: one line per interface address |
| 34 | ) |
| 35 | |
| 36 | // localPrefixes is everything that routes to this box: each IPv4 LOCAL |
| 37 | // route (an interface address, or a whole local range such as |
| 38 | // 127.0.0.0/8), and each IPv6 interface address as a /128. |
| 39 | func localPrefixes(fibTrie, ifInet6 string) ([]netip.Prefix, error) { |
| 40 | b, err := os.ReadFile(fibTrie) |
| 41 | if err != nil { |
| 42 | return nil, err |
| 43 | } |
| 44 | out, err := parseFibTrie(b) |
| 45 | if err != nil { |
| 46 | return nil, fmt.Errorf("%s: %w", fibTrie, err) |
| 47 | } |
| 48 | b, err = os.ReadFile(ifInet6) |
| 49 | switch { |
| 50 | case errors.Is(err, fs.ErrNotExist): |
| 51 | // No IPv6 in this kernel: no v6 address can be the box's. |
| 52 | return out, nil |
| 53 | case err != nil: |
| 54 | return nil, err |
| 55 | } |
| 56 | v6, err := parseIfInet6(b) |
| 57 | if err != nil { |
| 58 | return nil, fmt.Errorf("%s: %w", ifInet6, err) |
| 59 | } |
| 60 | return append(out, v6...), nil |
| 61 | } |
| 62 | |
| 63 | // parseFibTrie reads /proc/net/fib_trie: a leaf line "|-- <addr>" is |
| 64 | // followed by one line per route at that address, "/<len> <scope> |
| 65 | // <type>". A type of LOCAL is a destination this box answers for itself. |
| 66 | // A file with no LOCAL entry at all is refused rather than read as "no |
| 67 | // addresses": every box has at least 127.0.0.1, so it is not the file |
| 68 | // this parser knows. |
| 69 | func parseFibTrie(b []byte) ([]netip.Prefix, error) { |
| 70 | var out []netip.Prefix |
| 71 | var leaf netip.Addr |
| 72 | sc := bufio.NewScanner(bytes.NewReader(b)) |
| 73 | for sc.Scan() { |
| 74 | line := strings.TrimSpace(sc.Text()) |
| 75 | if a, ok := strings.CutPrefix(line, "|-- "); ok { |
| 76 | leaf, _ = netip.ParseAddr(strings.TrimSpace(a)) |
| 77 | continue |
| 78 | } |
| 79 | f := strings.Fields(line) |
| 80 | if len(f) < 3 || !strings.HasPrefix(f[0], "/") || f[len(f)-1] != "LOCAL" || !leaf.IsValid() { |
| 81 | continue |
| 82 | } |
| 83 | bits, err := strconv.Atoi(f[0][1:]) |
| 84 | if err != nil { |
| 85 | continue |
| 86 | } |
| 87 | if p, err := leaf.Prefix(bits); err == nil { |
| 88 | out = append(out, p) |
| 89 | } |
| 90 | } |
| 91 | if err := sc.Err(); err != nil { |
| 92 | return nil, err |
| 93 | } |
| 94 | if len(out) == 0 { |
| 95 | return nil, errors.New("no LOCAL route") |
| 96 | } |
| 97 | return out, nil |
| 98 | } |
| 99 | |
| 100 | // parseIfInet6 reads /proc/net/if_inet6: each line starts with the |
| 101 | // address as 32 hex digits. |
| 102 | func parseIfInet6(b []byte) ([]netip.Prefix, error) { |
| 103 | var out []netip.Prefix |
| 104 | sc := bufio.NewScanner(bytes.NewReader(b)) |
| 105 | for sc.Scan() { |
| 106 | f := strings.Fields(sc.Text()) |
| 107 | if len(f) == 0 { |
| 108 | continue |
| 109 | } |
| 110 | raw, err := hex.DecodeString(f[0]) |
| 111 | if err != nil || len(raw) != 16 { |
| 112 | return nil, fmt.Errorf("%q is not an address", f[0]) |
| 113 | } |
| 114 | out = append(out, netip.PrefixFrom(netip.AddrFrom16([16]byte(raw)), 128)) |
| 115 | } |
| 116 | return out, sc.Err() |
| 117 | } |
| 118 | |
| 119 | // isLocal names the local prefix a holds, if any. |
| 120 | func isLocal(a netip.Addr, local []netip.Prefix) (netip.Prefix, bool) { |
| 121 | for _, p := range local { |
| 122 | if p.Contains(a) { |
| 123 | return p, true |
| 124 | } |
| 125 | } |
| 126 | return netip.Prefix{}, false |
| 127 | } |
| 128 | |