mesh/merge-gate head 4cc8588afb87 merged into trunk 0a1dc5f93fcb — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 4cc8588afb87 merged into trunk 0a1dc5f93fcb — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/154-local-network delivered: every member is delivered
The review asked for the IPv6 side of the rule that local-network cannot open a port to the internet.
196 lines
8.3 KiB
Go
196 lines
8.3 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)
|
|
}
|
|
}
|
|
|
|
// A global unicast IPv6 subnet on a link facing outside is public, and never counts: the IPv6 side of the
|
|
// rule that `local-network` cannot open a port to the internet (ADR 0340 §2).
|
|
func TestAGlobalIPv6SubnetIsPublicAndNeverCounts(t *testing.T) {
|
|
got := JudgeLocalNetworks([]AttachedNetwork{{Link: "eth0", Network: "2001:db8:1234::7/48"}}, []string{"eth0"}, "wg-mesh")
|
|
if len(got) != 1 {
|
|
t.Fatalf("judged %+v", got)
|
|
}
|
|
if got[0].Counts {
|
|
t.Fatalf("the global IPv6 subnet %s counts as a local network", got[0].Network)
|
|
}
|
|
if got[0].Network != "2001:db8:1234::/48" || !strings.Contains(got[0].Why, "public, never counted") {
|
|
t.Fatalf("judged %s with why %q, want it named public and never counted", got[0].Network, got[0].Why)
|
|
}
|
|
if n := CountedLocalNetworks(got); len(n) != 0 {
|
|
t.Fatalf("counted %v", n)
|
|
}
|
|
}
|