Files
tomswall/internal/nftables/engine.go
T
unkinben 2a3eb3b04d Initial scaffold for tomswall
Spiritual successor to shorewall — manages nftables directly via
google/nftables. Reads a single YAML config covering zones, interfaces,
hosts, policy, rules, snat, and named portgroups. Computes differential
changes against the running nftables state and applies them atomically.
Supports detecting and purging rules added outside of tomswall.
2026-06-28 23:43:16 +10:00

189 lines
4.0 KiB
Go

package nftables
import (
"fmt"
"github.com/google/nftables"
"git.unkin.net/unkin/tomswall/internal/config"
)
type Engine struct {
cfg *config.Config
conn *nftables.Conn
}
func NewEngine(cfg *config.Config) (*Engine, error) {
conn, err := nftables.New()
if err != nil {
return nil, fmt.Errorf("connecting to nftables: %w", err)
}
return &Engine{cfg: cfg, conn: conn}, nil
}
func (e *Engine) ensureTable() *nftables.Table {
return e.conn.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: e.cfg.Settings.TableName,
})
}
func (e *Engine) ensureChains(table *nftables.Table) map[string]*nftables.Chain {
chains := map[string]*nftables.Chain{
"input": {
Name: "input",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
Policy: policyPtr(nftables.ChainPolicyDrop),
},
"forward": {
Name: "forward",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityFilter,
Policy: policyPtr(nftables.ChainPolicyDrop),
},
"output": {
Name: "output",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityFilter,
Policy: policyPtr(nftables.ChainPolicyAccept),
},
"postrouting": {
Name: "postrouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource,
},
"prerouting": {
Name: "prerouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPrerouting,
Priority: nftables.ChainPriorityNATDest,
},
}
for name, chain := range chains {
chains[name] = e.conn.AddChain(chain)
}
return chains
}
func (e *Engine) Plan() (*ChangeSet, error) {
compiler := NewCompiler(e.cfg)
desired, err := compiler.Compile()
if err != nil {
return nil, fmt.Errorf("compiling config: %w", err)
}
current, err := e.readCurrentState()
if err != nil {
return nil, fmt.Errorf("reading current state: %w", err)
}
return computeDiff(current, desired), nil
}
func (e *Engine) Apply(changes *ChangeSet) error {
table := e.ensureTable()
chains := e.ensureChains(table)
for _, r := range changes.Remove {
e.conn.DelRule(&nftables.Rule{
Table: table,
Chain: chains[r.Chain],
Handle: r.Handle,
})
}
for _, r := range changes.Add {
chain, ok := chains[r.Chain]
if !ok {
return fmt.Errorf("unknown chain %q", r.Chain)
}
e.conn.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: r.Exprs,
UserData: []byte(r.Tag),
})
}
return e.conn.Flush()
}
func (e *Engine) Flush() error {
tables, err := e.conn.ListTables()
if err != nil {
return fmt.Errorf("listing tables: %w", err)
}
for _, t := range tables {
if t.Name == e.cfg.Settings.TableName {
e.conn.DelTable(t)
return e.conn.Flush()
}
}
return nil
}
func (e *Engine) readCurrentState() (*FirewallState, error) {
state := &FirewallState{
Rules: make(map[string][]ManagedRule),
}
tables, err := e.conn.ListTables()
if err != nil {
return state, nil
}
var ourTable *nftables.Table
for _, t := range tables {
if t.Name == e.cfg.Settings.TableName && t.Family == nftables.TableFamilyINet {
ourTable = t
break
}
}
if ourTable == nil {
return state, nil
}
chains, err := e.conn.ListChainsOfTableFamily(nftables.TableFamilyINet)
if err != nil {
return state, nil
}
for _, chain := range chains {
if chain.Table.Name != e.cfg.Settings.TableName {
continue
}
rules, err := e.conn.GetRules(ourTable, chain)
if err != nil {
continue
}
for _, rule := range rules {
state.Rules[chain.Name] = append(state.Rules[chain.Name], ManagedRule{
Chain: chain.Name,
Handle: rule.Handle,
Exprs: rule.Exprs,
Tag: string(rule.UserData),
})
}
}
return state, nil
}
func policyPtr(p nftables.ChainPolicy) *nftables.ChainPolicy {
return &p
}