8d9a76c751
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.
1534 lines
34 KiB
Go
1534 lines
34 KiB
Go
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")
|
|
}
|