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}})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"
Summary
Functions
The visitor's address as best a site behind a reverse proxy can tell.
The visitor's address as a LiveView socket knows it — or nil when the socket cannot tell.
Extracts the visitor's IP address from a Plug.Conn.
Extracts the visitor's IP address from a Phoenix.LiveView socket.
Extracts IP address from peer_data map.
The network an address belongs to — what to count or compare visitors by.
Functions
@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 loopback or private address — a proxy on the same box or the
same network — 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. A public remote_ip is trusted
as is. Returns nil when nothing is known.
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"
@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 public :peer_data address
is the visitor; a loopback or private one 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.
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 loopback or private address (a
reverse proxy on the same box or network), and trusts a public
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/1directly)
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 loopback
or private address, and trusts a public :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/1directly). "unknown" also covers a host endpoint that never declared:peer_datain the socket'sconnect_info— see that function's docs.
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:addresskey containing IP tuple, or nil
Returns
- IPv4 as "a.b.c.d" string
- IPv6 in its standard compressed hex form ("2001:db8::1")
- "unknown" for invalid or missing data
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"