| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- package firewall
- import (
- "encoding/json"
- "os"
- "os/exec"
- "strings"
- "testing"
- "time"
- )
- func TestNativeValidationAndCriteria(t *testing.T) {
- for _, r := range []FilterRule{{Chain: "input", Action: "tarpit"}, {Chain: "input", Action: "accept", Layer7Protocol: "regex"}, {Chain: "input", Action: "drop", TLSHost: "*.example.com"}, {Chain: "input", Action: "accept", Protocol: "tcp\" -j ACCEPT"}, {Chain: "input", Action: "accept", ID: "id\" -j ACCEPT"}} {
- if validateRule("filter", r) == nil {
- t.Fatalf("accepted unsafe/unsupported rule: %+v", r)
- }
- }
- r := FilterRule{ID: "filter-test", Chain: "forward", Action: "drop", Protocol: "tcp", DstPort: "!80-90", AnyPort: "443", PacketMark: "!100", SrcMACAddress: "02:00:00:00:00:01", DSCP: "0", PacketSize: "64-1500", TCPFlags: "syn,!ack", Limit: "100,5:packet", Time: "08:00-17:00", Days: "mon,fri", Log: true, LogPrefix: "audit\"prefix"}
- if err := validateRule("filter", r); err != nil {
- t.Fatal(err)
- }
- payload := BuildIPTablesRestore([]FilterRule{r}, nil, nil)
- for _, want := range []string{"! --dport 80:90", "--ports 443", "-m mark ! --mark 100", "--mac-source 02:00:00:00:00:01", "--dscp 0", "--length 64:1500", "--tcp-flags SYN,ACK SYN", "--limit 100.000000/second", "--weekdays Mon,Fri", "-j LOG"} {
- if !strings.Contains(payload, want) {
- t.Errorf("missing %q in %s", want, payload)
- }
- }
- if validateRule("filter", FilterRule{Chain: "input", Action: "accept", Time: "24:00-25:00"}) == nil {
- t.Fatal("invalid time accepted")
- }
- if _, err := timeoutSeconds("1d"); err != nil {
- t.Fatal(err)
- }
- if GetIPSetName("a b") == GetIPSetName("a_b") {
- t.Fatal("colliding set names")
- }
- for _, s := range []ConnectionSelector{{}, {Protocol: "tcp", Src: "192.0.2.1", Dst: "198.51.100.1"}, {Protocol: "udp", Src: "192.0.2.1", Dst: "bad", Sport: 10, Dport: 11}} {
- if _, err := connectionDeleteArgs(s); err == nil {
- t.Fatal("incomplete tuple accepted")
- }
- }
- c := parseConntrackLine("ipv4 2 tcp 6 432000 ESTABLISHED src=192.0.2.1 dst=198.51.100.1 sport=123 dport=443 packets=4 bytes=400 src=198.51.100.1 dst=192.0.2.1 sport=443 dport=123 packets=3 bytes=300 [ASSURED] mark=7 zone=2")
- if c.Timeout != 432000 || c.Bytes != 700 || c.Packets != 7 || c.Zone != 2 || c.TCPState != "ESTABLISHED" {
- t.Fatalf("bad conntrack parse: %+v", c)
- }
- }
- func TestFirewallNativeKernel(t *testing.T) {
- if os.Getenv("DEBTIK_FIREWALL_NATIVE") != "1" {
- t.Skip("isolated mount/network namespaces only")
- }
- run := func(binary string, args ...string) string {
- t.Helper()
- out, err := exec.Command(binary, args...).CombinedOutput()
- if err != nil {
- t.Fatalf("%s %v: %s: %v", binary, args, out, err)
- }
- return string(out)
- }
- run("ip", "link", "set", "lo", "up")
- dir := t.TempDir()
- run("mount", "--bind", dir, "/etc/proxmox-router/firewall")
- meta := t.TempDir() + "/metadata.json"
- os.WriteFile(meta, []byte(`{"interfaces":{"wan0":{"role":"WAN"},"lan0":{"role":"LAN"}}}`), 0600)
- run("mount", "--bind", meta, "/etc/proxmox-router/metadata.json")
- os.MkdirAll(GeoIPZonesDir, 0755)
- os.WriteFile(GeoIPConfigFile, []byte(`{"auto_update":false,"interval_hours":24,"enabled_countries":[]}`), 0600)
- os.WriteFile(GeoIPZonesDir+"/gr.zone", []byte("192.0.2.0/24\n"), 0600)
- // Validate the kernel accepts every supported native match in a suitable chain.
- samples := []map[string]any{{"any_port": "443"}, {"log": true, "log_prefix": "audit\"safe"}, {"dst_port": "!80-90"}, {"packet_mark": "!100"}, {"connection_type": "ftp"}, {"connection_state": []string{"new"}, "connection_nat_state": []string{"dstnat"}}, {"content": "example.com"}, {"src_mac_address": "02:00:00:00:00:01"}, {"in_bridge_port": "lan0"}, {"out_bridge_port": "wan0"}, {"ipsec_policy": "in,none"}, {"dscp": "0"}, {"tcp_mss": "1200-1500"}, {"tos": "min-delay"}, {"packet_size": "64-1500"}, {"random": "25"}, {"tcp_flags": "syn,!ack"}, {"ttl": "greater-than:20"}, {"limit": "100,5:packet"}, {"time": "08:00-17:00", "days": "mon,fri"}}
- for _, sample := range samples {
- sample["id"] = "filter-fields"
- sample["chain"] = "forward"
- sample["action"] = "accept"
- sample["protocol"] = "tcp"
- b, _ := json.Marshal(sample)
- var r FilterRule
- json.Unmarshal(b, &r)
- if err := ApplyDualStack([]FilterRule{r}, nil, nil); err != nil {
- t.Fatalf("match %v failed: %v", sample, err)
- }
- }
- m := NewFirewallManager()
- if !m.applied {
- t.Fatal(m.lastError)
- }
- for _, r := range m.ListFilterRules() {
- if err := m.DeleteFilterRule(r.ID); err != nil {
- t.Fatal(err)
- }
- }
- if m = NewFirewallManager(); len(m.ListFilterRules()) != 0 {
- t.Fatal("empty filter list resurrected")
- }
- r := FilterRule{ID: "filter-count", Chain: "output", Action: "accept", Protocol: "icmp"}
- if err := m.SaveFilterRule(r); err != nil {
- t.Fatal(err)
- }
- run("ping", "-c", "1", "-W", "1", "127.0.0.1")
- before, _ := ReadRuleCounters(nil, nil, nil)
- if before[r.ID][0] == 0 {
- t.Fatal("no counter")
- }
- r.Comment = "comment change"
- if err := m.SaveFilterRule(r); err != nil {
- t.Fatal(err)
- }
- after, _ := ReadRuleCounters(nil, nil, nil)
- if after[r.ID][0] != before[r.ID][0] {
- t.Fatalf("counters lost: %v -> %v", before, after)
- }
- if err := m.ResetRuleCounters("filter", r.ID); err != nil {
- t.Fatal(err)
- }
- counts, _ := ReadRuleCounters(nil, nil, nil)
- if counts[r.ID][0] != 0 {
- t.Fatal("single reset failed")
- }
- // First marking rule must stop the managed mangle chain even after changing
- // the mark that formed part of its own criterion.
- for _, r := range []MangleRule{{ID: "mangle-first", Chain: "output", Action: "mark-packet", NewPacketMark: "100", Passthrough: false, Protocol: "icmp"}, {ID: "mangle-second", Chain: "output", Action: "mark-packet", NewPacketMark: "200", Passthrough: true, Protocol: "icmp"}} {
- if err := m.SaveMangleRule(r); err != nil {
- t.Fatal(err)
- }
- }
- run("ping", "-c", "1", "-W", "1", "127.0.0.1")
- counts, _ = ReadRuleCounters(nil, nil, nil)
- if counts["mangle-first"][0] == 0 || counts["mangle-second"][0] != 0 {
- t.Fatalf("passthrough=false failed: %v", counts)
- }
- if err := m.DeleteFilterRule(r.ID); err != nil {
- t.Fatal(err)
- }
- dyn := FilterRule{ID: "filter-dynamic", Chain: "output", Action: "add-src-to-address-list", AddressList: "dynamic", AddressListTimeout: "2s", Protocol: "icmp"}
- if err := m.SaveFilterRule(dyn); err != nil {
- t.Fatal(err)
- }
- run("ping", "-c", "1", "-W", "1", "127.0.0.1")
- run("ipset", "test", GetDynamicIPSetName("dynamic"), "127.0.0.1")
- visible := false
- for _, e := range m.ListAddressLists() {
- visible = visible || e.Dynamic && e.Address == "127.0.0.1"
- }
- if !visible {
- t.Fatal("dynamic address not visible")
- }
- dyn.Comment = "unrelated edit"
- if err := m.SaveFilterRule(dyn); err != nil {
- t.Fatal(err)
- }
- run("ipset", "test", GetDynamicIPSetName("dynamic"), "127.0.0.1")
- if err := m.SaveAddressListEntry(AddressListEntry{Name: "dynamic", Address: "127.0.0.2"}); err != nil {
- t.Fatal(err)
- }
- run("ipset", "test", GetIPSetName("dynamic"), staticIPSetName("dynamic"))
- run("ipset", "test", staticIPSetName("dynamic"), "127.0.0.2")
- time.Sleep(2200 * time.Millisecond)
- if exec.Command("ipset", "test", GetDynamicIPSetName("dynamic"), "127.0.0.1").Run() == nil {
- t.Fatal("dynamic address did not expire")
- }
- if err := m.DeleteFilterRule(dyn.ID); err != nil {
- t.Fatal(err)
- }
- if err := m.SaveFilterRule(FilterRule{ID: "filter-list-match", Chain: "output", Action: "drop", Protocol: "icmp", SrcAddressList: "dynamic"}); err != nil {
- t.Fatal(err)
- }
- if exec.Command("ping", "-I", "127.0.0.2", "-c", "1", "-W", "1", "127.0.0.1").Run() == nil {
- t.Fatal("static list member did not match union set")
- }
- if err := m.SaveFilterRule(FilterRule{ID: "filter-geo", Chain: "input", Action: "drop", SrcGeoIP: "GR"}); err != nil {
- t.Fatal(err)
- }
- if m.GeoIP().ToggleCountry("GR", false) == nil {
- t.Fatal("disabled a referenced GeoIP set")
- }
- run("ipset", "test", GetGeoIPSetName("GR"), "192.0.2.1")
- if err := m.GeoIP().BatchToggle("custom", true, []string{"ZZ"}); err == nil {
- t.Fatal("invalid batch falsely succeeded")
- }
- // IPv6 mangle ICMP and hop-limit are native IPv6 operations.
- if err := ApplyDualStack(nil, nil, []MangleRule{{IPVersion: 6, ID: "mangle-v6", Chain: "output", Action: "drop", Protocol: "icmp", Passthrough: true}}); err != nil {
- t.Fatal(err)
- }
- if exec.Command("ping", "-6", "-c", "1", "-W", "1", "::1").Run() == nil {
- t.Fatal("IPv6 ICMP drop did not match")
- }
- for _, r := range []MangleRule{{IPVersion: 6, ID: "mangle-ttl", Chain: "output", Action: "change-ttl", NewTTL: "64", Passthrough: true}, {ID: "mangle-mss", Chain: "forward", Protocol: "tcp", Action: "change-mss", NewMSS: "1400", Passthrough: true}, {ID: "mangle-dscp", Chain: "output", Action: "change-dscp", NewDSCP: 0, Passthrough: true}} {
- if err := ApplyDualStack(nil, nil, []MangleRule{r}); err != nil {
- t.Fatal(err)
- }
- }
- t.Log("Native firewall integration complete")
- }
- func TestNATPortProtocolMustBeExplicit(t *testing.T) {
- r := NATRule{Chain: "dstnat", Action: "dst-nat", Protocol: "all", DstPort: "53", ToAddresses: "192.0.2.53"}
- if err := validateRule("nat", r); err == nil || !strings.Contains(err.Error(), "TCP or UDP") {
- t.Fatalf("protocol all accepted: %v", err)
- }
- for _, protocol := range []string{"tcp", "udp"} {
- r.Protocol = protocol
- if err := validateRule("nat", r); err != nil {
- t.Fatal(err)
- }
- payload := BuildIPTablesRestore(nil, []NATRule{r}, nil)
- for _, want := range []string{"-p " + protocol, "--dport 53", "-j DNAT --to-destination 192.0.2.53"} {
- if !strings.Contains(payload, want) {
- t.Fatalf("missing %s: %s", want, payload)
- }
- }
- }
- }
- func TestTTLTargetSyntax(t *testing.T) {
- for _, tc := range []struct{ value, want string }{{"64", "--ttl-set 64"}, {"-1", "--ttl-dec 1"}, {"+1", "--ttl-inc 1"}} {
- r := MangleRule{Chain: "output", Action: "change-ttl", NewTTL: tc.value, Passthrough: true}
- if err := ValidateRule("mangle", r); err != nil {
- t.Fatalf("%s: %v", tc.value, err)
- }
- if payload := BuildIPTablesRestore(nil, nil, []MangleRule{r}); !strings.Contains(payload, tc.want) {
- t.Errorf("%s missing %s", tc.value, tc.want)
- }
- }
- }
|