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>
96 lines
1.8 KiB
Go
96 lines
1.8 KiB
Go
package firewall
|
|
|
|
import (
|
|
"net/netip"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRateLimiterAllow(t *testing.T) {
|
|
rl := newRateLimiter(3, time.Minute)
|
|
defer rl.Stop()
|
|
|
|
addr := netip.MustParseAddr("10.0.0.1")
|
|
|
|
for i := 0; i < 3; i++ {
|
|
if !rl.Allow(addr) {
|
|
t.Fatalf("call %d: expected Allow=true", i+1)
|
|
}
|
|
}
|
|
|
|
if rl.Allow(addr) {
|
|
t.Fatal("call 4: expected Allow=false (over limit)")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterDifferentIPs(t *testing.T) {
|
|
rl := newRateLimiter(2, time.Minute)
|
|
defer rl.Stop()
|
|
|
|
a := netip.MustParseAddr("10.0.0.1")
|
|
b := netip.MustParseAddr("10.0.0.2")
|
|
|
|
// Exhaust a's limit.
|
|
for i := 0; i < 2; i++ {
|
|
rl.Allow(a)
|
|
}
|
|
if rl.Allow(a) {
|
|
t.Fatal("a should be rate limited")
|
|
}
|
|
|
|
// b should be independent.
|
|
if !rl.Allow(b) {
|
|
t.Fatal("b should not be rate limited")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterWindowReset(t *testing.T) {
|
|
rl := newRateLimiter(2, time.Minute)
|
|
defer rl.Stop()
|
|
|
|
var fakeNow atomic.Int64
|
|
fakeNow.Store(time.Now().UnixNano())
|
|
rl.now = func() time.Time {
|
|
return time.Unix(0, fakeNow.Load())
|
|
}
|
|
|
|
addr := netip.MustParseAddr("10.0.0.1")
|
|
|
|
// Exhaust the limit.
|
|
rl.Allow(addr)
|
|
rl.Allow(addr)
|
|
if rl.Allow(addr) {
|
|
t.Fatal("should be rate limited")
|
|
}
|
|
|
|
// Advance past the window.
|
|
fakeNow.Add(int64(2 * time.Minute))
|
|
|
|
// Should be allowed again.
|
|
if !rl.Allow(addr) {
|
|
t.Fatal("should be allowed after window reset")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterCleanup(t *testing.T) {
|
|
rl := newRateLimiter(10, 50*time.Millisecond)
|
|
defer rl.Stop()
|
|
|
|
addr := netip.MustParseAddr("10.0.0.1")
|
|
rl.Allow(addr)
|
|
|
|
// Entry should exist.
|
|
if _, ok := rl.entries.Load(addr); !ok {
|
|
t.Fatal("entry should exist")
|
|
}
|
|
|
|
// Wait for 2*window + a cleanup cycle to pass.
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Entry should have been cleaned up.
|
|
if _, ok := rl.entries.Load(addr); ok {
|
|
t.Fatal("stale entry should have been cleaned up")
|
|
}
|
|
}
|