# Client addresses

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](rate-limits.md)),
and `jadpo dev`'s request lines and the
[request timeline](the-request-timeline-in-jadpo-dev.md)
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:

```jadpo
use edge/types { ProxyHops }

config {
    proxy_hops: ProxyHops {
        default: 1
    }
}
```

```jadpo
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 its `for` parameter (any case; a token or a quoted
  string); an element without one is an entry that is not an address. A
  `Forwarded` header that does not parse (an unterminated quote, two `for`
  in one element) is invalid as a whole.
- **An address.** Anything `IpAddress` accepts, 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 one `CF-Connecting-IP` line 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.md)). A Workers
deployable that reads one doesn't start: it logs `client_address_refused`
and answers every request with a 503.
