package resolution import ( "fmt" "net/netip" "slices" "sort" "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) // 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 []mcat.AttachedNetwork, outward []string, tunnel string) []mcat.LocalNetwork { seen := map[[2]string]bool{} var out []mcat.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 mcat.AttachedNetwork, outward []string, tunnel string) mcat.LocalNetwork { j := mcat.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 []mcat.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 } // LocalNetworkPorts is every rule admitted from `local-network`, with the networks it resolved to. func LocalNetworkPorts(rules []mcat.Rule, networks []string) []mcat.LocalNetworkPort { var out []mcat.LocalNetworkPort for _, rule := range rules { if rule.From != mcat.FromLocalNetwork { continue } out = append(out, mcat.LocalNetworkPort{Port: rule.Port, Protocol: rule.Protocol, Because: rule.Because, Networks: networks}) } return out }