Files
mesh-controller/internal/catalogue/localnetwork.go
T
jochen 6e6cd5e26d
mesh/delivery-group group fix/154-local-network checking: 0 of 2 member(s) ready
mesh/merge-gate out of date: judged against trunk 1826b68a22b5, the trunk is now 404b4409e70e; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 1826b68a22b5, the trunk is now 404b4409e70e; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
Admit a port from the machine's own local network, and never a public subnet (issue 154, ADR 0340)
A local service could only be declared public, which says the internet. local-network is a source and a reach: the private network and the private networks the machine reported on a link facing outside, judged here, shown by plan and node, and said in the rule set when there are none.
2026-10-13 11:20:22 +02:00

155 lines
5.6 KiB
Go

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, ", "))
}