Add per-IP rate limiting and Unix socket support for gRPC admin API
Rate limiting: per-source-IP connection rate limiter in the firewall layer with configurable limit and sliding window. Blocklisted IPs are rejected before rate limit evaluation to avoid wasting quota. Unix socket: the gRPC admin API can now listen on a Unix domain socket (no TLS required), secured by file permissions (0600), as a simpler alternative for local-only access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,11 @@ package firewall
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEmptyFirewall(t *testing.T) {
|
||||
fw, err := New("", nil, nil, nil)
|
||||
fw, err := New("", nil, nil, nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -22,7 +23,7 @@ func TestEmptyFirewall(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIPBlocking(t *testing.T) {
|
||||
fw, err := New("", []string{"192.0.2.1", "2001:db8::dead"}, nil, nil)
|
||||
fw, err := New("", []string{"192.0.2.1", "2001:db8::dead"}, nil, nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func TestIPBlocking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCIDRBlocking(t *testing.T) {
|
||||
fw, err := New("", nil, []string{"198.51.100.0/24", "2001:db8::/32"}, nil)
|
||||
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)
|
||||
}
|
||||
@@ -73,7 +74,7 @@ func TestCIDRBlocking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIPv4MappedIPv6(t *testing.T) {
|
||||
fw, err := New("", []string{"192.0.2.1"}, nil, nil)
|
||||
fw, err := New("", []string{"192.0.2.1"}, nil, nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -86,21 +87,21 @@ func TestIPv4MappedIPv6(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidIP(t *testing.T) {
|
||||
_, err := New("", []string{"not-an-ip"}, nil, nil)
|
||||
_, 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)
|
||||
_, 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)
|
||||
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)
|
||||
}
|
||||
@@ -124,8 +125,53 @@ func TestCombinedRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 TestRuntimeMutation(t *testing.T) {
|
||||
fw, err := New("", nil, nil, nil)
|
||||
fw, err := New("", nil, nil, nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user