diff --git a/internal/catalogue/filtering.go b/internal/catalogue/filtering.go index 1e202f3..b81bd10 100644 --- a/internal/catalogue/filtering.go +++ b/internal/catalogue/filtering.go @@ -444,6 +444,18 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int, b.WriteString("\t\t# this machine's own guests reaching outward: not a port opened to anybody\n") b.WriteString(fmt.Sprintf("\t\tiifname != { %s } accept\n", inward)) } + // **The mesh passing through, not arriving.** A machine the mesh routes through — the hub, for + // every path between machines that are not co-located (novox/hq ADR 0007) — relays a packet that + // came in on the tunnel and leaves on it again, addressed to another machine of the mesh. That is + // no port of this machine's: the machine it is for filters it against its own rules. Without + // this, the chain below judged a relayed packet by this machine's own published ports, so two + // machines behind the hub reached each other only on ports the hub happened to publish for itself + // (novox/hq issue 196). In and out on the tunnel both: a packet off the tunnel for this machine's + // own containers leaves by a bridge, and still meets the rules below. + if tunnel != "" { + b.WriteString("\t\t# the mesh passing through to another of its machines, which filters it itself\n") + b.WriteString(fmt.Sprintf("\t\tiifname %q oifname %q accept\n", tunnel, tunnel)) + } if len(rules) > 0 { b.WriteString("\n") diff --git a/internal/catalogue/filtering_test.go b/internal/catalogue/filtering_test.go index 5048a98..b36e763 100644 --- a/internal/catalogue/filtering_test.go +++ b/internal/catalogue/filtering_test.go @@ -887,3 +887,34 @@ func TestAPublicPortNeedsNoGuestLine(t *testing.T) { t.Fatalf("a public port was given a guest line it does not need:\n%s", nft) } } + +// **The hub relays the mesh** (novox/hq ADR 0007, issue 196). Two machines that are not co-located +// reach each other through the hub, so the hub forwards a packet that arrives on the tunnel and +// leaves on it. The forward chain judged that packet by the hub's own published ports, and two +// machines behind the hub reached each other only on the ports the hub happened to publish. +// +// Measured: from one home machine to another through the hub, 17 of 55 ports answered, and they +// were exactly the hub's own; the SYN for the rest never left the hub. +func TestTheMeshPassingThroughIsRelayedNotJudgedAsThisMachines(t *testing.T) { + nft := AsNftables(nil, []string{"10.42.0.1", "10.42.0.2"}, false, nil, []string{"eth0"}, "mesh0") + relay := `iifname "mesh0" oifname "mesh0" accept` + if !strings.Contains(chainBody(t, nft, "forward"), relay) { + t.Errorf("the forward chain does not relay the mesh through this machine:\n%s", chainBody(t, nft, "forward")) + } + // Relaying is not receiving: nothing in the input chain opens because of it. + if strings.Contains(chainBody(t, nft, "input"), "oifname") { + t.Errorf("the input chain names an outgoing interface, which no packet for this machine has:\n%s", + chainBody(t, nft, "input")) + } + // And off the tunnel into this machine's own containers is still judged: the tunnel is not + // accepted wholesale, only in and out on it. + if strings.Contains(chainBody(t, nft, "forward"), `iifname "mesh0" accept`) { + t.Errorf("the forward chain accepts everything off the tunnel:\n%s", chainBody(t, nft, "forward")) + } + + // A machine with no tunnel relays nothing, and names no interface it does not have. + alone := AsNftables(nil, nil, false, nil, []string{"eth0"}, "") + if strings.Contains(alone, "oifname") { + t.Errorf("a machine with no tunnel was given a relay rule:\n%s", alone) + } +}