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:
2026-03-17 14:37:21 -07:00
parent e84093b7fb
commit b25e1b0e79
16 changed files with 694 additions and 43 deletions

View File

@@ -5,9 +5,9 @@ import (
"net/netip"
"strings"
"sync"
"time"
"github.com/oschwald/maxminddb-golang"
)
type geoIPRecord struct {
@@ -23,17 +23,23 @@ type Firewall struct {
blockedCountries map[string]struct{}
geoDBPath string
geoDB *maxminddb.Reader
rl *rateLimiter
mu sync.RWMutex // protects all mutable state
}
// New creates a Firewall from raw rule lists and an optional GeoIP database path.
func New(geoIPPath string, ips, cidrs, countries []string) (*Firewall, error) {
// If rateLimit > 0, per-source-IP rate limiting is enabled with the given window.
func New(geoIPPath string, ips, cidrs, countries []string, rateLimit int64, rateWindow time.Duration) (*Firewall, error) {
f := &Firewall{
blockedIPs: make(map[netip.Addr]struct{}),
blockedCountries: make(map[string]struct{}),
geoDBPath: geoIPPath,
}
if rateLimit > 0 && rateWindow > 0 {
f.rl = newRateLimiter(rateLimit, rateWindow)
}
for _, ip := range ips {
addr, err := netip.ParseAddr(ip)
if err != nil {
@@ -89,6 +95,12 @@ func (f *Firewall) Blocked(addr netip.Addr) bool {
}
}
// Rate limiting is checked after blocklist — no point tracking state
// for already-blocked IPs.
if f.rl != nil && !f.rl.Allow(addr) {
return true
}
return false
}
@@ -190,6 +202,10 @@ func (f *Firewall) ReloadGeoIP() error {
// Close releases resources held by the firewall.
func (f *Firewall) Close() error {
if f.rl != nil {
f.rl.Stop()
}
f.mu.Lock()
defer f.mu.Unlock()