package firewall import ( "net/netip" "testing" "time" ) func TestEmptyFirewall(t *testing.T) { fw, err := New("", nil, nil, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() addrs := []string{"192.168.1.1", "10.0.0.1", "::1", "2001:db8::1"} for _, a := range addrs { addr := netip.MustParseAddr(a) if fw.Blocked(addr) { t.Fatalf("empty firewall blocked %s", addr) } } } func TestIPBlocking(t *testing.T) { fw, err := New("", []string{"192.0.2.1", "2001:db8::dead"}, nil, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() tests := []struct { addr string blocked bool }{ {"192.0.2.1", true}, {"192.0.2.2", false}, {"2001:db8::dead", true}, {"2001:db8::beef", false}, } for _, tt := range tests { addr := netip.MustParseAddr(tt.addr) if got := fw.Blocked(addr); got != tt.blocked { t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked) } } } func TestCIDRBlocking(t *testing.T) { fw, err := New("", nil, []string{"198.51.100.0/24", "2001:db8::/32"}, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() tests := []struct { addr string blocked bool }{ {"198.51.100.1", true}, {"198.51.100.254", true}, {"198.51.101.1", false}, {"2001:db8::1", true}, {"2001:db9::1", false}, } for _, tt := range tests { addr := netip.MustParseAddr(tt.addr) if got := fw.Blocked(addr); got != tt.blocked { t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked) } } } func TestIPv4MappedIPv6(t *testing.T) { fw, err := New("", []string{"192.0.2.1"}, nil, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() addr := netip.MustParseAddr("::ffff:192.0.2.1") if !fw.Blocked(addr) { t.Fatal("expected IPv4-mapped IPv6 address to be blocked") } } func TestInvalidIP(t *testing.T) { _, err := New("", []string{"not-an-ip"}, nil, nil, 0, 0) if err == nil { t.Fatal("expected error for invalid IP") } } func TestInvalidCIDR(t *testing.T) { _, err := New("", nil, []string{"not-a-cidr"}, nil, 0, 0) if err == nil { t.Fatal("expected error for invalid CIDR") } } func TestCombinedRules(t *testing.T) { fw, err := New("", []string{"10.0.0.1"}, []string{"192.168.0.0/16"}, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() tests := []struct { addr string blocked bool }{ {"10.0.0.1", true}, {"10.0.0.2", false}, {"192.168.1.1", true}, {"172.16.0.1", false}, } for _, tt := range tests { addr := netip.MustParseAddr(tt.addr) if got := fw.Blocked(addr); got != tt.blocked { t.Fatalf("Blocked(%s) = %v, want %v", tt.addr, got, tt.blocked) } } } func TestRateLimitBlocking(t *testing.T) { fw, err := New("", nil, nil, nil, 2, time.Minute) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() addr := netip.MustParseAddr("10.0.0.1") if fw.Blocked(addr) { t.Fatal("first request should be allowed") } if fw.Blocked(addr) { t.Fatal("second request should be allowed") } if !fw.Blocked(addr) { t.Fatal("third request should be blocked (limit=2)") } } func TestRateLimitBlocklistFirst(t *testing.T) { // A blocklisted IP should be blocked without consuming rate limit quota. fw, err := New("", []string{"10.0.0.1"}, nil, nil, 1, time.Minute) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() blockedAddr := netip.MustParseAddr("10.0.0.1") otherAddr := netip.MustParseAddr("10.0.0.2") // Blocked by blocklist — should not touch the rate limiter. if !fw.Blocked(blockedAddr) { t.Fatal("blocklisted IP should be blocked") } // Other address should still have its full rate limit quota. if fw.Blocked(otherAddr) { t.Fatal("other IP should be allowed (within rate limit)") } if !fw.Blocked(otherAddr) { t.Fatal("other IP should be blocked after exceeding rate limit") } } func TestBlockedWithReason(t *testing.T) { fw, err := New("", []string{"10.0.0.1"}, []string{"192.168.0.0/16"}, nil, 2, time.Minute) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() tests := []struct { addr string wantBlock bool wantReason string }{ {"10.0.0.1", true, "ip"}, {"192.168.1.1", true, "cidr"}, {"172.16.0.1", false, ""}, } for _, tt := range tests { addr := netip.MustParseAddr(tt.addr) blocked, reason := fw.BlockedWithReason(addr) if blocked != tt.wantBlock { t.Fatalf("BlockedWithReason(%s) blocked = %v, want %v", tt.addr, blocked, tt.wantBlock) } if reason != tt.wantReason { t.Fatalf("BlockedWithReason(%s) reason = %q, want %q", tt.addr, reason, tt.wantReason) } } // Test rate limit reason: use a fresh IP that will exceed the limit. rlAddr := netip.MustParseAddr("10.10.10.10") fw.BlockedWithReason(rlAddr) // 1 fw.BlockedWithReason(rlAddr) // 2 blocked, reason := fw.BlockedWithReason(rlAddr) // 3 — should be blocked if !blocked { t.Fatal("expected rate limit block") } if reason != "rate_limit" { t.Fatalf("reason = %q, want %q", reason, "rate_limit") } } func TestRuntimeMutation(t *testing.T) { fw, err := New("", nil, nil, nil, 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } defer fw.Close() addr := netip.MustParseAddr("10.0.0.1") if fw.Blocked(addr) { t.Fatal("should not be blocked initially") } if err := fw.AddIP("10.0.0.1"); err != nil { t.Fatalf("add IP: %v", err) } if !fw.Blocked(addr) { t.Fatal("should be blocked after AddIP") } if err := fw.RemoveIP("10.0.0.1"); err != nil { t.Fatalf("remove IP: %v", err) } if fw.Blocked(addr) { t.Fatal("should not be blocked after RemoveIP") } }