package catalogue import ( "fmt" "net/netip" "slices" "sort" "strings" ) // AttachedNetwork is one network a machine said it is directly attached to: the link it is on and the // network as address and prefix (novox/hq ADR 0340 §7), as the node-engine reports it among its facts. type AttachedNetwork struct { Link string `json:"link"` Network string `json:"network"` } // LocalNetwork is one attached network judged: whether it counts as this machine's local network for a // port reached from `local-network`, and why — said for one that counts as well as for one that does // not, so `node` can show which counted and why the others did not (ADR 0340 §8). type LocalNetwork struct { Link string Network string Counts bool Why string } // privateRanges are the only ranges a local network may lie in (ADR 0340 §2): RFC 1918 for IPv4 and the // unique local addresses for IPv6. A network counts only when it lies wholly inside one of them. var privateRanges = []struct { prefix netip.Prefix name string }{ {netip.MustParsePrefix("10.0.0.0/8"), "a private range (RFC 1918, 10.0.0.0/8)"}, {netip.MustParsePrefix("172.16.0.0/12"), "a private range (RFC 1918, 172.16.0.0/12)"}, {netip.MustParsePrefix("192.168.0.0/16"), "a private range (RFC 1918, 192.168.0.0/16)"}, {netip.MustParsePrefix("fc00::/7"), "the IPv6 unique local range (fc00::/7)"}, } // JudgeLocalNetworks says, for each network a machine reported, whether it counts as its local network. // // **A public subnet never counts** (ADR 0340 §2): a network counts only when it lies wholly in a private // range, so `local-network` can never open a port to the internet — a machine sitting on a public /24 has // that /24 judged and refused, never rendered. A link-local network does not count either. And only a // link the machine reported as facing outside it counts (§3): the private network's own link and a // container bridge are this machine's own, not its local network, whatever their addresses. // // Sorted by link and then network, one entry per (link, network), so what `node` shows and what the // filter is rendered from do not depend on the order a machine happened to say them in. func JudgeLocalNetworks(attached []AttachedNetwork, outward []string, tunnel string) []LocalNetwork { seen := map[[2]string]bool{} var out []LocalNetwork for _, a := range attached { key := [2]string{a.Link, a.Network} if seen[key] { continue } seen[key] = true out = append(out, judgeOne(a, outward, tunnel)) } sort.Slice(out, func(i, j int) bool { if out[i].Link != out[j].Link { return out[i].Link < out[j].Link } return out[i].Network < out[j].Network }) return out } func judgeOne(a AttachedNetwork, outward []string, tunnel string) LocalNetwork { j := LocalNetwork{Link: a.Link, Network: a.Network} prefix, err := netip.ParsePrefix(strings.TrimSpace(a.Network)) if err != nil { j.Why = "not a network the mesh can read (" + err.Error() + ")" return j } prefix = prefix.Masked() j.Network = prefix.String() switch { case tunnel != "" && a.Link == tunnel: j.Why = "the private network's own link, not the local network" return j case !slices.Contains(outward, a.Link): j.Why = fmt.Sprintf("%s does not face outside this machine (a bridge or a link of its own)", a.Link) return j case prefix.Addr().IsLoopback(): j.Why = "loopback" return j case prefix.Addr().IsLinkLocalUnicast(): j.Why = "link-local, never counted" return j } for _, r := range privateRanges { if r.prefix.Contains(prefix.Addr()) && prefix.Bits() >= r.prefix.Bits() { j.Counts = true j.Why = "in " + r.name return j } } if prefix.Addr().IsPrivate() { // Overlaps a private range without lying wholly inside it (a /7 around 10.0.0.0/8, say). j.Why = "not wholly in a private range, so never counted" return j } j.Why = "public, never counted: local-network may not open a port to the internet" return j } // CountedLocalNetworks is the networks that count, as the filter renders them: sorted, without repeats. func CountedLocalNetworks(judged []LocalNetwork) []string { var out []string for _, j := range judged { if j.Counts && !slices.Contains(out, j.Network) { out = append(out, j.Network) } } sort.Strings(out) return out } // LocalNetworkPort is one port the filter admits from `local-network`, and what that resolved to on this // machine (ADR 0340 §8): what `plan` shows for it. type LocalNetworkPort struct { Port int Protocol string Because []string // Networks is the local networks it is admitted from; empty when the machine reported none (§6). Networks []string } // LocalNetworkPorts is every rule admitted from `local-network`, with the networks it resolved to. func LocalNetworkPorts(rules []Rule, networks []string) []LocalNetworkPort { var out []LocalNetworkPort for _, rule := range rules { if rule.From != FromLocalNetwork { continue } out = append(out, LocalNetworkPort{Port: rule.Port, Protocol: rule.Protocol, Because: rule.Because, Networks: networks}) } return out } // Said is how `plan` says one such port: the networks, or that there are none and the port is admitted // from the private network only. func (p LocalNetworkPort) Said() string { by := strings.Join(p.Because, ", ") if len(p.Networks) == 0 { return fmt.Sprintf("%s/%d (%s) from the local network: none — this machine reported no private "+ "network on a link facing outside, so it is admitted from the private network only", p.Protocol, p.Port, by) } return fmt.Sprintf("%s/%d (%s) from the local network: %s, and the private network", p.Protocol, p.Port, by, strings.Join(p.Networks, ", ")) }