From d9bee18d445157b76168bc8845a3e4214a363330 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 31 Aug 2026 00:34:20 +0200 Subject: [PATCH] A mesh on both address families renders both nftables matches ip and ip6 separately and one set holding both is a syntax error, so the file would not load: the service reports a configuration fault and the machine filters nothing. Also a make target for the builder image, which the lab now stocks. --- internal/catalogue/filtering.go | 28 ++++++++++++++++++++++++++-- internal/catalogue/filtering_test.go | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/catalogue/filtering.go b/internal/catalogue/filtering.go index e61da66..04f7730 100644 --- a/internal/catalogue/filtering.go +++ b/internal/catalogue/filtering.go @@ -168,8 +168,15 @@ func AsNftables(rules []Rule, mesh []string) string { rule.Protocol, rule.Port)) break } - b.WriteString(fmt.Sprintf("\t\tip saddr { %s } %s dport %d accept\n", - strings.Join(mesh, ", "), rule.Protocol, rule.Port)) + four, six := byFamily(mesh) + if len(four) > 0 { + b.WriteString(fmt.Sprintf("\t\tip saddr { %s } %s dport %d accept\n", + strings.Join(four, ", "), rule.Protocol, rule.Port)) + } + if len(six) > 0 { + b.WriteString(fmt.Sprintf("\t\tip6 saddr { %s } %s dport %d accept\n", + strings.Join(six, ", "), rule.Protocol, rule.Port)) + } case FromEverywhere: b.WriteString(fmt.Sprintf("\t\t%s dport %d accept\n", rule.Protocol, rule.Port)) } @@ -190,3 +197,20 @@ func AsNftables(rules []Rule, mesh []string) string { b.WriteString("}\n") return b.String() } + +// byFamily splits addresses into the two nftables understands separately. +// +// `ip saddr` and `ip6 saddr` are different matches, and one set holding both families is a syntax +// error — which means the whole file fails to load and the machine filters nothing while the +// service reports a configuration fault. A mesh that is entirely one family renders one line, and +// a mixed one renders both rather than dropping half its nodes. +func byFamily(addresses []string) (four []string, six []string) { + for _, a := range addresses { + if strings.Contains(a, ":") { + six = append(six, a) + continue + } + four = append(four, a) + } + return four, six +} diff --git a/internal/catalogue/filtering_test.go b/internal/catalogue/filtering_test.go index a098b8f..c7bc2e7 100644 --- a/internal/catalogue/filtering_test.go +++ b/internal/catalogue/filtering_test.go @@ -219,3 +219,21 @@ func TestAskingForTheRuleSetWithNowhereToPutItIsRefused(t *testing.T) { t.Fatal("a module asked for the rule set and named no path; it would receive nothing") } } + +// nftables matches the two address families separately, and one set holding both is a syntax +// error — so the file fails to load, the service reports a fault, and the machine filters nothing. +func TestAMeshOnBothAddressFamiliesRendersBoth(t *testing.T) { + nft := AsNftables((Resolution{Modules: []Manifest{ + {Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}}, + }}).Filtering(), []string{"198.51.100.2", "2001:db8::2"}) + if !strings.Contains(nft, "ip saddr { 198.51.100.2 } tcp dport 5432 accept") { + t.Fatalf("the machines with v4 addresses were dropped:\n%s", nft) + } + if !strings.Contains(nft, "ip6 saddr { 2001:db8::2 } tcp dport 5432 accept") { + t.Fatalf("the machines with v6 addresses were dropped:\n%s", nft) + } + // And never in one set, which is the syntax error this exists to avoid. + if strings.Contains(nft, "198.51.100.2, 2001:db8::2") { + t.Fatalf("both families are in one set, so the file will not load:\n%s", nft) + } +}