package nftables import ( "testing" "github.com/google/nftables/expr" "git.unkin.net/unkin/tomswall/internal/config" ) func TestSplitZoneSpec(t *testing.T) { tests := []struct { input string wantZone string wantAddr string }{ {"net", "net", ""}, {"net:192.168.1.0/24", "net", "192.168.1.0/24"}, {"loc:10.0.0.1", "loc", "10.0.0.1"}, {"fw", "fw", ""}, {"all", "all", ""}, {"dmz:2001:db8::/32", "dmz", "2001:db8::/32"}, {"", "", ""}, } for _, tt := range tests { zone, addr := splitZoneSpec(tt.input) if zone != tt.wantZone || addr != tt.wantAddr { t.Errorf("splitZoneSpec(%q) = (%q, %q), want (%q, %q)", tt.input, zone, addr, tt.wantZone, tt.wantAddr) } } } func TestParsePort(t *testing.T) { tests := []struct { input string want uint16 wantErr bool }{ {"22", 22, false}, {"80", 80, false}, {"443", 443, false}, {"65535", 65535, false}, {"0", 0, false}, {"1", 1, false}, {"65536", 0, true}, {"-1", 0, true}, {"abc", 0, true}, {"", 0, true}, {"99999", 0, true}, } for _, tt := range tests { got, err := parsePort(tt.input) if tt.wantErr { if err == nil { t.Errorf("parsePort(%q) = %d, want error", tt.input, got) } continue } if err != nil { t.Errorf("parsePort(%q) returned error: %v", tt.input, err) continue } if got != tt.want { t.Errorf("parsePort(%q) = %d, want %d", tt.input, got, tt.want) } } } func TestNewCompiler(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", LogLevel: "info", }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) if c == nil { t.Fatal("NewCompiler returned nil") } if c.cfg != cfg { t.Error("compiler cfg does not match input cfg") } } func TestCompiler_SelectChain(t *testing.T) { cfg := &config.Config{ Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) tests := []struct { src, dst, fw string want string }{ {"net", "fw", "fw", "input"}, {"fw", "net", "fw", "output"}, {"net", "loc", "fw", "forward"}, {"loc", "net", "fw", "forward"}, } for _, tt := range tests { got := c.selectChain(tt.src, tt.dst, tt.fw) if got != tt.want { t.Errorf("selectChain(%q, %q, %q) = %q, want %q", tt.src, tt.dst, tt.fw, got, tt.want) } } } func TestCompiler_ResolveZoneInterfaces(t *testing.T) { cfg := &config.Config{ Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, {Zone: "loc", Interface: "eth1"}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) ifaces := c.resolveZoneInterfaces("net") if len(ifaces) != 1 || ifaces[0] != "eth0" { t.Errorf("resolveZoneInterfaces(net) = %v, want [eth0]", ifaces) } ifaces = c.resolveZoneInterfaces("all") if len(ifaces) != 1 || ifaces[0] != "" { t.Errorf("resolveZoneInterfaces(all) = %v, want [\"\"]", ifaces) } ifaces = c.resolveZoneInterfaces("fw") if len(ifaces) != 1 || ifaces[0] != "" { t.Errorf("resolveZoneInterfaces(fw) = %v, want [\"\"]", ifaces) } } func TestCompiler_ExpandZoneRef(t *testing.T) { cfg := &config.Config{ Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) zones := c.expandZoneRef("net") if len(zones) != 1 || zones[0] != "net" { t.Errorf("expandZoneRef(net) = %v, want [net]", zones) } zones = c.expandZoneRef("all") if len(zones) != 3 { t.Errorf("expandZoneRef(all) = %v, want 3 zones", zones) } seen := make(map[string]bool) for _, z := range zones { seen[z] = true } for _, name := range []string{"fw", "net", "loc"} { if !seen[name] { t.Errorf("expandZoneRef(all) missing zone %q", name) } } zones = c.expandZoneRef("all+") if len(zones) != 3 { t.Errorf("expandZoneRef(all+) = %v, want 3 zones", zones) } } func TestMatchSourceCIDR_IPv6(t *testing.T) { tests := []struct { input string wantLen int wantErr bool }{ {"192.168.1.0/24", 3, false}, {"10.0.0.1", 2, false}, {"fd10:10:9::/64", 3, false}, {"2001:db8::1", 2, false}, {"fd74:212::/48", 3, false}, {"invalid", 0, true}, } for _, tt := range tests { exprs, err := matchSourceCIDR(tt.input) if tt.wantErr { if err == nil { t.Errorf("matchSourceCIDR(%q) should fail", tt.input) } continue } if err != nil { t.Errorf("matchSourceCIDR(%q) error: %v", tt.input, err) continue } if len(exprs) != tt.wantLen { t.Errorf("matchSourceCIDR(%q) returned %d expressions, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestMatchDestCIDR_IPv6(t *testing.T) { tests := []struct { input string wantLen int wantErr bool }{ {"192.168.1.0/24", 3, false}, {"10.0.0.1", 2, false}, {"fd10:10:9::/64", 3, false}, {"2001:db8::1", 2, false}, {"invalid", 0, true}, } for _, tt := range tests { exprs, err := matchDestCIDR(tt.input) if tt.wantErr { if err == nil { t.Errorf("matchDestCIDR(%q) should fail", tt.input) } continue } if err != nil { t.Errorf("matchDestCIDR(%q) error: %v", tt.input, err) continue } if len(exprs) != tt.wantLen { t.Errorf("matchDestCIDR(%q) returned %d expressions, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestParsePortOrRange(t *testing.T) { tests := []struct { input string wantLen int wantErr bool }{ {"80", 2, false}, {"443", 2, false}, {"1024-65535", 3, false}, {"80-90", 3, false}, {"abc", 0, true}, {"80-abc", 0, true}, } for _, tt := range tests { exprs, err := parsePortOrRange(tt.input) if tt.wantErr { if err == nil { t.Errorf("parsePortOrRange(%q) should fail", tt.input) } continue } if err != nil { t.Errorf("parsePortOrRange(%q) error: %v", tt.input, err) continue } if len(exprs) != tt.wantLen { t.Errorf("parsePortOrRange(%q) returned %d expressions, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestParseSPortOrRange(t *testing.T) { tests := []struct { input string wantLen int wantErr bool }{ {"22", 2, false}, {"1024-65535", 3, false}, {"bad", 0, true}, } for _, tt := range tests { exprs, err := parseSPortOrRange(tt.input) if tt.wantErr { if err == nil { t.Errorf("parseSPortOrRange(%q) should fail", tt.input) } continue } if err != nil { t.Errorf("parseSPortOrRange(%q) error: %v", tt.input, err) continue } if len(exprs) != tt.wantLen { t.Errorf("parseSPortOrRange(%q) returned %d expressions, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestMatchIfaceName_Wildcard(t *testing.T) { exact := matchIfaceName(true, "eth0") if len(exact) != 2 { t.Fatalf("matchIfaceName(true, eth0) returned %d exprs, want 2", len(exact)) } wild := matchIfaceName(true, "tun+") if len(wild) != 2 { t.Fatalf("matchIfaceName(true, tun+) returned %d exprs, want 2", len(wild)) } } func TestMatchCtState(t *testing.T) { exprs := matchCtState(ctStateEstablished | ctStateRelated) if len(exprs) != 3 { t.Errorf("matchCtState returned %d expressions, want 3", len(exprs)) } } func TestCompile_ConntrackFastPath(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, chain := range []string{"input", "forward", "output"} { rules := state.Rules[chain] foundFastpath := false foundInvalid := false for _, r := range rules { if r.Tag == "ct:fastpath:"+chain { foundFastpath = true } if r.Tag == "ct:invalid:"+chain { foundInvalid = true } } if !foundFastpath { t.Errorf("chain %q missing ct:fastpath rule", chain) } if !foundInvalid { t.Errorf("chain %q missing ct:invalid rule", chain) } } } func TestCompile_IntraZone(t *testing.T) { routeback := true cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "loc", Interface: "eth1", Options: config.InterfaceOptions{RouteBack: &routeback}}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["forward"] { if r.Tag == "intra:loc:eth1" { found = true break } } if !found { t.Error("no intra-zone rule found for loc/eth1 in forward chain") } } func TestCompile_DNAT(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, {Zone: "loc", Interface: "eth1"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleDNAT, Source: "net", Dest: "loc:192.168.1.5:22", Proto: "tcp", DPort: config.PortSpec{"2222"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["prerouting"] { if r.Tag == "rule:0" { found = true break } } if !found { t.Error("no DNAT rule found in prerouting chain") } } func TestCompile_StaticNAT(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, StaticNAT: []config.StaticNAT{ { External: "203.0.113.10", Interface: "eth0", Internal: "192.168.1.10", }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } dnatFound := false snatFound := false for _, r := range state.Rules["prerouting"] { if r.Tag == "staticnat:dnat:0" { dnatFound = true } } for _, r := range state.Rules["postrouting"] { if r.Tag == "staticnat:snat:0" { snatFound = true } } if !dnatFound { t.Error("no static NAT DNAT rule found in prerouting") } if !snatFound { t.Error("no static NAT SNAT rule found in postrouting") } } func TestCompile_Logging(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "net", Dest: "all", Action: config.PolicyDrop, Log: "info"}, {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["input"] { if r.Tag == "policy:0" { found = true if len(r.Exprs) < 4 { t.Errorf("policy:0 has %d exprs, want >= 4 (iface + log + verdict)", len(r.Exprs)) } break } } if !found { t.Error("policy:0 not found in input chain") } } func TestCompile_ConntrackNoTrack(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Conntrack: []config.ConntrackRule{ { Action: config.ConntrackNoTrack, Source: "net", Dest: "fw", Proto: "udp", DPort: config.PortSpec{"53"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["prerouting"] { if r.Tag == "conntrack:0:prerouting" { found = true break } } if !found { t.Error("no notrack rule found in prerouting chain") } } func TestLogLevelToNF(t *testing.T) { tests := []struct { input string want expr.LogLevel }{ {"emerg", expr.LogLevelEmerg}, {"alert", expr.LogLevelAlert}, {"crit", expr.LogLevelCrit}, {"err", expr.LogLevelErr}, {"error", expr.LogLevelErr}, {"warn", expr.LogLevelWarning}, {"warning", expr.LogLevelWarning}, {"notice", expr.LogLevelNotice}, {"info", expr.LogLevelInfo}, {"debug", expr.LogLevelDebug}, {"unknown", expr.LogLevelWarning}, } for _, tt := range tests { got := logLevelToNF(tt.input) if got != tt.want { t.Errorf("logLevelToNF(%q) = %d, want %d", tt.input, got, tt.want) } } } func TestDiffEngine_DetectsModifications(t *testing.T) { current := &FirewallState{ Rules: map[string][]ManagedRule{ "input": { {Chain: "input", Tag: "rule:0", Exprs: []expr.Any{ &expr.Verdict{Kind: expr.VerdictAccept}, }}, }, }, } desired := &FirewallState{ Rules: map[string][]ManagedRule{ "input": { {Chain: "input", Tag: "rule:0", Exprs: []expr.Any{ &expr.Verdict{Kind: expr.VerdictDrop}, }}, }, }, } cs := computeDiff(current, desired) if len(cs.Remove) != 1 { t.Errorf("expected 1 removal, got %d", len(cs.Remove)) } if len(cs.Add) != 1 { t.Errorf("expected 1 addition, got %d", len(cs.Add)) } } func TestDiffEngine_NoChangeWhenIdentical(t *testing.T) { state := &FirewallState{ Rules: map[string][]ManagedRule{ "input": { {Chain: "input", Tag: "rule:0", Exprs: []expr.Any{ &expr.Verdict{Kind: expr.VerdictAccept}, }}, }, }, } cs := computeDiff(state, state) if !cs.Empty() { t.Errorf("expected empty changeset, got %d adds and %d removes", len(cs.Add), len(cs.Remove)) } } func TestCompile_SPortMatching(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, SPort: config.PortSpec{"1024-65535"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { found = true break } } if !found { t.Error("rule with sport not found in input chain") } } func TestCompile_PortRange(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"1024-65535"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { found = true break } } if !found { t.Error("rule with port range not found") } } func TestCompile_LogAction(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleLog, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, Log: "info", }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { found = true break } } if !found { t.Error("log rule not found") } } func TestMatchSection(t *testing.T) { tests := []struct { section config.RuleSection wantLen int }{ {config.SectionEstablished, 3}, {config.SectionRelated, 3}, {config.SectionInvalid, 3}, {config.SectionUntracked, 3}, {config.SectionNew, 3}, {config.SectionAll, 0}, {"", 0}, } for _, tt := range tests { exprs := matchSection(tt.section) if len(exprs) != tt.wantLen { t.Errorf("matchSection(%q) returned %d exprs, want %d", tt.section, len(exprs), tt.wantLen) } } } func TestCompile_RuleSection(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, Section: config.SectionEstablished, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { if len(r.Exprs) < 6 { t.Errorf("rule with section should have >= 6 exprs (iface+proto+dport+ctstate+verdict), got %d", len(r.Exprs)) } return } } t.Error("rule:0 not found in input chain") } func TestParseRateLimit(t *testing.T) { tests := []struct { input string wantLen int }{ {"10/sec", 1}, {"5/min", 1}, {"100/hour", 1}, {"1000/day", 1}, {"s:10/sec:20", 1}, {"invalid", 0}, {"", 0}, } for _, tt := range tests { exprs := parseRateLimit(tt.input) if len(exprs) != tt.wantLen { t.Errorf("parseRateLimit(%q) returned %d exprs, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestCompile_RateLimit(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, RateLimit: "10/sec:5", }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { hasLimit := false for _, e := range r.Exprs { if _, ok := e.(*expr.Limit); ok { hasLimit = true } } if !hasLimit { t.Error("rule with rate_limit should have Limit expression") } return } } t.Error("rule:0 not found in input chain") } func TestNegatedAddress(t *testing.T) { exprs, err := matchSourceCIDR("!192.168.1.0/24") if err != nil { t.Fatalf("matchSourceCIDR(!192.168.1.0/24) error: %v", err) } if len(exprs) != 3 { t.Fatalf("expected 3 exprs, got %d", len(exprs)) } cmp := exprs[2].(*expr.Cmp) if cmp.Op != expr.CmpOpNeq { t.Errorf("negated address should use CmpOpNeq, got %v", cmp.Op) } exprs, err = matchDestCIDR("!10.0.0.1") if err != nil { t.Fatalf("matchDestCIDR(!10.0.0.1) error: %v", err) } if len(exprs) != 2 { t.Fatalf("expected 2 exprs, got %d", len(exprs)) } cmp = exprs[1].(*expr.Cmp) if cmp.Op != expr.CmpOpNeq { t.Errorf("negated address should use CmpOpNeq, got %v", cmp.Op) } } func TestRejectTCPRST(t *testing.T) { exprs := rejectExprs("tcp", config.FamilyINET) if len(exprs) != 1 { t.Fatalf("expected 1 expr, got %d", len(exprs)) } rej := exprs[0].(*expr.Reject) if rej.Type != 1 { t.Errorf("TCP reject should use NFT_REJECT_TCP_RST (1), got %d", rej.Type) } exprs = rejectExprs("udp", config.FamilyINET) rej = exprs[0].(*expr.Reject) if rej.Type != 2 { t.Errorf("non-TCP reject should use NFT_REJECT_ICMPX_UNREACH (2), got %d", rej.Type) } } func TestCompile_DHCP(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0", Options: config.InterfaceOptions{DHCP: true}}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } foundIn := false foundOut := false for _, r := range state.Rules["input"] { if r.Tag == "dhcp:in:eth0" || r.Tag == "dhcp:reply:eth0" { foundIn = true } } for _, r := range state.Rules["output"] { if r.Tag == "dhcp:out:eth0" { foundOut = true } } if !foundIn { t.Error("no DHCP input rule found for eth0") } if !foundOut { t.Error("no DHCP output rule found for eth0") } } func TestMatchUID(t *testing.T) { exprs := matchUID("1000") if len(exprs) != 2 { t.Fatalf("matchUID(1000) returned %d exprs, want 2", len(exprs)) } cmp := exprs[1].(*expr.Cmp) if cmp.Op != expr.CmpOpEq { t.Error("non-negated UID should use CmpOpEq") } exprs = matchUID("!0") if len(exprs) != 2 { t.Fatalf("matchUID(!0) returned %d exprs, want 2", len(exprs)) } cmp = exprs[1].(*expr.Cmp) if cmp.Op != expr.CmpOpNeq { t.Error("negated UID should use CmpOpNeq") } } func TestMatchMark(t *testing.T) { exprs := matchMark("0x10/0xff") if len(exprs) != 3 { t.Fatalf("matchMark(0x10/0xff) returned %d exprs, want 3 (load+bitwise+cmp)", len(exprs)) } exprs = matchMark("42") if len(exprs) != 2 { t.Fatalf("matchMark(42) returned %d exprs, want 2 (load+cmp)", len(exprs)) } exprs = matchMark("!5") cmp := exprs[1].(*expr.Cmp) if cmp.Op != expr.CmpOpNeq { t.Error("negated mark should use CmpOpNeq") } } func TestSetMarkExprs(t *testing.T) { exprs := setMarkExprs("0x10") if len(exprs) != 2 { t.Fatalf("setMarkExprs(0x10) returned %d exprs, want 2", len(exprs)) } exprs = setMarkExprs("0x10/0xff00") if len(exprs) != 3 { t.Fatalf("setMarkExprs(0x10/0xff00) returned %d exprs, want 3", len(exprs)) } } func TestCompile_Loopback(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } foundInput := false foundOutput := false for _, r := range state.Rules["input"] { if r.Tag == "loopback:input" { foundInput = true } } for _, r := range state.Rules["output"] { if r.Tag == "loopback:output" { foundOutput = true } } if !foundInput { t.Error("loopback:input rule not found") } if !foundOutput { t.Error("loopback:output rule not found") } } func TestCompile_AntiSpoof(t *testing.T) { tcpflags := true cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0", Options: config.InterfaceOptions{ NoSmurfs: true, TCPFlags: &tcpflags, }}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } foundSmurf := false foundFlags := false for _, r := range state.Rules["input"] { if r.Tag == "antismurf:eth0" { foundSmurf = true } if r.Tag == "tcpflags:eth0" { foundFlags = true } } if !foundSmurf { t.Error("antismurf rule not found") } if !foundFlags { t.Error("tcpflags rule not found") } } func TestCompile_MSSClamp(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "loc", Interface: "eth1", Options: config.InterfaceOptions{MSS: 1400}}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } found := false for _, r := range state.Rules["forward"] { if r.Tag == "mss:eth1" { found = true } } if !found { t.Error("MSS clamp rule not found in forward chain") } } func TestCompile_PolicyExclusion(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, {Zone: "loc", Interface: "eth1"}, }, Policy: []config.Policy{ {Source: "all!net", Dest: "all", Action: config.PolicyAccept}, {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["forward"] { if r.Tag == "policy:0" { return } } for _, r := range state.Rules["input"] { if r.Tag == "policy:0" { return } } for _, r := range state.Rules["output"] { if r.Tag == "policy:0" { return } } t.Error("policy:0 (all!net exclusion) not found in any chain") } func TestMatchICMPType(t *testing.T) { tests := []struct { input string wantLen int }{ {"echo-request", 2}, {"8", 2}, {"3/4", 4}, {"destination-unreachable", 2}, } for _, tt := range tests { exprs := matchICMPType(tt.input) if len(exprs) != tt.wantLen { t.Errorf("matchICMPType(%q) returned %d exprs, want %d", tt.input, len(exprs), tt.wantLen) } } } func TestCompile_ICMPRule(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "icmp", DPort: config.PortSpec{"echo-request"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { if len(r.Exprs) < 5 { t.Errorf("ICMP rule should have >= 5 exprs (iface+proto+icmptype+verdict), got %d", len(r.Exprs)) } return } } t.Error("rule:0 not found in input chain") } func TestMatchConnLimit(t *testing.T) { exprs := matchConnLimit("20") if len(exprs) != 1 { t.Fatalf("matchConnLimit(20) returned %d exprs, want 1", len(exprs)) } cl := exprs[0].(*expr.Connlimit) if cl.Count != 20 { t.Errorf("Connlimit.Count = %d, want 20", cl.Count) } exprs = matchConnLimit("d:10") cl = exprs[0].(*expr.Connlimit) if cl.Flags != 1 { t.Errorf("d: prefix should set Flags=1, got %d", cl.Flags) } } func TestCompile_ConnLimit(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleAccept, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"22"}, ConnLimit: "20", }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { hasConnLimit := false for _, e := range r.Exprs { if _, ok := e.(*expr.Connlimit); ok { hasConnLimit = true } } if !hasConnLimit { t.Error("rule with conn_limit should have Connlimit expression") } return } } t.Error("rule:0 not found in input chain") } func TestCompile_NFQUEUE(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleNFQueue, Source: "net", Dest: "fw", Proto: "tcp", DPort: config.PortSpec{"80"}, NFQueue: 1, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "rule:0" { hasQueue := false for _, e := range r.Exprs { if q, ok := e.(*expr.Queue); ok { hasQueue = true if q.Num != 1 { t.Errorf("Queue.Num = %d, want 1", q.Num) } } } if !hasQueue { t.Error("NFQUEUE rule should have Queue expression") } return } } t.Error("rule:0 not found in input chain") } func TestCompile_NONAT(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, "loc": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, {Zone: "loc", Interface: "eth1"}, }, Policy: []config.Policy{ {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, Rules: []config.Rule{ { Action: config.RuleNoNAT, Source: "net", Dest: "loc", Proto: "tcp", DPort: config.PortSpec{"80"}, }, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["forward"] { if r.Tag == "rule:0" { hasReturn := false for _, e := range r.Exprs { if v, ok := e.(*expr.Verdict); ok && v.Kind == expr.VerdictReturn { hasReturn = true } } if !hasReturn { t.Error("NONAT rule should have RETURN verdict") } return } } t.Error("rule:0 not found in forward chain") } func TestCompile_PolicyRateLimit(t *testing.T) { cfg := &config.Config{ Settings: config.Settings{ TableName: "test", AddressFamily: config.FamilyINET, }, Zones: map[string]config.Zone{ "fw": {Type: config.ZoneFirewall}, "net": {Type: config.ZoneIP}, }, Interfaces: []config.Interface{ {Zone: "net", Interface: "eth0"}, }, Policy: []config.Policy{ {Source: "net", Dest: "fw", Action: config.PolicyDrop, RateLimit: "5/sec"}, {Source: "all", Dest: "all", Action: config.PolicyDrop}, }, PortGroups: make(map[string]config.PortGroup), } c := NewCompiler(cfg) state, err := c.Compile() if err != nil { t.Fatalf("Compile() error: %v", err) } for _, r := range state.Rules["input"] { if r.Tag == "policy:0" { hasLimit := false for _, e := range r.Exprs { if _, ok := e.(*expr.Limit); ok { hasLimit = true } } if !hasLimit { t.Error("policy with rate_limit should have Limit expression") } return } } t.Error("policy:0 not found in input chain") }