Files
tomswall/internal/nftables/diff.go
T
unkinben 8d9a76c751 Add comprehensive nftables compiler with shorewall feature parity
Rewrites the compiler from ~440 to ~1700 lines covering all major shorewall
firewall features: loopback, conntrack fast-path, anti-spoof, DHCP, intra-zone,
blacklist/whitelist, conntrack notrack, tunnels (13 types), rules with sections,
DNAT/redirect, SNAT/masquerade, static NAT, policies with zone exclusions,
MSS clamping, rate limiting, connection limiting, negated addresses, ICMP type
matching, TCP RST reject, user/UID matching, mark match/set, NFQUEUE, NONAT,
and policy-level rate/conn limiting.

Adds full config types for all shorewall subsystems (mangle, accounting, maclist,
netmap, providers, tunnels, conntrack, blrules, proxyarp/ndp, routes, tc, secmarks),
shorewall migration tooling, expanded CLI commands, expression-level diff engine,
and 49 unit tests.
2026-07-01 23:56:44 +10:00

110 lines
2.1 KiB
Go

package nftables
import (
"fmt"
"strings"
"github.com/google/nftables/expr"
)
type ManagedRule struct {
Chain string
Handle uint64
Exprs []expr.Any
Tag string
}
type FirewallState struct {
Rules map[string][]ManagedRule
}
type ChangeSet struct {
Add []ManagedRule
Remove []ManagedRule
}
func (cs *ChangeSet) Empty() bool {
return len(cs.Add) == 0 && len(cs.Remove) == 0
}
func (cs *ChangeSet) Summary() string {
var b strings.Builder
if len(cs.Add) > 0 {
fmt.Fprintf(&b, " + %d rule(s) to add\n", len(cs.Add))
for _, r := range cs.Add {
fmt.Fprintf(&b, " + [%s] %s\n", r.Chain, r.Tag)
}
}
if len(cs.Remove) > 0 {
fmt.Fprintf(&b, " - %d rule(s) to remove\n", len(cs.Remove))
for _, r := range cs.Remove {
fmt.Fprintf(&b, " - [%s] %s (handle %d)\n", r.Chain, r.Tag, r.Handle)
}
}
return b.String()
}
func computeDiff(current, desired *FirewallState) *ChangeSet {
cs := &ChangeSet{}
currentByTag := make(map[string][]ManagedRule)
for _, rules := range current.Rules {
for _, r := range rules {
if r.Tag != "" {
currentByTag[r.Tag] = append(currentByTag[r.Tag], r)
}
}
}
desiredByTag := make(map[string][]ManagedRule)
for _, rules := range desired.Rules {
for _, r := range rules {
desiredByTag[r.Tag] = append(desiredByTag[r.Tag], r)
}
}
for tag, desiredRules := range desiredByTag {
currentRules, exists := currentByTag[tag]
if !exists {
cs.Add = append(cs.Add, desiredRules...)
continue
}
if !rulesMatch(currentRules, desiredRules) {
cs.Remove = append(cs.Remove, currentRules...)
cs.Add = append(cs.Add, desiredRules...)
}
}
for tag, currentRules := range currentByTag {
if _, exists := desiredByTag[tag]; !exists {
cs.Remove = append(cs.Remove, currentRules...)
}
}
return cs
}
func rulesMatch(a, b []ManagedRule) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Chain != b[i].Chain {
return false
}
if !exprsEqual(a[i].Exprs, b[i].Exprs) {
return false
}
}
return true
}
func exprsEqual(a, b []expr.Any) bool {
if len(a) != len(b) {
return false
}
as := fmt.Sprintf("%v", a)
bs := fmt.Sprintf("%v", b)
return as == bs
}