package catalogue import ( "encoding/json" "reflect" "strings" "testing" ) // novox/hq ADR 0100: on an adopted node the found firewall stays in force. The mesh declares // openings where it would have loaded a filter, and guards its own ports in a table that only // refuses. // hub is the private network's generator on the hub: it opens the hub's port from anywhere. type hub struct{} func (hub) Resources(string) ([]map[string]any, bool, error) { return []map[string]any{{"id": "config", "type": "file", "path": "/etc/wireguard/mesh0.conf", "content": "[Interface]\n"}}, true, nil } func (hub) Listens(string) ([]Listening, error) { return []Listening{{Port: 51820, Protocol: "udp", From: FromEverywhere}}, nil } // anAdoptedAnchor is the control-node's set: the store, the bus, the registry, the private network, // a served module and the filter module. func anAdoptedAnchor() Resolution { return Resolution{Node: "anchor", Modules: []Manifest{ {Module: "network", Computed: "overlay"}, {Module: "postgres", Guards: []int{5432}, Listens: []Listening{{Port: 5432, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store", "ports": []any{"5432:5432"}}}}, {Module: "lavinmq", Guards: []int{15672}, Listens: []Listening{{Port: 5671, From: FromMesh}, {Port: 5672, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-broker", "ports": []any{"5671:5671", "5672:5672", "127.0.0.1:15672:15672"}}}}, {Module: "distribution", Listens: []Listening{{Port: 5000, From: FromMesh}}, Resources: []map[string]any{{"id": "store", "type": "container", "name": "registry", "ports": []any{"5000"}}}}, {Module: "hello-web", Listens: []Listening{{Port: 8080, From: FromEverywhere}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "hello-web", "ports": []any{"8080"}}}}, {Module: "helper", Listens: []Listening{{Port: 9000, From: FromMachine}}}, {Module: "nftables", Filtering: &Filtering{Into: "/etc/nftables.conf"}, Resources: []map[string]any{{"id": "load", "type": "service", "unit": "mesh-filter.service", "state": "running", "restart-on": []any{"filtering"}}}}, }} } func anchorRendering(adopted bool) Rendering { return Rendering{ Generators: map[string]Generator{"overlay": hub{}}, Ports: map[string]map[int]int{"distribution": {5000: 5000}, "hello-web": {8080: 20001}}, Settings: SettingsBy{"distribution": {{From: "node anchor", Values: map[string]any{ExposeSetting: map[string]any{"5000": FromEverywhere}}}}}, Mesh: []string{"10.42.0.1"}, Foundation: []int{5671}, Adopted: adopted, // Genesis takes the foundation's modules. Taken: map[string]bool{"postgres": true, "lavinmq": true}, } } func byID(resources []map[string]any) map[string]map[string]any { out := map[string]map[string]any{} for _, r := range resources { out[r["id"].(string)] = r } return out } func TestAnAdoptedNodeIsDeclaredOpeningsFromTheSameInputsAsTheFilter(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(true)) if err != nil { t.Fatal(err) } got := byID(composed.Resources) want := map[string]map[string]any{ // The hub's port, from anywhere, received. "adoption.opening-udp-51820-incoming": {"port": 51820, "protocol": "udp", "from": "everywhere", "path": "incoming"}, // The store's port from the private network only, and forwarded: a container publishes it. "adoption.opening-tcp-5432-forwarded": {"port": 5432, "protocol": "tcp", "from": "mesh", "path": "forwarded", "to": 5432}, // The bus from anywhere: a node enrols over it before it has a private address. "adoption.opening-tcp-5671-forwarded": {"port": 5671, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 5671}, "adoption.opening-tcp-5672-forwarded": {"port": 5672, "protocol": "tcp", "from": "mesh", "path": "forwarded", "to": 5672}, // The registry from anywhere, by its node's exposure setting. "adoption.opening-tcp-5000-forwarded": {"port": 5000, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 5000}, // A published port names the machine port and the container port it is forwarded to. "adoption.opening-tcp-20001-forwarded": {"port": 20001, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 8080}, } for id, fields := range want { opening, ok := got[id] if !ok { t.Errorf("no %s among %v", id, keys(got)) continue } if opening["type"] != "opening" { t.Errorf("%s is a %v", id, opening["type"]) } for k, v := range fields { if opening[k] != v { t.Errorf("%s: %s is %v, want %v", id, k, opening[k], v) } } } for id := range got { if strings.HasPrefix(id, "adoption.opening-") && want[id] == nil { t.Errorf("an opening nothing asked for: %s", id) } } // A port for this machine only opens nothing, and the management port is not opened at all. for id := range got { if strings.Contains(id, "-9000-") || strings.Contains(id, "-15672-") { t.Errorf("%s is opened", id) } } // And openings come first, in the order the machine applies them. if !strings.HasPrefix(composed.Resources[0]["id"].(string), "adoption.opening-") { t.Errorf("openings are not first: %v", composed.Resources[0]["id"]) } } func TestAnAdoptedNodeLoadsNoFilterOfTheMeshs(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(true)) if err != nil { t.Fatal(err) } for _, r := range composed.Resources { if r["path"] == "/etc/nftables.conf" || strings.HasPrefix(r["id"].(string), "nftables.") { t.Fatalf("an adopted node is declared the filter module's %v", r["id"]) } if content, _ := r["content"].(string); strings.Contains(content, "policy drop") { t.Fatalf("an adopted node is declared a table that drops by default: %v", r["id"]) } // Nothing but refusals: the only accept in the guard is its policy. if content, _ := r["content"].(string); r["id"] == GuardID() && strings.Count(content, "accept") != 1 { t.Fatalf("the guard holds an accept:\n%s", content) } } got := byID(composed.Resources) guard := got[GuardID()] if guard == nil || got[GuardUnitID()] == nil || got[GuardRunningID()] == nil { t.Fatalf("no guard: %v", keys(got)) } if guard["content"] != AsGuard([]int{5432, 5672, 15672}) { t.Fatalf("the guard does not guard the store, the broker and its management port:\n%s", guard["content"]) } // The tool that loads it comes first, and the table after it: a node joining adopted has no // filter module and may have no nft. pkg, table := -1, -1 for i, r := range composed.Resources { switch r["id"] { case GuardPackageID(): pkg = i if r["type"] != "package" || r["package"] != "nftables" { t.Fatalf("the guard's package is %v", r) } case GuardID(): table = i } } if pkg < 0 || pkg > table { t.Fatalf("nftables is not declared before the guard's table (%d, %d)", pkg, table) } if !reflect.DeepEqual(got[GuardRunningID()]["restart-on"], []any{GuardID(), GuardUnitID()}) { t.Fatalf("the guard is not reloaded when its table changes: %v", got[GuardRunningID()]) } // Nothing of the mesh's own is anybody's to hold. for id, module := range composed.Owner { if strings.HasPrefix(id, AdoptionPrefix) { t.Fatalf("%s is owned by %s", id, module) } } } func TestAConvergedNodeIsDeclaredItsFilterAndNoOpenings(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(false)) if err != nil { t.Fatal(err) } got := byID(composed.Resources) if got["nftables.filtering"] == nil || got["nftables.load"] == nil { t.Fatalf("a converged node lost its filter: %v", keys(got)) } for id := range got { if strings.HasPrefix(id, AdoptionPrefix) { t.Fatalf("a converged node is declared %s", id) } } plain, err := anAdoptedAnchor().Declaration(anchorRendering(false)) if err != nil { t.Fatal(err) } a, _ := json.Marshal(plain) b, _ := json.Marshal(composed.Resources) if string(a) != string(b) { t.Fatal("Compose and Declaration disagree on a converged node") } } // The table the installer raises and the controller declares, character for character. func TestTheGuardIsExactlyThisTable(t *testing.T) { const golden = `table inet mesh_guard {} delete table inet mesh_guard table inet mesh_guard { chain prerouting { type filter hook prerouting priority raw; policy accept; fib daddr type local iifname != "lo" iifname != "docker0" iifname != "br-*" iifname != "mesh0" tcp dport { 5432, 15672 } drop } } ` if got := AsGuard([]int{15672, 5432}); got != golden { t.Fatalf("the guard changed:\n%s", got) } const unit = `[Unit] Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100) DefaultDependencies=no Wants=network-pre.target Before=network-pre.target shutdown.target Conflicts=shutdown.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=nft -f /etc/mesh/guard.nft ExecReload=nft -f /etc/mesh/guard.nft ExecStop=nft delete table inet mesh_guard [Install] WantedBy=multi-user.target ` if got := GuardUnitText(); got != unit { t.Fatalf("the guard's unit changed:\n%s", got) } if GuardResources(nil) != nil { t.Fatal("a guard with nothing to guard is an empty set nft refuses to load") } } func TestAGuardedPortMustBeAPort(t *testing.T) { if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[5432]}`)); err != nil { t.Fatalf("guards is refused: %v", err) } if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[0]}`)); err == nil { t.Fatal("guarding port 0 was accepted") } } func keys[V any](m map[string]V) []string { return sortedKeys(m) } // novox/hq ADR 0100: the foundation's ports are the node's. Given 5433 for the store, every place // that uses the port reads it from there: the container, the filter, the openings, the guard. func TestAGivenPortIsUsedEverywhereThePortIs(t *testing.T) { given := map[string]map[int]int{"postgres": {5432: 5433}, "lavinmq": {15672: 15673}} for _, adopted := range []bool{true, false} { with := anchorRendering(adopted) with.Given = given with.Ports["postgres"] = map[int]int{5432: 5433} composed, err := anAdoptedAnchor().Compose(with) if err != nil { t.Fatal(err) } got := byID(composed.Resources) if ports := got["postgres.server"]["ports"]; !reflect.DeepEqual(ports, []any{"5433:5432"}) { t.Fatalf("the store's container publishes %v", ports) } if ports := got["lavinmq.server"]["ports"]; !reflect.DeepEqual(ports, []any{"5671:5671", "5672:5672", "127.0.0.1:15673:15672"}) { t.Fatalf("the broker's container publishes %v", ports) } if !adopted { filter, _ := got["nftables.filtering"]["content"].(string) if !strings.Contains(filter, "tcp dport 5433 accept") || strings.Contains(filter, "5432") { t.Fatalf("the filter does not use the given port:\n%s", filter) } continue } if o := got["adoption.opening-tcp-5433-forwarded"]; o == nil || o["to"] != 5432 { t.Fatalf("no opening for the given port: %v", keys(got)) } if guard := got[GuardID()]["content"]; guard != AsGuard([]int{5433, 5672, 15673}) { t.Fatalf("the guard does not guard the given ports:\n%s", guard) } } } func TestAGivenPortIsTheNodesAndReachesSomething(t *testing.T) { store := anAdoptedAnchor().Modules[1] node := func(v any) []Layer { return []Layer{{From: "anchor", Values: map[string]any{PortsSetting: v}}} } if got, err := GivenPorts(store, node(map[string]any{"5432": float64(5433)})); err != nil || got[5432] != 5433 { t.Fatalf("a node's given port was not read: %v %v", got, err) } if _, err := GivenPorts(store, []Layer{{From: MeshWideLayer, Values: map[string]any{PortsSetting: map[string]any{"5432": float64(5433)}}}}); err == nil { t.Fatal("a port given for the whole mesh was accepted") } if _, err := GivenPorts(store, node(map[string]any{"6000": float64(6001)})); err == nil { t.Fatal("a port the module neither listens on, publishes nor guards was given") } if _, err := GivenPorts(store, node(map[string]any{"5432": float64(70000)})); err == nil { t.Fatal("a machine port that is not a port was given") } if stray := UnusedSettings(store, node(map[string]any{"5432": float64(5433)})); len(stray) != 0 { t.Fatalf("a given port is called stray: %v", stray) } } // novox/hq ADR 0103: the guard is derived, and from taken modules only — every machine port a taken // module publishes that the filter admits from the private network only, and the ports its // manifest guards. A module assigned but not taken is not guarded: its port may still be the // predecessor's. func TestTheGuardIsDerivedFromTakenModulesOnly(t *testing.T) { guardOf := func(with Rendering) string { t.Helper() composed, err := anAdoptedAnchor().Compose(with) if err != nil { t.Fatal(err) } content, _ := byID(composed.Resources)[GuardID()]["content"].(string) return content } // The broker taken, the store not: the broker's plain port follows from its listens (from // the mesh, published), its management port from its manifest; the store is not guarded, and // neither is the bus, which the mesh needs from everywhere. with := anchorRendering(true) with.Taken = map[string]bool{"lavinmq": true} if got := guardOf(with); got != AsGuard([]int{5672, 15672}) { t.Fatalf("the guard is not the taken broker's ports:\n%s", got) } // A taken module publishing a port admitted from everywhere is not guarded; one admitted from // the mesh is. The registry is exposed everywhere on this node, and hello-web listens from // everywhere. with.Taken = map[string]bool{"distribution": true, "hello-web": true} if got := guardOf(with); got != "" { t.Fatalf("a port admitted from everywhere is guarded:\n%s", got) } with.Settings = nil if got := guardOf(with); got != AsGuard([]int{5000}) { t.Fatalf("the registry, from the mesh only, is not guarded:\n%s", got) } // Nothing taken, nothing guarded — and no guard at all rather than an empty set. with = anchorRendering(true) with.Taken = nil if got := guardOf(with); got != "" { t.Fatalf("an untaken store is guarded:\n%s", got) } // A given port is followed: where the machine put it is what is refused. with = anchorRendering(true) with.Given = map[string]map[int]int{"lavinmq": {5672: 5682, 15672: 15673}} with.Ports["lavinmq"] = map[int]int{5671: 5671, 5672: 5682} if got := guardOf(with); got != AsGuard([]int{5432, 5682, 15673}) { t.Fatalf("the guard does not follow the given ports:\n%s", got) } } // A mapping bound to loopback is not published to anything off the machine — in either address // family — and an address's own colons never shift the ports. func TestPublishedLeavesOutLoopbackInBothFamilies(t *testing.T) { got := Published([]map[string]any{{"type": "container", "ports": []any{ "127.0.0.1:15672:15672", "[::1]:8080:80", "localhost:9090:90", "[::]:8443:443", "0.0.0.0:5000:5000", "5353:53/udp"}}}) want := map[string]map[int]int{"tcp": {8443: 443, 5000: 5000}, "udp": {5353: 53}} if !reflect.DeepEqual(got, want) { t.Fatalf("published is %v, want %v", got, want) } }