feat(net): canonical trusted-client-IP resolver (#442) [stacked on #472] - #473
Open
olegbrok wants to merge 1 commit into
Open
feat(net): canonical trusted-client-IP resolver (#442) [stacked on #472]#473olegbrok wants to merge 1 commit into
olegbrok wants to merge 1 commit into
Conversation
PR-2 of the #433 web-exposed hardening track. Establishes one canonical "what is the real client IP for this request?" answer that #436 (LAN filter), #438 (rate limit key), #440 (audit attribution), and #441 (boot guards) all read from instead of each parsing X-Forwarded-For independently. Stacks on #472 (PINKY_DEPLOYMENT_MODE). What ---- * New `pinky_daemon.net` subpackage for cross-cutting network helpers. * `pinky_daemon.net.client_identity`: - `ClientIdentity` frozen dataclass: `ip`, `via_proxy`, `raw_peer`, `forwarded_chain` — what every IP-consuming feature operates on. - `parse_trusted_proxies(value)` — comma-separated CIDRs → tuple of IPNetwork. Bare IPs accepted as /32 or /128. Invalid entries logged and skipped (don't crash startup on operator typos in the list). - `resolve_client_identity(request, mode, trusted_proxies)`: * `trusted` → ip = raw_peer; XFF / Forwarded ignored entirely. * `lan` / `web` → XFF + RFC 7239 Forwarded parsed, but only honored when `raw_peer` is in `trusted_proxies`. Otherwise the chain is dropped and a throttled WARNING is emitted (suspected header spoofing). * Forwarded wins over X-Forwarded-For. * IPv6-mapped IPv4 (`::ffff:1.2.3.4`) normalised to v4 before CIDR comparison so dual-stack listeners actually match v4 trust rules. * Right-to-left walk of the chain, skipping trusted hops; falls back to leftmost if the entire chain is trusted. * Malformed XFF / Forwarded → fall back to raw_peer; warning throttled to once per minute per failure class. - `get_client_identity(request)` — FastAPI dependency. Reads `app.state.deployment_mode` and `app.state.trusted_proxies`; falls back to env when state isn't set so tests don't have to wire it. * `create_api`: - Reads `PINKY_TRUSTED_PROXIES` once at startup, caches parsed CIDRs on `app.state.trusted_proxies`. - Loopback is **not** trusted by default — operator opt-in only, otherwise audit/rate-limit attribution silently flattens to 127.0.0.1 when something upstream goes wrong. Tests (47 new) -------------- * `parse_trusted_proxies`: empty/None/whitespace; bare v4/v6; multiple with whitespace; invalid entries skipped; host-bits-set accepted. * `trusted` mode: peer wins, XFF + Forwarded both ignored. * `lan` and `web` modes (parametrised): no-XFF; untrusted-peer XFF dropped; trusted-peer XFF honored; multi-hop chain skips trusted hops; all-trusted-chain returns leftmost; IPv6-mapped IPv4 normalisation; Forwarded precedence; quoted bracketed IPv6 in Forwarded; XFF with `host:port` form; fully malformed → fallback to peer; partial malformed → keep good entries; missing `client` attribute → 0.0.0.0. * `ClientIdentity` immutability and default empty-tuple chain. * `get_client_identity` reads from app.state and falls back to env. * End-to-end through `create_api` + TestClient. * Untrusted-peer warning is logged. 286 existing api tests + 26 deployment-mode tests still green. Closes part of #433. Refs #442. 🤖 Authored by Barsik
This was referenced May 13, 2026
Open
Collaborator
Author
CI status on stacked PRs #473–#479These 7 PRs target their parent stack branch (not `main`), so GitHub Actions workflows — which are gated on `pull_request: branches: [main]` in both `ci.yml` and `codeql.yml` — don't fire. This is expected for stacked PRs but worth flagging for the review pass. Options:
Recommend option 2 — I can spin that up overnight if you want. Otherwise option 1 is fine; the bottom of the stack (#472) ran clean except for the CodeQL findings I commented there. — Barsik 🤖 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR-2 of the #433 web-exposed hardening track. Single source of truth for what is the real client IP for this request? — every IP-consuming feature in the rest of the track reads from here instead of parsing `X-Forwarded-For` independently.
Stacked on #472 (depends on `PINKY_DEPLOYMENT_MODE` + `app.state.deployment_mode`).
Closes part of #433. Refs #442.
What
Issue spec coverage
Tests (47 new)
`tests/test_client_identity.py` covers:
286 existing api tests + 26 deployment-mode tests still green locally.
Test plan
Out of scope
The features that consume `ClientIdentity` ship in subsequent PRs:
🤖 Opened by Barsik