diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-14 11:42:05 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-14 13:40:03 +0000 |
| commit | 50000cd9d47681390c3c45feef98fe51c7b79a0f (patch) | |
| tree | 53ede8cb63ac2c5fe2321a6ecd9c87956537bbc7 | |
| parent | e3792b9abefd43b4594af2640ad4665c006fa3b0 (diff) | |
Add explicit rate limits and total connection limit
- Make RateLimit explicit in relay builder (500 subs, 60 events/min)
- Add NGIT_MAX_CONNECTIONS config option (default: 500)
- Update all 4 config locations (src, nix, docs, .env.example)
- Fix documentation error: filter limit 5000→500
- Document Phase 2 deferral decision (per-IP enforcement)
Addresses primary DoS vector (connection exhaustion) with minimal code.
Per-IP rate limiting deferred until abuse detected in production.
Related: issue ff38 (git endpoint throttling - separate concern)
| -rw-r--r-- | .env.example | 12 | ||||
| -rw-r--r-- | docs/explanation/defensive-measures.md | 211 | ||||
| -rw-r--r-- | docs/reference/configuration.md | 40 | ||||
| -rw-r--r-- | nix/module.nix | 7 | ||||
| -rw-r--r-- | src/config.rs | 6 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 8 |
6 files changed, 283 insertions, 1 deletions
diff --git a/.env.example b/.env.example index 707efd4..953ae93 100644 --- a/.env.example +++ b/.env.example | |||
| @@ -277,4 +277,14 @@ | |||
| 277 | # Examples: | 277 | # Examples: |
| 278 | # NGIT_EVENT_BLACKLIST=npub1spam... | 278 | # NGIT_EVENT_BLACKLIST=npub1spam... |
| 279 | # NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser... | 279 | # NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser... |
| 280 | # NGIT_EVENT_BLACKLIST= \ No newline at end of file | 280 | # NGIT_EVENT_BLACKLIST= |
| 281 | |||
| 282 | # ============================================================================ | ||
| 283 | # RATE LIMITING & DOS PROTECTION | ||
| 284 | # ============================================================================ | ||
| 285 | |||
| 286 | # Maximum total connections to the relay | ||
| 287 | # Prevents connection exhaustion DoS attacks | ||
| 288 | # CLI: --max-connections <count> | ||
| 289 | # Default: 500 | ||
| 290 | # NGIT_MAX_CONNECTIONS=500 \ No newline at end of file | ||
diff --git a/docs/explanation/defensive-measures.md b/docs/explanation/defensive-measures.md new file mode 100644 index 0000000..f7abc30 --- /dev/null +++ b/docs/explanation/defensive-measures.md | |||
| @@ -0,0 +1,211 @@ | |||
| 1 | # Defensive Measures & Rate Limiting | ||
| 2 | |||
| 3 | This document describes the defensive measures implemented in ngit-grasp to protect against abuse, spam, and denial-of-service attacks. | ||
| 4 | |||
| 5 | ## Overview | ||
| 6 | |||
| 7 | ngit-grasp employs multiple layers of defense: | ||
| 8 | |||
| 9 | 1. **Connection & Subscription Limits** - Per-connection limits on subscriptions and event publishing | ||
| 10 | 2. **Content Filtering** - Blacklist/whitelist system for repositories and event authors | ||
| 11 | 3. **Event Validation** - Strict GRASP-01 protocol validation | ||
| 12 | 4. **Relay Health Management** - Intelligent handling of problematic remote relays | ||
| 13 | |||
| 14 | ## What's Implemented | ||
| 15 | |||
| 16 | ### Per-Connection Rate Limits | ||
| 17 | |||
| 18 | **Source:** Built-in to rust-nostr relay-builder | ||
| 19 | |||
| 20 | - **Subscription limit:** Max 500 concurrent subscriptions per connection | ||
| 21 | - **Event publishing limit:** Max 60 events per minute per connection | ||
| 22 | - **Subscription ID length:** Max 250 characters | ||
| 23 | - **Filter limit:** Max 500 results per query (default) | ||
| 24 | |||
| 25 | These limits prevent individual connections from overwhelming the relay. | ||
| 26 | |||
| 27 | ### Per-IP Connection Monitoring | ||
| 28 | |||
| 29 | **Source:** Custom ngit-grasp implementation | ||
| 30 | **Location:** `src/metrics/connection.rs` | ||
| 31 | |||
| 32 | - **Status:** Monitoring only (does NOT enforce limits) | ||
| 33 | - Tracks connections per IP address internally | ||
| 34 | - Flags IPs exceeding threshold (default: 10 connections) | ||
| 35 | - **Privacy:** IP addresses never exposed in Prometheus metrics, only aggregate counts | ||
| 36 | - Logs warnings when threshold exceeded | ||
| 37 | |||
| 38 | **Future:** Could be extended to enforce per-IP connection limits. | ||
| 39 | |||
| 40 | ### Content Filtering (Blacklists/Whitelists) | ||
| 41 | |||
| 42 | **Source:** Custom ngit-grasp implementation | ||
| 43 | **Location:** `src/config.rs`, `src/nostr/builder.rs` | ||
| 44 | |||
| 45 | **Event Blacklist:** | ||
| 46 | - Block ALL events from specific authors (npubs) | ||
| 47 | - Takes precedence over all other validation | ||
| 48 | - Events never reach storage or purgatory | ||
| 49 | |||
| 50 | **Repository Blacklist:** | ||
| 51 | - Block specific repositories, developers, or identifiers | ||
| 52 | - Takes precedence over whitelists | ||
| 53 | - Three formats: `npub`, `npub/identifier`, `identifier` | ||
| 54 | |||
| 55 | **Repository Whitelist:** | ||
| 56 | - Curate which repositories are accepted (GRASP-01 mode) | ||
| 57 | - Only accept announcements that both list your service AND match whitelist | ||
| 58 | - Same three formats as blacklist | ||
| 59 | |||
| 60 | **Archive Whitelist (GRASP-05):** | ||
| 61 | - Mirror specific repositories even if they don't list your service | ||
| 62 | - Same three formats as blacklist | ||
| 63 | - Default: read-only mode when enabled | ||
| 64 | |||
| 65 | **Privacy:** Blacklists not advertised in NIP-11 metadata. | ||
| 66 | |||
| 67 | ### Event Validation Plugin System | ||
| 68 | |||
| 69 | **Source:** Built-in to rust-nostr relay-builder | ||
| 70 | **Implementation:** Custom GRASP-01 validation in `src/nostr/builder.rs` | ||
| 71 | |||
| 72 | - **WritePolicy trait:** Controls which events are accepted | ||
| 73 | - **QueryPolicy trait:** Controls which queries are allowed (not currently used) | ||
| 74 | - Access to client IP address for future per-IP rate limiting | ||
| 75 | - Modular sub-policies for different event types (announcements, state events, PRs) | ||
| 76 | |||
| 77 | ### Relay Health Management (GRASP-02 Sync) | ||
| 78 | |||
| 79 | **Source:** Custom ngit-grasp implementation | ||
| 80 | **Location:** `src/sync/health.rs` | ||
| 81 | |||
| 82 | **Exponential Backoff:** | ||
| 83 | - Failed connections trigger increasing delays: 5s → 10s → 20s → ... → 1 hour max | ||
| 84 | - Prevents hammering dead or slow relays | ||
| 85 | |||
| 86 | **Naughty List:** | ||
| 87 | - Tracks relays with persistent infrastructure issues (DNS, TLS, protocol errors) | ||
| 88 | - Separate from normal connection failures | ||
| 89 | - 12-hour expiration (configurable) | ||
| 90 | - Reduces retry frequency for broken relays | ||
| 91 | |||
| 92 | **Rate Limit Detection:** | ||
| 93 | - Detects when remote relay rate limits us | ||
| 94 | - Automatic 65-second cooldown | ||
| 95 | - Prevents hammering relays that tell us to slow down | ||
| 96 | |||
| 97 | **Domain Throttling (Git Data Fetching):** | ||
| 98 | - Max 5 concurrent requests per domain | ||
| 99 | - Max 30 requests per minute per domain | ||
| 100 | - Respectful rate limiting when fetching missing git data | ||
| 101 | |||
| 102 | ## What's NOT Implemented | ||
| 103 | |||
| 104 | ### Per-IP Rate Limiting | ||
| 105 | |||
| 106 | - **Per-IP connection limits:** Not enforced (only monitored) | ||
| 107 | - **Per-IP subscription limits:** Not supported | ||
| 108 | - **Per-IP event publishing limits:** Not supported | ||
| 109 | |||
| 110 | **Why:** rust-nostr relay-builder tracks limits per WebSocket connection, not per IP address. | ||
| 111 | |||
| 112 | **To implement:** Would require custom middleware/WritePolicy to aggregate across connections from the same IP. | ||
| 113 | |||
| 114 | ### Total Connection Limit | ||
| 115 | |||
| 116 | **Status:** Supported by relay-builder but not currently configured in ngit-grasp. | ||
| 117 | |||
| 118 | **To implement:** Add `max_connections(n)` to relay builder configuration. | ||
| 119 | |||
| 120 | ### Query Filtering | ||
| 121 | |||
| 122 | **Status:** QueryPolicy trait available but not currently used. | ||
| 123 | |||
| 124 | **Potential uses:** Rate limit queries per IP, block expensive queries, restrict access to certain event kinds. | ||
| 125 | |||
| 126 | ## Future Enhancements: Per-IP Rate Limiting (Deferred) | ||
| 127 | |||
| 128 | ### Decision: Defer Until Abuse Detected | ||
| 129 | |||
| 130 | After comprehensive review (2026-01-14), we decided to defer per-IP rate limiting (Phase 2 & 3) until abuse patterns are detected in production. | ||
| 131 | |||
| 132 | **Current protection (Phase 1):** | ||
| 133 | - Per-connection limits: 500 subscriptions, 60 events/min | ||
| 134 | - Total connection limit: 500 (configurable via `NGIT_MAX_CONNECTIONS`) | ||
| 135 | - Connection monitoring: Tracks IPs, flags abuse at 10 connections | ||
| 136 | - Content filtering: Event blacklist, repository blacklist/whitelist | ||
| 137 | |||
| 138 | **Deferred features (Phase 2 & 3):** | ||
| 139 | - Per-IP connection enforcement (reject after 10 connections) | ||
| 140 | - Per-IP event rate limiting (reject after 100 events/min) | ||
| 141 | |||
| 142 | ### Rationale for Deferral | ||
| 143 | |||
| 144 | 1. **Config-only approach sufficient** - Total connection limit addresses primary DoS vector | ||
| 145 | 2. **Git relay context** - Developer users less likely to abuse than general public | ||
| 146 | 3. **Existing protections strong** - Per-connection limits + content filtering already robust | ||
| 147 | 4. **Data-driven approach** - Monitor ConnectionTracker metrics, implement if needed | ||
| 148 | 5. **Minimal maintenance** - Avoid custom rate limiting code until proven necessary | ||
| 149 | |||
| 150 | ### Implementation Path if Needed | ||
| 151 | |||
| 152 | **Preferred approach:** Contribute to rust-nostr/relay-builder as PR | ||
| 153 | - Propose IP-based rate limiting as optional feature | ||
| 154 | - Let upstream maintain the code | ||
| 155 | - Benefits entire Nostr ecosystem | ||
| 156 | |||
| 157 | **Fallback:** Implement in ngit-grasp | ||
| 158 | - Per-IP connection enforcement via actix middleware | ||
| 159 | - Per-IP event rate limiting via token bucket in WritePolicy | ||
| 160 | - See issue d6ee for detailed implementation plan | ||
| 161 | |||
| 162 | ### Monitoring for Abuse | ||
| 163 | |||
| 164 | Watch these metrics to determine if Phase 2 is needed: | ||
| 165 | - `ngit_connections_per_ip` - IPs exceeding 10 connections | ||
| 166 | - `ngit_flagged_abusers` - IPs flagged by ConnectionTracker | ||
| 167 | - Event publishing patterns from single IPs | ||
| 168 | |||
| 169 | **Trigger for Phase 2:** If abuse detected for 2-4 weeks after Phase 1 deployment | ||
| 170 | |||
| 171 | ### Related Work | ||
| 172 | |||
| 173 | **Git endpoint throttling:** Separate concern, tracked in issue ff38 | ||
| 174 | - Git HTTP endpoints have different threat model (bandwidth/CPU intensive) | ||
| 175 | - Requires separate IP-based throttling (5 concurrent, 30/min per IP) | ||
| 176 | - No interaction with relay code | ||
| 177 | |||
| 178 | ## Summary Table | ||
| 179 | |||
| 180 | | Feature | Status | Enforced? | Configurable? | | ||
| 181 | |---------|--------|-----------|---------------| | ||
| 182 | | **Per-Connection Limits** | | ||
| 183 | | Max subscriptions (500) | ✅ Active | Yes | No (relay-builder default) | | ||
| 184 | | Event rate limit (60/min) | ✅ Active | Yes | No (relay-builder default) | | ||
| 185 | | **Total Connection Limit** | | ||
| 186 | | Max connections (500) | ✅ Active | Yes | Yes (`NGIT_MAX_CONNECTIONS`) | | ||
| 187 | | **Per-IP Monitoring** | | ||
| 188 | | Connection tracking | ✅ Active | No (monitor only) | Threshold only | | ||
| 189 | | **Content Filtering** | | ||
| 190 | | Event blacklist | ✅ Active | Yes | Yes | | ||
| 191 | | Repository blacklist | ✅ Active | Yes | Yes | | ||
| 192 | | Repository whitelist | ✅ Active | Yes (if set) | Yes | | ||
| 193 | | Archive whitelist | ✅ Active | Yes (if set) | Yes | | ||
| 194 | | **Event Validation** | | ||
| 195 | | GRASP-01 validation | ✅ Active | Yes | Via WritePolicy | | ||
| 196 | | **Relay Sync Protection** | | ||
| 197 | | Exponential backoff | ✅ Active | Yes | Yes | | ||
| 198 | | Naughty list | ✅ Active | Yes | Yes (12h default) | | ||
| 199 | | Rate limit detection | ✅ Active | Yes | Automatic | | ||
| 200 | | Domain throttling | ✅ Active | Yes | Hardcoded (5/30) | | ||
| 201 | | **Deferred (Phase 2)** | | ||
| 202 | | Per-IP connection limit | ⚠️ Deferred | No | - | | ||
| 203 | | Per-IP rate limiting | ⚠️ Deferred | No | - | | ||
| 204 | | Query filtering | ⚠️ Available | No | Not implemented | | ||
| 205 | |||
| 206 | ## Related Documentation | ||
| 207 | |||
| 208 | - [Configuration Reference](../reference/configuration.md) - All config options for defensive features | ||
| 209 | - [Monitoring Overview](monitoring.md) - Prometheus metrics for tracking abuse | ||
| 210 | - [GRASP-05 Archive](grasp-05-archive.md) - Archive whitelist details | ||
| 211 | - [Architecture](architecture.md) - Overall system design | ||
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8b49297..c3001d3 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md | |||
| @@ -925,6 +925,46 @@ Event blacklist does **not** affect NIP-11 metadata: | |||
| 925 | 925 | ||
| 926 | --- | 926 | --- |
| 927 | 927 | ||
| 928 | ### Rate Limiting & DoS Protection | ||
| 929 | |||
| 930 | #### `NGIT_MAX_CONNECTIONS` | ||
| 931 | |||
| 932 | **Description:** Maximum total connections to the relay. Prevents connection exhaustion DoS attacks. | ||
| 933 | **Type:** Integer | ||
| 934 | **Default:** `500` | ||
| 935 | **Required:** No | ||
| 936 | |||
| 937 | **Examples:** | ||
| 938 | |||
| 939 | ```bash | ||
| 940 | # Default: 500 connections | ||
| 941 | NGIT_MAX_CONNECTIONS=500 | ||
| 942 | |||
| 943 | # Higher limit for large public relay | ||
| 944 | NGIT_MAX_CONNECTIONS=1000 | ||
| 945 | |||
| 946 | # Lower limit for private relay | ||
| 947 | NGIT_MAX_CONNECTIONS=100 | ||
| 948 | ``` | ||
| 949 | |||
| 950 | **Notes:** | ||
| 951 | |||
| 952 | - Limits total concurrent WebSocket connections to the relay | ||
| 953 | - Prevents connection exhaustion attacks | ||
| 954 | - Works in conjunction with per-connection limits (500 subscriptions, 60 events/min) | ||
| 955 | - When limit is reached, new connections are rejected | ||
| 956 | - Existing connections continue to work normally | ||
| 957 | |||
| 958 | **Related Limits:** | ||
| 959 | |||
| 960 | Per-connection limits (built-in to relay-builder, not configurable): | ||
| 961 | - Max subscriptions per connection: 500 | ||
| 962 | - Max events per minute per connection: 60 | ||
| 963 | - Max subscription ID length: 250 characters | ||
| 964 | - Max results per filter: 500 | ||
| 965 | |||
| 966 | --- | ||
| 967 | |||
| 928 | ### Logging Configuration | 968 | ### Logging Configuration |
| 929 | 969 | ||
| 930 | #### `RUST_LOG` | 970 | #### `RUST_LOG` |
diff --git a/nix/module.nix b/nix/module.nix index 09c56c1..4117b6d 100644 --- a/nix/module.nix +++ b/nix/module.nix | |||
| @@ -250,6 +250,12 @@ let | |||
| 250 | ''; | 250 | ''; |
| 251 | }; | 251 | }; |
| 252 | 252 | ||
| 253 | maxConnections = mkOption { | ||
| 254 | type = types.int; | ||
| 255 | default = 500; | ||
| 256 | description = "Maximum total connections to the relay"; | ||
| 257 | }; | ||
| 258 | |||
| 253 | user = mkOption { | 259 | user = mkOption { |
| 254 | type = types.str; | 260 | type = types.str; |
| 255 | default = "ngit-grasp-${name}"; | 261 | default = "ngit-grasp-${name}"; |
| @@ -295,6 +301,7 @@ let | |||
| 295 | NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist; | 301 | NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist; |
| 296 | NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist; | 302 | NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist; |
| 297 | NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist; | 303 | NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist; |
| 304 | NGIT_MAX_CONNECTIONS = toString cfg.maxConnections; | ||
| 298 | RUST_LOG = cfg.logLevel; | 305 | RUST_LOG = cfg.logLevel; |
| 299 | } // optionalAttrs (cfg.relayName != null) { | 306 | } // optionalAttrs (cfg.relayName != null) { |
| 300 | NGIT_RELAY_NAME = cfg.relayName; | 307 | NGIT_RELAY_NAME = cfg.relayName; |
diff --git a/src/config.rs b/src/config.rs index 0f0d853..0014003 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -469,6 +469,11 @@ pub struct Config { | |||
| 469 | /// All events from these authors are blocked from both relay storage and purgatory | 469 | /// All events from these authors are blocked from both relay storage and purgatory |
| 470 | #[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")] | 470 | #[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")] |
| 471 | pub event_blacklist: String, | 471 | pub event_blacklist: String, |
| 472 | |||
| 473 | /// Maximum total connections to the relay (default: 500) | ||
| 474 | /// Prevents connection exhaustion DoS attacks | ||
| 475 | #[arg(long, env = "NGIT_MAX_CONNECTIONS", default_value_t = 500)] | ||
| 476 | pub max_connections: usize, | ||
| 472 | } | 477 | } |
| 473 | 478 | ||
| 474 | impl Config { | 479 | impl Config { |
| @@ -703,6 +708,7 @@ impl Config { | |||
| 703 | repository_whitelist: String::new(), | 708 | repository_whitelist: String::new(), |
| 704 | repository_blacklist: String::new(), | 709 | repository_blacklist: String::new(), |
| 705 | event_blacklist: String::new(), | 710 | event_blacklist: String::new(), |
| 711 | max_connections: 500, | ||
| 706 | } | 712 | } |
| 707 | } | 713 | } |
| 708 | } | 714 | } |
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index c2de1df..ef1b700 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -624,6 +624,14 @@ pub async fn create_relay( | |||
| 624 | let relay = LocalRelayBuilder::default() | 624 | let relay = LocalRelayBuilder::default() |
| 625 | .database(database.clone()) | 625 | .database(database.clone()) |
| 626 | .write_policy(write_policy.clone()) | 626 | .write_policy(write_policy.clone()) |
| 627 | // Explicitly set rate limits (make defaults visible in code) | ||
| 628 | // Per-connection limits: 500 max subscriptions, 60 events/min | ||
| 629 | .rate_limit(RateLimit { | ||
| 630 | max_reqs: 500, // Max concurrent subscriptions per connection | ||
| 631 | notes_per_minute: 60, // Max events per minute per connection | ||
| 632 | }) | ||
| 633 | // Total connection limit to prevent DoS attacks | ||
| 634 | .max_connections(config.max_connections) | ||
| 627 | .build(); | 635 | .build(); |
| 628 | 636 | ||
| 629 | tracing::info!( | 637 | tracing::info!( |