Files
mesh-controller/internal/catalogue/localnetwork_test.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

178 lines
7.5 KiB
Go

package catalogue
import (
"strings"
"testing"
)
// The home server's case (novox/hq issue 154): it sits on 192.168.1.0/24, and also on a public /24 a
// provider handed it, beside the private network's own link and a container bridge.
var homeServer = []AttachedNetwork{
{Link: "eth0", Network: "192.168.1.10/24"},
{Link: "eth1", Network: "203.0.113.7/24"},
{Link: "eth0", Network: "fe80::1/64"},
{Link: "eth0", Network: "fd12:3456:789a::5/64"},
{Link: "wg-mesh", Network: "10.10.0.4/24"},
{Link: "docker0", Network: "172.17.0.1/16"},
}
func judged(t *testing.T) []LocalNetwork {
t.Helper()
return JudgeLocalNetworks(homeServer, []string{"eth0", "eth1"}, "wg-mesh")
}
// **A public subnet never counts** (ADR 0340 §2), and only a private one on a link facing outside does
// (§3): the tunnel's and a bridge's are this machine's own, whatever their addresses.
func TestOnlyAPrivateNetworkOnALinkFacingOutsideCounts(t *testing.T) {
got := CountedLocalNetworks(judged(t))
want := []string{"192.168.1.0/24", "fd12:3456:789a::/64"}
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Fatalf("counted %v, want %v", got, want)
}
for _, j := range judged(t) {
if j.Why == "" {
t.Errorf("%s on %s is judged without a why", j.Network, j.Link)
}
switch j.Network {
case "203.0.113.0/24":
if j.Counts || !strings.Contains(j.Why, "public") {
t.Errorf("the public subnet is judged %v (%s), want never counted as public", j.Counts, j.Why)
}
case "fe80::/64":
if j.Counts || !strings.Contains(j.Why, "link-local") {
t.Errorf("a link-local network is judged %v (%s)", j.Counts, j.Why)
}
case "10.10.0.0/24", "172.17.0.0/16":
if j.Counts {
t.Errorf("%s on %s counts, and it is not the local network", j.Network, j.Link)
}
}
}
}
// A network overlapping a private range without lying wholly inside it does not count either, so a
// wide prefix around a private range cannot carry the internet with it.
func TestANetworkNotWhollyInAPrivateRangeDoesNotCount(t *testing.T) {
for _, n := range []string{"10.0.0.0/7", "172.0.0.0/8", "100.64.0.0/10", "0.0.0.0/0", "::/0"} {
got := JudgeLocalNetworks([]AttachedNetwork{{Link: "eth0", Network: n}}, []string{"eth0"}, "")
if len(got) != 1 || got[0].Counts {
t.Errorf("%s counts as a local network: %+v", n, got)
}
}
}
// **The filter admits the local network on that port's rule alone** (§5), beside the private network,
// in both chains, and never the public subnet.
func TestALocalNetworkPortIsAdmittedFromItsNetworksAndTheMesh(t *testing.T) {
rules := []Rule{
{Port: 1883, Protocol: "tcp", From: FromLocalNetwork, Because: []string{"mosquitto"}},
{Port: 5432, Protocol: "tcp", From: FromMesh, Because: []string{"postgres"}},
}
local := CountedLocalNetworks(judged(t))
out := AsNftablesWithLocal(rules, []string{"10.10.0.1", "10.10.0.4"}, false, nil,
[]string{"eth0", "eth1"}, "wg-mesh", local)
for _, want := range []string{
"ip saddr { 10.10.0.1, 10.10.0.4, 192.168.1.0/24 } tcp dport 1883 accept",
"ip6 saddr { fd12:3456:789a::/64 } tcp dport 1883 accept",
"ip saddr { 10.10.0.1, 10.10.0.4, 192.168.1.0/24 } ct original proto-dst 1883 accept",
// The mesh-only port is not widened by the machine having a local network.
"ip saddr { 10.10.0.1, 10.10.0.4 } tcp dport 5432 accept",
} {
if !strings.Contains(out, want) {
t.Errorf("the rule set lacks %q:\n%s", want, out)
}
}
if strings.Contains(out, "203.0.113") {
t.Errorf("the public subnet reached the rule set:\n%s", out)
}
if strings.Contains(out, "tcp dport 1883 accept\n") && !strings.Contains(out, "saddr") {
t.Errorf("1883 is open to anywhere:\n%s", out)
}
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) == "tcp dport 1883 accept" {
t.Fatalf("1883 is open to anywhere:\n%s", out)
}
}
}
// **Nothing resolved is closed and said** (§6): a machine that reported no private network admits the
// port from the private network only, and the rule set says it found none — never widened to anywhere.
func TestNoLocalNetworkIsSaidAndNotWidened(t *testing.T) {
rules := []Rule{{Port: 1883, Protocol: "tcp", From: FromLocalNetwork, Because: []string{"mosquitto"}}}
out := AsNftablesWithLocal(rules, []string{"10.10.0.1"}, false, nil, []string{"eth0"}, "wg-mesh", nil)
if !strings.Contains(out, "1883 wanted the local network, and this machine reported no private network") {
t.Errorf("the rule set does not say the local network resolved to nothing:\n%s", out)
}
if !strings.Contains(out, "ip saddr { 10.10.0.1 } tcp dport 1883 accept") {
t.Errorf("the port is not admitted from the private network:\n%s", out)
}
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) == "tcp dport 1883 accept" {
t.Fatalf("an unresolved local network was widened to anywhere:\n%s", out)
}
}
said := LocalNetworkPorts(rules, nil)[0].Said()
if !strings.Contains(said, "none") {
t.Errorf("plan says %q, want it to say no local network was found", said)
}
}
// `local-network` is a source a manifest may name (§1, changing ADR 0045 §3), and an unknown source is
// still refused.
func TestAManifestMayListenFromTheLocalNetwork(t *testing.T) {
if _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"local-network"}]}`)); err != nil {
t.Fatalf("a listen from the local network is refused: %v", err)
}
_, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"lan"}]}`))
if err == nil || !strings.Contains(err.Error(), `from "lan"`) {
t.Fatalf("a source the mesh cannot render is not refused: %v", err)
}
}
// **A fourth reach** (§4, changing ADR 0138 §4): `internal` with the local networks added to what the
// filter opens — the internal name, not the public one.
func TestTheLocalNetworkReachIsInternalWithTheLocalNetworkAdded(t *testing.T) {
public, internal := namesFor(t, aRoutedWeb(), reachSet(ReachLocalNetwork))
if public != "" || internal != "app.anchor.internal" {
t.Fatalf("names are %q and %q, want the internal name only", public, internal)
}
bare := aRoutedWeb()
bare.Contributes = nil
r := Resolution{Node: "anchor", Modules: []Manifest{bare}}
rules, err := r.Rules(Rendering{Settings: reachSet(ReachLocalNetwork)})
if err != nil {
t.Fatalf("rules: %v", err)
}
if len(rules) != 1 || rules[0].From != FromLocalNetwork {
t.Fatalf("reach local-network made the filter say %+v", rules)
}
}
// Two modules wanting one port, one from the mesh and one from the local network, render one rule from
// the local network: it covers the mesh, and anywhere covers it.
func TestTheLocalNetworkSitsBetweenTheMeshAndAnywhere(t *testing.T) {
got := widest([]Rule{
{Port: 53, Protocol: "udp", From: FromMesh, Because: []string{"a"}},
{Port: 53, Protocol: "udp", From: FromLocalNetwork, Because: []string{"b"}},
})
if len(got) != 1 || got[0].From != FromLocalNetwork || len(got[0].Because) != 2 {
t.Fatalf("got %+v", got)
}
got = widest(append(got, Rule{Port: 53, Protocol: "udp", From: FromEverywhere, Because: []string{"c"}}))
if len(got) != 1 || got[0].From != FromEverywhere {
t.Fatalf("got %+v", got)
}
}
// On an adopted node the opening is the private network's part only: the found firewall admits the
// local network, and an opening that widened to everywhere would open the port to the internet.
func TestAnAdoptedNodeOpensTheLocalNetworkPortToTheMeshOnly(t *testing.T) {
out := Openings([]Rule{{Port: 1883, Protocol: "tcp", From: FromLocalNetwork}}, nil, nil)
if len(out) != 1 || out[0]["from"] != OpeningFromMesh {
t.Fatalf("openings %v", out)
}
}