package instancectl import ( "bufio" "bytes" "encoding/hex" "errors" "fmt" "io/fs" "net/netip" "os" "strconv" "strings" ) // The box's own addresses, for the resolver allow (#214 item 3, #126). // An allowed resolver must not be one of them: the allow opens an address // on every port, and on the box's own address that is every listener // bound to all addresses. // // They are read from procfs, not asked of the kernel over netlink, which // is what net.InterfaceAddrs does: this service runs with // RestrictAddressFamilies=AF_UNIX, and the probe unit, which used to make // this check, with AF_INET AF_INET6 AF_UNIX, so netlink is refused in // both (the v0.25.0 roll: "netlinkrib: address family not supported by // protocol" on every box, and no box ever ready). A file read needs no // socket at all, and widening either unit's families for one listing was // the alternative. // Where the box's addresses are read; tests point them at fixtures. const ( fibTriePath = "/proc/net/fib_trie" // IPv4: the routing tables, LOCAL entries ifInet6Path = "/proc/net/if_inet6" // IPv6: one line per interface address ) // localPrefixes is everything that routes to this box: each IPv4 LOCAL // route (an interface address, or a whole local range such as // 127.0.0.0/8), and each IPv6 interface address as a /128. func localPrefixes(fibTrie, ifInet6 string) ([]netip.Prefix, error) { b, err := os.ReadFile(fibTrie) if err != nil { return nil, err } out, err := parseFibTrie(b) if err != nil { return nil, fmt.Errorf("%s: %w", fibTrie, err) } b, err = os.ReadFile(ifInet6) switch { case errors.Is(err, fs.ErrNotExist): // No IPv6 in this kernel: no v6 address can be the box's. return out, nil case err != nil: return nil, err } v6, err := parseIfInet6(b) if err != nil { return nil, fmt.Errorf("%s: %w", ifInet6, err) } return append(out, v6...), nil } // parseFibTrie reads /proc/net/fib_trie: a leaf line "|-- " is // followed by one line per route at that address, "/ // ". A type of LOCAL is a destination this box answers for itself. // A file with no LOCAL entry at all is refused rather than read as "no // addresses": every box has at least 127.0.0.1, so it is not the file // this parser knows. func parseFibTrie(b []byte) ([]netip.Prefix, error) { var out []netip.Prefix var leaf netip.Addr sc := bufio.NewScanner(bytes.NewReader(b)) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if a, ok := strings.CutPrefix(line, "|-- "); ok { leaf, _ = netip.ParseAddr(strings.TrimSpace(a)) continue } f := strings.Fields(line) if len(f) < 3 || !strings.HasPrefix(f[0], "/") || f[len(f)-1] != "LOCAL" || !leaf.IsValid() { continue } bits, err := strconv.Atoi(f[0][1:]) if err != nil { continue } if p, err := leaf.Prefix(bits); err == nil { out = append(out, p) } } if err := sc.Err(); err != nil { return nil, err } if len(out) == 0 { return nil, errors.New("no LOCAL route") } return out, nil } // parseIfInet6 reads /proc/net/if_inet6: each line starts with the // address as 32 hex digits. func parseIfInet6(b []byte) ([]netip.Prefix, error) { var out []netip.Prefix sc := bufio.NewScanner(bytes.NewReader(b)) for sc.Scan() { f := strings.Fields(sc.Text()) if len(f) == 0 { continue } raw, err := hex.DecodeString(f[0]) if err != nil || len(raw) != 16 { return nil, fmt.Errorf("%q is not an address", f[0]) } out = append(out, netip.PrefixFrom(netip.AddrFrom16([16]byte(raw)), 128)) } return out, sc.Err() } // isLocal names the local prefix a holds, if any. func isLocal(a netip.Addr, local []netip.Prefix) (netip.Prefix, bool) { for _, p := range local { if p.Contains(a) { return p, true } } return netip.Prefix{}, false }