feat(relay): enforce [limits] + [authorization] config sections
Wires the parsed-but-unenforced config sections into actual relay
behavior via five new IRelayPolicy implementations under
quartz-relay/.../policies/. They compose through the existing
PolicyStack so operators stack only what they need; cheap rejection
paths run before expensive ones (rate-limit → AUTH → future/size/lists
→ signature verification).
New policies:
- KindAllowDenyPolicy — kind_whitelist + kind_blacklist
- PubkeyAllowDenyPolicy — pubkey_whitelist + pubkey_blacklist
(case-insensitive)
- RejectFutureEventsPolicy — options.reject_future_seconds
- MaxEventBytesPolicy — limits.max_event_bytes
(size of canonical NIP-01 JSON form)
- RateLimitPolicy — token-bucket per session for
messages_per_sec + subscriptions_per_min;
monotonic clock so wall-time jumps
don't reset buckets.
Plus:
- LocalRelayServer now honors limits.max_ws_frame_bytes /
limits.max_ws_message_bytes via Ktor's WebSockets.maxFrameSize.
- Main.kt builds the policy stack from RelayConfig in composePolicy().
Warning surface is reduced to only the three sections still
pending (max_subscriptions_per_session, max_filters_per_req,
network.remote_ip_header).
- PassThroughPolicy base lets each policy declare only the hook(s)
it actually enforces, keeping call sites readable.
Tests (20 new):
- PoliciesTest (16) — per-policy unit coverage for each accept/reject
boundary, including allow + deny precedence, case-insensitive pubkey
matching, deterministic rate-limit refill via injected clock, and
composition via IRelayPolicy.plus.
- PoliciesIntegrationTest (4) — end-to-end through NostrClient →
RelayHub → Relay; proves OK false comes back over the wire when
policies reject (kind blacklist, pubkey allow-list, future
timestamps, oversize events).
Total :quartz-relay tests: 62, 0 failures.
Also: bump Nip40 deleteExpiredEvents wait from 1500ms to 2500ms — the
previous margin was thin enough to flake on busy CI when the SQLite
unixepoch() rounds down across the wait.
This commit is contained in:
@@ -46,21 +46,29 @@ require_auth = false
|
||||
# future. Parsed today, enforced once the matching policy lands.
|
||||
# reject_future_seconds = 1800
|
||||
|
||||
# --- Sections below are parsed today but NOT YET ENFORCED. They are
|
||||
# accepted for forward compatibility — the matching enforcement code is
|
||||
# tracked separately. The relay logs a warning for each used section. ---
|
||||
|
||||
[limits]
|
||||
# Maximum byte size of an EVENT (canonical NIP-01 JSON form).
|
||||
# Enforced by MaxEventBytesPolicy.
|
||||
# max_event_bytes = 131072
|
||||
|
||||
# Maximum WebSocket frame size. Frames larger than this are dropped at
|
||||
# the WS layer. (max_ws_message_bytes maps to the same setting since
|
||||
# Ktor's WebSockets plugin only exposes per-frame caps.)
|
||||
# max_ws_message_bytes = 1048576
|
||||
# max_ws_frame_bytes = 1048576
|
||||
|
||||
# Per-session token-bucket caps. Enforced by RateLimitPolicy.
|
||||
# messages_per_sec = 10
|
||||
# subscriptions_per_min = 60
|
||||
|
||||
# Parsed but NOT YET ENFORCED.
|
||||
# max_subscriptions_per_session = 32
|
||||
# max_filters_per_req = 10
|
||||
|
||||
[authorization]
|
||||
# pubkey_whitelist = []
|
||||
# Allow / deny lists. Allow is a permissive ceiling; deny still
|
||||
# removes specific entries inside it. Enforced by Pubkey/KindAllowDenyPolicy.
|
||||
# pubkey_whitelist = ["abcdef...64hex..."]
|
||||
# pubkey_blacklist = []
|
||||
# kind_whitelist = []
|
||||
# kind_blacklist = []
|
||||
# kind_whitelist = [0, 1, 3, 7, 1059, 30023]
|
||||
# kind_blacklist = [4]
|
||||
|
||||
Reference in New Issue
Block a user