155 lines
5.6 KiB
Go
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, ", "))
|
|
}
|