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
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.
178 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|