2a3eb3b04d
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.
189 lines
4.0 KiB
Go
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
|
|
}
|