Skip to content

feat(geoblock): let callers choose how the client IP is resolved - #37

Open
geekbrother wants to merge 2 commits into
mainfrom
feat/geoblock-client-ip-extractor
Open

geekbrother wants to merge 2 commits into
mainfrom
feat/geoblock-client-ip-extractor

Conversation

@geekbrother

@geekbrother geekbrother commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Why

The geo-block resolved the caller's address from forwarding headers that the caller itself sets, so a visitor in a blocked country could name an address somewhere else and choose its own answer. The extractor was private to the crate, so a deployment that did know the real address stamped into a header by a gateway it controls had no way to say so.

That is pay-core's situation today: its rate limiter, telemetry and analytics all trust the gateway-stamped header, and the geo-block alone kept reading the caller's.

Decisions

Made the resolution a parameter instead of changing the default, so nothing using this crate breaks and moving to a trusted source stays a deliberate edit. new keeps its exact behaviour but is now named for what it trusts, which the old call site never said out loud. I rejected hard-coding a specific trusted header, because each deployment stamps a different one. None from an extractor is treated as an ordinary extraction failure so the existing BlockingPolicy decides, rather than adding a second policy axis.

Worth looking at

The insecure default reads seven client-settable headers, not just X-Forwarded-For, so a test pins that list, a dependency bump could otherwise change which headers decide a block without anyone noticing. I also added tower's util as a dev-dependency: the middleware tests reference ServiceExt and service_fn and so could never compile standalone before, and it's worth a second opinion on whether that's the right fix or whether I'm papering over a workspace feature quirk. Not a breaking change, and consumers need a tagged release before they can adopt it.

The Pay-Core sibling PR https://github.com/WalletConnect/pay-core/pull/1777

geekbrother and others added 2 commits September 21, 2026 17:33
`GeoBlockService::call` resolved the address through `InsecureClientIp`, which
reads the leftmost `X-Forwarded-For` entry and then falls through `Forwarded`,
`X-Real-Ip`, `Fly-Client-IP`, `True-Client-IP`, `CF-Connecting-IP` and
`CloudFront-Viewer-Address` before reaching the connection peer. Every one of
those is set by the caller, and a proxy in front of the service appends to
`X-Forwarded-For` rather than replacing it, so the leftmost entry stays
whatever the caller sent. A visitor in a blocked country could name an address
in an unblocked one and pick its own geo answer. The extractor was not
reachable from outside the crate, so a deployment that did know the real
address — stamped into a header by a gateway or load balancer it controls —
had no way to tell the layer to use it.

Make the resolution a parameter. `ClientIpExtractor` wraps a closure over the
request's headers and extensions, and `with_client_ip_extractor` on both the
layer and the service takes one.

`new` keeps its signature and its behaviour, now spelled
`ClientIpExtractor::insecure_from_forwarding_headers`, so existing callers
build and run unchanged and adopting the trusted path stays a deliberate edit.
The name and its documentation say what it trusts, which the old call site did
not.

`None` from an extractor is `UnableToExtractIPAddress`, so it reaches the
configured `BlockingPolicy` like any other extraction failure rather than
quietly allowing the request.

Also add tower's `util` feature as a dev-dependency. The middleware tests drive
the layer through `ServiceExt` and `ServiceBuilder::service_fn`, neither of
which is available without it, so `cargo test -p geoip --features middleware`
could not compile on its own before this — it only worked when another crate in
a wider build happened to turn the feature on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cargo clippy --workspace --all-features --all-targets -- -D warnings`, which
is what CI runs, rejects the inline `Arc<dyn Fn(..) -> ..>` under
`clippy::type_complexity`. Give it a name.

Keep the comment above the spoofed-header request under the comment width so
its formatting does not depend on which rustfmt wraps it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@geekbrother
geekbrother force-pushed the feat/geoblock-client-ip-extractor branch from ba5e79a to bf89495 Compare September 21, 2026 15:45
@geekbrother geekbrother self-assigned this Sep 21, 2026
@geekbrother
geekbrother marked this pull request as ready for review September 22, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants