Read a machine's iptables rules when it has no nft, instead of calling it unfiltered (hq ADR 0100)

This commit is contained in:
2026-09-22 19:59:22 +02:00
parent c7ff0b9026
commit b4c21b4f67
2 changed files with 46 additions and 3 deletions
+15 -3
View File
@@ -63,19 +63,31 @@ func Detect(ctx context.Context, run Runner) (Kind, string, error) {
ufwActive = statusActive(out) ufwActive = statusActive(out)
} }
noNft := false
out, err := run(ctx, "nft", "list", "ruleset") out, err := run(ctx, "nft", "list", "ruleset")
switch { switch {
case err == nil: case err == nil:
if refusing := Refusing(out, ufwActive); len(refusing) > 0 { if refusing := Refusing(out, ufwActive); len(refusing) > 0 {
return Unsupported, "nftables rules that refuse traffic, in " + strings.Join(refusing, ", "), nil 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) return "", "", fmt.Errorf("cannot read this machine's packet filter to know what it has: %w", err)
} }
if !ufwActive { if !ufwActive {
// iptables with the legacy backend is invisible to nft. // iptables with the legacy backend is invisible to nft; and where nft is not installed,
for _, legacy := range []string{"iptables-legacy", "ip6tables-legacy"} { // 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") out, err := run(ctx, legacy, "-S")
if err != nil { if err != nil {
continue continue
+31
View File
@@ -119,11 +119,20 @@ type fakeUFW struct {
// after it is disabled; empty is a machine without iptables. forward is a policy set since. // after it is disabled; empty is a machine without iptables. forward is a policy set since.
iptablesActive, iptablesInactive string iptablesActive, iptablesInactive string
forward 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 // iptables answers `iptables -S FORWARD` from the captured output for ufw's state, and records
// a forward policy set with -P. // a forward policy set with -P.
func (f *fakeUFW) iptables(name string, args []string) (string, error) { 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 == "" { if f.iptablesActive == "" {
return "", &exec.Error{Name: name, Err: exec.ErrNotFound} 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} return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
case "nft": case "nft":
if f.noNft {
return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
}
return f.ruleset, nil return f.ruleset, nil
case "iptables-legacy", "ip6tables-legacy": case "iptables-legacy", "ip6tables-legacy":
return "", &exec.Error{Name: name, Err: exec.ErrNotFound} 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") 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)
}
}