Files
ngit-grasp/d6ee-defensive-relay-features.md
T

594 lines
23 KiB
Markdown

# Defensive Relay Features
**ID:** d6ee
> **⚠️ COORDINATION REQUIRED:** This issue is part of the Administrator Observability and Management Strategy (issue ec1f).
> **Before starting work:** Check issue ec1f for current phase, dependencies, and coordination requirements.
> This issue contributes to **Phase 4 (Enforcement)**. Only Phase 1 (config-based protection) is currently approved.
## Issue Summary
~~Implement defensive rate limiting and resource controls to protect the relay from abuse. Focus on per-connection and per-IP rate limits using rust-nostr relay-builder capabilities and custom extensions where needed.~~
**UPDATE 2026-01-13:** Need to review and validate the current approach before implementing. Concerns about over-engineering and maintaining code that relay-builder should handle. Per-IP throttling may be more appropriate for git data fetching (separate issue) than general relay operations.
## ✅ DECISION: PHASE 1 ONLY (Config-Based Protection)
**Status:** APPROVED - Implement Phase 1, defer Phase 2/3
**Priority:** High (addresses critical DoS vulnerability)
### Review Complete - Key Decisions
**✅ Review Tasks Completed (2026-01-14):**
- [x] Task 1: Validated current state - only database + WritePolicy configured
- [x] Task 2: Validated docs - 96.7% accurate, one minor error found
- [x] Task 3: Gap analysis - total connection limit is critical missing piece
- [x] Task 4: Config-only path recommended (70% protection, 15% effort)
- [x] Task 5: Upstream contribution not recommended (no demand, specialized use case)
**Decision Rationale:**
1. **Config-only is sufficient** - Total connection limit addresses primary DoS vector
2. **Defer per-IP enforcement** - Implement only if abuse detected in production
3. **Git throttling separate** - Git endpoint needs separate IP-based throttling (analyze separately)
4. **Upstream later** - If Phase 2 needed, prefer PR to rust-nostr/relay-builder
### Phase 1: Explicit Rate Limits & Connection Limit (IMPLEMENT NOW)
**Scope:**
1. Make RateLimit explicit in code (currently using defaults)
- max_reqs: 500 (max subscriptions per connection)
- notes_per_minute: 60 (max events per minute per connection)
2. Add max_connections configuration (default: 500)
3. Update all 4 config locations (CRITICAL - see AGENTS.md)
4. Fix documentation error (filter limit 5000→500)
5. Document Phase 2 deferral decision in defensive-measures.md
**Files to modify:**
- `src/nostr/builder.rs` - Add explicit RateLimit and max_connections
- `src/config.rs` - Add max_connections field
- `.env.example` - Add NGIT_MAX_CONNECTIONS
- `nix/module.nix` - Add maxConnections option
- `docs/reference/configuration.md` - Document new option
- `docs/explanation/defensive-measures.md` - Fix filter limit error + document Phase 2 deferral
**Effort:** 2-3 hours
**Acceptance Criteria:**
- [ ] RateLimit explicitly configured in builder.rs with comments
- [ ] max_connections configurable via NGIT_MAX_CONNECTIONS (default: 500)
- [ ] All 4 config locations synced
- [ ] Documentation error fixed (filter limit 5000→500)
- [ ] Documentation updated with Phase 2 deferral decision
- [ ] Integration test: Connection rejected after reaching total limit
- [ ] All tests passing
- [ ] Changes committed
### Phase 2 & 3: Per-IP Enforcement (DEFERRED - Future Work)
**Status:** Deferred until abuse detected in production
**When to implement:**
- IF ConnectionTracker shows IPs exceeding 10 connections
- IF event spam patterns emerge from single IPs
- Monitor for 2-4 weeks after Phase 1 deployment
**Preferred Implementation Path:**
1. **First choice:** 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
- See: docs/explanation/defensive-analysis-of-other-relays.md for research
2. **Fallback:** Implement in ngit-grasp if upstream not interested
- Use original Phase 2 & 3 plan (see below)
- Per-IP connection enforcement (4-6 hours)
- Per-IP event rate limiting (6-8 hours)
**Implementation details preserved below for future reference.**
### Git Endpoint IP-Based Throttling (MOVED TO SEPARATE ISSUE)
**Status:** ✅ Spun out to issue ff38
**Rationale:**
- Git data fetching has different threat model than Nostr relay
- HTTP endpoints, not WebSocket connections
- Different attack vectors (bandwidth, CPU for pack generation)
- Should NOT interact with relay code
**See:** Issue ff38 (git-endpoint-ip-throttling) for full analysis and implementation plan
---
## PREVIOUS IMPLEMENTATION PLAN (FOR REFERENCE ONLY)
## Findings
### rust-nostr relay-builder Capabilities
**Built-in Features (✅ Available):**
- Per-connection subscription limits (`RateLimit.max_reqs`, default: 500)
- Per-connection event rate limits (`RateLimit.notes_per_minute`, default: 60)
- Total connection limit (`max_connections(n)`)
- Subscription ID length limit (`max_subid_length`, default: 250)
- Filter result limits (`max_filter_limit`, default: 5000)
**Custom ngit-grasp Features (✅ Implemented):**
- Per-IP connection monitoring (`ConnectionTracker` in `src/metrics/connection.rs`)
- Status: Monitoring only, does NOT enforce limits
- Tracks connections per IP, flags abusers at threshold (default: 10)
- Privacy-preserving: IP addresses never exposed in Prometheus metrics
- Event blacklist (block all events from specific authors)
- Repository blacklist (block specific repositories/developers)
- WritePolicy plugin system (access to IP address for custom validation)
**Not Supported:**
- Per-IP subscription limits (tracked per connection, not per IP)
- Per-IP event rate limits (tracked per connection, not per IP)
## Architecture Decision
### Rate Limiting Strategy
**Per-Connection Limits (relay-builder):**
- Use built-in `RateLimit` configuration
- Default values: 500 concurrent subscriptions, 60 events/min
- Prevents single connection from overwhelming relay
**Per-IP Limits (custom implementation):**
- Connection enforcement: Extend `ConnectionTracker` to reject connections
- Event rate limiting: Add `IpRateLimiter` to `Nip34WritePolicy`
- Hardcoded sensible defaults (configurable in future if needed)
**Privacy Model:**
- IP addresses tracked internally, NEVER exposed in Prometheus metrics
- Only aggregate counts exposed (total rate limited, flagged IPs)
- Rate limit events logged with IP for operator debugging
### Metrics Changes: OUT OF SCOPE
**Decision:** Do NOT modify metrics in this issue. Current metrics implementation needs architectural review.
**Reasoning:**
- Current metrics use "abuse detection" threshold model
- New rate limiting uses enforcement model
- These should be aligned but require broader metrics redesign
- Deferring to separate issue to avoid scope creep
**Future work:**
- Create separate issue for metrics architectural review
- Align metrics with policy enforcement triggers
- Add rate limiting specific metrics (events rejected, IPs in cooldown)
## Implementation Plan
### Phase 1: Configure Built-in Rate Limits ⚡ (Quick Win)
**Estimated Time:** 1-2 hours
**Can run in parallel:** No (sequential)
**Scope:**
1. Configure `RateLimit` in relay builder with defaults (500 subs, 60 events/min)
2. Add total connection limit (500 connections, configurable)
**Files to modify:**
- `src/nostr/builder.rs` - Add RateLimit and max_connections to LocalRelayBuilder
- `src/config.rs` - Add `max_connections` config field
- `.env.example` - Add NGIT_MAX_CONNECTIONS example
- `nix/module.nix` - Add maxConnections NixOS option
- `docs/reference/configuration.md` - Document new option
**Acceptance Criteria:**
- ✅ Relay configured with RateLimit (500 subs, 60 events/min)
- ✅ Total connection limit configurable via NGIT_MAX_CONNECTIONS (default: 500)
- ✅ Integration test: Connection rejected after reaching total limit
- ✅ All tests passing
- ✅ Changes committed with message: "Add per-connection rate limits and total connection limit"
- ✅ Documentation updated in defensive-measures.md
**Configuration sync (CRITICAL):**
- Update all FOUR locations: `src/config.rs`, `docs/reference/configuration.md`, `nix/module.nix`, `.env.example`
---
### Phase 2: Per-IP Connection Enforcement
**Estimated Time:** 4-6 hours
**Can run in parallel:** No (depends on Phase 1)
**Scope:**
1. Extend `ConnectionTracker` to enforce per-IP connection limit (hardcoded: 10)
2. Add actix-web middleware to check limit before WebSocket upgrade
3. Return 429 Too Many Requests with retry-after header
**Files to create/modify:**
- `src/metrics/connection.rs` - Modify `on_connect()` to return Result, check limit
- `src/middleware/ip_limits.rs` - NEW: Actix middleware for connection enforcement
- `src/middleware/mod.rs` - NEW: Module exports
- `src/lib.rs` - Export middleware module
- `src/main.rs` - Add middleware to actix-web app
**Implementation Details:**
**ConnectionTracker changes:**
```rust
pub struct ConnectionTracker {
max_per_ip: u32, // NEW: hardcoded to 10
// ... existing fields
}
pub fn on_connect(&self, ip: IpAddr) -> Result<(), String> {
// Check count BEFORE incrementing
let current = self.connections.get(&ip).map(|i| i.count).unwrap_or(0);
if current >= self.max_per_ip {
return Err(format!("Connection limit ({}) exceeded", self.max_per_ip));
}
// Existing increment logic...
Ok(())
}
```
**Actix middleware approach:**
```rust
// Middleware wraps relay handler
// Checks ConnectionTracker.on_connect() before WebSocket upgrade
// Returns 429 if limit exceeded
// Allows connection if under limit
```
**Acceptance Criteria:**
- ✅ ConnectionTracker enforces 10 connections per IP (hardcoded)
- ✅ 11th connection from same IP returns 429 Too Many Requests
- ✅ Error logged with IP address for operator debugging
- ✅ Integration test: Multiple connections from same IP rejected after limit
- ✅ Integration test: Connections from different IPs allowed
- ✅ All tests passing (including Phase 1 tests)
- ✅ Changes committed with message: "Enforce per-IP connection limit (max 10)"
- ✅ Documentation updated in defensive-measures.md
---
### Phase 3: Per-IP Event Rate Limiting
**Estimated Time:** 6-8 hours
**Can run in parallel:** No (depends on Phase 1 & 2)
**Scope:**
1. Implement token bucket rate limiter
2. Integrate with `Nip34WritePolicy` to check IP rate before event validation
3. Hardcoded limit: 100 events per minute per IP
**Files to create/modify:**
- `src/nostr/ip_rate_limiter.rs` - NEW: Token bucket implementation
- `src/nostr/builder.rs` - Add IpRateLimiter to Nip34WritePolicy
- `src/nostr/mod.rs` - Export ip_rate_limiter module
**Implementation Details:**
**Token Bucket Algorithm:**
```rust
pub struct IpRateLimiter {
limiters: DashMap<IpAddr, TokenBucket>,
events_per_minute: u32, // Hardcoded: 100
}
struct TokenBucket {
tokens: f64, // Current tokens (0.0 to events_per_minute)
last_refill: Instant, // Last refill time
capacity: f64, // Max tokens (events_per_minute)
refill_rate: f64, // Tokens per second (events_per_minute / 60.0)
}
impl IpRateLimiter {
pub fn check_and_consume(&self, ip: IpAddr) -> bool {
// Refill tokens based on time elapsed
// If tokens >= 1.0, consume 1 token and return true
// Else return false (rate limited)
}
}
```
**Nip34WritePolicy integration:**
```rust
impl WritePolicy for Nip34WritePolicy {
fn admit_event(&self, event: &Event, addr: &SocketAddr) -> BoxedFuture<WritePolicyResult> {
// Check IP rate limit FIRST (before blacklist checks)
if !self.ip_rate_limiter.check_and_consume(addr.ip()) {
tracing::warn!(ip = %addr.ip(), "Event rejected: IP rate limit exceeded");
return WritePolicyResult::reject("rate limited: too many events from your IP");
}
// Existing validation (blacklist, GRASP-01, etc.)...
}
}
```
**Cleanup task:**
- Spawn background task to remove stale IP entries (no activity for 5 minutes)
- Prevents unbounded memory growth
- Runs every 60 seconds
**Acceptance Criteria:**
- ✅ IpRateLimiter implements token bucket (100 events/min per IP)
- ✅ Integrated into WritePolicy (checked before event validation)
- ✅ 101st event from same IP within 1 minute rejected
- ✅ Events from different IPs allowed
- ✅ Stale IP entries cleaned up (no memory leak)
- ✅ Unit tests: Token bucket refill logic
- ✅ Unit tests: Rate limit enforcement
- ✅ Integration test: Send >100 events/min from single IP, verify rejection
- ✅ Integration test: Events accepted after rate limit window passes
- ✅ All tests passing (including Phase 1 & 2 tests)
- ✅ Changes committed with message: "Add per-IP event rate limiting (100 events/min)"
- ✅ Documentation updated in defensive-measures.md
---
### Phase 4: Documentation & Polish
**Estimated Time:** 2-3 hours
**Can run in parallel:** No (depends on all phases)
**Scope:**
1. Update all documentation to reflect implemented features
2. Add operational notes for relay operators
3. Update design docs with architectural decisions
**Files to modify:**
- `docs/explanation/defensive-measures.md` - Move features from "Not Implemented" to "Implemented"
- `README.md` - Update defensive measures section
- `AGENTS.md` - Add notes about new rate limiting code patterns (if needed)
**Acceptance Criteria:**
- ✅ defensive-measures.md accurately reflects all implemented features
- ✅ README.md defensive measures section updated
- ✅ Configuration docs show all new options
- ✅ All four config locations synced (src/config.rs, docs, nix/module.nix, .env.example)
- ✅ Changes committed with message: "docs: Update defensive measures documentation"
---
## Testing Strategy
### Unit Tests
- `ConnectionTracker::on_connect()` enforcement logic (Phase 2)
- `IpRateLimiter::check_and_consume()` token bucket (Phase 3)
- Token refill calculations (Phase 3)
- Config parsing for max_connections (Phase 1)
### Integration Tests
- Connection rejection after total limit (Phase 1)
- Connection rejection after per-IP limit (Phase 2)
- Event rejection after per-IP rate limit (Phase 3)
- Rate limit window expiry (Phase 3)
- Multi-IP scenarios (all phases)
### Manual Testing (after all phases)
- Use `grasp-audit` to simulate attack scenarios
- Test with multiple clients from same IP
- Test with rapid event publishing
- Verify metrics don't expose IP addresses
---
## Configuration Summary
### Phase 1: New Config Options
```bash
# Total connection limit (default: 500)
NGIT_MAX_CONNECTIONS=500
```
### Phase 2 & 3: Hardcoded Values
- Per-IP connection limit: **10 connections** (hardcoded in ConnectionTracker)
- Per-IP event rate limit: **100 events/min** (hardcoded in IpRateLimiter)
**Rationale for hardcoding:**
- Proven defensive values based on common relay patterns
- Reduces configuration surface area
- Can be made configurable later if operators need flexibility
- Simpler initial implementation
---
## Privacy & Logging
**IP Address Handling:**
- ✅ Tracked internally in DashMap for rate limiting
- ✅ Logged when limits exceeded (operator debugging)
- ❌ NEVER exposed in Prometheus metrics (only aggregate counts)
- ❌ NEVER logged in normal operation (only when rate limited)
**Log Examples:**
```
WARN: Connection rejected from 192.168.1.100: Connection limit (10) exceeded
WARN: Event rejected from 192.168.1.100: IP rate limit exceeded (100 events/min)
```
---
## Out of Scope (Future Work)
### Metrics Architectural Review (Separate Issue)
**Current state:**
- Metrics use "abuse detection threshold" model (monitoring only)
- New rate limiting uses "enforcement" model (reject at limit)
- These should be unified
**Future work:**
- Create new issue for metrics redesign
- Add enforcement-specific metrics:
- `ngit_connections_rejected_total{reason="total_limit|per_ip_limit"}`
- `ngit_events_rejected_total{reason="ip_rate_limit|blacklist|..."}`
- `ngit_ip_rate_limited_active` (gauge of IPs currently in cooldown)
- Align with policy enforcement points
- Remove or repurpose "abuse threshold" concept
### Per-IP Subscription Limits
**Why out of scope:**
- Requires complex middleware to intercept REQ messages
- relay-builder tracks subscriptions per connection, not per IP
- Lower priority than event rate limiting
### HTTP Endpoint Protection
**Why out of scope:**
- Different attack surface (HTTP vs WebSocket)
- Needs separate rate limiting strategy
- Lower priority for git-focused relay
### Query Filtering (QueryPolicy)
**Why out of scope:**
- Not a critical defensive measure for git relay
- Can be added later if abuse patterns emerge
### Per-IP Git Data Throttling (Potential Separate Issue)
**Why might need separate issue:**
- Git data fetching (packs, objects) has different characteristics than Nostr events
- Already have domain-based throttling for outbound git fetches (`ThrottleManager`)
- Inbound git push/fetch might benefit from per-IP limits
- Should be scoped separately from general relay rate limiting
- **TODO:** Consider creating new issue for git-specific per-IP throttling
---
## Risks & Mitigations
### Risk 1: Legitimate users behind NAT/proxy
**Scenario:** Multiple clients behind single IP hit connection/rate limits
**Mitigation:**
- Per-IP connection limit set reasonably high (10)
- Per-IP event rate limit set reasonably high (100/min)
- Monitor logs for false positives
- Can add IP whitelist in future if needed
### Risk 2: Middleware integration complexity
**Scenario:** Actix middleware doesn't work with relay-builder
**Mitigation:**
- Test middleware approach early in Phase 2
- Fallback: Log warnings but don't enforce (monitoring mode)
- Alternative: Contribute hooks to relay-builder upstream
### Risk 3: Memory growth from IP tracking
**Scenario:** DashMap grows unbounded with unique IPs
**Mitigation:**
- Cleanup task removes stale entries (5+ min inactive)
- Proven pattern already used in ConnectionTracker
- Monitor memory in production
---
## Success Criteria
**How we'll know this is working:**
1. **Protection active:**
- 11th connection from same IP rejected (Phase 2)
- 101st event/min from same IP rejected (Phase 3)
- Logs show IP addresses when limits hit
2. **No false positives:**
- Legitimate users can connect and publish normally
- Sync from other relays works without rate limiting issues
- No user complaints about false rejections
3. **Resource usage stable:**
- No memory leaks from IP tracking
- Cleanup task removes stale entries
- CPU usage reasonable with token bucket calculations
4. **Privacy maintained:**
- IP addresses NEVER in Prometheus metrics
- Only aggregate counts exposed
- Metrics requests don't expose tracked IPs
---
## Progress Tracking
- [ ] Phase 1: Configure built-in rate limits (1-2h)
- [ ] Phase 2: Per-IP connection enforcement (4-6h)
- [ ] Phase 3: Per-IP event rate limiting (6-8h)
- [ ] Phase 4: Documentation & polish (2-3h)
**Total estimated time:** 13-19 hours
---
## Progress
### 2026-01-14 [Session 14:30] - REVIEW COMPLETE, PHASE 1 APPROVED
- **Status Change:** Review complete, proceeding with Phase 1 only
- Completed: All 5 review tasks using subagents
- **Decision:** Implement Phase 1 (config-only) with explicit rate limits
- **Deferred:** Phase 2 & 3 (per-IP enforcement) until abuse detected in production
- **Separate concern:** Git endpoint IP-based throttling (analyze separately)
**Review Findings:**
- Task 1: Current config uses only database + WritePolicy, all else is defaults
- Task 2: Documentation 96.7% accurate (1 minor error: filter limit 5000→500)
- Task 3: Critical gap is total connection limit (DoS vulnerability)
- Task 4: Config-only approach gives 70% protection for 15% effort
- Task 5: Upstream contribution not recommended (specialized use case, no demand)
**Phase 1 Scope (APPROVED):**
- Add explicit RateLimit configuration (make defaults visible in code)
- Add max_connections config option (default: 500)
- Update all 4 config locations (src/config.rs, docs, nix/module.nix, .env.example)
- Fix documentation error (filter limit)
- Effort: 2-3 hours
**Phase 2 & 3 Deferred (Future Work if Abuse Detected):**
- Per-IP connection enforcement (10 per IP)
- Per-IP event rate limiting (100 events/min per IP)
- **Preferred approach:** Contribute to rust-nostr/relay-builder as PR
- **Fallback:** Implement in ngit-grasp if upstream not interested
- Document implementation plan for future reference
**Git Endpoint Throttling (Separate Analysis Needed):**
- Git data fetching has different threat model than Nostr relay
- Needs IP-based throttling with sensible limits
- Should NOT interact with relay code
- Next: Analyze requirements with architect subagent
### 2026-01-13 [Session 21:15] - IMPLEMENTATION PAUSED
- **Status Change:** Paused implementation for validation review
- Concern: May be over-engineering with custom per-IP rate limiting
- Concern: Should relay-builder handle IP-based throttling, not ngit-grasp?
- Concern: Per-IP throttling might be more relevant for git data fetching (separate issue)
- Added: Review tasks section to validate current state before proceeding
- Decision: Complete review tasks before any implementation
- Next: Work through review tasks to determine simplest path forward
**Key Questions:**
- Is per-connection limiting (already in relay-builder) sufficient?
- Should we contribute IP rate limiting to upstream relay-builder?
- Should git data fetching have separate per-IP throttling issue?
- What's the minimal viable protection (prefer config over code)?
### 2026-01-13 [Session 20:43] - RESEARCH COMPLETED
- Completed: Comprehensive research on defensive features in major Nostr relays (strfry, nostr-rs-relay, khatru)
- Completed: Analysis of ngit-grasp's current defensive capabilities
- Completed: Research on Rust rate limiting ecosystem (governor crate)
- Created: `docs/explanation/defensive-analysis-of-other-relays.md` with detailed findings
- ~~Decision: The existing implementation plan is well-aligned with industry best practices~~
- ~~Next: Proceed with Phase 1 implementation (configure built-in rate limits)~~ **SUPERSEDED - see above**
**Key Findings:**
- Most relays have permissive defaults; khatru is most opinionated (2 events per 3min/IP)
- Governor crate is industry standard for Rust rate limiting (GCRA algorithm)
- ngit-grasp already has good infrastructure (ConnectionTracker, ThrottleManager pattern)
- Proposed defaults are well-balanced compared to other relays
- **However:** Need to validate if this complexity is necessary for our use case
## References
- rust-nostr relay-builder: `nostr-relay-builder` v0.44.0 (git rev 4767ad13)
- Token bucket algorithm: https://en.wikipedia.org/wiki/Token_bucket
- Actix-web middleware: https://actix.rs/docs/middleware/
- Current implementation: `src/metrics/connection.rs` (ConnectionTracker)
- Design docs: `docs/explanation/defensive-measures.md`
- Research: `docs/explanation/defensive-analysis-of-other-relays.md` (comprehensive analysis)