Client addresses
On this page
By default a native app takes the client's address from the connection,
and an app on Workers from Cloudflare's CF-Connecting-IP header. You don't
write anything for that.
The address matters because rate limits count a request by its address
while no one is signed in (Rate limits),
and jadpo dev's request lines and the
request timeline
show it. Behind a load balancer or a CDN, the connection's own address is
the proxy's, so every client would share one allowance. The real address is
in a header the proxy adds, but anyone can send that header, so reading it
blindly lets a caller choose its own address.
client_address: in app.jadpo says, once, which header holds the real
address and how many proxies of your own deployment to believe:
use edge/types { ProxyHops }config { proxy_hops: ProxyHops { default: 1 }}
application { client_address: { source: ClientAddressSource.forwarded_for hops: config.proxy_hops }}
source: is one of the four values of ClientAddressSource:
| Source | The client's address is | hops: |
|---|---|---|
connection |
the connection's own address; no header is read (the native default) | not allowed |
forwarded_for |
in X-Forwarded-For |
required |
forwarded |
in RFC 7239's Forwarded: for= |
required |
cloudflare |
in CF-Connecting-IP (the default on Workers, the only source accepted there) |
not allowed |
hops:
hops: is how many proxies of your own deployment sit in front of the app.
It is a count, not a list of trusted address ranges, so it can't go out of
date when a provider's addresses change. It is never guessed:
forwarded_for and forwarded need it, and connection and cloudflare,
which don't count along a header, refuse it
(configuration.client_address_hops_unused).
Each trusted proxy adds the address it saw to the right of the header's
list, and whatever a caller sends sits to the left of that. So with
hops: N, the client is the N-th address from the right, counting every
line of the header joined in order. With one proxy,
X-Forwarded-For: 203.0.113.9 and, with a caller's own claim in front,
X-Forwarded-For: 10.9.9.9, 203.0.113.9, both give 203.0.113.9. If the
header has fewer than N addresses, or the entry chosen isn't an address,
the request counts as coming from the connection's own address, never from
a guess.
hops: is a whole number from 1. Write it out (hops: 1), or read it from
an Int setting, or a type built on one, that declares min: 1 or more,
so the app can't start with a count below 1. Anything else is
configuration.client_address_hops_invalid.
How a header is read
- Lines and entries. Every header line of the name counts, in the order
received, and each is split at commas. An empty entry still takes its
place in the count, so a gap never moves the client left, but it is not an
address. In
Forwarded, each element (split at commas outside quotes) gives one entry, from itsforparameter (any case; a token or a quoted string); an element without one is an entry that is not an address. AForwardedheader that does not parse (an unterminated quote, twoforin one element) is invalid as a whole. - An address. Anything
IpAddressaccepts, as IPv4 or IPv6, and in a header also with a port (203.0.113.9:4711) or in brackets ([2001:db8::17]:4711,[2001:db8::17]). It is kept in one spelling, so a client is one count whichever way a proxy writes it: IPv6 in lower case with the zeros shortened, and an IPv4 address written as IPv6 (::ffff:203.0.113.9) as IPv4. The connection's own address is spelled the same way. A name,unknown, an obfuscated identifier (_hidden) or a zone (fe80::1%eth0) is not an address. cloudflare. Exactly oneCF-Connecting-IPline holding one address; anything else falls back, as a header that is short or invalid does.- Falling back is whole. The connection's address replaces the choice; part of a header is never mixed with it.
The address chosen is the one the default rate limits count (an IPv6
address by its /64), the one jadpo dev's request lines show (after the
time, muted, and as address with --json), and the one the request
timeline records. A native deployable chooses an address the same way
jadpo dev does.
On Workers
On Workers there is no connection address to fall back to. Cloudflare sets
CF-Connecting-IP on every request from its edge, replacing any value a
caller sent. So cloudflare is the only source accepted there, and it is
what a Workers deployable uses when there is no client_address:. An
address Cloudflare didn't give is unknown there, and requests with no known
address share one allowance.
Any other source is refused when the project is built for Workers. jadpo check refuses it for each deployable with target: Target.wasm
(deployable.target_unsupported, see
A Workers deployable). A Workers
deployable that reads one doesn't start: it logs client_address_refused
and answers every request with a 503.
Markdown for agents: /docs/client-addresses.md.Built from Jadpo main at a707efb.