feat(proxy): fall back to TCP peer addr in extract_client_ip #33
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_proxy!33
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/peer-addr-fallback"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #31. Surfaces #32 (pre-existing X-Forwarded-For spoof gate, separate issue).
Summary
Adds the missing third tier to
extract_client_ip: TCP peer address from the kernel, used when neitherX-Real-IpnorX-Forwarded-Foris present. Without this, hero_proxy as the edge can never identify direct browser connections — which silently breaks the IP-auto-login feature configured viauser_networks.Changes
crates/hero_proxy_server/src/proxy.rsextract_client_ipsignature gainsOption<&SocketAddr>parameter and returns `peer_addr.map(crates/hero_proxy_server/src/main.rsinto_make_service()call sites (HTTPaxum::serve, HTTPS-ACMEaxum_server::bind, HTTPS-self-signedaxum_server::bind_rustls) →into_make_service_with_connect_info::<SocketAddr>()so the ConnectInfo extractor can populate the request extension.Net: +20 lines, -9 lines.
Why
extract_client_ipwas missing the standard third tier of every edge HTTP server's client-IP lookup chain (axum-extraClientIp, tower-httpRealIp, nginxreal_ip_module, actixConnectionInfo— all use this pattern):Since hero_proxy is designed as the edge (terminates TLS, owns auth, is the public listener), step 3 is what's needed for its actual deployment shape. Putting another reverse proxy in front of hero_proxy would defeat its purpose.
Concrete consequence today: an operator can add a
user_networksrow mapping127.0.0.1/32 → user 1via the admin UI, setauto_accept = 1, andfind_user_for_ipwill still never trigger for browser connections to:9997becauseextract_client_ipreturnsNone. The feature is dead from the deployment shape it was built for.Test plan
cargo build --release -p hero_proxy_servercleancurl -X POST http://127.0.0.1:9997/hero_collab/rpc/rpc -d '{"method":"user.me",...}'returns{authenticated:false, user:null}despiteuser_networksrow matching loopbackcurl -H "X-Real-Ip: 10.0.0.5" ...still resolves to 10.0.0.5axum_server::bind_rustls+into_make_service_with_connect_info::<SocketAddr>works end-to-end with TLS (compiles fine; runtime test would require ACME or self-signed cert provisioning that isn't part of CI today)Pre-existing trust gap (NOT fixed by this PR — see #32)
This patch inherits the existing trust gap:
extract_client_iphonorsX-Forwarded-Forfrom any peer, with no validation that the immediate TCP peer is a trusted upstream proxy. With IP auto-login now actually working via this PR, that gap becomes operationally exploitable — an attacker reaching the listener directly can spoofX-Forwarded-For: 127.0.0.1and impersonate the loopback-mapped user.The patch does not introduce that gap; it inherits it. Filed as #32. Recommended fix shape: add
trusted_proxies: Vec<IpNetwork>setting; only honorX-Real-Ip/X-Forwarded-Forwhen the immediate peer is in that list; default empty (= edge mode = ignore those headers).The dev-mode behaviour question (what happens when the operator wants the picker even with
user_networksconfigured) is handled separately on the hero_collab side: that service's--auth-mode=devwill be updated to ignore upstream identity entirely, making the auth contract honest in both modes. No coupling needed in this PR.Notes
axum_server(used for both HTTPS branches) supportsinto_make_service_with_connect_infonatively; the underlying TCP peer addr is what gets exposed (correct — you want the real client IP, not "TLS").ConnectInfo<SocketAddr>is infallible after the make_service wiring — no error paths added to the handler.`extract_client_ip` only read `X-Real-Ip` and `X-Forwarded-For`. Both are upstream-proxy headers — neither is set by a browser hitting hero_proxy directly. Result: `client_ip = None` → `find_user_for_ip` never matches → no `X-Hero-User` injected via IP auto-login → downstream apps in proxy mode fail-closed with 'No hero_collab account for "..."' even though the operator has a `user_networks` row matching their loopback IP. The pattern hero_proxy was missing is the same one every edge HTTP server uses: trust X-Forwarded-For when present (CDN deployments), trust X-Real-Ip when present (nginx/caddy fronts), otherwise read the kernel-supplied TCP peer address. This patch adds the third fallback. * `extract_client_ip` signature gains an `Option<&SocketAddr>` parameter and returns `peer_addr.map(|a| a.ip().to_string())` after the existing header lookups. * `proxy_handler` extracts `axum::extract::ConnectInfo<SocketAddr>` and passes it to `extract_client_ip`. * Listener wiring in `main.rs` switches three call sites from `into_make_service()` to `into_make_service_with_connect_info::<SocketAddr>()` — required for the ConnectInfo extractor to populate the request extension. Known follow-up (filed separately): the existing trust model honors X-Forwarded-For from any source. Anyone hitting `:9997` directly with a spoofed X-Forwarded-For gets the spoofed IP's auto-login access. This patch does not introduce that gap; it just inherits it. Fix shape: add `trusted_proxies: Vec<IpNetwork>` setting; only honor X-Real-Ip / X-Forwarded-For when peer is in that list.55620487bd54b0694a67