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)
This commit is contained in:
DanConwayDev
2026-01-14 13:40:03 +00:00
parent e3792b9abe
commit 50000cd9d4
6 changed files with 283 additions and 1 deletions
+11 -1
View File
@@ -277,4 +277,14 @@
# Examples: # Examples:
# NGIT_EVENT_BLACKLIST=npub1spam... # NGIT_EVENT_BLACKLIST=npub1spam...
# NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser... # NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser...
# NGIT_EVENT_BLACKLIST= # NGIT_EVENT_BLACKLIST=
# ============================================================================
# RATE LIMITING & DOS PROTECTION
# ============================================================================
# Maximum total connections to the relay
# Prevents connection exhaustion DoS attacks
# CLI: --max-connections <count>
# Default: 500
# NGIT_MAX_CONNECTIONS=500
+211
View File
@@ -0,0 +1,211 @@
# Defensive Measures & Rate Limiting
This document describes the defensive measures implemented in ngit-grasp to protect against abuse, spam, and denial-of-service attacks.
## Overview
ngit-grasp employs multiple layers of defense:
1. **Connection & Subscription Limits** - Per-connection limits on subscriptions and event publishing
2. **Content Filtering** - Blacklist/whitelist system for repositories and event authors
3. **Event Validation** - Strict GRASP-01 protocol validation
4. **Relay Health Management** - Intelligent handling of problematic remote relays
## What's Implemented
### Per-Connection Rate Limits
**Source:** Built-in to rust-nostr relay-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)
These limits prevent individual connections from overwhelming the relay.
### 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
**Future:** Could be extended to enforce per-IP connection limits.
### 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)
- Reduces retry frequency for broken relays
**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
## 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.
### Total Connection Limit
**Status:** Supported by relay-builder but not currently configured in ngit-grasp.
**To implement:** Add `max_connections(n)` to relay builder configuration.
### 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 (Deferred)
### Decision: Defer Until Abuse Detected
After comprehensive review (2026-01-14), we decided to defer per-IP rate limiting (Phase 2 & 3) until abuse patterns are detected in production.
**Current protection (Phase 1):**
- Per-connection limits: 500 subscriptions, 60 events/min
- Total connection limit: 500 (configurable via `NGIT_MAX_CONNECTIONS`)
- Connection monitoring: Tracks IPs, flags abuse at 10 connections
- Content filtering: Event blacklist, repository blacklist/whitelist
**Deferred features (Phase 2 & 3):**
- Per-IP connection enforcement (reject after 10 connections)
- Per-IP event rate limiting (reject after 100 events/min)
### Rationale for Deferral
1. **Config-only approach sufficient** - Total connection limit addresses primary DoS vector
2. **Git relay context** - Developer users less likely to abuse than general public
3. **Existing protections strong** - Per-connection limits + content filtering already robust
4. **Data-driven approach** - Monitor ConnectionTracker metrics, implement if needed
5. **Minimal maintenance** - Avoid custom rate limiting code until proven necessary
### Implementation Path if Needed
**Preferred approach:** Contribute to rust-nostr/relay-builder as PR
- Propose IP-based rate limiting as optional feature
- Let upstream maintain the code
- Benefits entire Nostr ecosystem
**Fallback:** Implement in ngit-grasp
- Per-IP connection enforcement via actix middleware
- Per-IP event rate limiting via token bucket in WritePolicy
- See issue d6ee for detailed implementation plan
### Monitoring for Abuse
Watch these metrics to determine if Phase 2 is needed:
- `ngit_connections_per_ip` - IPs exceeding 10 connections
- `ngit_flagged_abusers` - IPs flagged by ConnectionTracker
- Event publishing patterns from single IPs
**Trigger for Phase 2:** If abuse detected for 2-4 weeks after Phase 1 deployment
### Related Work
**Git endpoint throttling:** Separate concern, tracked in issue ff38
- Git HTTP endpoints have different threat model (bandwidth/CPU intensive)
- Requires separate IP-based throttling (5 concurrent, 30/min per IP)
- No interaction with relay code
## 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 (500) | ✅ Active | Yes | 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) |
| **Deferred (Phase 2)** |
| Per-IP connection limit | ⚠️ Deferred | No | - |
| Per-IP rate limiting | ⚠️ Deferred | No | - |
| Query filtering | ⚠️ Available | No | Not implemented |
## Related Documentation
- [Configuration Reference](../reference/configuration.md) - All config options for defensive features
- [Monitoring Overview](monitoring.md) - Prometheus metrics for tracking abuse
- [GRASP-05 Archive](grasp-05-archive.md) - Archive whitelist details
- [Architecture](architecture.md) - Overall system design
+40
View File
@@ -925,6 +925,46 @@ Event blacklist does **not** affect NIP-11 metadata:
--- ---
### Rate Limiting & DoS Protection
#### `NGIT_MAX_CONNECTIONS`
**Description:** Maximum total connections to the relay. Prevents connection exhaustion DoS attacks.
**Type:** Integer
**Default:** `500`
**Required:** No
**Examples:**
```bash
# Default: 500 connections
NGIT_MAX_CONNECTIONS=500
# Higher limit for large public relay
NGIT_MAX_CONNECTIONS=1000
# Lower limit for private relay
NGIT_MAX_CONNECTIONS=100
```
**Notes:**
- Limits total concurrent WebSocket connections to the relay
- Prevents connection exhaustion attacks
- Works in conjunction with per-connection limits (500 subscriptions, 60 events/min)
- When limit is reached, new connections are rejected
- Existing connections continue to work normally
**Related Limits:**
Per-connection limits (built-in to relay-builder, not configurable):
- Max subscriptions per connection: 500
- Max events per minute per connection: 60
- Max subscription ID length: 250 characters
- Max results per filter: 500
---
### Logging Configuration ### Logging Configuration
#### `RUST_LOG` #### `RUST_LOG`
+7
View File
@@ -250,6 +250,12 @@ let
''; '';
}; };
maxConnections = mkOption {
type = types.int;
default = 500;
description = "Maximum total connections to the relay";
};
user = mkOption { user = mkOption {
type = types.str; type = types.str;
default = "ngit-grasp-${name}"; default = "ngit-grasp-${name}";
@@ -295,6 +301,7 @@ let
NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist; NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist;
NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist; NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist;
NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist; NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist;
NGIT_MAX_CONNECTIONS = toString cfg.maxConnections;
RUST_LOG = cfg.logLevel; RUST_LOG = cfg.logLevel;
} // optionalAttrs (cfg.relayName != null) { } // optionalAttrs (cfg.relayName != null) {
NGIT_RELAY_NAME = cfg.relayName; NGIT_RELAY_NAME = cfg.relayName;
+6
View File
@@ -469,6 +469,11 @@ pub struct Config {
/// All events from these authors are blocked from both relay storage and purgatory /// All events from these authors are blocked from both relay storage and purgatory
#[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")] #[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")]
pub event_blacklist: String, pub event_blacklist: String,
/// Maximum total connections to the relay (default: 500)
/// Prevents connection exhaustion DoS attacks
#[arg(long, env = "NGIT_MAX_CONNECTIONS", default_value_t = 500)]
pub max_connections: usize,
} }
impl Config { impl Config {
@@ -703,6 +708,7 @@ impl Config {
repository_whitelist: String::new(), repository_whitelist: String::new(),
repository_blacklist: String::new(), repository_blacklist: String::new(),
event_blacklist: String::new(), event_blacklist: String::new(),
max_connections: 500,
} }
} }
} }
+8
View File
@@ -624,6 +624,14 @@ pub async fn create_relay(
let relay = LocalRelayBuilder::default() let relay = LocalRelayBuilder::default()
.database(database.clone()) .database(database.clone())
.write_policy(write_policy.clone()) .write_policy(write_policy.clone())
// Explicitly set rate limits (make defaults visible in code)
// Per-connection limits: 500 max subscriptions, 60 events/min
.rate_limit(RateLimit {
max_reqs: 500, // Max concurrent subscriptions per connection
notes_per_minute: 60, // Max events per minute per connection
})
// Total connection limit to prevent DoS attacks
.max_connections(config.max_connections)
.build(); .build();
tracing::info!( tracing::info!(