// Package outward reads which of this machine's links face outside it (novox/hq ADR 0140). // // The filter the mesh derives constrains traffic arriving from outside the machine and says nothing // about traffic that did not. To write that rule the mesh has to know which links "outside" arrives // on, and that is a thing only the machine can say — so it says it, once per report, the way it // already reports the kind of firewall it found and the tunnel it carried. // // **It replaces a list of addresses.** The filter used to allow the machine's own containers back // through by naming the address ranges they sit on: two ranges fixed in the control plane's source // and the rest typed by an operator. A range describes one machine and goes stale silently // (novox/hq 04-ISSUES/137 and /141). A link that carries the default route is a fact the machine // reads afresh every time, and it does not change when a module is added or removed. // // It reads the kernel's routing tables directly rather than asking a program. A module naming a // program the machine does not have is how the mesh already reported success while doing nothing // (novox/hq 04-ISSUES/136), and every machine has /proc. package outward import ( "bufio" "fmt" "os" "path/filepath" "sort" "strings" ) // ProcNet is where the kernel publishes its routing tables. A parameter so a test can hold a // routing table without one. const ProcNet = "/proc/net" // Links are the interfaces carrying a default route, for both address families, sorted and without // repeats. // // A machine may have more than one: a laptop with a cable and a radio has two, and both face // outside. A machine with none — no route off itself — returns nothing, and the mesh refuses to // compose a filter for it rather than writing a rule around a link with no name, which would be a // rule set that does not load and a machine filtering nothing while its unit reports success. func Links(procNet string) ([]string, error) { if procNet == "" { procNet = ProcNet } seen := map[string]bool{} four, err := defaultsV4(filepath.Join(procNet, "route")) if err != nil { return nil, err } six, err := defaultsV6(filepath.Join(procNet, "ipv6_route")) if err != nil { return nil, err } for _, name := range append(four, six...) { if name != "" && name != "lo" { seen[name] = true } } out := make([]string, 0, len(seen)) for name := range seen { out = append(out, name) } sort.Strings(out) return out, nil } // defaultsV4 reads /proc/net/route, whose columns are // // Iface Destination Gateway Flags RefCnt Use Metric Mask ... // // with addresses in hexadecimal. A default route is destination zero with mask zero — the mask // matters, because a route to the zero address with a real mask is not a default route. func defaultsV4(path string) ([]string, error) { lines, err := rows(path) if err != nil { return nil, err } var out []string for _, fields := range lines { if len(fields) < 8 { continue } if isZeroHex(fields[1]) && isZeroHex(fields[7]) { out = append(out, fields[0]) } } return out, nil } // defaultsV6 reads /proc/net/ipv6_route, whose columns are // // dest destprefix src srcprefix nexthop metric refcnt use flags iface // // A default route is the zero destination with a zero prefix length. func defaultsV6(path string) ([]string, error) { lines, err := rows(path) if err != nil { return nil, err } var out []string for _, fields := range lines { if len(fields) < 10 { continue } if isZeroHex(fields[0]) && isZeroHex(fields[1]) { out = append(out, fields[9]) } } return out, nil } // rows reads a routing table into fields per line, skipping a header and blank lines. A table that // is not there is not an error: a machine without the second address family has no file for it, // and that is not a machine that cannot be filtered. func rows(path string) ([][]string, error) { file, err := os.Open(path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, fmt.Errorf("cannot read the routing table at %s: %w", path, err) } defer file.Close() var out [][]string scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "Iface") { continue } out = append(out, strings.Fields(line)) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("cannot read the routing table at %s: %w", path, err) } return out, nil } // isZeroHex is whether a hexadecimal field is all zeroes, whatever its width — the v4 table writes // eight digits and the v6 table thirty-two, and a prefix length is two. func isZeroHex(field string) bool { if field == "" { return false } return strings.Trim(strings.ToLower(field), "0") == "" }