8ae09c0941
Map the rendered global-compiled tail into native config.Blrules/Conntrack/ Secmarks/Vars.
350 lines
11 KiB
Go
350 lines
11 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"git.unkin.net/unkin/tomswall/internal/config"
|
|
)
|
|
|
|
// Translate converts a control-plane RenderedConfig into a native tomswall
|
|
// config.Config that the existing differential engine can apply.
|
|
//
|
|
// The rendered model is interface-agnostic and address-matched; tomswall
|
|
// expresses that with the "all:<cidr>" source/dest form (zone "all" imposes no
|
|
// interface constraint, the CIDR is matched on saddr/daddr). Named sets are
|
|
// inlined as their concrete members: a rule element matching N source addresses
|
|
// against M dest addresses expands to N*M address-matched rules. This is a
|
|
// correct v1; native nftables set references (so membership churns without a
|
|
// rule rebuild) are a tracked follow-up.
|
|
func Translate(rc *RenderedConfig) (*config.Config, error) {
|
|
cfg := &config.Config{
|
|
Settings: config.Settings{
|
|
AddressFamily: config.AddressFamily(orDefault(rc.Settings.AddressFamily, "inet")),
|
|
IPForwarding: rc.Settings.IPForwarding,
|
|
LogLevel: orDefault(rc.Settings.LogLevel, "info"),
|
|
TableName: orDefault(rc.Settings.TableName, "tomswall"),
|
|
},
|
|
Zones: map[string]config.Zone{},
|
|
PortGroups: map[string]config.PortGroup{},
|
|
}
|
|
|
|
// The firewall zone is required; bound zones map to their local interfaces.
|
|
cfg.Zones["fw"] = config.Zone{Type: config.ZoneFirewall}
|
|
for zone, ifaces := range rc.Bindings {
|
|
cfg.Zones[zone] = config.Zone{Type: config.ZoneIP}
|
|
for _, iface := range ifaces {
|
|
cfg.Interfaces = append(cfg.Interfaces, config.Interface{Zone: zone, Interface: iface})
|
|
}
|
|
}
|
|
sort.Slice(cfg.Interfaces, func(i, j int) bool {
|
|
return cfg.Interfaces[i].Interface < cfg.Interfaces[j].Interface
|
|
})
|
|
|
|
setMembers := indexSets(rc.Sets)
|
|
|
|
for i, rr := range rc.Rules {
|
|
rules, err := translateRule(rr, setMembers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %d: %w", i, err)
|
|
}
|
|
cfg.Rules = append(cfg.Rules, rules...)
|
|
}
|
|
|
|
for _, p := range rc.Policies {
|
|
cfg.Policy = append(cfg.Policy, config.Policy{
|
|
Source: orDefault(p.Source, "all"),
|
|
Dest: orDefault(p.Dest, "all"),
|
|
Action: config.PolicyAction(p.Action),
|
|
Log: p.Log,
|
|
})
|
|
}
|
|
|
|
for _, s := range rc.SNAT {
|
|
cfg.SNAT = append(cfg.SNAT, translateSNAT(s)...)
|
|
}
|
|
for _, n := range rc.Netmap {
|
|
cfg.Netmap = append(cfg.Netmap, config.Netmap{
|
|
Type: config.NetmapType(n.Type),
|
|
Net1: n.FromNet,
|
|
Net2: n.ToNet,
|
|
Interface: n.Interface,
|
|
Comment: n.Comment,
|
|
})
|
|
}
|
|
for _, n := range rc.NAT {
|
|
cfg.StaticNAT = append(cfg.StaticNAT, config.StaticNAT{
|
|
External: n.External,
|
|
Internal: n.Internal,
|
|
Interface: n.Interface,
|
|
Comment: n.Comment,
|
|
})
|
|
}
|
|
|
|
for _, h := range rc.Hosts {
|
|
cfg.Hosts = append(cfg.Hosts, config.Host{
|
|
Zone: h.Zone, Interface: h.Interface, Addresses: h.Addresses,
|
|
Exclusions: h.Exclusions, Dynamic: h.Dynamic,
|
|
})
|
|
}
|
|
for _, p := range rc.Providers {
|
|
cfg.Providers = append(cfg.Providers, config.Provider{
|
|
Name: p.Name, Number: p.Number, Mark: p.Mark, Duplicate: p.Duplicate,
|
|
Interface: p.Interface, Gateway: p.Gateway, Copy: p.Copy,
|
|
})
|
|
}
|
|
for _, r := range rc.Routes {
|
|
cfg.Routes = append(cfg.Routes, config.StaticRoute{
|
|
Provider: r.Provider, Dest: r.Dest, Gateway: r.Gateway,
|
|
Device: r.Oif, Persistent: r.Persistent, Comment: r.Comment,
|
|
})
|
|
}
|
|
for _, r := range rc.RoutingRules {
|
|
cfg.RoutingRules = append(cfg.RoutingRules, config.RoutingRule{
|
|
Source: r.Source, Dest: r.Dest, Provider: r.Provider, Priority: r.Priority,
|
|
Persistent: r.Persistent, Mark: r.Mark, Comment: r.Comment,
|
|
})
|
|
}
|
|
|
|
for _, t := range rc.Tunnels {
|
|
cfg.Tunnels = append(cfg.Tunnels, config.Tunnel{
|
|
Type: t.Type, Zone: t.Zone, Gateways: t.Gateways,
|
|
GatewayZones: t.GatewayZones, Port: t.Port, Comment: t.Comment,
|
|
})
|
|
}
|
|
for _, r := range rc.StoppedRules {
|
|
cfg.StoppedRules = append(cfg.StoppedRules, config.StoppedRule{
|
|
Action: config.StoppedAction(r.Action), Source: r.Source, Dest: r.Dest,
|
|
Proto: r.Proto, DPort: config.PortSpec(r.DPort), SPort: config.PortSpec(r.SPort), Comment: r.Comment,
|
|
})
|
|
}
|
|
for _, p := range rc.ProxyARP {
|
|
cfg.ProxyARP = append(cfg.ProxyARP, config.ProxyARP{
|
|
Address: p.Address, Interface: p.Interface, External: p.External,
|
|
HaveRoute: p.HaveRoute, Persistent: p.Persistent, Comment: p.Comment,
|
|
})
|
|
}
|
|
for _, p := range rc.ProxyNDP {
|
|
cfg.ProxyNDP = append(cfg.ProxyNDP, config.ProxyNDP{
|
|
Address: p.Address, Interface: p.Interface, External: p.External,
|
|
HaveRoute: p.HaveRoute, Persistent: p.Persistent, Comment: p.Comment,
|
|
})
|
|
}
|
|
for _, a := range rc.ArpRules {
|
|
cfg.ArpRules = append(cfg.ArpRules, config.ArpRule{
|
|
Action: config.ArpAction(a.Action), ActionAddress: a.ActionAddress, ActionMAC: a.ActionMAC,
|
|
Source: a.Source, Dest: a.Dest, Opcode: a.Opcode, Comment: a.Comment,
|
|
})
|
|
}
|
|
for _, m := range rc.Maclist {
|
|
cfg.Maclist = append(cfg.Maclist, config.MaclistEntry{
|
|
Action: config.MaclistAction(m.Action), Interface: m.Interface, MAC: m.MAC,
|
|
Addresses: m.Addresses, Log: m.Log, Comment: m.Comment,
|
|
})
|
|
}
|
|
|
|
for _, m := range rc.Mangle {
|
|
mr := config.MangleRule{
|
|
Action: config.MangleAction(m.Action), Chain: config.MangleChain(m.Chain), MarkValue: m.MarkValue,
|
|
Source: m.Source, Dest: m.Dest, Proto: m.Proto, DPort: config.PortSpec(m.DPort), SPort: config.PortSpec(m.SPort),
|
|
User: m.User, Mark: m.Mark, Length: m.Length, TOS: m.TOS, Helper: m.Helper, Comment: m.Comment,
|
|
}
|
|
if m.Probability != nil {
|
|
mr.Probability = *m.Probability
|
|
}
|
|
cfg.Mangle = append(cfg.Mangle, mr)
|
|
}
|
|
for _, a := range rc.Accounting {
|
|
cfg.Accounting = append(cfg.Accounting, config.AccountingRule{
|
|
Action: config.AccountingAction(a.Action), Section: config.AccountingSection(a.Section), Chain: a.Chain,
|
|
Source: a.Source, Dest: a.Dest, Proto: a.Proto, DPort: config.PortSpec(a.DPort), SPort: config.PortSpec(a.SPort),
|
|
Mark: a.Mark, Comment: a.Comment,
|
|
})
|
|
}
|
|
for _, t := range rc.TCDevices {
|
|
cfg.TCDevices = append(cfg.TCDevices, config.TCDevice{
|
|
Interface: t.Interface, InBandwidth: t.InBandwidth, OutBandwidth: t.OutBandwidth, Comment: t.Comment,
|
|
})
|
|
}
|
|
for _, t := range rc.TCClasses {
|
|
cfg.TCClasses = append(cfg.TCClasses, config.TCClass{
|
|
Interface: t.Interface, Mark: t.Mark, Rate: t.Rate, Ceil: t.Ceil, Priority: t.Priority, Comment: t.Comment,
|
|
})
|
|
}
|
|
for _, t := range rc.TCFilters {
|
|
cfg.TCFilters = append(cfg.TCFilters, config.TCFilter{
|
|
Class: t.Class, Source: t.Source, Dest: t.Dest, Proto: t.Proto,
|
|
DPort: config.PortSpec(t.DPort), SPort: config.PortSpec(t.SPort),
|
|
TOS: t.TOS, Length: t.Length, Priority: t.Priority, Comment: t.Comment,
|
|
})
|
|
}
|
|
for _, t := range rc.TCInterfaces {
|
|
cfg.TCInterfaces = append(cfg.TCInterfaces, config.TCInterface{
|
|
Interface: t.Interface, Type: t.Type, InBandwidth: t.InBandwidth, OutBandwidth: t.OutBandwidth, Comment: t.Comment,
|
|
})
|
|
}
|
|
for _, t := range rc.TCPriorities {
|
|
cfg.TCPriorities = append(cfg.TCPriorities, config.TCPriority{
|
|
Band: t.Band, Proto: t.Proto, DPort: config.PortSpec(t.DPort), SPort: config.PortSpec(t.SPort),
|
|
Address: t.Address, Interface: t.Interface, Helper: t.Helper, Comment: t.Comment,
|
|
})
|
|
}
|
|
|
|
for _, b := range rc.Blrules {
|
|
cfg.Blrules = append(cfg.Blrules, config.BlruleRule{
|
|
Action: config.BlruleAction(b.Action), Source: b.Source, Dest: b.Dest, Proto: b.Proto,
|
|
DPort: config.PortSpec(b.DPort), SPort: config.PortSpec(b.SPort), Log: b.Log, Comment: b.Comment,
|
|
})
|
|
}
|
|
for _, c := range rc.Conntrack {
|
|
cfg.Conntrack = append(cfg.Conntrack, config.ConntrackRule{
|
|
Action: config.ConntrackAction(c.Action), Source: c.Source, Dest: c.Dest, Proto: c.Proto,
|
|
DPort: config.PortSpec(c.DPort), SPort: config.PortSpec(c.SPort),
|
|
Chain: config.ConntrackChain(c.Chain), Helper: c.Helper, User: c.User, Comment: c.Comment,
|
|
})
|
|
}
|
|
for _, sm := range rc.Secmarks {
|
|
cfg.Secmarks = append(cfg.Secmarks, config.SecmarkRule{
|
|
Secmark: sm.Secmark, Chain: sm.Chain, Source: sm.Source, Dest: sm.Dest, Proto: sm.Proto,
|
|
DPort: config.PortSpec(sm.DPort), SPort: config.PortSpec(sm.SPort), Comment: sm.Comment,
|
|
})
|
|
}
|
|
if len(rc.Vars) > 0 {
|
|
cfg.Vars = make(map[string]string, len(rc.Vars))
|
|
for k, v := range rc.Vars {
|
|
cfg.Vars[k] = v
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// translateSNAT expands a rendered SNAT (which carries a list of egress
|
|
// interfaces and source CIDRs) into native tomswall SNAT rules — one per
|
|
// (egress interface, source) pair, since a native rule takes a single Dest.
|
|
func translateSNAT(s RenderedSNAT) []config.SNATRule {
|
|
sources := s.Source
|
|
if len(sources) == 0 {
|
|
sources = []string{""}
|
|
}
|
|
var out []config.SNATRule
|
|
for _, egress := range s.Egress {
|
|
for _, src := range sources {
|
|
r := config.SNATRule{
|
|
Action: config.SNATAction(s.Action),
|
|
Source: src,
|
|
Dest: egress,
|
|
Address: s.Address,
|
|
Comment: s.Comment,
|
|
}
|
|
if s.Probability != nil {
|
|
r.Probability = *s.Probability
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// indexSets maps set name -> concrete member CIDRs (invalid members skipped).
|
|
func indexSets(sets []RenderedSet) map[string][]string {
|
|
m := make(map[string][]string, len(sets))
|
|
for _, s := range sets {
|
|
var members []string
|
|
for _, cidr := range s.staticMembers() {
|
|
if validateCIDR(cidr) == nil {
|
|
members = append(members, cidr)
|
|
}
|
|
}
|
|
m[s.Name] = members
|
|
}
|
|
return m
|
|
}
|
|
|
|
// addressesFor returns the union of concrete source/dest addresses for a
|
|
// direction's OR'd match elements. A match's addresses are its set members when
|
|
// a set is referenced, otherwise its zone subnets.
|
|
func addressesFor(matches []RenderedMatch, setMembers map[string][]string) []string {
|
|
seen := map[string]struct{}{}
|
|
var out []string
|
|
add := func(cidrs []string) {
|
|
for _, c := range cidrs {
|
|
if _, ok := seen[c]; ok {
|
|
continue
|
|
}
|
|
if validateCIDR(c) != nil {
|
|
continue
|
|
}
|
|
seen[c] = struct{}{}
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
for _, m := range matches {
|
|
if m.Set != "" {
|
|
add(setMembers[m.Set])
|
|
continue
|
|
}
|
|
add(m.Subnets)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// translateRule expands one rendered rule into address-matched tomswall rules.
|
|
func translateRule(rr RenderedRule, setMembers map[string][]string) ([]config.Rule, error) {
|
|
action, err := translateAction(rr.Action)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
srcAddrs := addressesFor(rr.Source, setMembers)
|
|
dstAddrs := addressesFor(rr.Dest, setMembers)
|
|
// A direction with no concrete addresses matches "any" for that side.
|
|
if len(srcAddrs) == 0 {
|
|
srcAddrs = []string{""}
|
|
}
|
|
if len(dstAddrs) == 0 {
|
|
dstAddrs = []string{""}
|
|
}
|
|
|
|
var out []config.Rule
|
|
for _, s := range srcAddrs {
|
|
for _, d := range dstAddrs {
|
|
out = append(out, config.Rule{
|
|
Action: action,
|
|
Source: anySpec(s),
|
|
Dest: anySpec(d),
|
|
Proto: rr.Proto,
|
|
DPort: config.PortSpec(rr.Ports),
|
|
Log: rr.Log,
|
|
Comment: rr.Comment,
|
|
})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// anySpec renders an interface-agnostic source/dest spec: "all" with an optional
|
|
// CIDR constraint.
|
|
func anySpec(cidr string) string {
|
|
if cidr == "" {
|
|
return "all"
|
|
}
|
|
return "all:" + cidr
|
|
}
|
|
|
|
func translateAction(a string) (config.RuleAction, error) {
|
|
switch config.RuleAction(a) {
|
|
case config.RuleAccept, config.RuleDrop, config.RuleReject,
|
|
config.RuleLog, config.RuleContinue, config.RuleCount:
|
|
return config.RuleAction(a), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported action %q", a)
|
|
}
|
|
}
|
|
|
|
func orDefault(v, def string) string {
|
|
if v == "" {
|
|
return def
|
|
}
|
|
return v
|
|
}
|