Files
tomswall/internal/config/config_test.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

1010 lines
23 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
// baseConfig returns a minimal valid Config with a firewall zone and policy.
// Tests append their own fields to this base.
func baseConfig() Config {
return Config{
Settings: Settings{AddressFamily: FamilyINET},
Zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
Interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
{Zone: "loc", Interface: "eth1"},
},
Policy: []Policy{
{Source: "all", Dest: "all", Action: PolicyDrop},
},
}
}
// checkErr is a test helper that verifies error expectations.
func checkErr(t *testing.T, err error, wantErr string) {
t.Helper()
if wantErr == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", wantErr)
}
if !strings.Contains(err.Error(), wantErr) {
t.Fatalf("expected error containing %q, got: %v", wantErr, err)
}
}
// ---------------------------------------------------------------------------
// Load
// ---------------------------------------------------------------------------
func TestLoad_YAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
data := []byte(`
zones:
fw:
type: firewall
net:
type: ip
policy:
- source: net
dest: fw
action: drop
settings:
ip_forwarding: true
`)
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load YAML: %v", err)
}
if cfg.Zones["fw"].Type != ZoneFirewall {
t.Errorf("expected firewall zone type, got %q", cfg.Zones["fw"].Type)
}
if !cfg.Settings.IPForwarding {
t.Error("expected ip_forwarding to be true")
}
// applyDefaults should fill in missing fields.
if cfg.Settings.TableName != "tomswall" {
t.Errorf("expected default table name %q, got %q", "tomswall", cfg.Settings.TableName)
}
if cfg.Settings.LogLevel != "info" {
t.Errorf("expected default log level %q, got %q", "info", cfg.Settings.LogLevel)
}
}
func TestLoad_JSON(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
data := []byte(`{
"zones": {
"fw": {"type": "firewall"},
"net": {"type": "ip"}
},
"policy": [
{"source": "net", "dest": "fw", "action": "drop"}
]
}`)
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load JSON: %v", err)
}
if cfg.Zones["fw"].Type != ZoneFirewall {
t.Errorf("expected firewall zone type, got %q", cfg.Zones["fw"].Type)
}
if cfg.Settings.TableName != "tomswall" {
t.Errorf("expected default table name, got %q", cfg.Settings.TableName)
}
}
func TestLoad_FileNotFound(t *testing.T) {
_, err := Load("/nonexistent/path/config.yaml")
if err == nil {
t.Fatal("expected error for missing file")
}
}
func TestLoad_InvalidYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.yaml")
if err := os.WriteFile(path, []byte("{{bad yaml"), 0644); err != nil {
t.Fatal(err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for invalid YAML")
}
}
func TestLoad_InvalidJSON(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.json")
if err := os.WriteFile(path, []byte("{bad json}"), 0644); err != nil {
t.Fatal(err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
// ---------------------------------------------------------------------------
// Zones
// ---------------------------------------------------------------------------
func TestValidateZones(t *testing.T) {
tests := []struct {
name string
zones map[string]Zone
wantErr string
}{
{
name: "valid minimal",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
},
{
name: "all valid zone types",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"vpn": {Type: ZoneIPSec},
"br": {Type: ZoneBPort},
"lo": {Type: ZoneLoopback},
"loc": {Type: ZoneLocal},
},
},
{
name: "valid parent zone",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"sub": {Type: ZoneIP, Parents: []string{"net"}},
},
},
{
name: "no zones defined",
zones: map[string]Zone{},
wantErr: "no zones defined",
},
{
name: "missing firewall zone",
zones: map[string]Zone{
"net": {Type: ZoneIP},
"dmz": {Type: ZoneIP},
},
wantErr: "exactly one firewall zone required, found 0",
},
{
name: "two firewall zones",
zones: map[string]Zone{
"fw1": {Type: ZoneFirewall},
"fw2": {Type: ZoneFirewall},
},
wantErr: "exactly one firewall zone required, found 2",
},
{
name: "invalid zone name starts with digit",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"1bad": {Type: ZoneIP},
},
wantErr: "must start with a letter",
},
{
name: "invalid character in zone name",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net-1": {Type: ZoneIP},
},
wantErr: "invalid character",
},
{
name: "reserved zone name all",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"all": {Type: ZoneIP},
},
wantErr: "reserved name",
},
{
name: "reserved zone name none",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"none": {Type: ZoneIP},
},
wantErr: "reserved name",
},
{
name: "reserved zone name any",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"any": {Type: ZoneIP},
},
wantErr: "reserved name",
},
{
name: "reserved zone name SOURCE",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"SOURCE": {Type: ZoneIP},
},
wantErr: "reserved name",
},
{
name: "reserved zone name DEST",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"DEST": {Type: ZoneIP},
},
wantErr: "reserved name",
},
{
name: "unknown zone type",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: "bogus"},
},
wantErr: "unknown type",
},
{
name: "empty zone type",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ""},
},
wantErr: "type required",
},
{
name: "parent zone not defined",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP, Parents: []string{"missing"}},
},
wantErr: "parent zone \"missing\" not defined",
},
{
name: "firewall zone with options",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall, Options: []string{"notrack"}},
},
wantErr: "firewall zone does not accept options",
},
{
name: "firewall zone with in_options",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall, InOptions: []string{"notrack"}},
},
wantErr: "firewall zone does not accept options",
},
{
name: "firewall zone with out_options",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall, OutOptions: []string{"notrack"}},
},
wantErr: "firewall zone does not accept options",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{Zones: tt.zones}
err := cfg.validateZones()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// Interfaces
// ---------------------------------------------------------------------------
func TestValidateInterfaces(t *testing.T) {
tests := []struct {
name string
interfaces []Interface
wantErr string
}{
{
name: "valid single interface",
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
},
{
name: "valid empty list",
interfaces: nil,
},
{
name: "firewall zone forbidden",
interfaces: []Interface{
{Zone: "fw", Interface: "eth0"},
},
wantErr: "firewall zone must not be listed in interfaces",
},
{
name: "virtual interface rejected",
interfaces: []Interface{
{Zone: "net", Interface: "eth0:0"},
},
wantErr: "virtual interfaces",
},
{
name: "optional and required mutually exclusive",
interfaces: []Interface{
{Zone: "net", Interface: "eth0", Options: InterfaceOptions{Optional: true, Required: true}},
},
wantErr: "optional and required are mutually exclusive",
},
{
name: "zone not defined",
interfaces: []Interface{
{Zone: "missing", Interface: "eth0"},
},
wantErr: "zone \"missing\" not defined",
},
{
name: "duplicate interface",
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
{Zone: "net", Interface: "eth0"},
},
wantErr: "duplicate interface",
},
{
name: "empty interface name",
interfaces: []Interface{
{Zone: "net", Interface: ""},
},
wantErr: "interface name required",
},
{
name: "unmanaged with zone set",
interfaces: []Interface{
{Zone: "net", Interface: "eth0", Options: InterfaceOptions{Unmanaged: true}},
},
wantErr: "unmanaged interfaces must have an empty zone",
},
{
name: "unmanaged without zone is valid",
interfaces: []Interface{
{Zone: "", Interface: "eth0", Options: InterfaceOptions{Unmanaged: true}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := baseConfig()
cfg.Interfaces = tt.interfaces
err := cfg.validateInterfaces()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// Hosts
// ---------------------------------------------------------------------------
func TestValidateHosts(t *testing.T) {
tests := []struct {
name string
zones map[string]Zone
interfaces []Interface
hosts []Host
wantErr string
}{
{
name: "valid host",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "loc", Interface: "eth0", Addresses: []string{"192.168.1.0/24"}},
},
},
{
name: "valid dynamic host",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "loc", Interface: "eth0", Dynamic: true},
},
},
{
name: "firewall zone forbidden",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "fw", Interface: "eth0", Addresses: []string{"10.0.0.1"}},
},
wantErr: "firewall zone must not be listed in hosts",
},
{
name: "missing interface",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "loc", Interface: "eth99", Addresses: []string{"10.0.0.1"}},
},
wantErr: "interface \"eth99\" not defined in interfaces",
},
{
name: "zone not defined",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "missing", Interface: "eth0", Addresses: []string{"10.0.0.1"}},
},
wantErr: "zone \"missing\" not defined",
},
{
name: "no address and not dynamic",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "loc", Interface: "eth0"},
},
wantErr: "at least one address required",
},
{
name: "empty zone",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "", Interface: "eth0", Addresses: []string{"10.0.0.1"}},
},
wantErr: "zone required",
},
{
name: "empty interface",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
"loc": {Type: ZoneIP},
},
interfaces: []Interface{
{Zone: "net", Interface: "eth0"},
},
hosts: []Host{
{Zone: "loc", Interface: "", Addresses: []string{"10.0.0.1"}},
},
wantErr: "interface required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{
Zones: tt.zones,
Interfaces: tt.interfaces,
Hosts: tt.hosts,
Policy: []Policy{{Source: "net", Dest: "fw", Action: PolicyDrop}},
}
err := cfg.validateHosts()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// Port groups
// ---------------------------------------------------------------------------
func TestValidatePortGroups(t *testing.T) {
tests := []struct {
name string
portgroups map[string]PortGroup
wantErr string
}{
{
name: "valid tcp portgroup",
portgroups: map[string]PortGroup{
"web": {Proto: "tcp", Ports: PortSpec{"80", "443"}},
},
},
{
name: "valid udp portgroup",
portgroups: map[string]PortGroup{
"dns": {Proto: "udp", Ports: PortSpec{"53"}},
},
},
{
name: "valid port range",
portgroups: map[string]PortGroup{
"high": {Proto: "tcp", Ports: PortSpec{"1024-65535"}},
},
},
{
name: "missing proto",
portgroups: map[string]PortGroup{
"web": {Proto: "", Ports: PortSpec{"80"}},
},
wantErr: "proto required",
},
{
name: "invalid proto",
portgroups: map[string]PortGroup{
"web": {Proto: "icmp", Ports: PortSpec{"80"}},
},
wantErr: "proto must be tcp or udp",
},
{
name: "empty ports",
portgroups: map[string]PortGroup{
"web": {Proto: "tcp", Ports: PortSpec{}},
},
wantErr: "at least one port required",
},
{
name: "no portgroups is valid",
portgroups: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := baseConfig()
cfg.PortGroups = tt.portgroups
err := cfg.validatePortGroups()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// Policy
// ---------------------------------------------------------------------------
func TestValidatePolicy(t *testing.T) {
tests := []struct {
name string
zones map[string]Zone
policy []Policy
wantErr string
}{
{
name: "valid policy",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "net", Dest: "fw", Action: PolicyDrop},
{Source: "fw", Dest: "net", Action: PolicyAccept},
},
},
{
name: "valid with all keyword",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "all", Dest: "all", Action: PolicyReject},
},
},
{
name: "no policies",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: nil,
wantErr: "no policies defined",
},
{
name: "unknown action",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "net", Dest: "fw", Action: "explode"},
},
wantErr: "unknown action",
},
{
name: "source zone not defined",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "missing", Dest: "fw", Action: PolicyDrop},
},
wantErr: "source zone \"missing\" not defined",
},
{
name: "dest zone not defined",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "net", Dest: "missing", Action: PolicyDrop},
},
wantErr: "dest zone \"missing\" not defined",
},
{
name: "source required",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "", Dest: "fw", Action: PolicyDrop},
},
wantErr: "source required",
},
{
name: "dest required",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "net", Dest: "", Action: PolicyDrop},
},
wantErr: "dest required",
},
{
name: "valid all actions",
zones: map[string]Zone{
"fw": {Type: ZoneFirewall},
"net": {Type: ZoneIP},
},
policy: []Policy{
{Source: "net", Dest: "fw", Action: PolicyAccept},
{Source: "fw", Dest: "net", Action: PolicyDrop},
{Source: "all", Dest: "all", Action: PolicyReject},
{Source: "net", Dest: "net", Action: PolicyContinue},
{Source: "net", Dest: "net", Action: PolicyNone},
{Source: "all", Dest: "all", Action: PolicyQueue},
{Source: "all", Dest: "all", Action: PolicyNFQueue},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{Zones: tt.zones, Policy: tt.policy}
err := cfg.validatePolicy()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// Rules
// ---------------------------------------------------------------------------
func TestValidateRules(t *testing.T) {
tests := []struct {
name string
portgroups map[string]PortGroup
rules []Rule
wantErr string
}{
{
name: "valid accept rule",
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: PortSpec{"22"}},
},
},
{
name: "valid rule with portgroup",
portgroups: map[string]PortGroup{
"web": {Proto: "tcp", Ports: PortSpec{"80", "443"}},
},
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", PortGroup: "web"},
},
},
{
name: "unknown action",
rules: []Rule{
{Action: "explode", Source: "net", Dest: "fw"},
},
wantErr: "unknown action",
},
{
name: "portgroup mutually exclusive with proto",
portgroups: map[string]PortGroup{
"web": {Proto: "tcp", Ports: PortSpec{"80"}},
},
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", PortGroup: "web", Proto: "tcp"},
},
wantErr: "portgroup is mutually exclusive with proto/dport",
},
{
name: "portgroup mutually exclusive with dport",
portgroups: map[string]PortGroup{
"web": {Proto: "tcp", Ports: PortSpec{"80"}},
},
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", PortGroup: "web", DPort: PortSpec{"443"}},
},
wantErr: "portgroup is mutually exclusive with proto/dport",
},
{
name: "portgroup not defined",
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", PortGroup: "missing"},
},
wantErr: "portgroup \"missing\" not defined",
},
{
name: "user requires firewall source",
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: "fw", User: "nobody"},
},
wantErr: "user match only valid when source is the firewall zone",
},
{
name: "user with firewall source is valid",
rules: []Rule{
{Action: RuleAccept, Source: "fw", Dest: "net", User: "nobody", Proto: "tcp", DPort: PortSpec{"80"}},
},
},
{
name: "mark action requires set_mark",
rules: []Rule{
{Action: RuleMark, Source: "net", Dest: "fw"},
},
wantErr: "set_mark required for mark action",
},
{
name: "connmark action requires set_mark",
rules: []Rule{
{Action: RuleConnMark, Source: "net", Dest: "fw"},
},
wantErr: "set_mark required for connmark action",
},
{
name: "mark with set_mark is valid",
rules: []Rule{
{Action: RuleMark, Source: "net", Dest: "fw", SetMark: "0x1"},
},
},
{
name: "connmark with set_mark is valid",
rules: []Rule{
{Action: RuleConnMark, Source: "net", Dest: "fw", SetMark: "0x2/0xff"},
},
},
{
name: "source required",
rules: []Rule{
{Action: RuleAccept, Source: "", Dest: "fw"},
},
wantErr: "source required",
},
{
name: "dest required",
rules: []Rule{
{Action: RuleAccept, Source: "net", Dest: ""},
},
wantErr: "dest required",
},
{
name: "source zone not defined",
rules: []Rule{
{Action: RuleAccept, Source: "missing", Dest: "fw"},
},
wantErr: "source zone \"missing\" not defined",
},
{
name: "all keyword is valid source",
rules: []Rule{
{Action: RuleDrop, Source: "all", Dest: "all"},
},
},
{
name: "tarpit with non-tcp proto",
rules: []Rule{
{Action: RuleTarpit, Source: "net", Dest: "fw", Proto: "udp"},
},
wantErr: "tarpit only works with proto tcp",
},
{
name: "tarpit with tcp is valid",
rules: []Rule{
{Action: RuleTarpit, Source: "net", Dest: "fw", Proto: "tcp"},
},
},
{
name: "empty rules list is valid",
rules: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := baseConfig()
cfg.PortGroups = tt.portgroups
cfg.Rules = tt.rules
err := cfg.validateRules()
checkErr(t, err, tt.wantErr)
})
}
}
// ---------------------------------------------------------------------------
// SNAT
// ---------------------------------------------------------------------------
func TestValidateSNAT(t *testing.T) {
tests := []struct {
name string
snat []SNATRule
wantErr string
}{
{
name: "valid masquerade",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0"},
},
},
{
name: "valid snat with address",
snat: []SNATRule{
{Action: SNATAddress, Address: "1.2.3.4", Dest: "eth0"},
},
},
{
name: "valid continue",
snat: []SNATRule{
{Action: SNATContinue, Dest: "eth0"},
},
},
{
name: "valid log",
snat: []SNATRule{
{Action: SNATLog, Dest: "eth0"},
},
},
{
name: "unknown action",
snat: []SNATRule{
{Action: "bogus", Dest: "eth0"},
},
wantErr: "unknown action",
},
{
name: "address required for snat action",
snat: []SNATRule{
{Action: SNATAddress, Address: "", Dest: "eth0"},
},
wantErr: "address required for snat action",
},
{
name: "dest required",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: ""},
},
wantErr: "dest required",
},
{
name: "probability too high",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Probability: 1.5},
},
wantErr: "probability must be between 0",
},
{
name: "probability negative",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Probability: -0.5},
},
wantErr: "probability must be between 0",
},
{
name: "probability exactly 1 is valid",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Probability: 1.0},
},
},
{
name: "probability 0.5 is valid",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Probability: 0.5},
},
},
{
name: "probability 0 is valid (skipped)",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Probability: 0},
},
},
{
name: "port_range requires proto",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", PortRange: "1024-65535"},
},
wantErr: "port_range requires proto",
},
{
name: "persistent requires address",
snat: []SNATRule{
{Action: SNATMasquerade, Dest: "eth0", Persistent: true},
},
wantErr: "persistent requires an address",
},
{
name: "empty snat list is valid",
snat: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := baseConfig()
cfg.SNAT = tt.snat
err := cfg.validateSNAT()
checkErr(t, err, tt.wantErr)
})
}
}