Add defensive relay features with rate limiting and connection limits

Implement defensive measures to protect against DoS attacks:
- Add explicit rate limits (500 subscriptions, 60 events/min per connection)
- Add total connection limit (default: 500, configurable via NGIT_MAX_CONNECTIONS)
- Update configuration across all 4 locations (src, nix, docs, .env.example)

Per-IP rate limiting deferred until abuse is detected in production or
implemented in rust-nostr relay-builder to benefit the entire Nostr ecosystem.

Documentation added explaining the defensive features and rationale.
Detailed analysis of other relay implementations preserved in commit history.
This commit is contained in:
DanConwayDev
2026-01-14 13:43:40 +00:00
8 changed files with 321 additions and 1 deletions
+11 -1
View File
@@ -277,4 +277,14 @@
# Examples:
# NGIT_EVENT_BLACKLIST=npub1spam...
# 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
+42
View File
@@ -237,6 +237,48 @@ NGIT_EVENT_BLACKLIST=npub1spam1...,npub1spam2...
**See**: [Configuration Reference](docs/reference/configuration.md) for complete details
## Defensive Measures & Rate Limiting
ngit-grasp implements multiple layers of defense against abuse, spam, and denial-of-service attacks:
**Per-Connection Rate Limits:**
- Max 500 concurrent subscriptions per connection
- Max 60 events published per minute per connection
- Built-in to rust-nostr relay-builder
**Per-IP Connection Monitoring:**
- Tracks connections per IP address (default threshold: 10)
- Flags potential abusers in logs and metrics
- **Does NOT enforce limits** (monitoring only)
- Privacy-preserving (IP addresses never exposed in Prometheus)
**Content Filtering (Blacklists/Whitelists):**
- **Event blacklist** - Block ALL events from specific authors (npubs)
- **Repository blacklist** - Block specific repositories/developers/identifiers
- **Repository whitelist** - Curate which repositories are accepted (GRASP-01 mode)
- **Archive whitelist** - Mirror specific repositories (GRASP-05 mode)
- See [Curation & Moderation](#curation--moderation) section above for details
**Relay Sync Protection (GRASP-02):**
- **Exponential backoff** - Failed connections: 5s → 10s → 20s → ... → 1 hour max
- **Naughty list** - Track relays with infrastructure issues separately (12h expiry)
- **Rate limit detection** - Auto 65s cooldown when remote relays rate limit us
- **Domain throttling** - Max 5 concurrent, 30/min per domain for git data fetching
**Event Validation:**
- Strict GRASP-01 protocol validation via WritePolicy plugin system
- Extensible for custom validation logic (has access to client IP address)
**Total Connection Limit:**
- Max 500 total connections (configurable via `NGIT_MAX_CONNECTIONS`)
- Prevents connection exhaustion DoS attacks
**Not Implemented:**
- Per-IP connection limits (only monitored, not enforced)
- Per-IP event rate limits (tracked per connection, not per IP)
**See**: [Defensive Measures](docs/explanation/defensive-measures.md) for complete details and future enhancements.
## Roadmap
### GRASP-02 Enhancements
+42
View File
@@ -151,6 +151,48 @@ Explanation documentation helps you **understand concepts** and design decisions
---
### [Defensive Measures & Rate Limiting](defensive-measures.md)
**Protection against abuse, spam, and denial-of-service attacks**
**Topics:**
- Connection and subscription management
- Event publishing rate limits
- Content filtering (blacklists/whitelists)
- Event validation plugin system (WritePolicy/QueryPolicy)
- Relay health management (naughty list, exponential backoff)
- Privacy-preserving IP tracking
- Future enhancements (per-IP rate limiting)
**Read when:** You want to understand how ngit-grasp protects against abuse and what defensive features are available
---
### [GRASP-05 Archive Mode](grasp-05-archive.md)
**Read-only mirroring of repositories**
**Topics:**
- Archive whitelist configuration
- Archive-all mode
- Read-only mode defaults
- Use cases for backup/mirror relays
**Read when:** You want to understand how to run an archive/backup relay
---
### [Deletion Requests](deletion-requests.md)
**Handling repository and event deletion**
**Topics:**
- Deletion request architecture
- Delete disrespector concept
- Preventing left-pad scenarios
- Archival policies
**Read when:** You want to understand how ngit-grasp handles deletion events (planned feature)
---
## Planned Explanation Documentation
### GRASP Protocol Design
+165
View File
@@ -0,0 +1,165 @@
# 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:
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
**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)
- 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.
### 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 were considered but deferred until abuse is detected in production. The current protections (per-connection limits, total connection limit, content filtering) are sufficient for the git relay use case.
**Decision rationale:** The primary DoS vector is connection exhaustion, which is addressed by the total connection limit (`NGIT_MAX_CONNECTIONS`). Per-IP enforcement would require custom middleware in rust-nostr relay-builder (which currently tracks limits per WebSocket connection, not per IP). If abuse is detected via the per-IP monitoring metrics, enforcement should be implemented as a PR to rust-nostr/relay-builder to benefit the entire Nostr ecosystem.
**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 (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) |
| **Not Implemented** |
| 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
#### `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 {
type = types.str;
default = "ngit-grasp-${name}";
@@ -295,6 +301,7 @@ let
NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist;
NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist;
NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist;
NGIT_MAX_CONNECTIONS = toString cfg.maxConnections;
RUST_LOG = cfg.logLevel;
} // optionalAttrs (cfg.relayName != null) {
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
#[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")]
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 {
@@ -703,6 +708,7 @@ impl Config {
repository_whitelist: String::new(),
repository_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()
.database(database.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();
tracing::info!(