diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 6b157b0..65c620a 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1141,6 +1141,16 @@ func adoptionFingerprint(r link.Report) string { for _, h := range r.Held { parts = append(parts, "held "+h.ID+"="+h.Changed) } + // And what filters the machine, with the found firewall's state (novox/hq ADR 0168): a rule the + // operator removes between declarations, or a front end enabled again, is said at the next + // reconcile rather than at the next push. + for _, f := range r.Filters { + parts = append(parts, "filter "+f.Owner+" "+f.Where+" "+f.Refuses) + } + if r.FoundFirewall != nil { + parts = append(parts, fmt.Sprintf("found-firewall %s active=%v retired-by=%s", r.FoundFirewall.Kind, + r.FoundFirewall.Active, r.FoundFirewall.RetiredBy)) + } for _, reach := range r.Reachable { parts = append(parts, fmt.Sprintf("reach %s %s:%d %s %v %d", reach.Protocol, reach.Address, reach.Port, reach.By, reach.Published, reach.ContainerPort)) @@ -1289,7 +1299,8 @@ func worthSaying(report link.Report) bool { if report.Refused != "" { return false } - return len(report.Held) > 0 || report.Firewall != "" || len(report.Outward) > 0 + return len(report.Held) > 0 || report.Firewall != "" || len(report.Outward) > 0 || + len(report.Filters) > 0 || report.FoundFirewall != nil } // applyDeclared applies a declaration that has already been proved to come from the mesh. @@ -1440,6 +1451,21 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D report.Strays = append(report.Strays, link.Stray{Kind: s.Kind, Name: s.Name, Detail: s.Detail}) } } + // What filters this machine, with owners, whatever its mode (novox/hq ADR 0168): the mesh says + // truthfully what filters a converged machine, and names what it did not write. + ufwActive := firewall.Active(ctx, apply.ExecRunner) + if filters, err := firewall.Collect(ctx, apply.ExecRunner, ufwActive); err != nil { + fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not read what filters this machine: %v\n", err) + } else { + for _, f := range filters { + report.Filters = append(report.Filters, link.Filter{Where: f.Where, Owner: f.Owner, Refuses: f.Refuses}) + } + } + if declared.Adoption == nil && updated.Firewall != nil && updated.Firewall.Kind == string(firewall.UFW) && updated.Firewall.WasActive { + // And, converged, the state of the firewall it was found with and who retired it. + report.FoundFirewall = &link.FoundFirewall{Kind: updated.Firewall.Kind, Active: ufwActive, + RetiredBy: updated.Firewall.RetiredBy} + } if declared.Adoption != nil { if updated.Firewall != nil { report.Firewall = updated.Firewall.Kind diff --git a/cmd/mesh-host/main_test.go b/cmd/mesh-host/main_test.go index ae91b75..77b3bb8 100644 --- a/cmd/mesh-host/main_test.go +++ b/cmd/mesh-host/main_test.go @@ -171,6 +171,22 @@ func TestAReconcileSpeaksOnlyWhenWhatIsHeldChanged(t *testing.T) { if !w.changed(link.Report{Firewall: "none", Held: rewritten.Held}) { t.Error("a changed firewall was not said") } + // What filters the machine is part of it (novox/hq ADR 0168): a predecessor's chain removed by + // hand, or the found firewall enabled again, is said without being asked. + filtered := link.Report{Firewall: "none", Held: rewritten.Held, + Filters: []link.Filter{{Where: "chain HAL-MESH-ONLY (iptables-legacy)", Owner: "other", Refuses: "-j DROP"}}} + if !w.changed(filtered) { + t.Error("a filter appearing was not said") + } + if !w.changed(link.Report{Firewall: "none", Held: rewritten.Held}) { + t.Error("a filter removed was not said") + } + if !w.changed(link.Report{Firewall: "none", Held: rewritten.Held, FoundFirewall: &link.FoundFirewall{Kind: "ufw", Active: true}}) { + t.Error("the found firewall coming back was not said") + } + if !worthSaying(link.Report{Filters: filtered.Filters}) { + t.Error("a report carrying only what filters the machine is not worth saying") + } } func TestWhatTheLinkPublishedCountsAsSaid(t *testing.T) { diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 4128d54..5efd3d4 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -27,6 +27,7 @@ import ( "time" "github.com/novox/mesh-host/internal/declaration" + "github.com/novox/mesh-host/internal/firewall" "github.com/novox/mesh-host/internal/store" "github.com/novox/mesh-host/internal/system" ) @@ -67,6 +68,10 @@ type Outcome struct { // Report is what an apply did, in the order it did it. type Report struct { Outcomes []Outcome `json:"outcomes"` + // Firewall is what this apply did about the firewall a converged machine was found with, when + // it did or declined anything: retired, retired again, or left in force and why (novox/hq ADR + // 0168). Said rather than an outcome: the plan says the same step the same way. + Firewall string `json:"firewall,omitempty"` // Tunnel is what this apply says about the tunnel the private network took over, when the // declaration names one (novox/hq ADR 0105). Tunnel *TakenTunnel `json:"tunnel,omitempty"` @@ -611,16 +616,24 @@ func ApplyKeeping( } // A converged node whose found firewall was in force retires it only now, once everything — - // the mesh's derived filter among it — applied cleanly (novox/hq ADR 0100). + // the mesh's derived filter among it — applied cleanly (novox/hq ADR 0100), and on every + // converged apply, not once (ADR 0168). Skipped, it is said: a step that does nothing is never + // silent (issue 143). if len(failures) == 0 { - if err := retireFirewall(ctx, d, origin, &known, run, log); err != nil { + did, err := retireFirewall(ctx, d, origin, &known, run, log) + if err != nil { return report, known, &Error{Resource: "the firewall found on this machine", Err: err, Done: report} } + report.Firewall = did for _, orphan := range protecting { if err := removeOrphan(orphan); err != nil { return report, known, err } } + } else if rec := known.Firewall; origin == store.OriginDeclared && d.Adoption == nil && rec != nil && + rec.Kind == string(firewall.UFW) && rec.WasActive && firewall.Active(ctx, run) { + report.Firewall = fmt.Sprintf("left in force: %d resource(s) failed, and the found firewall is retired only after a clean apply", len(failures)) + log(" kept ufw in force: " + report.Firewall) } if len(failures) > 0 { diff --git a/internal/apply/opening.go b/internal/apply/opening.go index 216fc7c..0390c8e 100644 --- a/internal/apply/opening.go +++ b/internal/apply/opening.go @@ -54,20 +54,43 @@ func foundFirewall(ctx context.Context, d *declaration.Declaration, known *store return kind, nil } -// retireFirewall disables the found firewall once a converged declaration has applied cleanly, -// which is when the mesh's derived filter has taken its place. Disabled, never flushed: its -// configuration stays on disk for a return to adopted, and the container runtime's rules are not -// its to take. +// retireFirewall keeps the found firewall retired on a converged machine (novox/hq ADR 0100, ADR +// 0168): disabled, never flushed, its configuration left on disk for a return to adopted, and the +// container runtime's rules not its to take. +// +// **Convergence is a state the host keeps, not a step it takes once.** Every converged apply reads +// whether the front end is in force; enabled again by a package, a boot or a hand, it is retired +// again and said. The record says how it came to be inactive — the mesh disabled it, or a reconcile +// found it so — and the two are never confused: a flip that did not take, followed by a hand that +// did, used to be recorded as the mesh's doing (issue 143). // // Only a declaration from the mesh converges a node. A carried bundle never says a node is adopted // — it cannot — so its silence is not the controller's word that the node was converged, and an // adopted node re-applying its bundle keeps the firewall it was found with. +// +// Returned is what this apply did about the found firewall, for the report; empty when the machine +// has none or is not converged. func retireFirewall(ctx context.Context, d *declaration.Declaration, origin string, known *store.State, - run Runner, log func(string)) error { + run Runner, log func(string)) (string, error) { rec := known.Firewall - if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive || - rec.DisabledByMesh { - return nil + if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive { + return "", nil + } + active := firewall.Active(ctx, run) + if !active && !(rec.Forward != nil && !rec.DisabledByMesh) { + // Inactive, and either the mesh's doing already or nobody's recorded here: said as found, + // never as done (issue 143's second fault). A retirement the mesh began and did not finish — + // the forward policy recorded, ufw down, the restore failed — is the one inactive state that + // is still the mesh's to complete, below. + if rec.RetiredBy == "" { + if rec.DisabledByMesh { + rec.RetiredBy = firewall.RetiredByMesh + } else { + rec.RetiredBy = firewall.RetiredFoundSo + log(" ufw is inactive on this converged node, and not by the mesh; recorded as found so") + } + } + return "", nil } // **Nothing is retired until what replaces it is in force** (novox/hq ADR 0100). The flip // loads the mesh's derived filter in ufw's place; disabling ufw before that table is actually @@ -75,10 +98,10 @@ func retireFirewall(ctx context.Context, d *declaration.Declaration, origin stri // no filter at all. loaded, err := firewall.MeshTableLoaded(ctx, run) if err != nil { - return err + return "", err } if !loaded { - return fmt.Errorf("this node is converged and the mesh's own filter (table %s) is not loaded on "+ + return "", fmt.Errorf("this node is converged and the mesh's own filter (table %s) is not loaded on "+ "this machine, so ufw was left in force: retiring it would leave the machine filtering "+ "nothing. Assign a filter module to this node, or return it to adopted", firewall.MeshTable) } @@ -88,11 +111,17 @@ func retireFirewall(ctx context.Context, d *declaration.Declaration, origin stri rec.Forward = firewall.ForwardPolicies(ctx, run) } if err := firewall.Disable(ctx, run, rec.Forward); err != nil { - return err + return "", err } + again := rec.DisabledByMesh || rec.RetiredBy != "" rec.DisabledByMesh = true + rec.RetiredBy = firewall.RetiredByMesh + if again { + log(" disabled ufw again: it had been enabled since the mesh retired it; this node is converged and filtered by the mesh") + return "disabled again: ufw had been enabled since the mesh retired it", nil + } log(" disabled ufw: this node is converged and filtered by the mesh; ufw's configuration is left on disk") - return nil + return "disabled: this node is converged and filtered by the mesh; ufw's configuration is left on disk", nil } // applyOpening makes one opening true through the firewall found here. diff --git a/internal/apply/opening_test.go b/internal/apply/opening_test.go index 15a3a90..4f6783e 100644 --- a/internal/apply/opening_test.go +++ b/internal/apply/opening_test.go @@ -189,13 +189,33 @@ func TestConvergingRetiresTheFoundFirewallAndReturningRestoresIt(t *testing.T) { } } - // Converged again: nothing more to retire. + // Converged again: nothing more to retire — the node asks ufw whether it is in force, which is + // what keeps convergence a state rather than a step taken once (novox/hq ADR 0168), and touches + // nothing else. u.asked = nil if _, state, err = applyWith(t, converged, state, u.run); err != nil { t.Fatal(err) } - if u.index("ufw") >= 0 { - t.Errorf("a converged node kept talking to a retired ufw: %v", u.asked) + for _, a := range u.asked { + if strings.HasPrefix(a, "ufw") && a != "ufw status" { + t.Errorf("a converged node kept talking to a retired ufw: %v", u.asked) + } + } + if state.Firewall.RetiredBy != "mesh" { + t.Errorf("the record does not say the mesh retired it: %+v", state.Firewall) + } + // Enabled again by a hand: retired again, and said. + u.active = true + u.asked = nil + report, state, err := applyWith(t, converged, state, u.run) + if err != nil { + t.Fatal(err) + } + if u.active || u.index("ufw disable") < 0 { + t.Fatalf("ufw enabled again on a converged node was not retired again: %v", u.asked) + } + if !strings.Contains(report.Firewall, "disabled again") { + t.Errorf("retiring it again was not said: %q", report.Firewall) } // Returned to adopted: ufw is enabled before the opening is converged through it. diff --git a/internal/firewall/filters.go b/internal/firewall/filters.go new file mode 100644 index 0000000..8b564fa --- /dev/null +++ b/internal/firewall/filters.go @@ -0,0 +1,326 @@ +package firewall + +import ( + "context" + "fmt" + "regexp" + "sort" + "strings" +) + +// What filters a machine, said with an owner (novox/hq ADR 0168). +// +// "The firewall found" names one front end, and a machine carries rules from several sources: the +// front end's own, the container runtime's plumbing, a ban list, the mesh's own tables, and whatever +// a predecessor installed directly — on both machines of the first mesh, in the user chain the +// runtime leaves for an administrator, where the mesh's reader of rules counted it as the runtime's. +// So the host reports every table and chain that refuses traffic, each with whose it is, and the +// mesh says truthfully what filters a converged machine. It removes none of it. + +// Owners of a refusal. +const ( + // OwnerMesh is the mesh's own tables: the derived filter and the guard. + OwnerMesh = "mesh" + // OwnerFoundFirewall is the front end found on the machine — ufw's chains. + OwnerFoundFirewall = "found-firewall" + // OwnerRuntime is the container runtime's own plumbing: its chains, the forward policy it sets + // when it turns forwarding on, its guard against reaching a container's address from off its + // bridge. Not the user chain it leaves for an administrator. + OwnerRuntime = "runtime" + // OwnerBan is a refusal that names the sources it refuses, in a chain that accepts nothing — a + // ban list, which is not a firewall. + OwnerBan = "ban" + // OwnerOther is everything else: rules the mesh did not write and cannot attribute. Where a + // predecessor's rules live. + OwnerOther = "other" +) + +// A Filter is one place on the machine that refuses traffic: a chain of a table, or a chain of the +// legacy filter, with its owner and what it refuses in one line. +type Filter struct { + // Where names the chain: "table ip filter, chain DOCKER-USER", or "chain HAL-MESH-ONLY + // (iptables-legacy)". + Where string `json:"where"` + // Owner is one of the owners above. + Owner string `json:"owner"` + // Refuses is the first refusing line, counters stripped, and how many more there are. + Refuses string `json:"refuses"` + + table, chain string +} + +// userChain is the chain the container runtime creates empty and leaves for an administrator's +// rules, consulted before its own forwarding. Nothing in it is the runtime's. +const userChain = "DOCKER-USER" + +// Filters classifies every refusing chain of an `nft list ruleset` and of the legacy filter's `-S` +// listings (by tool: iptables-legacy, ip6tables-legacy), in the order they appear. +func Filters(ruleset string, legacy map[string]string, ufwActive bool) []Filter { + var out []Filter + r := parseNft(ruleset) + refusing := map[string][]nftRule{} // by "table\x00chain" + for _, rule := range r.refusals { + k := rule.table + "\x00" + rule.chain + refusing[k] = append(refusing[k], rule) + } + for _, k := range r.chainOrder { + c := r.chains[k] + table, chain, _ := strings.Cut(k, "\x00") + rules := refusing[k] + if !c.dropping && len(rules) == 0 { + continue + } + f := Filter{table: table, chain: chain, Where: "table " + table + ", chain " + chain} + switch { + case table == MeshTable || table == "inet mesh_guard": + f.Owner = OwnerMesh + case strings.HasPrefix(chain, "ufw"): + f.Owner = OwnerFoundFirewall + if !ufwActive { + // Left behind by a retired front end, and still refusing: not ufw's any more in + // any sense that matters, since nothing maintains it. + f.Owner = OwnerOther + } + case chain == userChain: + f.Owner = OwnerOther + case c.dropping && (r.managed[table] || iptablesTable(table)) && runtimes(table, chain, c.policyLine): + f.Owner = OwnerRuntime + case len(rules) > 0 && (r.managed[table] || iptablesTable(table)) && allRuntimes(table, chain, rules): + f.Owner = OwnerRuntime + case len(rules) > 0 && allBans(r, rules): + f.Owner = OwnerBan + case c.dropping && !iptablesTable(table) && !r.managed[table] && len(rules) == 0: + // A table of its own whose base chain drops by policy: a firewall nobody declared. + f.Owner = OwnerOther + default: + f.Owner = OwnerOther + } + if ufwActive && (r.managed[table] || iptablesTable(table)) && f.Owner == OwnerOther && len(rules) == 0 && c.dropping { + // A base chain ufw set to drop while it is in force is ufw's. + f.Owner = OwnerFoundFirewall + } + f.Refuses = refusesLine(c, rules) + out = append(out, f) + } + tools := make([]string, 0, len(legacy)) + for tool := range legacy { + tools = append(tools, tool) + } + sort.Strings(tools) + for _, tool := range tools { + out = append(out, legacyFilters(legacy[tool], tool, ufwActive)...) + } + return out +} + +// allRuntimes is whether every refusal in a chain is the runtime's own. +func allRuntimes(table, chain string, rules []nftRule) bool { + for _, rule := range rules { + if !runtimes(table, chain, rule.line) { + return false + } + } + return true +} + +// allBans is whether every refusal in a chain only bans the sources it names. +func allBans(r *nftRuleset, rules []nftRule) bool { + for _, rule := range rules { + if !r.onlyBans(rule) { + return false + } + } + return true +} + +var counters = regexp.MustCompile(`\s*counter packets \d+ bytes \d+`) + +// refusesLine is one line a person reads: the policy when the chain drops by policy, else the first +// refusing rule with its counters stripped, and how many more there are. +func refusesLine(c *nftChain, rules []nftRule) string { + var parts []string + if c.dropping { + parts = append(parts, "policy drop") + } + if len(rules) > 0 { + line := strings.TrimSpace(counters.ReplaceAllString(rules[0].line, "")) + if len(rules) > 1 { + line += fmt.Sprintf(" (and %d more)", len(rules)-1) + } + parts = append(parts, line) + } + return strings.Join(parts, "; ") +} + +// legacyFilters classifies the chains of an `iptables-legacy -S` listing that refuse. +func legacyFilters(rules, tool string, ufwActive bool) []Filter { + policy := map[string]string{} + accepting := map[string]bool{} + jumpedFrom := map[string][]string{} + for _, line := range strings.Split(rules, "\n") { + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + switch fields[0] { + case "-P": + policy[fields[1]] = fields[2] + case "-A": + for i, f := range fields { + if (f == "-j" || f == "-g") && i+1 < len(fields) { + switch fields[i+1] { + case "ACCEPT": + accepting[fields[1]] = true + case "DROP", "REJECT", "RETURN", "LOG": + default: + jumpedFrom[fields[i+1]] = append(jumpedFrom[fields[i+1]], fields[1]) + } + } + } + } + } + var entered func(chain string, seen map[string]bool) bool + entered = func(chain string, seen map[string]bool) bool { + if seen[chain] || accepting[chain] || len(jumpedFrom[chain]) == 0 { + return false + } + seen[chain] = true + for _, from := range jumpedFrom[chain] { + if p, builtIn := policy[from]; builtIn { + if p != "ACCEPT" { + return false + } + continue + } + if !entered(from, seen) { + return false + } + } + return true + } + ban := func(chain, line string) bool { + return bansSources(line) && entered(chain, map[string]bool{}) + } + type seen struct { + owner string + lines []string + } + chains := map[string]*seen{} + var order []string + note := func(chain, owner, line string) { + s := chains[chain] + if s == nil { + s = &seen{owner: owner} + chains[chain] = s + order = append(order, chain) + } + if owner == OwnerOther || s.owner == "" { + s.owner = owner + } + s.lines = append(s.lines, line) + } + for _, line := range strings.Split(rules, "\n") { + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + chain := fields[1] + switch fields[0] { + case "-P": + if fields[2] != "DROP" { + continue + } + owner := OwnerOther + if chain == "FORWARD" { + owner = OwnerRuntime + } + if ufwActive { + owner = OwnerFoundFirewall + } + note(chain, owner, "policy DROP") + case "-A": + refuses := false + for i, f := range fields { + if f == "-j" && i+1 < len(fields) && (fields[i+1] == "DROP" || fields[i+1] == "REJECT") { + refuses = true + } + } + if !refuses { + continue + } + owner := OwnerOther + switch { + case strings.HasPrefix(chain, "ufw"): + owner = OwnerFoundFirewall + if !ufwActive { + owner = OwnerOther + } + case chain != userChain && strings.HasPrefix(chain, "DOCKER"): + owner = OwnerRuntime + case ban(chain, line): + owner = OwnerBan + } + note(chain, owner, strings.TrimSpace(line)) + } + } + var out []Filter + for _, chain := range order { + s := chains[chain] + refuses := s.lines[0] + if len(s.lines) > 1 { + refuses += fmt.Sprintf(" (and %d more)", len(s.lines)-1) + } + out = append(out, Filter{Where: "chain " + chain + " (" + tool + ")", Owner: s.owner, Refuses: refuses}) + } + return out +} + +// Collect reads what filters this machine now: its nftables ruleset and, where the legacy tools +// exist, their listings. A machine without nft is read through iptables, as Detect reads it. +func Collect(ctx context.Context, run Runner, ufwActive bool) ([]Filter, error) { + ruleset := "" + noNft := false + out, err := run(ctx, "nft", "list", "ruleset") + switch { + case err == nil: + ruleset = out + case missing(err): + noNft = true + default: + return nil, fmt.Errorf("cannot read this machine's packet filter: %w", err) + } + legacy := map[string]string{} + tools := []string{"iptables-legacy", "ip6tables-legacy"} + if noNft { + tools = append(tools, "iptables", "ip6tables") + } + for _, tool := range tools { + if out, err := run(ctx, tool, "-S"); err == nil && strings.TrimSpace(out) != "" { + legacy[tool] = out + } + } + return Filters(ruleset, legacy, ufwActive), nil +} + +// Alone is whether a machine is filtered by the mesh alone: nothing in the list but the mesh's +// own tables, the runtime's plumbing and bans (novox/hq ADR 0168). +func Alone(filters []Filter) bool { + for _, f := range filters { + if f.Owner == OwnerOther || f.Owner == OwnerFoundFirewall { + return false + } + } + return true +} + +// Active says whether ufw is in force on this machine now. A machine without ufw is not. +func Active(ctx context.Context, run Runner) bool { + out, err := run(ctx, "ufw", "status") + return err == nil && statusActive(out) +} + +// Retirements of a found firewall, as the host records them. +const ( + RetiredByMesh = "mesh" + RetiredFoundSo = "found-inactive" +) diff --git a/internal/firewall/filters_test.go b/internal/firewall/filters_test.go new file mode 100644 index 0000000..79de063 --- /dev/null +++ b/internal/firewall/filters_test.go @@ -0,0 +1,130 @@ +package firewall + +import ( + "os" + "strings" + "testing" +) + +func fixture(t *testing.T, name string) string { + t.Helper() + raw, err := os.ReadFile("testdata/" + name) + if err != nil { + t.Fatal(err) + } + return string(raw) +} + +func ownerOf(filters []Filter, where string) string { + for _, f := range filters { + if f.Where == where { + return f.Owner + } + } + return "(not reported)" +} + +// Every refusing table and chain is classified with an owner (novox/hq ADR 0168), over rulesets +// captured from three machines of the first mesh. The control node: a ban list reached through the +// runtime's user chain is a ban; a refusal left in that chain, and a chain a retired front end left +// behind, are *other*; the runtime's own and the mesh's own are theirs. +func TestTheControlNodesRefusalsAreClassified(t *testing.T) { + got := Filters(fixture(t, "control-node.nft"), nil, false) + for where, want := range map[string]string{ + "table ip filter, chain f2b-recidive": OwnerBan, + "table ip filter, chain DOCKER": OwnerRuntime, + "table ip raw, chain PREROUTING": OwnerRuntime, + "table inet mesh, chain input": OwnerMesh, + "table inet mesh, chain forward": OwnerMesh, + "table ip6 filter, chain DOCKER-USER": OwnerOther, + "table ip6 filter, chain ufw6-docker-logging-deny": OwnerOther, + } { + if o := ownerOf(got, where); o != want { + t.Errorf("%s: %s, want %s", where, o, want) + } + } + if Alone(got) { + t.Error("a machine with a refusal in the runtime's user chain reads as filtered by the mesh alone") + } + // What refuses adoption does not move (rule 4): the user chain's refusals are reported, not + // refused. The chain a retired front end left behind, still dropping, is what it always was + // to Detect — a refusal nobody speaks for, in one table. + if refusing := Refusing(fixture(t, "control-node.nft"), false); len(refusing) != 1 || refusing[0] != "table ip6 filter" { + t.Errorf("adoption's threshold moved: %v", refusing) + } + // The counters are stripped from what a person reads. + for _, f := range got { + if strings.Contains(f.Refuses, "counter packets") { + t.Errorf("counters in the line: %s", f.Refuses) + } + } +} + +// The laptop: the runtime's forward policy and bridge guards, a virtualisation host and an endpoint +// agent that refuse nothing, and the mesh — filtered by the mesh alone. +func TestTheLaptopIsFilteredByTheMeshAlone(t *testing.T) { + got := Filters(fixture(t, "laptop.nft"), nil, false) + for where, want := range map[string]string{ + "table ip filter, chain FORWARD": OwnerRuntime, + "table ip filter, chain DOCKER": OwnerRuntime, + "table ip raw, chain PREROUTING": OwnerRuntime, + "table inet mesh, chain input": OwnerMesh, + } { + if o := ownerOf(got, where); o != want { + t.Errorf("%s: %s, want %s", where, o, want) + } + } + for _, f := range got { + if strings.Contains(f.Where, "incus") || strings.Contains(f.Where, "fct_") { + t.Errorf("a table that refuses nothing is reported: %+v", f) + } + } + if !Alone(got) { + t.Errorf("the laptop is not read as filtered by the mesh alone: %+v", got) + } +} + +// The home server: its rules are in the legacy filter, where a predecessor's chain still drops what +// arrives on the outward link for the forwarded path — invisible to the mesh until now (issue 144). +func TestThePredecessorsChainInTheLegacyFilterIsOther(t *testing.T) { + mesh := "table inet mesh {\n\tchain forward {\n\t\ttype filter hook forward priority filter; policy drop;\n\t}\n}\n" + got := Filters(mesh, map[string]string{"iptables-legacy": fixture(t, "home-server-legacy-S.txt")}, false) + for where, want := range map[string]string{ + "table inet mesh, chain forward": OwnerMesh, + "chain FORWARD (iptables-legacy)": OwnerRuntime, + "chain DOCKER (iptables-legacy)": OwnerRuntime, + "chain HAL-MESH-ONLY (iptables-legacy)": OwnerOther, + } { + if o := ownerOf(got, where); o != want { + t.Errorf("%s: %s, want %s", where, o, want) + } + } + var other Filter + for _, f := range got { + if f.Owner == OwnerOther { + other = f + } + } + if !strings.Contains(other.Refuses, "-j DROP") { + t.Errorf("what the predecessor's chain refuses is not said: %+v", other) + } + if Alone(got) { + t.Error("a machine with a predecessor's chain reads as filtered by the mesh alone") + } +} + +// With the front end in force, its chains are its own; retired, a chain it left behind that still +// refuses is nobody's and said so. +func TestAFrontEndsChainsAreItsWhileItIsInForce(t *testing.T) { + ruleset := dockerOnly(t) + ufwChains + for _, f := range Filters(ruleset, nil, true) { + if strings.Contains(f.Where, "ufw") && f.Owner != OwnerFoundFirewall { + t.Errorf("active: %+v", f) + } + } + for _, f := range Filters(ruleset, nil, false) { + if strings.Contains(f.Where, "ufw") && f.Owner != OwnerOther { + t.Errorf("retired: %+v", f) + } + } +} diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index 37b9052..e178b3c 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -128,42 +128,82 @@ func statusActive(out string) bool { // mesh needs, so a refusal that names the sources it refuses, in a table or a chain that accepts // nothing and is entered only from chains whose policy accepts, is not counted. func Refusing(ruleset string, ufwActive bool) []string { - type rule struct{ table, chain, line string } - type chainOf struct { - base, dropping, accepts bool - policyLine string - jumpedFrom []string - } - chains := map[string]*chainOf{} // by "table\x00chain" - tableAccepts := map[string]bool{} - var tables []string - var refusals []rule - managed := map[string]bool{} - var table, chain string - get := func(t, c string) *chainOf { - k := t + "\x00" + c - if chains[k] == nil { - chains[k] = &chainOf{} + var refusing []string + for _, f := range Filters(ruleset, nil, ufwActive) { + if f.Owner != OwnerOther || f.chain == userChain { + // A refusal in the runtime's user chain is reported as *other* and does not refuse + // adoption (novox/hq ADR 0168, rule 4): both predecessors kept their rules there. + continue + } + name := "table " + f.table + if len(refusing) == 0 || refusing[len(refusing)-1] != name { + if !contains(refusing, name) { + refusing = append(refusing, name) + } } - return chains[k] } + return refusing +} + +func contains(list []string, s string) bool { + for _, x := range list { + if x == s { + return true + } + } + return false +} + +// nftRule is one line of a ruleset that refuses, with where it is. +type nftRule struct{ table, chain, line string } + +// nftChain is what a parse knows about one chain. +type nftChain struct { + base, dropping, accepts bool + policyLine string + jumpedFrom []string +} + +// nftRuleset is `nft list ruleset`, read: its tables in order, its chains, every refusing line, +// and which tables iptables-nft manages. +type nftRuleset struct { + tables []string + chains map[string]*nftChain // by "table\x00chain" + chainOrder []string + tableAccepts map[string]bool + refusals []nftRule + managed map[string]bool +} + +func (r *nftRuleset) get(t, c string) *nftChain { + k := t + "\x00" + c + if r.chains[k] == nil { + r.chains[k] = &nftChain{} + r.chainOrder = append(r.chainOrder, k) + } + return r.chains[k] +} + +func parseNft(ruleset string) *nftRuleset { + r := &nftRuleset{chains: map[string]*nftChain{}, tableAccepts: map[string]bool{}, managed: map[string]bool{}} + var table, chain string for _, raw := range strings.Split(ruleset, "\n") { line := strings.TrimSpace(raw) switch { case strings.HasPrefix(line, "# Warning: table ") && strings.Contains(line, "managed by iptables-nft"): name := strings.TrimPrefix(line, "# Warning: table ") name, _, _ = strings.Cut(name, " is managed") - managed[name] = true + r.managed[name] = true continue case strings.HasPrefix(line, "table "): table = strings.TrimSuffix(strings.TrimSpace(strings.TrimPrefix(line, "table ")), "{") table = strings.TrimSpace(table) - tables = append(tables, table) + r.tables = append(r.tables, table) chain = "" continue case strings.HasPrefix(line, "chain "): chain = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "chain "), "{")) - get(table, chain) + r.get(table, chain) continue case strings.HasPrefix(line, "set ") || strings.HasPrefix(line, "map ") || strings.HasPrefix(line, "flowtable "): @@ -172,7 +212,7 @@ func Refusing(ruleset string, ufwActive bool) []string { case line == "" || line == "}" || strings.HasPrefix(line, "#") || chain == "": continue } - c := get(table, chain) + c := r.get(table, chain) if strings.HasPrefix(line, "type ") { c.base = true c.policyLine = line @@ -183,85 +223,66 @@ func Refusing(ruleset string, ufwActive bool) []string { if i := strings.Index(line, verb); i >= 0 { target := strings.Fields(line[i+len(verb):]) if len(target) > 0 { - get(table, target[0]).jumpedFrom = append(get(table, target[0]).jumpedFrom, chain) + r.get(table, target[0]).jumpedFrom = append(r.get(table, target[0]).jumpedFrom, chain) } } } if accepts(line) { c.accepts = true - tableAccepts[table] = true + r.tableAccepts[table] = true } if verdictRefuses(line) { - refusals = append(refusals, rule{table, chain, line}) + r.refusals = append(r.refusals, nftRule{table, chain, line}) } } + return r +} - skipped := func(table string) bool { - if table == "inet mesh" || table == "inet mesh_guard" { - return true - } - return (managed[table] || iptablesTable(table)) && ufwActive +// onlyBans is whether a refusal only refuses the sources it names: in a table that accepts nothing +// and whose base chains all accept by default, or in a chain that accepts nothing and is entered +// only from base chains that accept by default. +func (r *nftRuleset) onlyBans(rule nftRule) bool { + if !bansSources(rule.line) { + return false } - // onlyBans is whether a refusal only refuses the sources it names: in a table that accepts - // nothing and whose base chains all accept by default, or in a chain that accepts nothing and - // is entered only from base chains that accept by default. - onlyBans := func(r rule) bool { - if !bansSources(r.line) { - return false - } - allAccepting := true - for k, c := range chains { - if strings.HasPrefix(k, r.table+"\x00") && c.base && !strings.Contains(c.policyLine, "policy accept") { - allAccepting = false - } - } - if !tableAccepts[r.table] && allAccepting { - return true - } - c := get(r.table, r.chain) - if c.base || c.accepts || len(c.jumpedFrom) == 0 { - return false - } - for _, from := range c.jumpedFrom { - caller := get(r.table, from) - if !caller.base || !strings.Contains(caller.policyLine, "policy accept") { - return false - } + allAccepting := true + for k, c := range r.chains { + if strings.HasPrefix(k, rule.table+"\x00") && c.base && !strings.Contains(c.policyLine, "policy accept") { + allAccepting = false } + } + if !r.tableAccepts[rule.table] && allAccepting { return true } + return r.enteredAccepting(rule.table, rule.chain, map[string]bool{}) +} - counted := map[string]bool{} - for k, c := range chains { - t, name, _ := strings.Cut(k, "\x00") - if skipped(t) || !c.dropping { - continue - } - if (managed[t] || iptablesTable(t)) && runtimes(t, name, c.policyLine) { - continue - } - counted[t] = true +// enteredAccepting is whether a chain accepts nothing and is entered only through chains that +// accept by default — base chains whose policy accepts, or chains that are themselves entered that +// way and accept nothing. A ban list jumped to from the runtime's user chain, which the forward +// chain enters with an accepting policy, is still a ban list. +func (r *nftRuleset) enteredAccepting(table, chain string, seen map[string]bool) bool { + if seen[chain] { + return false } - for _, r := range refusals { - if skipped(r.table) || counted[r.table] { - continue - } - if (managed[r.table] || iptablesTable(r.table)) && runtimes(r.table, r.chain, r.line) { - continue - } - if onlyBans(r) { - continue - } - counted[r.table] = true + seen[chain] = true + c := r.get(table, chain) + if c.base || c.accepts || len(c.jumpedFrom) == 0 { + return false } - var refusing []string - for _, t := range tables { - if counted[t] { - counted[t] = false - refusing = append(refusing, "table "+t) + for _, from := range c.jumpedFrom { + caller := r.get(table, from) + if caller.base { + if !strings.Contains(caller.policyLine, "policy accept") { + return false + } + continue + } + if caller.accepts || !r.enteredAccepting(table, from, seen) { + return false } } - return refusing + return true } // iptablesTable is whether a table is one iptables-nft writes. Named rather than read from the diff --git a/internal/firewall/testdata/control-node.nft b/internal/firewall/testdata/control-node.nft new file mode 100644 index 0000000..8a11774 --- /dev/null +++ b/internal/firewall/testdata/control-node.nft @@ -0,0 +1,588 @@ +# Warning: table ip filter is managed by iptables-nft, do not touch! +table ip filter { + chain INPUT { + type filter hook input priority filter; policy accept; + ip protocol tcp counter packets 945757787 bytes 1737008792038 jump f2b-sshd + ip protocol tcp counter packets 945756610 bytes 1737008898620 jump f2b-recidive + counter packets 2862213204 bytes 3144751431654 jump ufw-before-logging-input + counter packets 2862213204 bytes 3144751431654 jump ufw-before-input + counter packets 989333889 bytes 1776344988272 jump ufw-after-input + counter packets 989303248 bytes 1776343408920 jump ufw-after-logging-input + counter packets 989303248 bytes 1776343408920 jump ufw-reject-input + counter packets 989303248 bytes 1776343408920 jump ufw-track-input + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + oifname "mesh0" counter packets 1613103 bytes 2577614868 accept + iifname "mesh0" counter packets 995195 bytes 84526284 accept + counter packets 20454697 bytes 11504107676 jump DOCKER-USER + counter packets 20442192 bytes 11503368404 jump DOCKER-FORWARD + counter packets 12438285 bytes 10907281833 jump ufw-before-logging-forward + counter packets 12438285 bytes 10907281833 jump ufw-before-forward + counter packets 384 bytes 39643 jump ufw-after-forward + counter packets 384 bytes 39643 jump ufw-after-logging-forward + counter packets 384 bytes 39643 jump ufw-reject-forward + counter packets 384 bytes 39643 jump ufw-track-forward + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + counter packets 3195070897 bytes 3951725261199 jump ufw-before-logging-output + counter packets 3195070897 bytes 3951725261199 jump ufw-before-output + counter packets 945745931 bytes 1778546547406 jump ufw-after-output + counter packets 945745931 bytes 1778546547406 jump ufw-after-logging-output + counter packets 945745931 bytes 1778546547406 jump ufw-reject-output + counter packets 945745931 bytes 1778546547406 jump ufw-track-output + } + + chain DOCKER-FORWARD { + counter packets 20442192 bytes 11503368404 jump DOCKER-CT + counter packets 8079126 bytes 1230017985 jump DOCKER-INTERNAL + counter packets 8079126 bytes 1230017985 jump DOCKER-BRIDGE + iifname "br-cadedce55fe9" counter packets 0 bytes 0 accept + iifname "br-dd007c7e67bc" counter packets 0 bytes 0 accept + iifname "br-a5fbc29c2c2a" counter packets 0 bytes 0 accept + iifname "br-6eb1e7f7f847" counter packets 0 bytes 0 accept + iifname "br-8ce143481a5b" counter packets 14700 bytes 2493600 accept + iifname "br-84e7d0cfeada" counter packets 0 bytes 0 accept + iifname "br-f8b083119d99" counter packets 264 bytes 57438 accept + iifname "br-0d1490cc67c9" counter packets 732468 bytes 351624109 accept + iifname "br-3b338a381229" counter packets 137 bytes 11876 accept + iifname "br-3008d408e73a" counter packets 25380 bytes 1564417 accept + iifname "br-ca07a9577a7f" counter packets 0 bytes 0 accept + iifname "docker0" counter packets 6695791 bytes 795364128 accept + iifname "br-a63fa64a9e18" counter packets 0 bytes 0 accept + iifname "br-1ccb887b3344" counter packets 237174 bytes 36804979 accept + iifname "br-9fd22324ec08" counter packets 0 bytes 0 accept + iifname "br-73641cceafc3" counter packets 36 bytes 6614 accept + iifname "br-77eb8a9e2ba1" counter packets 0 bytes 0 accept + iifname "br-e99ce5248c84" counter packets 0 bytes 0 accept + iifname "br-e5d78502832d" counter packets 0 bytes 0 accept + iifname "br-2e4a76a7cd2e" counter packets 20339 bytes 1799711 accept + iifname "br-72fd626a8ff7" counter packets 0 bytes 0 accept + } + + chain DOCKER-USER { + ip protocol tcp counter packets 3054221 bytes 3329963005 jump f2b-sshd + ip protocol tcp counter packets 3054221 bytes 3329963005 jump f2b-recidive + counter packets 1421378091 bytes 2045004412819 return + } + + chain ufw-before-logging-input { + } + + chain ufw-before-logging-output { + } + + chain ufw-before-logging-forward { + } + + chain ufw-before-input { + } + + chain ufw-before-output { + } + + chain ufw-before-forward { + } + + chain ufw-after-input { + } + + chain ufw-after-output { + } + + chain ufw-after-forward { + } + + chain ufw-after-logging-input { + } + + chain ufw-after-logging-output { + } + + chain ufw-after-logging-forward { + } + + chain ufw-reject-input { + } + + chain ufw-reject-output { + } + + chain ufw-reject-forward { + } + + chain ufw-track-input { + } + + chain ufw-track-output { + } + + chain ufw-track-forward { + } + + chain DOCKER { + ip daddr 172.17.0.7 iifname != "docker0" oifname "docker0" tcp dport 5432 counter packets 8 bytes 480 accept + ip daddr 172.19.0.2 iifname != "br-72fd626a8ff7" oifname "br-72fd626a8ff7" tcp dport 8080 counter packets 0 bytes 0 accept + ip daddr 172.17.0.6 iifname != "docker0" oifname "docker0" tcp dport 9443 counter packets 0 bytes 0 accept + ip daddr 172.17.0.6 iifname != "docker0" oifname "docker0" tcp dport 9000 counter packets 0 bytes 0 accept + ip daddr 192.168.176.2 iifname != "br-f8b083119d99" oifname "br-f8b083119d99" tcp dport 9001 counter packets 0 bytes 0 accept + ip daddr 192.168.176.2 iifname != "br-f8b083119d99" oifname "br-f8b083119d99" tcp dport 9000 counter packets 47769 bytes 2866140 accept + ip daddr 172.20.0.2 iifname != "br-6eb1e7f7f847" oifname "br-6eb1e7f7f847" tcp dport 8080 counter packets 0 bytes 0 accept + ip daddr 172.27.0.2 iifname != "br-3008d408e73a" oifname "br-3008d408e73a" tcp dport 3000 counter packets 0 bytes 0 accept + ip daddr 192.168.48.5 iifname != "br-77eb8a9e2ba1" oifname "br-77eb8a9e2ba1" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 192.168.48.4 iifname != "br-77eb8a9e2ba1" oifname "br-77eb8a9e2ba1" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 192.168.48.3 iifname != "br-77eb8a9e2ba1" oifname "br-77eb8a9e2ba1" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 192.168.48.2 iifname != "br-77eb8a9e2ba1" oifname "br-77eb8a9e2ba1" tcp dport 9000 counter packets 0 bytes 0 accept + ip daddr 172.18.0.2 iifname != "br-2e4a76a7cd2e" oifname "br-2e4a76a7cd2e" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 172.17.0.5 iifname != "docker0" oifname "docker0" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 172.17.0.3 iifname != "docker0" oifname "docker0" tcp dport 8222 counter packets 0 bytes 0 accept + ip daddr 172.17.0.3 iifname != "docker0" oifname "docker0" tcp dport 4222 counter packets 97 bytes 5744 accept + ip daddr 172.28.0.2 iifname != "br-8ce143481a5b" oifname "br-8ce143481a5b" tcp dport 1433 counter packets 0 bytes 0 accept + ip daddr 192.168.80.2 iifname != "br-e99ce5248c84" oifname "br-e99ce5248c84" tcp dport 8080 counter packets 0 bytes 0 accept + ip daddr 192.168.112.3 iifname != "br-e5d78502832d" oifname "br-e5d78502832d" tcp dport 9000 counter packets 0 bytes 0 accept + ip daddr 192.168.112.2 iifname != "br-e5d78502832d" oifname "br-e5d78502832d" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 192.168.128.2 iifname != "br-73641cceafc3" oifname "br-73641cceafc3" tcp dport 27017 counter packets 14 bytes 840 accept + ip daddr 192.168.208.2 iifname != "br-9fd22324ec08" oifname "br-9fd22324ec08" tcp dport 35621 counter packets 0 bytes 0 accept + ip daddr 192.168.203.13 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 4243 counter packets 0 bytes 0 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 995 counter packets 194 bytes 11000 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 993 counter packets 188 bytes 9394 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 587 counter packets 444 bytes 23312 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 465 counter packets 104 bytes 5852 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 443 counter packets 0 bytes 0 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 143 counter packets 443 bytes 25280 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 110 counter packets 192 bytes 9561 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 80 counter packets 0 bytes 0 accept + ip daddr 192.168.203.12 iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" tcp dport 25 counter packets 430 bytes 22919 accept + ip daddr 172.17.0.2 iifname != "docker0" oifname "docker0" tcp dport 3000 counter packets 0 bytes 0 accept + ip daddr 172.17.0.2 iifname != "docker0" oifname "docker0" tcp dport 22 counter packets 1578 bytes 93884 accept + ip daddr 172.17.0.4 iifname != "docker0" oifname "docker0" tcp dport 5000 counter packets 25 bytes 1492 accept + iifname != "br-cadedce55fe9" oifname "br-cadedce55fe9" counter packets 0 bytes 0 drop + iifname != "br-dd007c7e67bc" oifname "br-dd007c7e67bc" counter packets 0 bytes 0 drop + iifname != "br-a5fbc29c2c2a" oifname "br-a5fbc29c2c2a" counter packets 0 bytes 0 drop + iifname != "br-6eb1e7f7f847" oifname "br-6eb1e7f7f847" counter packets 0 bytes 0 drop + iifname != "br-8ce143481a5b" oifname "br-8ce143481a5b" counter packets 0 bytes 0 drop + iifname != "br-84e7d0cfeada" oifname "br-84e7d0cfeada" counter packets 0 bytes 0 drop + iifname != "br-f8b083119d99" oifname "br-f8b083119d99" counter packets 0 bytes 0 drop + iifname != "br-0d1490cc67c9" oifname "br-0d1490cc67c9" counter packets 0 bytes 0 drop + iifname != "br-3b338a381229" oifname "br-3b338a381229" counter packets 0 bytes 0 drop + iifname != "br-3008d408e73a" oifname "br-3008d408e73a" counter packets 0 bytes 0 drop + iifname != "br-ca07a9577a7f" oifname "br-ca07a9577a7f" counter packets 0 bytes 0 drop + iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop + iifname != "br-a63fa64a9e18" oifname "br-a63fa64a9e18" counter packets 0 bytes 0 drop + iifname != "br-1ccb887b3344" oifname "br-1ccb887b3344" counter packets 0 bytes 0 drop + iifname != "br-9fd22324ec08" oifname "br-9fd22324ec08" counter packets 0 bytes 0 drop + iifname != "br-73641cceafc3" oifname "br-73641cceafc3" counter packets 0 bytes 0 drop + iifname != "br-77eb8a9e2ba1" oifname "br-77eb8a9e2ba1" counter packets 0 bytes 0 drop + iifname != "br-e99ce5248c84" oifname "br-e99ce5248c84" counter packets 0 bytes 0 drop + iifname != "br-e5d78502832d" oifname "br-e5d78502832d" counter packets 0 bytes 0 drop + iifname != "br-2e4a76a7cd2e" oifname "br-2e4a76a7cd2e" counter packets 0 bytes 0 drop + iifname != "br-72fd626a8ff7" oifname "br-72fd626a8ff7" counter packets 0 bytes 0 drop + } + + chain DOCKER-BRIDGE { + oifname "br-cadedce55fe9" counter packets 0 bytes 0 jump DOCKER + oifname "br-dd007c7e67bc" counter packets 0 bytes 0 jump DOCKER + oifname "br-a5fbc29c2c2a" counter packets 0 bytes 0 jump DOCKER + oifname "br-6eb1e7f7f847" counter packets 799 bytes 47940 jump DOCKER + oifname "br-8ce143481a5b" counter packets 0 bytes 0 jump DOCKER + oifname "br-84e7d0cfeada" counter packets 0 bytes 0 jump DOCKER + oifname "br-f8b083119d99" counter packets 98911 bytes 5934660 jump DOCKER + oifname "br-0d1490cc67c9" counter packets 69740 bytes 4118476 jump DOCKER + oifname "br-3b338a381229" counter packets 32 bytes 1920 jump DOCKER + oifname "br-3008d408e73a" counter packets 458 bytes 27480 jump DOCKER + oifname "br-ca07a9577a7f" counter packets 0 bytes 0 jump DOCKER + oifname "docker0" counter packets 87073 bytes 5223529 jump DOCKER + oifname "br-a63fa64a9e18" counter packets 1353 bytes 81180 jump DOCKER + oifname "br-1ccb887b3344" counter packets 7662 bytes 419862 jump DOCKER + oifname "br-9fd22324ec08" counter packets 173 bytes 10380 jump DOCKER + oifname "br-73641cceafc3" counter packets 162 bytes 9720 jump DOCKER + oifname "br-77eb8a9e2ba1" counter packets 94 bytes 5640 jump DOCKER + oifname "br-e99ce5248c84" counter packets 8 bytes 480 jump DOCKER + oifname "br-e5d78502832d" counter packets 26 bytes 1560 jump DOCKER + oifname "br-2e4a76a7cd2e" counter packets 7 bytes 420 jump DOCKER + oifname "br-72fd626a8ff7" counter packets 0 bytes 0 jump DOCKER + } + + chain DOCKER-CT { + oifname "br-cadedce55fe9" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-dd007c7e67bc" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-a5fbc29c2c2a" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-6eb1e7f7f847" xt match "conntrack" counter packets 38458 bytes 6234236 accept + oifname "br-8ce143481a5b" xt match "conntrack" counter packets 60403 bytes 20478794 accept + oifname "br-84e7d0cfeada" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-f8b083119d99" xt match "conntrack" counter packets 1008024 bytes 206364436 accept + oifname "br-0d1490cc67c9" xt match "conntrack" counter packets 871134 bytes 1416174426 accept + oifname "br-3b338a381229" xt match "conntrack" counter packets 4649 bytes 2311375 accept + oifname "br-3008d408e73a" xt match "conntrack" counter packets 13415 bytes 1974731 accept + oifname "br-ca07a9577a7f" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "docker0" xt match "conntrack" counter packets 8909862 bytes 7892163419 accept + oifname "br-a63fa64a9e18" xt match "conntrack" counter packets 16688 bytes 6822829 accept + oifname "br-1ccb887b3344" xt match "conntrack" counter packets 461453 bytes 141913887 accept + oifname "br-9fd22324ec08" xt match "conntrack" counter packets 1677 bytes 427538 accept + oifname "br-73641cceafc3" xt match "conntrack" counter packets 417619 bytes 35343624 accept + oifname "br-77eb8a9e2ba1" xt match "conntrack" counter packets 125437 bytes 94155193 accept + oifname "br-e99ce5248c84" xt match "conntrack" counter packets 91 bytes 19173 accept + oifname "br-e5d78502832d" xt match "conntrack" counter packets 128653 bytes 40539569 accept + oifname "br-2e4a76a7cd2e" xt match "conntrack" counter packets 20257 bytes 158631592 accept + oifname "br-72fd626a8ff7" xt match "conntrack" counter packets 0 bytes 0 accept + } + + chain DOCKER-INTERNAL { + } + + chain f2b-recidive { + ip saddr 2.57.122.209 counter packets 0 bytes 0 xt target "REJECT" + ip saddr 2.57.122.76 counter packets 127 bytes 7600 xt target "REJECT" + ip saddr 195.178.110.228 counter packets 17 bytes 1000 xt target "REJECT" + ip saddr 2.57.122.74 counter packets 11 bytes 620 xt target "REJECT" + ip saddr 195.178.110.26 counter packets 56 bytes 3360 xt target "REJECT" + ip saddr 92.118.39.77 counter packets 2 bytes 80 xt target "REJECT" + ip saddr 92.118.39.71 counter packets 1 bytes 40 xt target "REJECT" + ip saddr 45.148.10.240 counter packets 0 bytes 0 xt target "REJECT" + ip saddr 195.178.110.30 counter packets 8 bytes 320 xt target "REJECT" + counter packets 948810608 bytes 1740338848565 return + } + + chain f2b-sshd { + counter packets 948810709 bytes 1740338655735 return + } +} +# Warning: table ip6 filter is managed by iptables-nft, do not touch! +table ip6 filter { + chain INPUT { + type filter hook input priority filter; policy accept; + counter packets 5426360 bytes 34419159588 jump ufw6-before-logging-input + counter packets 5426360 bytes 34419159588 jump ufw6-before-input + counter packets 367982 bytes 3415126642 jump ufw6-after-input + counter packets 367982 bytes 3415126642 jump ufw6-after-logging-input + counter packets 367982 bytes 3415126642 jump ufw6-reject-input + counter packets 367982 bytes 3415126642 jump ufw6-track-input + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + counter packets 0 bytes 0 jump DOCKER-USER + counter packets 0 bytes 0 jump DOCKER-FORWARD + counter packets 0 bytes 0 jump ufw6-before-logging-forward + counter packets 0 bytes 0 jump ufw6-before-forward + counter packets 0 bytes 0 jump ufw6-after-forward + counter packets 0 bytes 0 jump ufw6-after-logging-forward + counter packets 0 bytes 0 jump ufw6-reject-forward + counter packets 0 bytes 0 jump ufw6-track-forward + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + counter packets 6004354 bytes 1866587952 jump ufw6-before-logging-output + counter packets 6004354 bytes 1866587952 jump ufw6-before-output + counter packets 2241898 bytes 639173314 jump ufw6-after-output + counter packets 2241898 bytes 639173314 jump ufw6-after-logging-output + counter packets 2241898 bytes 639173314 jump ufw6-reject-output + counter packets 2241898 bytes 639173314 jump ufw6-track-output + } + + chain DOCKER-FORWARD { + counter packets 0 bytes 0 jump DOCKER-CT + counter packets 0 bytes 0 jump DOCKER-INTERNAL + counter packets 0 bytes 0 jump DOCKER-BRIDGE + } + + chain DOCKER-USER { + counter packets 0 bytes 0 jump ufw6-user-forward + xt match "conntrack" counter packets 0 bytes 0 return + xt match "conntrack" counter packets 0 bytes 0 drop + iifname "docker0" oifname "docker0" counter packets 0 bytes 0 accept + ip6 saddr fd00::/8 counter packets 0 bytes 0 return + ip6 daddr fd00::/8 xt match "conntrack" counter packets 0 bytes 0 jump ufw6-docker-logging-deny + counter packets 0 bytes 0 return + } + + chain ufw6-before-logging-input { + } + + chain ufw6-before-logging-output { + } + + chain ufw6-before-logging-forward { + } + + chain ufw6-before-input { + } + + chain ufw6-before-output { + } + + chain ufw6-before-forward { + } + + chain ufw6-after-input { + } + + chain ufw6-after-output { + } + + chain ufw6-after-forward { + } + + chain ufw6-after-logging-input { + } + + chain ufw6-after-logging-output { + } + + chain ufw6-after-logging-forward { + } + + chain ufw6-reject-input { + } + + chain ufw6-reject-output { + } + + chain ufw6-reject-forward { + } + + chain ufw6-track-input { + } + + chain ufw6-track-output { + } + + chain ufw6-track-forward { + } + + chain ufw6-user-forward { + } + + chain ufw6-docker-logging-deny { + limit rate 3/minute burst 10 packets counter packets 0 bytes 0 xt target "LOG" + counter packets 0 bytes 0 drop + } + + chain DOCKER { + } + + chain DOCKER-BRIDGE { + } + + chain DOCKER-CT { + } + + chain DOCKER-INTERNAL { + } +} +# Warning: table ip nat is managed by iptables-nft, do not touch! +table ip nat { + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 10757093 bytes 647829087 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip daddr != 127.0.0.0/8 xt match "addrtype" counter packets 128611 bytes 7705178 jump DOCKER + } + + chain POSTROUTING { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 172.19.0.0/16 oifname != "br-72fd626a8ff7" counter packets 0 bytes 0 xt target "MASQUERADE" + ip saddr 172.18.0.0/16 oifname != "br-2e4a76a7cd2e" counter packets 825 bytes 49500 xt target "MASQUERADE" + ip saddr 192.168.112.0/20 oifname != "br-e5d78502832d" counter packets 126 bytes 7560 xt target "MASQUERADE" + ip saddr 192.168.80.0/20 oifname != "br-e99ce5248c84" counter packets 0 bytes 0 xt target "MASQUERADE" + ip saddr 192.168.48.0/20 oifname != "br-77eb8a9e2ba1" counter packets 99 bytes 5940 xt target "MASQUERADE" + ip saddr 192.168.128.0/20 oifname != "br-73641cceafc3" counter packets 536 bytes 32160 xt target "MASQUERADE" + ip saddr 192.168.208.0/20 oifname != "br-9fd22324ec08" counter packets 2 bytes 120 xt target "MASQUERADE" + ip saddr 192.168.203.0/24 oifname != "br-1ccb887b3344" counter packets 37248 bytes 2854476 xt target "MASQUERADE" + ip saddr 192.168.64.0/20 oifname != "br-a63fa64a9e18" counter packets 353 bytes 21180 xt target "MASQUERADE" + ip saddr 172.17.0.0/16 oifname != "docker0" counter packets 99933 bytes 6001436 xt target "MASQUERADE" + ip saddr 172.21.0.0/16 oifname != "br-84e7d0cfeada" counter packets 2 bytes 128 xt target "MASQUERADE" + ip saddr 192.168.176.0/20 oifname != "br-f8b083119d99" counter packets 209 bytes 12644 xt target "MASQUERADE" + ip saddr 172.20.0.0/16 oifname != "br-6eb1e7f7f847" counter packets 699 bytes 42516 xt target "MASQUERADE" + ip saddr 172.28.0.0/16 oifname != "br-8ce143481a5b" counter packets 1934 bytes 116040 xt target "MASQUERADE" + ip saddr 172.27.0.0/16 oifname != "br-3008d408e73a" counter packets 2904 bytes 174240 xt target "MASQUERADE" + ip saddr 172.25.0.0/16 oifname != "br-cadedce55fe9" counter packets 0 bytes 0 xt target "MASQUERADE" + ip saddr 172.24.0.0/16 oifname != "br-3b338a381229" counter packets 385 bytes 23164 xt target "MASQUERADE" + ip saddr 192.168.224.0/20 oifname != "br-ca07a9577a7f" counter packets 0 bytes 0 xt target "MASQUERADE" + ip saddr 192.168.0.0/20 oifname != "br-a5fbc29c2c2a" counter packets 10 bytes 600 xt target "MASQUERADE" + ip saddr 172.31.0.0/16 oifname != "br-dd007c7e67bc" counter packets 0 bytes 0 xt target "MASQUERADE" + ip saddr 192.168.240.0/20 oifname != "br-0d1490cc67c9" counter packets 587045 bytes 35224163 xt target "MASQUERADE" + } + + chain DOCKER { + iifname != "docker0" tcp dport 5100 counter packets 8247 bytes 494812 xt target "DNAT" + iifname != "docker0" tcp dport 222 counter packets 2304 bytes 137444 xt target "DNAT" + iifname != "docker0" tcp dport 20000 counter packets 1532 bytes 91584 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 25 counter packets 433 bytes 23099 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 7080 counter packets 35 bytes 1864 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 110 counter packets 195 bytes 9741 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 143 counter packets 448 bytes 25580 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 7443 counter packets 58 bytes 2868 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 465 counter packets 107 bytes 6032 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 587 counter packets 447 bytes 23492 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 993 counter packets 201 bytes 10174 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 995 counter packets 197 bytes 11180 xt target "DNAT" + iifname != "br-1ccb887b3344" tcp dport 20004 counter packets 5 bytes 300 xt target "DNAT" + iifname != "br-9fd22324ec08" tcp dport 20005 counter packets 5 bytes 300 xt target "DNAT" + iifname != "br-73641cceafc3" tcp dport 20006 counter packets 19 bytes 1140 xt target "DNAT" + iifname != "br-e5d78502832d" tcp dport 20007 counter packets 5 bytes 284 xt target "DNAT" + iifname != "br-e5d78502832d" tcp dport 20008 counter packets 4 bytes 240 xt target "DNAT" + iifname != "br-e99ce5248c84" tcp dport 1842 counter packets 12 bytes 720 xt target "DNAT" + iifname != "br-8ce143481a5b" tcp dport 4848 counter packets 40 bytes 1960 xt target "DNAT" + iifname != "docker0" tcp dport 4222 counter packets 3846 bytes 231012 xt target "DNAT" + ip daddr 127.0.0.1 iifname != "docker0" tcp dport 8222 counter packets 0 bytes 0 xt target "DNAT" + iifname != "docker0" tcp dport 20003 counter packets 18942 bytes 1136512 xt target "DNAT" + iifname != "br-2e4a76a7cd2e" tcp dport 9070 counter packets 195 bytes 11676 xt target "DNAT" + iifname != "br-77eb8a9e2ba1" tcp dport 9102 counter packets 13 bytes 772 xt target "DNAT" + iifname != "br-77eb8a9e2ba1" tcp dport 8102 counter packets 17 bytes 944 xt target "DNAT" + iifname != "br-77eb8a9e2ba1" tcp dport 8104 counter packets 16 bytes 916 xt target "DNAT" + iifname != "br-77eb8a9e2ba1" tcp dport 8103 counter packets 13 bytes 756 xt target "DNAT" + iifname != "br-3008d408e73a" tcp dport 1212 counter packets 189 bytes 11188 xt target "DNAT" + iifname != "br-6eb1e7f7f847" tcp dport 20009 counter packets 138 bytes 8280 xt target "DNAT" + iifname != "br-f8b083119d99" tcp dport 20001 counter packets 47780 bytes 2866736 xt target "DNAT" + iifname != "br-f8b083119d99" tcp dport 20002 counter packets 7 bytes 404 xt target "DNAT" + iifname != "docker0" tcp dport 20010 counter packets 74 bytes 4424 xt target "DNAT" + iifname != "docker0" tcp dport 20011 counter packets 4 bytes 240 xt target "DNAT" + iifname != "br-72fd626a8ff7" tcp dport 20012 counter packets 237 bytes 14220 xt target "DNAT" + iifname != "docker0" tcp dport 6852 counter packets 16489 bytes 989324 xt target "DNAT" + } +} +# Warning: table ip6 nat is managed by iptables-nft, do not touch! +table ip6 nat { + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 399 bytes 22104 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip6 daddr != ::1 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER + } + + chain DOCKER { + } +} +table ip raw { + chain PREROUTING { + type filter hook prerouting priority raw; policy accept; + ip daddr 127.0.0.1 iifname != "lo" tcp dport 8222 counter packets 0 bytes 0 drop + } +} +table ip mangle { + chain FORWARD { + type filter hook forward priority mangle; policy accept; + } +} +table inet mesh { + chain input { + type filter hook input priority filter; policy drop; + ct state established,related accept + ct state invalid drop + iif "lo" accept + iifname != { "mesh0", "enp9s0" } accept + icmp type echo-request accept + icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept + iifname != { "mesh0", "enp9s0" } udp dport { 53, 67 } accept + iifname != { "mesh0", "enp9s0" } tcp dport 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 22 accept + tcp dport 22 accept + tcp dport 4222 accept + tcp dport 22 accept + tcp dport 25 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } udp dport 53 accept + tcp dport 80 accept + tcp dport 110 accept + tcp dport 143 accept + tcp dport 222 accept + tcp dport 443 accept + tcp dport 465 accept + tcp dport 587 accept + tcp dport 993 accept + tcp dport 995 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 1212 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 1842 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 4222 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 4848 accept + tcp dport 5100 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 6852 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 7080 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 7443 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 8102 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 8103 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 8104 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 9000 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 9070 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 9102 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20000 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20001 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20002 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20003 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20004 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20005 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20006 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20007 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20008 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20009 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20010 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20011 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 20012 accept + udp dport 51820 accept + } + + chain output { + type filter hook output priority filter; policy accept; + } + + chain forward { + type filter hook forward priority filter; policy drop; + ct state established,related accept + ct state invalid drop + iifname != { "mesh0", "enp9s0" } accept + iifname "mesh0" oifname "mesh0" accept + ct original proto-dst 22 accept + ct original proto-dst 25 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 53 accept + ct original proto-dst 80 accept + ct original proto-dst 110 accept + ct original proto-dst 143 accept + ct original proto-dst 222 accept + ct original proto-dst 443 accept + ct original proto-dst 465 accept + ct original proto-dst 587 accept + ct original proto-dst 993 accept + ct original proto-dst 995 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 1212 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 1842 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 4222 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 4848 accept + ct original proto-dst 5100 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 6852 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 7080 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 7443 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 8102 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 8103 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 8104 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 9000 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 9070 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 9102 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20000 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20001 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20002 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20003 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20004 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20005 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20006 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20007 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20008 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20009 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20010 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20011 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 20012 accept + ct original proto-dst 51820 accept + ct original proto-dst 4222 accept + } +} diff --git a/internal/firewall/testdata/home-server-legacy-S.txt b/internal/firewall/testdata/home-server-legacy-S.txt new file mode 100644 index 0000000..727df97 --- /dev/null +++ b/internal/firewall/testdata/home-server-legacy-S.txt @@ -0,0 +1,149 @@ +-P INPUT ACCEPT +-P FORWARD DROP +-P OUTPUT ACCEPT +-N DOCKER +-N DOCKER-BRIDGE +-N DOCKER-CT +-N DOCKER-FORWARD +-N DOCKER-INTERNAL +-N DOCKER-USER +-N HAL-MESH-ONLY +-N ufw-after-forward +-N ufw-after-input +-N ufw-after-logging-forward +-N ufw-after-logging-input +-N ufw-after-logging-output +-N ufw-after-output +-N ufw-before-forward +-N ufw-before-input +-N ufw-before-logging-forward +-N ufw-before-logging-input +-N ufw-before-logging-output +-N ufw-before-output +-N ufw-reject-forward +-N ufw-reject-input +-N ufw-reject-output +-N ufw-track-forward +-N ufw-track-input +-N ufw-track-output +-A INPUT -j ufw-before-logging-input +-A INPUT -j ufw-before-input +-A INPUT -j ufw-after-input +-A INPUT -j ufw-after-logging-input +-A INPUT -j ufw-reject-input +-A INPUT -j ufw-track-input +-A FORWARD -j DOCKER-USER +-A FORWARD -j DOCKER-FORWARD +-A FORWARD -j ufw-before-logging-forward +-A FORWARD -j ufw-before-forward +-A FORWARD -j ufw-after-forward +-A FORWARD -j ufw-after-logging-forward +-A FORWARD -j ufw-reject-forward +-A FORWARD -j ufw-track-forward +-A OUTPUT -j ufw-before-logging-output +-A OUTPUT -j ufw-before-output +-A OUTPUT -j ufw-after-output +-A OUTPUT -j ufw-after-logging-output +-A OUTPUT -j ufw-reject-output +-A OUTPUT -j ufw-track-output +-A DOCKER -d 172.17.0.18/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8686 -j ACCEPT +-A DOCKER -d 172.17.0.14/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8989 -j ACCEPT +-A DOCKER -d 172.17.0.15/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 7878 -j ACCEPT +-A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9117 -j ACCEPT +-A DOCKER -d 172.17.0.13/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6789 -j ACCEPT +-A DOCKER -d 172.19.0.2/32 ! -i br-32062158f584 -o br-32062158f584 -p tcp -m tcp --dport 8080 -j ACCEPT +-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5432 -j ACCEPT +-A DOCKER -d 172.27.0.2/32 ! -i br-0910a98c6158 -o br-0910a98c6158 -p tcp -m tcp --dport 5678 -j ACCEPT +-A DOCKER -d 172.17.0.21/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3579 -j ACCEPT +-A DOCKER -d 172.17.0.19/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8181 -j ACCEPT +-A DOCKER -d 172.17.0.17/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8787 -j ACCEPT +-A DOCKER -d 172.17.0.16/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6767 -j ACCEPT +-A DOCKER -d 172.17.0.12/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT +-A DOCKER -d 172.17.0.11/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT +-A DOCKER -d 172.17.0.10/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9443 -j ACCEPT +-A DOCKER -d 172.17.0.10/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 9000 -j ACCEPT +-A DOCKER -d 172.17.0.9/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT +-A DOCKER -d 172.17.0.7/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 1880 -j ACCEPT +-A DOCKER -d 172.28.0.2/32 ! -i br-b11461b5b028 -o br-b11461b5b028 -p tcp -m tcp --dport 80 -j ACCEPT +-A DOCKER -d 172.23.0.14/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 6543 -j ACCEPT +-A DOCKER -d 172.23.0.14/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 5432 -j ACCEPT +-A DOCKER -d 172.23.0.5/32 ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -p tcp -m tcp --dport 8000 -j ACCEPT +-A DOCKER -d 172.26.0.3/32 ! -i br-b0fec361ccaa -o br-b0fec361ccaa -p tcp -m tcp --dport 6167 -j ACCEPT +-A DOCKER -d 172.26.0.2/32 ! -i br-b0fec361ccaa -o br-b0fec361ccaa -p tcp -m tcp --dport 80 -j ACCEPT +-A DOCKER -d 172.17.0.8/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8000 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 10001 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8880 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8843 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8443 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8080 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 6789 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 5514 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 3478 -j ACCEPT +-A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p udp -m udp --dport 1900 -j ACCEPT +-A DOCKER -d 172.25.0.3/32 ! -i br-b98821f7dc38 -o br-b98821f7dc38 -p tcp -m tcp --dport 8000 -j ACCEPT +-A DOCKER -d 172.18.0.3/32 ! -i br-442a0bfc65f8 -o br-442a0bfc65f8 -p tcp -m tcp --dport 1433 -j ACCEPT +-A DOCKER -d 172.20.0.3/32 ! -i br-afa37ac8b33d -o br-afa37ac8b33d -p tcp -m tcp --dport 8081 -j ACCEPT +-A DOCKER -d 172.20.0.3/32 ! -i br-afa37ac8b33d -o br-afa37ac8b33d -p tcp -m tcp --dport 1883 -j ACCEPT +-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 8086 -j ACCEPT +-A DOCKER -d 172.21.0.2/32 ! -i br-df15d8e19ec7 -o br-df15d8e19ec7 -p tcp -m tcp --dport 6379 -j ACCEPT +-A DOCKER -d 172.30.0.3/32 ! -i br-521eab9a3a5e -o br-521eab9a3a5e -p tcp -m tcp --dport 8283 -j ACCEPT +-A DOCKER -d 172.17.0.3/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 3000 -j ACCEPT +-A DOCKER ! -i br-32062158f584 -o br-32062158f584 -j DROP +-A DOCKER ! -i docker0 -o docker0 -j DROP +-A DOCKER ! -i br-521eab9a3a5e -o br-521eab9a3a5e -j DROP +-A DOCKER ! -i br-df15d8e19ec7 -o br-df15d8e19ec7 -j DROP +-A DOCKER ! -i br-afa37ac8b33d -o br-afa37ac8b33d -j DROP +-A DOCKER ! -i br-442a0bfc65f8 -o br-442a0bfc65f8 -j DROP +-A DOCKER ! -i br-b98821f7dc38 -o br-b98821f7dc38 -j DROP +-A DOCKER ! -i br-b0fec361ccaa -o br-b0fec361ccaa -j DROP +-A DOCKER ! -i br-66ffa5c1cba5 -o br-66ffa5c1cba5 -j DROP +-A DOCKER ! -i br-b11461b5b028 -o br-b11461b5b028 -j DROP +-A DOCKER ! -i br-2df4e541b877 -o br-2df4e541b877 -j DROP +-A DOCKER ! -i br-0910a98c6158 -o br-0910a98c6158 -j DROP +-A DOCKER-BRIDGE -o br-32062158f584 -j DOCKER +-A DOCKER-BRIDGE -o docker0 -j DOCKER +-A DOCKER-BRIDGE -o br-521eab9a3a5e -j DOCKER +-A DOCKER-BRIDGE -o br-df15d8e19ec7 -j DOCKER +-A DOCKER-BRIDGE -o br-afa37ac8b33d -j DOCKER +-A DOCKER-BRIDGE -o br-442a0bfc65f8 -j DOCKER +-A DOCKER-BRIDGE -o br-b98821f7dc38 -j DOCKER +-A DOCKER-BRIDGE -o br-b0fec361ccaa -j DOCKER +-A DOCKER-BRIDGE -o br-66ffa5c1cba5 -j DOCKER +-A DOCKER-BRIDGE -o br-b11461b5b028 -j DOCKER +-A DOCKER-BRIDGE -o br-2df4e541b877 -j DOCKER +-A DOCKER-BRIDGE -o br-0910a98c6158 -j DOCKER +-A DOCKER-CT -o br-32062158f584 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-521eab9a3a5e -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-df15d8e19ec7 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-afa37ac8b33d -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-442a0bfc65f8 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-b98821f7dc38 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-b0fec361ccaa -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-66ffa5c1cba5 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-b11461b5b028 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-2df4e541b877 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-CT -o br-0910a98c6158 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A DOCKER-FORWARD -j DOCKER-CT +-A DOCKER-FORWARD -j DOCKER-INTERNAL +-A DOCKER-FORWARD -j DOCKER-BRIDGE +-A DOCKER-FORWARD -i br-32062158f584 -j ACCEPT +-A DOCKER-FORWARD -i docker0 -j ACCEPT +-A DOCKER-FORWARD -i br-521eab9a3a5e -j ACCEPT +-A DOCKER-FORWARD -i br-df15d8e19ec7 -j ACCEPT +-A DOCKER-FORWARD -i br-afa37ac8b33d -j ACCEPT +-A DOCKER-FORWARD -i br-442a0bfc65f8 -j ACCEPT +-A DOCKER-FORWARD -i br-b98821f7dc38 -j ACCEPT +-A DOCKER-FORWARD -i br-b0fec361ccaa -j ACCEPT +-A DOCKER-FORWARD -i br-66ffa5c1cba5 -j ACCEPT +-A DOCKER-FORWARD -i br-b11461b5b028 -j ACCEPT +-A DOCKER-FORWARD -i br-2df4e541b877 -j ACCEPT +-A DOCKER-FORWARD -i br-0910a98c6158 -j ACCEPT +-A DOCKER-USER -i enp6s0 -p tcp -m conntrack --ctstate NEW -j HAL-MESH-ONLY +-A HAL-MESH-ONLY -m conntrack --ctorigdstport 6881 -j RETURN +-A HAL-MESH-ONLY -m conntrack --ctorigdstport 80 -j RETURN +-A HAL-MESH-ONLY -m conntrack --ctorigdstport 443 -j RETURN +-A HAL-MESH-ONLY -s 10.0.0.0/8 -j RETURN +-A HAL-MESH-ONLY -s 172.16.0.0/12 -j RETURN +-A HAL-MESH-ONLY -s 192.168.0.0/16 -j RETURN +-A HAL-MESH-ONLY -m comment --comment "HAL: not public -> mesh only" -j DROP diff --git a/internal/firewall/testdata/laptop.nft b/internal/firewall/testdata/laptop.nft new file mode 100644 index 0000000..fbfecde --- /dev/null +++ b/internal/firewall/testdata/laptop.nft @@ -0,0 +1,327 @@ +table ip mangle { + chain FORWARD { + type filter hook forward priority mangle; policy accept; + } +} +# Warning: table ip nat is managed by iptables-nft, do not touch! +table ip nat { + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 12072 bytes 4564241 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip daddr != 127.0.0.0/8 xt match "addrtype" counter packets 1854 bytes 111240 jump DOCKER + } + + chain POSTROUTING { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 172.17.0.0/16 oifname != "docker0" counter packets 903 bytes 61577 xt target "MASQUERADE" + ip saddr 172.21.0.0/16 oifname != "br-86a5d6b30e2b" counter packets 344 bytes 27744 xt target "MASQUERADE" + ip saddr 172.25.0.0/16 oifname != "br-61495e14a004" counter packets 374 bytes 33016 xt target "MASQUERADE" + ip saddr 172.30.0.0/16 oifname != "br-5107796ee9b4" counter packets 352 bytes 28224 xt target "MASQUERADE" + ip saddr 172.18.0.0/16 oifname != "br-cfd337ac4e58" counter packets 339 bytes 27444 xt target "MASQUERADE" + ip saddr 172.19.0.0/16 oifname != "br-8f0c6ee01425" counter packets 351 bytes 28164 xt target "MASQUERADE" + ip saddr 172.22.0.0/16 oifname != "br-75ac3c36e87f" counter packets 333 bytes 27084 xt target "MASQUERADE" + ip saddr 172.20.0.0/16 oifname != "br-0529801521bc" counter packets 343 bytes 27404 xt target "MASQUERADE" + } + + chain DOCKER { + iifname != "br-61495e14a004" tcp dport 5680 counter packets 2 bytes 120 xt target "DNAT" + ip daddr 127.0.0.1 iifname != "br-61495e14a004" tcp dport 15673 counter packets 0 bytes 0 xt target "DNAT" + ip daddr 127.0.0.1 iifname != "docker0" tcp dport 55432 counter packets 0 bytes 0 xt target "DNAT" + } +} +# Warning: table ip filter is managed by iptables-nft, do not touch! +table ip filter { + chain DOCKER-FORWARD { + counter packets 702328 bytes 1801910999 jump DOCKER-CT + counter packets 337315 bytes 23928864 jump DOCKER-INTERNAL + counter packets 337315 bytes 23928864 jump DOCKER-BRIDGE + iifname "br-75ac3c36e87f" counter packets 0 bytes 0 accept + iifname "br-86a5d6b30e2b" counter packets 0 bytes 0 accept + iifname "br-8f0c6ee01425" counter packets 0 bytes 0 accept + iifname "br-cfd337ac4e58" counter packets 0 bytes 0 accept + iifname "br-0529801521bc" counter packets 0 bytes 0 accept + iifname "br-5107796ee9b4" counter packets 0 bytes 0 accept + iifname "br-61495e14a004" counter packets 0 bytes 0 accept + iifname "docker0" counter packets 337315 bytes 23928864 accept + } + + chain FORWARD { + type filter hook forward priority filter; policy drop; + counter packets 702328 bytes 1801910999 jump DOCKER-USER + counter packets 702328 bytes 1801910999 jump DOCKER-FORWARD + } + + chain DOCKER-USER { + ip protocol tcp counter packets 702510 bytes 1801965868 jump f2b-sshd + oifname "incusbr0" counter packets 0 bytes 0 accept + iifname "incusbr0" counter packets 0 bytes 0 accept + } + + chain f2b-sshd { + counter packets 10423854 bytes 13891318049 return + } + + chain INPUT { + type filter hook input priority filter; policy accept; + ip protocol tcp counter packets 9721344 bytes 12089352181 jump f2b-sshd + } + + chain DOCKER { + ip daddr 172.17.0.2 iifname != "docker0" oifname "docker0" tcp dport 5432 counter packets 0 bytes 0 accept + ip daddr 172.25.0.2 iifname != "br-61495e14a004" oifname "br-61495e14a004" tcp dport 15672 counter packets 0 bytes 0 accept + ip daddr 172.25.0.2 iifname != "br-61495e14a004" oifname "br-61495e14a004" tcp dport 5672 counter packets 0 bytes 0 accept + iifname != "br-75ac3c36e87f" oifname "br-75ac3c36e87f" counter packets 0 bytes 0 drop + iifname != "br-86a5d6b30e2b" oifname "br-86a5d6b30e2b" counter packets 0 bytes 0 drop + iifname != "br-8f0c6ee01425" oifname "br-8f0c6ee01425" counter packets 0 bytes 0 drop + iifname != "br-cfd337ac4e58" oifname "br-cfd337ac4e58" counter packets 0 bytes 0 drop + iifname != "br-0529801521bc" oifname "br-0529801521bc" counter packets 0 bytes 0 drop + iifname != "br-5107796ee9b4" oifname "br-5107796ee9b4" counter packets 0 bytes 0 drop + iifname != "br-61495e14a004" oifname "br-61495e14a004" counter packets 0 bytes 0 drop + iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop + } + + chain DOCKER-BRIDGE { + oifname "br-75ac3c36e87f" counter packets 0 bytes 0 jump DOCKER + oifname "br-86a5d6b30e2b" counter packets 0 bytes 0 jump DOCKER + oifname "br-8f0c6ee01425" counter packets 0 bytes 0 jump DOCKER + oifname "br-cfd337ac4e58" counter packets 0 bytes 0 jump DOCKER + oifname "br-0529801521bc" counter packets 0 bytes 0 jump DOCKER + oifname "br-5107796ee9b4" counter packets 0 bytes 0 jump DOCKER + oifname "br-61495e14a004" counter packets 0 bytes 0 jump DOCKER + oifname "docker0" counter packets 0 bytes 0 jump DOCKER + } + + chain DOCKER-CT { + oifname "br-75ac3c36e87f" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-86a5d6b30e2b" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-8f0c6ee01425" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-cfd337ac4e58" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-0529801521bc" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-5107796ee9b4" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "br-61495e14a004" xt match "conntrack" counter packets 0 bytes 0 accept + oifname "docker0" xt match "conntrack" counter packets 365013 bytes 1777982135 accept + } + + chain DOCKER-INTERNAL { + } +} +# Warning: table ip6 nat is managed by iptables-nft, do not touch! +table ip6 nat { + chain PREROUTING { + type nat hook prerouting priority dstnat; policy accept; + xt match "addrtype" counter packets 363 bytes 67927 jump DOCKER + } + + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + ip6 daddr != ::1 xt match "addrtype" counter packets 0 bytes 0 jump DOCKER + } + + chain DOCKER { + } +} +table ip6 filter { + chain DOCKER-FORWARD { + counter packets 0 bytes 0 jump DOCKER-CT + counter packets 0 bytes 0 jump DOCKER-INTERNAL + counter packets 0 bytes 0 jump DOCKER-BRIDGE + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + counter packets 0 bytes 0 jump DOCKER-USER + counter packets 0 bytes 0 jump DOCKER-FORWARD + } + + chain DOCKER-USER { + } + + chain DOCKER { + } + + chain DOCKER-BRIDGE { + } + + chain DOCKER-CT { + } + + chain DOCKER-INTERNAL { + } +} +table ip raw { + chain PREROUTING { + type filter hook prerouting priority raw; policy accept; + ip daddr 172.25.0.2 iifname != "br-61495e14a004" counter packets 0 bytes 0 drop + ip daddr 127.0.0.1 iifname != "lo" tcp dport 15673 counter packets 0 bytes 0 drop + ip daddr 172.17.0.2 iifname != "docker0" counter packets 0 bytes 0 drop + ip daddr 127.0.0.1 iifname != "lo" tcp dport 55432 counter packets 0 bytes 0 drop + } +} +table inet incus { + set bridges { + type ifname + elements = { "incusbr0" } + } + + chain pstrt.incusbr0 { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 10.7.169.0/24 oifname @bridges accept + ip saddr 10.7.169.0/24 ip daddr != 10.7.169.0/24 masquerade + ip6 saddr fd42:cbc4:e123:f6::/64 oifname @bridges accept + ip6 saddr fd42:cbc4:e123:f6::/64 ip6 daddr != fd42:cbc4:e123:f6::/64 masquerade + } + + chain fwd.incusbr0 { + type filter hook forward priority filter; policy accept; + ip version 4 oifname "incusbr0" accept + ip version 4 iifname "incusbr0" accept + ip6 version 6 oifname "incusbr0" accept + ip6 version 6 iifname "incusbr0" accept + } + + chain in.incusbr0 { + type filter hook input priority filter; policy accept; + iifname "incusbr0" tcp dport 53 accept + iifname "incusbr0" udp dport 53 accept + iifname "incusbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept + iifname "incusbr0" udp dport 67 accept + iifname "incusbr0" ip protocol udp udp checksum set 0 + iifname "incusbr0" icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert, mld2-listener-report } accept + iifname "incusbr0" udp dport 547 accept + } + + chain out.incusbr0 { + type filter hook output priority filter; policy accept; + oifname "incusbr0" tcp sport 53 accept + oifname "incusbr0" udp sport 53 accept + oifname "incusbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept + oifname "incusbr0" udp sport 67 accept + oifname "incusbr0" ip protocol udp udp checksum set 0 + oifname "incusbr0" icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, mld2-listener-report } accept + oifname "incusbr0" udp sport 547 accept + } +} +table ip fct_filter { + chain OUTPUT { + type filter hook output priority filter; policy accept; + } + + chain FCT-QUARANTINE-EMS { + } + + chain FCT-QUARANTINE-FAZ { + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + } + + chain FCT-WEBFILTER-QUIC-CHAIN { + } + + chain INPUT { + type filter hook input priority filter; policy accept; + } + + chain FCT-QUARANTINE { + } + + chain FCT-DNS-QUIC-FILTER { + } + + chain FCT-VPN-CHAIN { + } +} +table ip fct_nat { + chain OUTPUT { + type nat hook output priority dstnat; policy accept; + } + + chain FCT-DNS-UDP-CHAIN-STAGE-2 { + } + + chain FCT-DNS-UDP-CHAIN-STAGE-1 { + } + + chain FCT-TCP-CHAIN { + } + + chain FCT-DNS-DOH-CHAIN-STAGE-1 { + } + + chain FCT-WEBFILTER-CHAIN { + } + + chain FCT-DNS-DOH-CHAIN-STAGE-2 { + } +} +table ip6 fct_filter { + chain FCT-QUARANTINE { + } + + chain INPUT { + type filter hook input priority filter; policy accept; + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + } +} +table ip fct_mangle { + chain PREROUTING { + type filter hook prerouting priority mangle; policy accept; + } + + chain FCT-UDP-STAGE-1 { + } + + chain OUTPUT { + type route hook output priority mangle; policy accept; + } + + chain FCT-UDP-STAGE-2 { + } + + chain FCT-UDP-OUTPUT { + } +} +table inet mesh { + chain input { + type filter hook input priority filter; policy drop; + ct state established,related accept + ct state invalid drop + iif "lo" accept + iifname != { "mesh0", "wlp3s0" } accept + icmp type echo-request accept + icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept + iifname != { "mesh0", "wlp3s0" } udp dport { 53, 67 } accept + iifname != { "mesh0", "wlp3s0" } tcp dport 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 22 accept + tcp dport 22 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } tcp dport 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } udp dport 53 accept + } + + chain output { + type filter hook output priority filter; policy accept; + } + + chain forward { + type filter hook forward priority filter; policy drop; + ct state established,related accept + ct state invalid drop + iifname != { "mesh0", "wlp3s0" } accept + iifname "mesh0" oifname "mesh0" accept + ct original proto-dst 22 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 53 accept + ip saddr { 10.10.0.1, 10.10.0.2, 10.10.0.3, 10.10.0.4 } ct original proto-dst 53 accept + } +} diff --git a/internal/link/messages.go b/internal/link/messages.go index dd63573..ed559c6 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -110,6 +110,17 @@ type Report struct { // and the mesh's up in its place, and where the found configuration's original was kept. Tunnel *CarriedTunnel `json:"tunnel,omitempty"` + // Filters is what filters this machine now, every table and chain that refuses traffic with its + // owner — the mesh's, the found firewall's, the container runtime's own, a ban list, or other + // (novox/hq ADR 0168). Every node reports it, adopted or converged, so the mesh can say + // truthfully what filters a converged machine and name what it did not write. + Filters []Filter `json:"filters,omitempty"` + + // FoundFirewall is the state of the firewall a converged machine was found with: whether it is + // in force now, and how it came to be inactive — the mesh disabled it, or a reconcile found it so + // (ADR 0168). Nil on a machine found with none, and on an adopted one, where Firewall says it. + FoundFirewall *FoundFirewall `json:"found_firewall,omitempty"` + // Strays is what runs on the machine that the mesh neither wrote nor holds (novox/hq ADR // 0163): containers nobody declared and nobody holds, the ones a cutover leaves behind. Strays []Stray `json:"strays,omitempty"` @@ -233,3 +244,18 @@ type Reach struct { Published bool `json:"published,omitempty"` ContainerPort int `json:"container-port,omitempty"` } + +// A Filter is one place on the machine that refuses traffic, with its owner (novox/hq ADR 0168): +// the same shape the host's firewall package reads, carried as data. +type Filter struct { + Where string `json:"where"` + Owner string `json:"owner"` + Refuses string `json:"refuses"` +} + +// FoundFirewall is the state of a converged machine's found firewall (ADR 0168). +type FoundFirewall struct { + Kind string `json:"kind"` + Active bool `json:"active"` + RetiredBy string `json:"retired_by,omitempty"` +} diff --git a/internal/store/store.go b/internal/store/store.go index 53296ed..55efedf 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -232,8 +232,13 @@ type FoundFirewall struct { // retires, and returning it to adopted restores. WasActive bool `json:"was_active,omitempty"` // DisabledByMesh is set when converging retired it, so returning to adopted enables it again - // and nothing else ever does. + // and nothing else ever does. It means exactly that (novox/hq ADR 0168): a reconcile that finds + // the firewall already inactive records RetiredBy and never this. DisabledByMesh bool `json:"disabled_by_mesh,omitempty"` + // RetiredBy says how the found firewall came to be inactive on a converged machine: "mesh" when + // the mesh disabled it, "found-inactive" when a reconcile found it so and nothing of the mesh's + // had done it. Empty while it is in force or the machine is adopted. + RetiredBy string `json:"retired_by,omitempty"` // Forward is each family's forward policy as it was before the mesh disabled the firewall, // by the tool that sets it — recorded before, so a retirement retried puts back what the // machine had.