package shorewall import ( "os" "path/filepath" "testing" "git.unkin.net/unkin/tomswall/internal/config" ) // writeFile is a helper to create a file in the temp dir. func writeFile(t *testing.T, dir, name, content string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { t.Fatal(err) } } // minimalShorewallDir creates a temp directory with the minimum shorewall config // files needed for Convert() to succeed. func minimalShorewallDir(t *testing.T) string { t.Helper() dir := t.TempDir() writeFile(t, dir, "shorewall.conf", ` IP_FORWARDING=Yes LOG_LEVEL=info `) writeFile(t, dir, "zones", ` fw firewall net ipv4 loc ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 dhcp,tcpflags,routefilter,nosmurfs loc eth1 tcpflags,nosmurfs `) writeFile(t, dir, "policy", ` loc net ACCEPT net all DROP info all all REJECT info `) // Create empty files for optional configs so ParseFile returns nil, nil // (they would return nil,nil on os.IsNotExist anyway). return dir } func TestConvert_MinimalConfig(t *testing.T) { dir := minimalShorewallDir(t) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } // Check zones if len(cfg.Zones) != 3 { t.Fatalf("expected 3 zones, got %d", len(cfg.Zones)) } fwZone, ok := cfg.Zones["fw"] if !ok { t.Fatal("expected fw zone") } if fwZone.Type != config.ZoneFirewall { t.Errorf("fw zone type = %q, want %q", fwZone.Type, config.ZoneFirewall) } netZone, ok := cfg.Zones["net"] if !ok { t.Fatal("expected net zone") } if netZone.Type != config.ZoneIP { t.Errorf("net zone type = %q, want %q", netZone.Type, config.ZoneIP) } locZone, ok := cfg.Zones["loc"] if !ok { t.Fatal("expected loc zone") } if locZone.Type != config.ZoneIP { t.Errorf("loc zone type = %q, want %q", locZone.Type, config.ZoneIP) } } func TestConvert_Interfaces(t *testing.T) { dir := minimalShorewallDir(t) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Interfaces) != 2 { t.Fatalf("expected 2 interfaces, got %d", len(cfg.Interfaces)) } // Check first interface (net eth0) netIface := cfg.Interfaces[0] if netIface.Zone != "net" { t.Errorf("interface 0 zone = %q, want %q", netIface.Zone, "net") } if netIface.Interface != "eth0" { t.Errorf("interface 0 name = %q, want %q", netIface.Interface, "eth0") } if !netIface.Options.DHCP { t.Error("interface 0 should have DHCP enabled") } if netIface.Options.TCPFlags == nil || !*netIface.Options.TCPFlags { t.Error("interface 0 should have tcpflags enabled") } if netIface.Options.RouteFilter == nil || *netIface.Options.RouteFilter != 1 { t.Error("interface 0 should have routefilter=1") } if !netIface.Options.NoSmurfs { t.Error("interface 0 should have nosmurfs enabled") } // Check second interface (loc eth1) locIface := cfg.Interfaces[1] if locIface.Zone != "loc" { t.Errorf("interface 1 zone = %q, want %q", locIface.Zone, "loc") } if locIface.Interface != "eth1" { t.Errorf("interface 1 name = %q, want %q", locIface.Interface, "eth1") } } func TestConvert_Policy(t *testing.T) { dir := minimalShorewallDir(t) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Policy) != 3 { t.Fatalf("expected 3 policies, got %d", len(cfg.Policy)) } // loc -> net ACCEPT p0 := cfg.Policy[0] if p0.Source != "loc" || p0.Dest != "net" { t.Errorf("policy 0: source=%q dest=%q, want loc/net", p0.Source, p0.Dest) } if p0.Action != config.PolicyAccept { t.Errorf("policy 0 action = %q, want %q", p0.Action, config.PolicyAccept) } // net -> all DROP info p1 := cfg.Policy[1] if p1.Source != "net" || p1.Dest != "all" { t.Errorf("policy 1: source=%q dest=%q, want net/all", p1.Source, p1.Dest) } if p1.Action != config.PolicyDrop { t.Errorf("policy 1 action = %q, want %q", p1.Action, config.PolicyDrop) } if p1.Log != "info" { t.Errorf("policy 1 log = %q, want %q", p1.Log, "info") } // all -> all REJECT info p2 := cfg.Policy[2] if p2.Action != config.PolicyReject { t.Errorf("policy 2 action = %q, want %q", p2.Action, config.PolicyReject) } } func TestConvert_Settings(t *testing.T) { dir := minimalShorewallDir(t) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if cfg.Settings.TableName != "tomswall" { t.Errorf("table name = %q, want %q", cfg.Settings.TableName, "tomswall") } if cfg.Settings.LogLevel != "info" { t.Errorf("log level = %q, want %q", cfg.Settings.LogLevel, "info") } if !cfg.Settings.IPForwarding { t.Error("IP forwarding should be enabled") } } func TestConvert_ParamsSubstitution(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "params", ` NET_IF=eth0 LOC_IF=eth1 `) writeFile(t, dir, "shorewall.conf", ` IP_FORWARDING=Yes `) writeFile(t, dir, "zones", ` fw firewall net ipv4 loc ipv4 `) writeFile(t, dir, "interfaces", ` net $NET_IF dhcp loc $LOC_IF - `) writeFile(t, dir, "policy", ` loc net ACCEPT all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } // Verify params were stored if cfg.Vars["NET_IF"] != "eth0" { t.Errorf("Vars[NET_IF] = %q, want %q", cfg.Vars["NET_IF"], "eth0") } if cfg.Vars["LOC_IF"] != "eth1" { t.Errorf("Vars[LOC_IF] = %q, want %q", cfg.Vars["LOC_IF"], "eth1") } // Verify substitution worked in interfaces if len(cfg.Interfaces) != 2 { t.Fatalf("expected 2 interfaces, got %d", len(cfg.Interfaces)) } if cfg.Interfaces[0].Interface != "eth0" { t.Errorf("interface 0 = %q, want %q (after param subst)", cfg.Interfaces[0].Interface, "eth0") } if cfg.Interfaces[1].Interface != "eth1" { t.Errorf("interface 1 = %q, want %q (after param subst)", cfg.Interfaces[1].Interface, "eth1") } } func TestConvert_ZonesWithParents(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 loc ipv4 dmz:net ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - loc eth1 - `) writeFile(t, dir, "policy", ` all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } dmz, ok := cfg.Zones["dmz"] if !ok { t.Fatal("expected dmz zone") } if len(dmz.Parents) != 1 || dmz.Parents[0] != "net" { t.Errorf("dmz parents = %v, want [net]", dmz.Parents) } } func TestConvert_Rules(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 loc ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - loc eth1 - `) writeFile(t, dir, "policy", ` all all DROP `) writeFile(t, dir, "rules", ` SECTION NEW ACCEPT net fw tcp 22 DROP net fw udp 53 `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Rules) != 2 { t.Fatalf("expected 2 rules, got %d", len(cfg.Rules)) } r0 := cfg.Rules[0] if r0.Action != config.RuleAccept { t.Errorf("rule 0 action = %q, want %q", r0.Action, config.RuleAccept) } if r0.Source != "net" { t.Errorf("rule 0 source = %q, want %q", r0.Source, "net") } if r0.Dest != "fw" { t.Errorf("rule 0 dest = %q, want %q", r0.Dest, "fw") } if r0.Proto != "tcp" { t.Errorf("rule 0 proto = %q, want %q", r0.Proto, "tcp") } if len(r0.DPort) != 1 || r0.DPort[0] != "22" { t.Errorf("rule 0 dport = %v, want [22]", r0.DPort) } if r0.Section != "new" { t.Errorf("rule 0 section = %q, want %q", r0.Section, "new") } r1 := cfg.Rules[1] if r1.Action != config.RuleDrop { t.Errorf("rule 1 action = %q, want %q", r1.Action, config.RuleDrop) } } func TestConvert_FWBuiltinVariable(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - `) writeFile(t, dir, "policy", ` $FW all ACCEPT net all DROP all all REJECT `) writeFile(t, dir, "rules", ` SECTION NEW ACCEPT net $FW tcp 22 `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if cfg.Vars["FW"] != "fw" { t.Errorf("Vars[FW] = %q, want %q", cfg.Vars["FW"], "fw") } if cfg.Policy[0].Source != "fw" { t.Errorf("policy 0 source = %q, want %q (after $FW substitution)", cfg.Policy[0].Source, "fw") } if cfg.Rules[0].Dest != "fw" { t.Errorf("rule 0 dest = %q, want %q (after $FW substitution)", cfg.Rules[0].Dest, "fw") } } func TestConvert_ConntrackHelperChain(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - `) writeFile(t, dir, "policy", ` all all DROP `) writeFile(t, dir, "conntrack", ` CT:helper:ftp:PO - - tcp 21 CT:helper:sip:P - - udp 5060 CT:helper:tftp:O - - udp 69 CT:helper:irc - - tcp 6667 `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Conntrack) != 4 { t.Fatalf("expected 4 conntrack rules, got %d", len(cfg.Conntrack)) } tests := []struct { helper string chain string }{ {"ftp", "both"}, {"sip", "prerouting"}, {"tftp", "output"}, {"irc", ""}, } for i, tt := range tests { if cfg.Conntrack[i].Helper != tt.helper { t.Errorf("conntrack[%d] helper = %q, want %q", i, cfg.Conntrack[i].Helper, tt.helper) } if string(cfg.Conntrack[i].Chain) != tt.chain { t.Errorf("conntrack[%d] chain = %q, want %q", i, cfg.Conntrack[i].Chain, tt.chain) } } } func TestSplitHelperChain(t *testing.T) { tests := []struct { input string wantName string wantChain string }{ {"ftp:PO", "ftp", "both"}, {"sip:P", "sip", "prerouting"}, {"tftp:O", "tftp", "output"}, {"irc", "irc", ""}, {"Q.931:PO", "Q.931", "both"}, {"netbios-ns:PO", "netbios-ns", "both"}, } for _, tt := range tests { name, chain := splitHelperChain(tt.input) if name != tt.wantName { t.Errorf("splitHelperChain(%q) name = %q, want %q", tt.input, name, tt.wantName) } if chain != tt.wantChain { t.Errorf("splitHelperChain(%q) chain = %q, want %q", tt.input, chain, tt.wantChain) } } } func TestConvert_MultiZoneRules(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 loc ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - loc eth1 - `) writeFile(t, dir, "policy", ` all all DROP `) writeFile(t, dir, "rules", ` SECTION NEW ACCEPT net,loc fw tcp 8080,8501 `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Rules) != 1 { t.Fatalf("expected 1 rule, got %d", len(cfg.Rules)) } if cfg.Rules[0].Source != "net,loc" { t.Errorf("rule 0 source = %q, want %q", cfg.Rules[0].Source, "net,loc") } if err := cfg.Validate(); err != nil { t.Errorf("multi-zone rule should validate: %v", err) } } func TestConvert_ImplicitContinue(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", ` IP_FORWARDING=Yes IMPLICIT_CONTINUE=Yes `) writeFile(t, dir, "zones", ` fw firewall net ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - `) writeFile(t, dir, "policy", ` all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if !cfg.Settings.ImplicitContinue { t.Error("ImplicitContinue should be true") } } // --- shorewall6 tests --- func TestConvert_IPv6Detection(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall6.conf", ` IP_FORWARDING=On IMPLICIT_CONTINUE=Yes `) writeFile(t, dir, "zones", ` fw firewall net ipv6 `) writeFile(t, dir, "interfaces", ` net eth0 dhcp,accept_ra `) writeFile(t, dir, "policy", ` all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if cfg.Settings.AddressFamily != config.FamilyIP6 { t.Errorf("AddressFamily = %q, want %q", cfg.Settings.AddressFamily, config.FamilyIP6) } } func TestConvert_IPv4Detection(t *testing.T) { dir := minimalShorewallDir(t) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if cfg.Settings.AddressFamily != config.FamilyIP { t.Errorf("AddressFamily = %q, want %q", cfg.Settings.AddressFamily, config.FamilyIP) } } func TestConvert_IPv6ProxyNDP(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall6.conf", `IP_FORWARDING=On`) writeFile(t, dir, "zones", ` fw firewall net ipv6 `) writeFile(t, dir, "interfaces", ` net eth0 - `) writeFile(t, dir, "policy", ` all all DROP `) writeFile(t, dir, "proxyndp", ` fd10::100 eth0 eth1 No Yes 2001:db8::1 eth2 eth3 Yes No `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.ProxyNDP) != 2 { t.Fatalf("expected 2 proxyndp entries, got %d", len(cfg.ProxyNDP)) } p0 := cfg.ProxyNDP[0] if p0.Address != "fd10::100" { t.Errorf("proxyndp[0] address = %q, want %q", p0.Address, "fd10::100") } if p0.Interface != "eth0" { t.Errorf("proxyndp[0] interface = %q, want %q", p0.Interface, "eth0") } if p0.External != "eth1" { t.Errorf("proxyndp[0] external = %q, want %q", p0.External, "eth1") } if p0.HaveRoute { t.Error("proxyndp[0] haveroute should be false") } if !p0.Persistent { t.Error("proxyndp[0] persistent should be true") } p1 := cfg.ProxyNDP[1] if !p1.HaveRoute { t.Error("proxyndp[1] haveroute should be true") } if p1.Persistent { t.Error("proxyndp[1] persistent should be false") } // IPv6 config should NOT have proxyarp entries if len(cfg.ProxyARP) != 0 { t.Errorf("IPv6 config should have 0 proxyarp entries, got %d", len(cfg.ProxyARP)) } } func TestConvert_IPv6AcceptRA(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall6.conf", `IP_FORWARDING=On`) writeFile(t, dir, "zones", ` fw firewall net ipv6 `) writeFile(t, dir, "interfaces", ` net eth0 dhcp,accept_ra=2 `) writeFile(t, dir, "policy", ` all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Interfaces) != 1 { t.Fatalf("expected 1 interface, got %d", len(cfg.Interfaces)) } opts := cfg.Interfaces[0].Options if opts.AcceptRA == nil { t.Fatal("accept_ra should be set") } if *opts.AcceptRA != 2 { t.Errorf("accept_ra = %d, want 2", *opts.AcceptRA) } } func TestConvert_IPv6ZoneTypes(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall6.conf", `IP_FORWARDING=On`) writeFile(t, dir, "zones", ` fw firewall net ipv6 vpn ipsec6 dmz bport6 `) writeFile(t, dir, "interfaces", ` net eth0 - vpn ipsec0 - dmz br0 - `) writeFile(t, dir, "policy", ` all all DROP `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if cfg.Zones["net"].Type != config.ZoneIP { t.Errorf("net zone type = %q, want %q", cfg.Zones["net"].Type, config.ZoneIP) } if cfg.Zones["vpn"].Type != config.ZoneIPSec { t.Errorf("vpn zone type = %q, want %q", cfg.Zones["vpn"].Type, config.ZoneIPSec) } if cfg.Zones["dmz"].Type != config.ZoneBPort { t.Errorf("dmz zone type = %q, want %q", cfg.Zones["dmz"].Type, config.ZoneBPort) } } func TestConvert_SecmarksConverter(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", `IP_FORWARDING=Yes`) writeFile(t, dir, "zones", ` fw firewall net ipv4 `) writeFile(t, dir, "interfaces", ` net eth0 - `) writeFile(t, dir, "policy", ` all all DROP `) writeFile(t, dir, "secmarks", ` system_u:object_r:http_t:s0 P net fw tcp 80 `) cfg, err := Convert(dir) if err != nil { t.Fatalf("Convert: %v", err) } if len(cfg.Secmarks) != 1 { t.Fatalf("expected 1 secmark, got %d", len(cfg.Secmarks)) } if cfg.Secmarks[0].Secmark != "system_u:object_r:http_t:s0" { t.Errorf("secmark = %q", cfg.Secmarks[0].Secmark) } if cfg.Secmarks[0].Proto != "tcp" { t.Errorf("proto = %q, want tcp", cfg.Secmarks[0].Proto) } } func TestIsIPv6Dir(t *testing.T) { t.Run("shorewall6 dir", func(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall6.conf", "IP_FORWARDING=On\n") if !IsIPv6Dir(dir) { t.Error("should detect IPv6 dir") } }) t.Run("shorewall dir", func(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "shorewall.conf", "IP_FORWARDING=Yes\n") if IsIPv6Dir(dir) { t.Error("should not detect IPv6 for shorewall dir") } }) }