From b4c21b4f6707eed1373f63bc93b794473abb3943 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 19:59:22 +0200 Subject: [PATCH] Read a machine's iptables rules when it has no nft, instead of calling it unfiltered (hq ADR 0100) --- internal/firewall/firewall.go | 18 ++++++++++++++--- internal/firewall/firewall_test.go | 31 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index 91a0f7c..37b9052 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -63,19 +63,31 @@ func Detect(ctx context.Context, run Runner) (Kind, string, error) { ufwActive = statusActive(out) } + noNft := false out, err := run(ctx, "nft", "list", "ruleset") switch { case err == nil: if refusing := Refusing(out, ufwActive); len(refusing) > 0 { return Unsupported, "nftables rules that refuse traffic, in " + strings.Join(refusing, ", "), nil } - case !missing(err): + case missing(err): + // **No nft on this machine does not mean no rules.** iptables-nft writes tables nft would + // have shown, and a machine whose only tool is iptables answers about them through that. + // Read as "nothing filters here", a machine with an iptables firewall would be adopted + // with no openings and nothing would reach the mesh (novox/hq ADR 0100). + noNft = true + default: return "", "", fmt.Errorf("cannot read this machine's packet filter to know what it has: %w", err) } if !ufwActive { - // iptables with the legacy backend is invisible to nft. - for _, legacy := range []string{"iptables-legacy", "ip6tables-legacy"} { + // iptables with the legacy backend is invisible to nft; and where nft is not installed, + // the iptables command is the only way to see anything at all. + tools := []string{"iptables-legacy", "ip6tables-legacy"} + if noNft { + tools = append(tools, "iptables", "ip6tables") + } + for _, legacy := range tools { out, err := run(ctx, legacy, "-S") if err != nil { continue diff --git a/internal/firewall/firewall_test.go b/internal/firewall/firewall_test.go index 426c7d5..acee5d2 100644 --- a/internal/firewall/firewall_test.go +++ b/internal/firewall/firewall_test.go @@ -119,11 +119,20 @@ type fakeUFW struct { // after it is disabled; empty is a machine without iptables. forward is a policy set since. iptablesActive, iptablesInactive string forward string + // noNft is a machine with no nft binary; iptablesRules is what `iptables -S` prints there. + noNft bool + iptablesRules string } // iptables answers `iptables -S FORWARD` from the captured output for ufw's state, and records // a forward policy set with -P. func (f *fakeUFW) iptables(name string, args []string) (string, error) { + if f.iptablesRules != "" && len(args) == 1 && args[0] == "-S" { + if name == "ip6tables" { + return "", nil + } + return f.iptablesRules, nil + } if f.iptablesActive == "" { return "", &exec.Error{Name: name, Err: exec.ErrNotFound} } @@ -189,6 +198,9 @@ func (f *fakeUFW) run(_ context.Context, name string, args ...string) (string, e } return "", &exec.Error{Name: name, Err: exec.ErrNotFound} case "nft": + if f.noNft { + return "", &exec.Error{Name: name, Err: exec.ErrNotFound} + } return f.ruleset, nil case "iptables-legacy", "ip6tables-legacy": return "", &exec.Error{Name: name, Err: exec.ErrNotFound} @@ -756,3 +768,22 @@ func TestIncomingIsUfwsDefaultDirection(t *testing.T) { t.Error("an incoming refusal ufw would merge was not refused") } } + +func TestAMachineWithIptablesRulesAndNoNftIsNotReadAsUnfiltered(t *testing.T) { + // nft is not installed, and iptables-nft holds a firewall of somebody's. Read as "nothing + // filters here" the mesh would adopt it, open nothing, and be unreachable (novox/hq ADR 0100). + rules := "-P INPUT DROP\n-P FORWARD DROP\n-P OUTPUT ACCEPT\n-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT\n" + f := &fakeUFW{noNft: true, iptablesRules: rules} + kind, what, err := Detect(context.Background(), f.run) + if err != nil { + t.Fatal(err) + } + if kind != Unsupported { + t.Errorf("a machine filtered by iptables with no nft read as %s (%s)", kind, what) + } + // And a machine with nothing but the runtime's own rules and no nft is still unfiltered. + docker := "-P INPUT ACCEPT\n-P FORWARD DROP\n-P OUTPUT ACCEPT\n-N DOCKER\n-A DOCKER -i docker0 -j DROP\n" + if kind, _, err := Detect(context.Background(), (&fakeUFW{noNft: true, iptablesRules: docker}).run); err != nil || kind != None { + t.Errorf("a machine with only the runtime's rules read as %s: %v", kind, err) + } +}