Production gitnostr.com repeatedly received CLOSED responses from relay.ngit.dev after reconnect: its 34-filter live set for 924 full repositories, 89 state-only repositories, and 3,748 roots crossed rust-nostr 0.45's newly selected 1 MiB cumulative subscription-state limit. Fourteen representative REQs were accepted and the remainder were refused, silently leaving persistent live coverage incomplete; cooldown recovery only recreated the same impossible set. Raise the explicitly selected per-connection retained subscription-state allowance to 5 MiB. This remains a finite boundary, matches the largest individual WebSocket message already admitted, and leaves roughly four times the observed working-set headroom without promising the theoretical 500 x 96 KiB maximum. Document the exact serving policy and its lack of NIP-11 negotiation. A scenario opens 17 persistent sub-96 KiB REQs carrying 34 filters. It failed unchanged 2.1.1 at live-14 with the production CLOSED reason and passes with the new bound. Correctness assumes this allowance is enforced per connection by rust-nostr; client-side adaptation to unknown third-party cumulative byte limits and multi-connection sharding remain excluded because NIP-11 exposes no such capability. Validation: focused scenario passed; 658 library tests passed; git diff --check passed; nix build .#ngit-grasp passed.
12 KiB
Defensive Measures & Rate Limiting
This document describes the defensive measures implemented in ngit-grasp to protect against abuse, spam, and denial-of-service attacks.
Note: A point-in-time analysis of defensive measures in other Nostr relays (strfry, nostr-rs-relay, khatru) was conducted to inform these design decisions. The analysis examined connection limits, rate limiting approaches, and per-IP enforcement strategies across the ecosystem.
Overview
ngit-grasp employs multiple layers of defense:
- Connection & Subscription Limits - Per-connection limits on subscriptions and event publishing
- Content Filtering - Blacklist/whitelist system for repositories and event authors
- Event Validation - Strict GRASP-01 protocol validation
- Relay Health Management - Intelligent handling of problematic remote relays
What's Implemented
Per-Connection Rate Limits
Source: Enforced by rust-nostr relay-builder and explicitly selected by ngit-grasp. Discoverable sync limits and the Git-specific event bound are operator configurable; other dependency hardening is pinned in the builder.
- Subscription limit: Max 500 concurrent subscriptions per connection
- Event publishing limit: Max 60 events per minute per connection
- Subscription ID length: Max 250 characters
- Filter limit: Max 500 results per query (default)
- Event size: Max 192 KiB by default, raised from rust-nostr's 64 KiB default because valid NIP-34 patch events in production reach about 149 KiB
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 1,200 queries,
30 authentication events, and 6,000 WebSocket messages per minute; 20 filters
per REQ; 5 MiB subscription state (raised from rust-nostr's 1 MiB default so
repository-scale persistent live filters remain admitted); 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
Source: Custom ngit-grasp implementation
Location: src/metrics/connection.rs
- Status: Monitoring only (does NOT enforce limits)
- Tracks connections per IP address internally
- Flags IPs exceeding threshold (default: 10 connections)
- Privacy: IP addresses never exposed in Prometheus metrics, only aggregate counts
- Logs warnings when threshold exceeded
Note on enforcement: Per-IP connection limits are not built into rust-nostr relay-builder (tracks per WebSocket connection, not per IP). If abuse is detected via metrics, enforcement should be implemented as a PR to rust-nostr/relay-builder to benefit the entire Nostr ecosystem, rather than custom code in ngit-grasp.
Content Filtering (Blacklists/Whitelists)
Source: Custom ngit-grasp implementation
Location: src/config.rs, src/nostr/builder.rs
Event Blacklist:
- Block ALL events from specific authors (npubs)
- Takes precedence over all other validation
- Events never reach storage or purgatory
Repository Blacklist:
- Block specific repositories, developers, or identifiers
- Takes precedence over whitelists
- Three formats:
npub,npub/identifier,identifier
Repository Whitelist:
- Curate which repositories are accepted (GRASP-01 mode)
- Only accept announcements that both list your service AND match whitelist
- Same three formats as blacklist
Archive Whitelist (GRASP-05):
- Mirror specific repositories even if they don't list your service
- Same three formats as blacklist
- Default: read-only mode when enabled
Privacy: Blacklists not advertised in NIP-11 metadata.
Event Validation Plugin System
Source: Built-in to rust-nostr relay-builder
Implementation: Custom GRASP-01 validation in src/nostr/builder.rs
- WritePolicy trait: Controls which events are accepted
- QueryPolicy trait: Controls which queries are allowed (not currently used)
- Access to client IP address for future per-IP rate limiting
- Modular sub-policies for different event types (announcements, state events, PRs)
Relay Health Management (GRASP-02 Sync)
Source: Custom ngit-grasp implementation
Location: src/sync/health.rs
Exponential Backoff:
- Failed connections trigger increasing delays: 5s → 10s → 20s → ... → 1 hour max
- Prevents hammering dead or slow relays
Naughty List:
- Tracks relays with persistent infrastructure issues (DNS, TLS, protocol errors)
- Separate from normal connection failures
- 12-hour expiration (configurable)
- Suppresses connection attempts until the entry expires
Rate Limit Detection:
- Detects when remote relay rate limits us
- Automatic 65-second cooldown
- Prevents hammering relays that tell us to slow down
Domain Throttling (Git Data Fetching):
- Max 5 concurrent requests per domain
- Max 30 requests per minute per domain
- Respectful rate limiting when fetching missing git data
Outbound Target Policy (SSRF Protection)
Source: Custom ngit-grasp implementation
Location: src/outbound.rs
Repository announcements, state events, and PR events are untrusted input,
but their relays and clone tags direct proactive sync's outbound
WebSocket connections and purgatory git fetches. Production logs showed
event-directed sync dialling ws://localhost:3334, ws://127.0.0.1:7334,
and ws://100.125.184.46:7334 (CGNAT); a crafted event could use this to
probe or attack the relay host's local network.
One fail-closed policy is applied immediately before every event-directed outbound sink:
- Relay connections (
RelayConnection::connect): re-checked before every dial, including reconnects, plus a registration-time gate so forbidden targets never enter the reconnect lifecycle. - Git fetches (
RealSyncContext::fetch_oids): checked immediately before spawning the pass's git subprocesses (git ls-remoteandgit fetch), with the vetted DNS answers pinned onto each of them.
The policy enforces:
- Scheme allowlist per sink -
ws/wssfor relays,http/httpsfor git; nofile:,ssh:,git:, or other protocol escapes. - No credentials in URLs.
- No local hostnames -
localhost, single-label names, and IANA special-use suffixes (.local,.internal,.home.arpa,.onion,.test,.invalid,.localdomain). - Globally reachable addresses only - loopback, private (RFC 1918), CGNAT (RFC 6598), link-local, unspecified, multicast, broadcast, documentation, benchmarking, reserved, and unique-local ranges are all rejected, for IP literals and for every DNS answer (resolution failure fails closed).
- Exact service matching - GRASP-01 admission (
lists_service) and the don't-fetch-from-ourselves filter compare parsed host and port, sogitnostr.com.attacker.exampleorhttps://evil.example/gitnostr.com/cannot satisfy a check forgitnostr.com.
The operator-configured bootstrap relay (NGIT_SYNC_BOOTSTRAP_RELAY_URL)
is trusted and exempt, so a local bootstrap relay keeps working. Trust is
carried by the source of the URL, never by URL comparison: an event-provided
URL that merely resembles the bootstrap relay is still rejected.
Git subprocesses are additionally confined so an authorized URL cannot escape
the decision: GIT_ALLOW_PROTOCOL=http:https, http.followRedirects=false,
proxies disabled (config and environment), credential.helper= cleared, and
the vetted DNS answers pinned via http.curloptResolve.
Known limitation (DNS rebinding, relay connections only): the vetted DNS answers cannot be pinned onto nostr-sdk's connector because its connect API accepts a URL, not a pre-resolved socket address. Relay DNS is therefore re-validated immediately before every connection attempt but re-resolved by the SDK during the dial, leaving a narrow time-of-check/time-of-use window. Closing it needs upstream connector support for pre-resolved addresses (rust-nostr). Git fetches do not share this window because their DNS answers are pinned.
The escape hatch NGIT_SYNC_ALLOW_NON_GLOBAL_TARGETS=true disables the
reachability checks (scheme and credential checks remain) for integration
tests and closed development networks. Production relays must leave it unset.
What's NOT Implemented
Per-IP Rate Limiting
- Per-IP connection limits: Not enforced (only monitored)
- Per-IP subscription limits: Not supported
- Per-IP event publishing limits: Not supported
Why: rust-nostr relay-builder tracks limits per WebSocket connection, not per IP address.
To implement: Would require custom middleware/WritePolicy to aggregate across connections from the same IP.
Query Filtering
Status: QueryPolicy trait available but not currently used.
Potential uses: Rate limit queries per IP, block expensive queries, restrict access to certain event kinds.
Future Enhancements
Per-IP Rate Limiting
Per-IP connection and event rate limiting remain a separate design concern.
When NGIT_MAX_CONNECTIONS is unset, ngit-grasp deliberately has no
application-level total connection cap and delegates resource protection to OS
and infrastructure limits. Operators can set an explicit total cap, but that
does not provide fairness between clients.
Design constraint: Per-IP enforcement requires trustworthy client identity through the reverse proxy plus aggregation across rust-nostr WebSocket sessions. Forwarded-IP headers must only be accepted from configured trusted proxies; otherwise clients could spoof addresses and evade or weaponise the limit.
Related: Git endpoint throttling (issue ff38) is a separate concern with different requirements.
Summary Table
| Feature | Status | Enforced? | Configurable? |
|---|---|---|---|
| Per-Connection Limits | |||
| Max subscriptions (500) | ✅ Active | Yes | No (relay-builder default) |
| Event rate limit (60/min) | ✅ Active | Yes | No (relay-builder default) |
| Total Connection Limit | |||
| Max connections (unlimited by default) | Optional | When configured | Yes (NGIT_MAX_CONNECTIONS) |
| Per-IP Monitoring | |||
| Connection tracking | ✅ Active | No (monitor only) | Threshold only |
| Content Filtering | |||
| Event blacklist | ✅ Active | Yes | Yes |
| Repository blacklist | ✅ Active | Yes | Yes |
| Repository whitelist | ✅ Active | Yes (if set) | Yes |
| Archive whitelist | ✅ Active | Yes (if set) | Yes |
| Event Validation | |||
| GRASP-01 validation | ✅ Active | Yes | Via WritePolicy |
| Relay Sync Protection | |||
| Exponential backoff | ✅ Active | Yes | Yes |
| Naughty list | ✅ Active | Yes | Yes (12h default) |
| Rate limit detection | ✅ Active | Yes | Automatic |
| Domain throttling | ✅ Active | Yes | Hardcoded (5/30) |
| Outbound target policy (SSRF) | ✅ Active | Yes | Yes (NGIT_SYNC_ALLOW_NON_GLOBAL_TARGETS) |
| Not Implemented | |||
| Per-IP connection limit | ⚠️ Deferred | No | - |
| Per-IP rate limiting | ⚠️ Deferred | No | - |
| Query filtering | ⚠️ Available | No | Not implemented |
Related Documentation
- Configuration Reference - All config options for defensive features
- Monitoring Overview - Prometheus metrics for tracking abuse
- GRASP-05 Archive - Archive whitelist details
- Architecture - Overall system design