chore(relay): relax temporary query allowance

rust-nostr 0.45 newly enables a 120-query-per-minute LocalRelay default. Production sync exhausted it because each SDK-managed NIP-77 NEG-MSG continuation consumes the same per-connection bucket, while the small per-connection quota alone is weak DoS protection because clients can multiply connections.

Override the embedded relay allowance by 10x to 1,200 queries per minute while retaining a finite safety backstop. Keep the value internal rather than expanding the operator configuration surface, and document the effective limit and its rationale.

Correctness assumes the existing subscription, message, write, event-size, and connection-admission controls remain the primary resource bounds. Per-IP or global rate limiting and proactive outbound historic pacing are deliberately excluded.

This override is temporary: review its value after rust-nostr separates or otherwise revises NIP-77 continuation accounting.

Validation: nix develop -c cargo check --lib.
This commit is contained in:
DanConwayDev
2026-08-08 08:55:17 +00:00
parent 7b5b81ff14
commit 21f3a46ca4
5 changed files with 33 additions and 4 deletions
+5
View File
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Temporarily override rust-nostr 0.45's newly introduced LocalRelay query
allowance from 120 to 1,200 messages per minute per connection. This retains
a finite DoS backstop while avoiding an unusually restrictive default that
also charges every SDK-managed NIP-77 `NEG-MSG` continuation. Re-evaluate
the 10× allowance after upstream revises that NIP-77 accounting.
- Pace query starts after a relay reports its per-connection query-rate budget
is exhausted, preventing fixed cooldown recovery from replaying the same
fast historic-sync burst indefinitely. If an SDK-managed NIP-77 exchange
+5 -1
View File
@@ -31,11 +31,15 @@ operator configurable; other dependency hardening is pinned in the builder.
These limits prevent individual connections from overwhelming the relay.
The relay advertises the standard `max_subscriptions`, `max_limit`,
`default_limit`, `max_message_length`, and `max_subid_length` NIP-11 fields.
rust-nostr 0.45 also enforces fixed ngit-grasp-selected defaults of 120 queries,
rust-nostr 0.45 also enforces fixed ngit-grasp-selected defaults of 1,200 queries,
30 authentication events, and 6,000 WebSocket messages per minute; 20 filters
per REQ; 1 MiB subscription state; 10 active negentropy sessions and 50,000
negentropy items per connection; a 5 MiB WebSocket message; and a 10-second
handshake deadline. NIP-11 has no standard fields for most of those controls.
The query allowance is a temporary 10× override of rust-nostr's newly added
120/minute default because NIP-77 currently charges each SDK-managed `NEG-MSG`
continuation separately; it must be reviewed when upstream revises that
accounting.
### Per-IP Connection Monitoring
+4 -1
View File
@@ -170,7 +170,7 @@ though they advertise the larger accepted `max_limit`.
#### Admission and rate limits (condensed)
Native rate limiting varies wildly and is invisible to clients. Our own
embedded relay enforces per-connection per-minute quotas (120 queries, 6,000
embedded relay enforces per-connection per-minute quotas (1,200 queries, 6,000
WebSocket messages, 60 event writes); nostream ships per-IP connection-attempt
and kind-specific event quotas with EWMA decay; khatru and haven offer
discrete leaky counters that drain over minutes; nostr-rs-relay, relayer,
@@ -190,6 +190,9 @@ itself because a hostile client can multiply connections; per-IP admission and
global resource bounds address that threat more directly. rust-nostr also
charges SDK-managed NIP-77 `NEG-MSG` continuation frames to this same bucket,
so one application-started reconciliation can consume multiple query tokens.
ngit-grasp temporarily overrides that default to 1,200 queries/minute while
retaining a finite per-connection backstop. Re-evaluate the 10× value after
upstream separates or otherwise revises NIP-77 continuation accounting.
NIP-11 describes hard relay limitations, not rate-limit algorithms. The
standard fields relevant here are `max_limit` and
+9 -1
View File
@@ -13,7 +13,7 @@ production admission policy.
| Results per filter | 500 | `NGIT_RELAY_FILTER_LIMIT` | `max_limit`, `default_limit` |
| Serialized event size | 192 KiB | `NGIT_RELAY_MAX_EVENT_SIZE_BYTES` | No equivalent field |
| Event writes per minute | 60 | Fixed | No standard field |
| Queries per minute | 120 | Fixed | No standard field |
| Queries per minute | 1,200 | Fixed temporary override | No standard field |
| Authentication events per minute | 30 | Fixed | No standard field |
| WebSocket messages per minute | 6,000 | Fixed | No standard field |
| WebSocket message size | 5 MiB | Fixed | `max_message_length` |
@@ -35,6 +35,14 @@ Production history contains a valid NIP-34 patch event of about 149 KiB, so
64 KiB is incompatible with ngit-grasp's purpose. The raised limit remains
bounded and below the 5 MiB WebSocket message ceiling.
The 1,200-query allowance temporarily overrides rust-nostr 0.45's newly
introduced 120/minute default. A finite per-connection bound remains one useful
DoS layer, but the upstream default is unusually restrictive for sync clients
and currently counts every SDK-managed NIP-77 `NEG-MSG` continuation as another
query. Review this 10× override after upstream changes that accounting; a small
per-connection quota is not a substitute for per-IP admission or global
resource bounds because clients can multiply connections.
## Client adaptation
ngit-grasp refetches NIP-11 per connection session. Its outbound sync ledger
+10 -1
View File
@@ -43,6 +43,15 @@ use crate::sync::rejected_index::RejectedEventsIndex;
/// tighter operation quotas in charge of valid protocol work.
const CLIENT_MESSAGES_PER_MINUTE: u32 = 6_000;
/// Temporary compatibility override for rust-nostr 0.45's newly introduced
/// 120-query-per-minute default.
///
/// A finite per-connection bound remains useful as one DoS layer, but 120 is
/// too restrictive for legitimate sync and NIP-77 currently charges every
/// SDK-managed `NEG-MSG` continuation against the same allowance. Re-evaluate
/// this 10× value after upstream separates or otherwise revises that accounting.
const CLIENT_QUERIES_PER_MINUTE: u32 = 1_200;
/// NIP-34 Write Policy — admission and routing for GRASP-01 events
///
/// Acts as the top-level admission gate and router. Each incoming event is:
@@ -1021,7 +1030,7 @@ pub async fn create_relay(
max_reqs: config.relay_max_subscriptions,
notes_per_minute: 60,
})
.queries_per_minute(120)
.queries_per_minute(CLIENT_QUERIES_PER_MINUTE)
.auth_events_per_minute(30)
.messages_per_minute(CLIENT_MESSAGES_PER_MINUTE)
.max_websocket_message_size(5 * 1024 * 1024)