fix(server): trusted-proxy client-IP extraction for rate-limit/audit keying
Resolves coord todo 3c1f372a (Task-4 review SHOULD-FIX). Behind NPM-on-loopback, ConnectInfo was 127.0.0.1 so the rate limiter + lockout bucketed every client under one IP. New shared utils::ip_extract::client_ip() honors X-Real-IP / X-Forwarded-For (rightmost-untrusted hop) ONLY when the TCP peer is a configured trusted proxy (CONNECT_TRUSTED_PROXIES env, default loopback, fail-closed); untrusted peers are keyed by their true peer IP (forged headers ignored). Wired into the 3 rate-limit middleware, the validate_code lockout feed, and the agent/ viewer WS handlers so the limiter, lockout, and audit ip_address all key on the real client consistently. 13 unit tests (spoof rejection, XFF walk, fail-safe defaults). Code-reviewed APPROVED. Not cargo-check-verified locally (no toolchain); build-host/CI verification follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,7 @@ pub async fn agent_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<AgentParams>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
// The CLIENT-SUPPLIED agent_id. For a per-agent-key (managed) agent this is
|
||||
@@ -96,7 +97,12 @@ pub async fn agent_ws_handler(
|
||||
.unwrap_or_else(|| agent_id.clone());
|
||||
let support_code = params.support_code.clone();
|
||||
let api_key = params.api_key.clone();
|
||||
let client_ip = addr.ip();
|
||||
// Real client IP via the trusted-proxy-aware extractor (shared with the rate
|
||||
// limiter and audit log). Behind NPM on loopback, `addr.ip()` is 127.0.0.1;
|
||||
// this resolves the actual remote agent IP from forwarding headers when the
|
||||
// peer is a trusted proxy, and ignores those headers (using the peer) when it
|
||||
// is not — so the connection-rejected events below record the real client.
|
||||
let client_ip = crate::utils::ip_extract::client_ip(&addr, &headers, &state.trusted_proxies);
|
||||
|
||||
// SECURITY: Agent must provide either a support code OR an API key
|
||||
// Support code = ad-hoc support session (technician generated code)
|
||||
@@ -408,9 +414,13 @@ pub async fn viewer_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<ViewerParams>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let client_ip = addr.ip();
|
||||
// Real client IP via the trusted-proxy-aware extractor (shared with the rate
|
||||
// limiter and audit log) so the viewer audit events record the real client,
|
||||
// not the proxy's loopback address.
|
||||
let client_ip = crate::utils::ip_extract::client_ip(&addr, &headers, &state.trusted_proxies);
|
||||
|
||||
// The session the viewer is asking to join. Parse early — a malformed UUID
|
||||
// can never match a viewer token's `session_id` claim, so reject up front.
|
||||
|
||||
Reference in New Issue
Block a user