Files
ngit-grasp/ff38-git-endpoint-ip-throttling.md
T

248 lines
8.8 KiB
Markdown

# Git Endpoint IP-Based Throttling
**ID:** ff38
## Problem
Git HTTP endpoints currently have **no IP-based rate limiting**, making the relay vulnerable to resource exhaustion attacks:
- **Bandwidth exhaustion:** Repeated large clone operations can saturate network and incur high egress costs
- **CPU exhaustion:** Pack generation for complex histories can saturate server CPU
- **Connection exhaustion:** Many concurrent git operations can exhaust file descriptors and memory
- **Slowloris-style attacks:** Slow pack downloads holding connections open
### Current State
**Git HTTP endpoints** (implemented in `src/git/handlers.rs`, routed via `src/http/mod.rs`):
- `/<npub>/<id>.git/info/refs?service=git-upload-pack` (clone/fetch ref advertisement)
- `/<npub>/<id>.git/info/refs?service=git-receive-pack` (push ref advertisement)
- `/<npub>/<id>.git/git-upload-pack` (clone/fetch pack transfer)
- `/<npub>/<id>.git/git-receive-pack` (push pack transfer)
**Current protection level:** NONE
- No rate limiting middleware
- No concurrent connection limits per IP
- No bandwidth throttling
- Only GRASP authorization for push operations
### Threat Model
Git operations are fundamentally different from Nostr relay operations:
| Aspect | Git Endpoints | Nostr Relay |
|--------|---------------|-------------|
| **Resource intensity** | Very high (pack generation, large transfers) | Low (small JSON events) |
| **Connection duration** | Long (minutes for large repos) | Variable (seconds to hours) |
| **Bandwidth** | Potentially GB per operation | KB per event |
| **CPU** | High (delta compression, pack generation) | Low (signature verification) |
This is why git endpoint throttling must be **separate** from relay rate limiting (issue d6ee).
## Plan
### Phase 1: Implement GitThrottleManager (9-12 hours)
**Scope:**
- Create `src/git/throttle.rs` with `GitThrottleManager`
- Per-IP concurrent operation limit: **5 concurrent git operations**
- Per-IP rate limit: **30 requests per minute**
- Hardcoded sensible defaults (no configuration initially)
**Files to create:**
- `src/git/throttle.rs` - NEW: GitThrottleManager implementation
**Files to modify:**
- `src/git/mod.rs` - Export throttle module
- `src/http/mod.rs` - Integrate throttle check before git handler dispatch
- `src/main.rs` - Initialize GitThrottleManager, spawn cleanup task
**Architecture:**
```rust
// src/git/throttle.rs
use dashmap::DashMap;
use std::collections::VecDeque;
use std::net::IpAddr;
use std::time::{Duration, Instant};
/// Per-IP throttle state for git operations
struct IpThrottleState {
/// Current in-flight git operations
in_flight: u32,
/// Request timestamps (sliding window for rate limiting)
request_times: VecDeque<Instant>,
}
/// Manages IP-based rate limiting for git HTTP endpoints
pub struct GitThrottleManager {
states: DashMap<IpAddr, IpThrottleState>,
max_concurrent_per_ip: u32, // Hardcoded: 5
max_per_minute_per_ip: u32, // Hardcoded: 30
}
impl GitThrottleManager {
pub fn with_defaults() -> Self {
Self::new(5, 30)
}
/// Check if IP can proceed with git operation
/// Returns Ok(guard) if allowed, Err(reason) if throttled
pub fn try_acquire(&self, ip: IpAddr) -> Result<ThrottleGuard, ThrottleError>;
/// Cleanup expired entries (called periodically)
pub fn cleanup(&self);
}
/// RAII guard that releases throttle on drop
pub struct ThrottleGuard { /* ... */ }
pub enum ThrottleError {
TooManyConcurrent { current: u32, max: u32 },
RateLimitExceeded { requests_in_window: u32, max: u32 },
}
```
**Integration in `src/http/mod.rs`:**
```rust
// In HttpService::call(), before git handler dispatch
if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) {
let ip = self.remote.ip();
let throttle_guard = match self.git_throttle.try_acquire(ip) {
Ok(guard) => guard,
Err(e) => {
tracing::warn!(ip = %ip, error = %e, "Git request throttled");
return Box::pin(async move {
Ok(add_cors_headers(Response::builder())
.status(hyper::StatusCode::TOO_MANY_REQUESTS)
.header("Retry-After", "60")
.body(Full::new(Bytes::from(format!("Rate limited: {}", e))))
.unwrap())
});
}
};
// ... existing git handling ...
// throttle_guard dropped automatically when response completes
}
```
**Cleanup strategy:**
- RAII: `ThrottleGuard` decrements `in_flight` on drop
- Background task: Every 60 seconds, remove IPs with no activity in last 5 minutes
- Sliding window: Prune request timestamps older than 60 seconds on each access
**Acceptance Criteria:**
- [ ] GitThrottleManager implements concurrent limit (5 per IP)
- [ ] GitThrottleManager implements rate limit (30/min per IP)
- [ ] 6th concurrent git operation from same IP returns 429
- [ ] 31st request in 60 seconds from same IP returns 429
- [ ] Different IPs can each have 5 concurrent operations
- [ ] ThrottleGuard properly releases on drop (RAII)
- [ ] Cleanup task removes stale IP entries (no memory leak)
- [ ] Unit tests: Concurrent limit enforcement
- [ ] Unit tests: Rate limit sliding window
- [ ] Unit tests: Cleanup logic
- [ ] Integration test: Multiple concurrent git clones from same IP
- [ ] Integration test: Rapid git requests hitting rate limit
- [ ] All tests passing
- [ ] Changes committed with message: "Add IP-based throttling for git HTTP endpoints"
### Phase 2: Documentation & Metrics (2-3 hours)
**Scope:**
- Document git throttling in defensive-measures.md
- Add Prometheus metrics for git throttling
- Update README with git endpoint protection
**Files to modify:**
- `docs/explanation/defensive-measures.md` - Add git throttling section
- `README.md` - Update defensive measures section
- `src/git/throttle.rs` - Add Prometheus metrics (optional)
**Metrics to add (optional):**
- `ngit_git_requests_throttled_total{reason="concurrent|rate_limit"}`
- `ngit_git_concurrent_operations{ip_count}` (gauge of active IPs)
**Acceptance Criteria:**
- [ ] defensive-measures.md documents git throttling
- [ ] README updated with git endpoint protection
- [ ] Metrics added (if implemented)
- [ ] Changes committed with message: "docs: Document git endpoint throttling"
### Phase 3: Configuration Support (Future - If Needed)
**Status:** Deferred until operators request tuning
**Scope:**
- Add environment variables for git throttle limits
- Update all 4 config locations (src/config.rs, docs, nix/module.nix, .env.example)
**Proposed config:**
```bash
NGIT_GIT_MAX_CONCURRENT_PER_IP=5
NGIT_GIT_MAX_REQUESTS_PER_MINUTE_PER_IP=30
```
## Progress
### 2026-01-14 [Session 14:45]
- Created: Issue ff38 for git endpoint IP-based throttling
- Separated from d6ee (relay rate limiting) due to different threat model
- Completed: Comprehensive threat analysis via architect subagent
- Completed: Architecture design with GitThrottleManager
- Decision: Hardcoded defaults (5 concurrent, 30/min) initially
- Decision: No interaction with relay code (HTTP layer only)
- Next: Implement Phase 1 after d6ee Phase 1 completes
**Key Design Decisions:**
- Reuse patterns from ThrottleManager (outbound git) and ConnectionTracker (relay)
- RAII guard pattern for automatic cleanup
- DashMap for concurrent access
- Sliding window for rate limiting
- Privacy-preserving (IPs not exposed in metrics)
## Notes
### Relationship to Issue d6ee
**d6ee (Defensive Relay Features):**
- Focuses on Nostr relay WebSocket connections
- Per-connection limits (500 subs, 60 events/min)
- Total connection limit (500)
- Deferred: Per-IP relay enforcement (Phase 2/3)
**ff38 (Git Endpoint Throttling):**
- Focuses on git HTTP endpoints
- Per-IP concurrent operations (5)
- Per-IP request rate (30/min)
- Independent implementation (no relay code interaction)
### Legitimate Use Cases to Preserve
- **CI/CD pipelines:** Multiple concurrent clones from same IP (5 allows this)
- **Corporate proxies/NAT:** Many developers behind single IP (30/min allows burst activity)
- **Shallow clones:** Partial fetches for large repos
- **Incremental fetches:** Small delta updates
- **Mirror operations:** Periodic full syncs
### Comparison with Existing Throttling
**ThrottleManager** (`src/purgatory/sync/throttle.rs`):
- Purpose: Rate limit **outbound** git fetches to remote servers
- Key: Domain (remote server)
- Limits: 5 concurrent, 30/min per domain
- Queue: Yes (identifier queue for processing)
**GitThrottleManager** (this issue):
- Purpose: Rate limit **inbound** git requests from clients
- Key: IP address (client)
- Limits: 5 concurrent, 30/min per IP
- Queue: No (immediate reject with 429)
### References
- Threat analysis: Session 2026-01-14 architect subagent analysis
- Related issue: d6ee (relay rate limiting)
- Existing patterns: `src/purgatory/sync/throttle.rs`, `src/metrics/connection.rs`
- Git HTTP protocol: https://git-scm.com/docs/http-protocol