Add example rate limit configuration to the sample config file,
demonstrating:
- Token bucket parameters: pathregexp, requests, per, burst
- Multiple rules per service with different strictness levels
- Documentation of per-L402 token ID scoping with IP fallback
Add comprehensive unit tests for the rate limiter implementation:
- TestRateLimiterBasic: Verify basic token bucket behavior
- TestRateLimiterNoMatchingRules: Requests pass when no rules match
- TestRateLimiterLRUEviction: Cache respects max size limit
- TestRateLimiterPathMatching: Different paths have independent limits
- TestRateLimiterMultipleRulesAllMustPass: Strictest matching rule wins
- TestRateLimiterPerKeyIsolation: Different users have separate quotas
- TestExtractRateLimitKeyIP/IPv6: IP-based key extraction
- TestRateLimitConfigRate/EffectiveBurst/Matches: Config calculations
- TestSendRateLimitResponseHTTP/GRPC: Response format verification
- TestRateLimiterTokenRefill: Token bucket refills over time
Add rate limiting integration to the aperture proxy:
- Service struct: Add RateLimits configuration field and rateLimiter
instance field
- prepareServices(): Validate rate limit config at startup, compile
path regexes, and initialize RateLimiter instances per service
- ServeHTTP(): Check rate limits after auth level determination but
before authentication, applying to all requests including
auth-whitelisted paths
- sendRateLimitResponse(): Return HTTP 429 with Retry-After header for
REST clients, or gRPC ResourceExhausted status for gRPC clients
The rate limiter key is extracted from the L402 token ID when present,
falling back to the client IP address for unauthenticated requests.
Implement a token-bucket rate limiter for aperture that limits requests
per service endpoint. The rate limiter uses golang.org/x/time/rate and
provides per-key limiting with L402 token ID extraction (falling back
to IP address for unauthenticated requests).
Key components:
- RateLimitConfig: Configuration struct with path regex, requests/per/burst
- RateLimiter: Manages per-key rate.Limiter instances with LRU eviction
to prevent memory exhaustion (default 10,000 entries)
- Prometheus metrics: allowed/denied counters, cache size, evictions
This addresses GitHub issue #200 for DoS protection on authenticated
endpoints that are free of charge after L402 payment.
Fix flaky tests. Reproducer:
go test -run TestHashMailServerReturnStream -count=20
TestHashMailServerReturnStream fails because the test cancels a read stream
and immediately dials RecvStream again expecting the same stream to be handed
out once the server returns it. The hashmail server implemented
RequestReadStream/RequestWriteStream with a non-blocking channel poll and
returned "read/write stream occupied" as soon as the mailbox was busy. That
raced with the deferred ReturnStream call and the reconnect often happened
before the stream got pushed back, so clients received the occupancy error
instead of the context cancellation they triggered.
Teach RequestReadStream/RequestWriteStream to wait for the stream to become
available (or the caller's context / server shutdown) with a bounded timeout.
If the wait expires we still return the "... stream occupied" error, so callers
that legitimately pile up can see that signal. The new streamAcquireTimeout
constant documents the policy, and the blocking select removes the race, so
reconnect attempts now either succeed or surface the original context error.
Register the Aperture instance created in setupAperture with t.Cleanup so
that every test stops its own server even if it fails. This keeps the global
HashMail stream map clean and prevents TestHashMailServerLargeMessage from
inheriting leftover streams from TestHashMailServerReturnStream.
This prevents cascading test failures, when a failure in one test is replicated
as many failures in many tests, complicating debugging from logs.
Go 1.25 tightened x509 validation and now rejects empty dNSName entries, causing
the default self-signed cert generation to fail when ServerName is left unset
(`x509: SAN dNSName is malformed`). Filter out empty host names before calling
cert.GenCertPair and reuse the same SAN list when renewing, allowing the default
config to keep working. Add a unit test that reproduces the failure.