# `PhoenixKit.Utils.IpAddress`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/utils/ip_address.ex#L1)

Utilities for extracting and formatting IP addresses.

Supports both IPv4 and IPv6 addresses with proper error handling.
Prevents Protocol.UndefinedError when working with IPv6 tuples.

## Usage

Extract from LiveView socket:
    ip = PhoenixKit.Utils.IpAddress.extract_from_socket(socket)

Extract from Plug.Conn:
    ip = PhoenixKit.Utils.IpAddress.extract_from_conn(conn)

Extract from peer_data directly:
    ip = PhoenixKit.Utils.IpAddress.extract_ip_address(%{address: {192, 168, 1, 1}})

## Trusted proxies

`X-Forwarded-For` and `X-Real-IP` are written by whoever sent the request, so
they are only believed when the connection comes from a proxy of yours.
`config :phoenix_kit, :trusted_proxies` says which peers those are:

    # Believe the headers only from these peers (CIDR blocks or bare addresses)
    config :phoenix_kit, :trusted_proxies, ["172.18.0.7/32", "10.0.0.0/8"]

    # Believe them from nobody: the client is always the connection's peer
    config :phoenix_kit, :trusted_proxies, []

- **Not set (the default).** A loopback or private peer (127.0.0.0/8, 10/8,
  172.16/12, 192.168/16, `::1`, `fc00::/7`) is taken for a proxy and its
  headers are believed. This is what installs got before the option existed.
  Any process that can open a connection from such an address can then
  claim any client address, so set the option on a production install.
  The first request on a VM whose forwarded header is honoured this way logs
  one warning.
- **A list.** The headers are believed only when the peer is inside one of
  the blocks — public or private alike. A malformed entry matches nothing.
- **`[]`.** Nobody is trusted; the client is the peer.
- **Not a list** (a string, a map) trusts nobody. Inside a list, an entry
  that does not parse is ignored and the valid entries still apply. Either
  case logs one warning per VM. Write IPv4 blocks as IPv4
  (`"172.18.0.0/16"`), not as `::ffff:` IPv6.

One proxy hop is supported: the client is the LAST `x-forwarded-for` entry,
even when that entry is another trusted proxy, so a chain such as
CDN -> Caddy -> app reports the CDN. In Docker the proxy's address changes
when its container is recreated; pin it (`ipv4_address` in compose) or
resolve it by name at boot (`config/runtime.exs`), otherwise a fixed `/32`
silently stops matching and every client appears as the proxy.

The same rule applies to `client_address/1` (Plug.Conn) and
`client_address_from_socket/1` (LiveView), and through `trusted_proxy?/1`
to the other readers of a forwarded header: `X-Forwarded-Proto` in
`PhoenixKit.API.RequestContext` and `PhoenixKitWeb.Plugs.EnsureOAuthScheme`,
and the proxy rebind of `PhoenixKit.Utils.SessionFingerprint`.

## Examples

    iex> PhoenixKit.Utils.IpAddress.extract_ip_address(%{address: {192, 168, 1, 1}})
    "192.168.1.1"

    iex> PhoenixKit.Utils.IpAddress.extract_ip_address(%{address: {8193, 3512, 1, 0, 0, 0, 0, 1}})
    "2001:db8:1::1"

    iex> PhoenixKit.Utils.IpAddress.extract_ip_address(nil)
    "unknown"

    iex> PhoenixKit.Utils.IpAddress.extract_ip_address(%{})
    "unknown"

# `client_address`

```elixir
@spec client_address(Plug.Conn.t()) :: String.t() | nil
```

The visitor's address as best a site behind a reverse proxy can tell.

`extract_from_conn/1` answers the TCP peer, which behind nginx (or in a
container) is the proxy's own address for every visitor. This one takes
`conn.remote_ip` (which a host's RemoteIp plug already rewrites) and, when
that is a trusted proxy — by default a loopback or private address, a proxy
on the same box or the same network; with `:trusted_proxies` set, only an
address inside it — the LAST entry of `x-forwarded-for`, the one that proxy
appended (a visitor can send their own header; they cannot control what
nginx appends after it), then `x-real-ip`. Any other `remote_ip` is the
answer as is. Returns nil when nothing is known.

A forwarded address may carry the port the proxy saw (Caddy's `{remote}`
writes `203.0.113.7:28858` and `[2001:db8::1]:28858`); the port is dropped.
An IPv4 visitor reaching a dual-stack listener as `::ffff:a.b.c.d` is
reported as `a.b.c.d`.

    iex> conn = %Plug.Conn{remote_ip: {127, 0, 0, 1}, req_headers: [{"x-forwarded-for", "9.9.9.9, 203.0.113.7"}]}
    iex> PhoenixKit.Utils.IpAddress.client_address(conn)
    "203.0.113.7"

# `client_address_from_socket`

```elixir
@spec client_address_from_socket(Phoenix.LiveView.Socket.t()) :: String.t() | nil
```

The visitor's address as a LiveView socket knows it — or nil when the
socket cannot tell.

From the connect info the endpoint passes: a `:peer_data` address that is
not a trusted proxy is the visitor; a trusted one (by default any loopback
or private address, see "Trusted proxies" above) is a proxy, and only the
forwarded address in `:x_headers` (`x-forwarded-for`, `x-real-ip`) names
the visitor then. An endpoint that passes no `:x_headers` — core's
installed endpoint passes `:peer_data` only — leaves a proxied socket
with no answer, and nil is that answer: the caller must not treat the
proxy as the visitor.

# `extract_from_conn`

Extracts the visitor's IP address from a `Plug.Conn`.

Proxy-aware: delegates to `client_address/1`, which reads `x-forwarded-for`
/ `x-real-ip` when `conn.remote_ip` is a trusted proxy (by default a
loopback or private address — see "Trusted proxies" above), and takes any
other `conn.remote_ip` as-is.

## Parameters

- `conn`: Plug.Conn struct

## Returns

- IP address string or "unknown" (never nil — callers that need to tell
  "known" apart from "unknown" should call `client_address/1` directly)

# `extract_from_socket`

Extracts the visitor's IP address from a Phoenix.LiveView socket.

Proxy-aware: delegates to `client_address_from_socket/1`, which reads the
connect info's `:x_headers` when the socket's `:peer_data` is a trusted
proxy (by default a loopback or private address — see "Trusted proxies"
above), and takes any other `:peer_data` address as-is.

## Parameters

- `socket`: Phoenix.LiveView.Socket struct

## Returns

- IP address string or "unknown" (never nil — callers that need to tell
  "known" apart from "unknown" should call `client_address_from_socket/1`
  directly). "unknown" also covers a host endpoint that never declared
  `:peer_data` in the socket's `connect_info` — see that function's docs.

# `extract_ip_address`

Extracts IP address from peer_data map.

Handles both IPv4 (4-tuple) and IPv6 (8-tuple) addresses.
Returns "unknown" for nil, invalid, or missing data.

## Parameters

- `peer_data`: Map with `:address` key containing IP tuple, or nil

## Returns

- IPv4 as "a.b.c.d" string (an IPv4-mapped `::ffff:a.b.c.d` included)
- IPv6 in its standard compressed hex form ("2001:db8::1")
- "unknown" for invalid or missing data

# `local_address?`

```elixir
@spec local_address?(String.t() | :inet.ip_address() | nil) :: boolean()
```

Whether `address` (a string or an `:inet` tuple) is loopback or private —
where a reverse proxy on the same box or network connects from, the
addresses `client_address/1` looks past. Anything that does not parse is
not.

This is the default rule `client_address/1` applies when `:trusted_proxies`
is not set; once that option is set, trust follows the list instead (see
`trusted_proxy?/1`).

    iex> PhoenixKit.Utils.IpAddress.local_address?("172.18.0.8")
    true

    iex> PhoenixKit.Utils.IpAddress.local_address?("203.0.113.7")
    false

# `network`

```elixir
@spec network(String.t() | nil) :: String.t() | nil
```

The network an address belongs to — what to count or compare visitors by.

An IPv4 address is its own network. An IPv6 address is its `/64`: that is
the block one household, phone or server is handed, and every address in
it is theirs to use — operating systems rotate a temporary address inside
it daily, and anyone can pick a fresh one per request. Keyed on the full
address, a rate limit is one new address away from empty and a session
"changes IP" every day.

Exceptions, where a `/64` would put many hosts in one bucket:

- IPv4-mapped (`::ffff:a.b.c.d`) and well-known NAT64 (`64:ff9b::/96`)
  unmap to the embedded IPv4, as does deprecated IPv4-compatible
  (`::a.b.c.d`)
- loopback (`::1`), unspecified (`::`), and link-local (`fe80::/10`)
  are the address itself

Anything that does not parse (including "unknown") comes back unchanged.

    iex> PhoenixKit.Utils.IpAddress.network("2a0d:3344:6a:c310:88f8:482c:e41a:9ef5")
    "2a0d:3344:6a:c310::/64"

    iex> PhoenixKit.Utils.IpAddress.network("203.0.113.7")
    "203.0.113.7"

    iex> PhoenixKit.Utils.IpAddress.network("::ffff:203.0.113.7")
    "203.0.113.7"

    iex> PhoenixKit.Utils.IpAddress.network("unknown")
    "unknown"

# `trusted_proxy?`

```elixir
@spec trusted_proxy?(String.t() | :inet.ip_address() | nil) :: boolean()
```

Whether `address` (a string or an `:inet` tuple) is a trusted proxy: a peer
whose forwarded headers may be believed. The rule `client_address/1`
applies, for callers that read another forwarded header
(`x-forwarded-proto`) or decide something by the peer alone. With
`config :phoenix_kit, :trusted_proxies` unset, any loopback or private
address (`local_address?/1`); with it set, only an address inside the
list; with an invalid value, nobody (and the one warning per VM about the
invalid value is logged here as well). Anything that does not parse is not.

    iex> PhoenixKit.Utils.IpAddress.trusted_proxy?("203.0.113.7")
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
