Malachi. IPAddress
(malachi v0.22.7)
View Source
The one canonical string form of a client address, and its inverse.
A client address is not display material: the string this module produces is the per-IP key of
Malachi.ConnectionLimiter, the auth key of Malachi.RateLimiter, half of the replicated lockout
key in Malachi.Auth.LockoutManager, the value compared by session binding in
Malachi.Auth.SessionManager, and the ip field of every Malachi.AuditLog event. Two modules
that format the same address differently split one client into two limiter buckets, stop the
lockout manager recognising repeated attempts, and write one address two ways in the audit log.
Six modules used to format addresses three different ways, and for one IPv6 address they disagreed
(0:0:0:0:0:FFFF:7F00:1 against ::ffff:127.0.0.1). Everything goes through here now.
The canonical form
:inet.ntoa/1, which is the RFC 5952 form: lowercase hex, zero runs compressed to ::, and an
IPv4-mapped address written as ::ffff:127.0.0.1. It is the form that round-trips through
:inet.parse_address/1 (see parse/1), which is what a CIDR allowlist and an operator grepping
the audit log both expect.
The sentinel rule
Written once, so no caller invents its own:
- a binary passes through unchanged, so a caller that already formatted once is never reformatted;
:inet.ntoa/1accepted it, so the canonical form;- it was a tuple and
:inet.ntoa/1rejected it, so"invalid": it was shaped like an address and was not one; - anything else,
nilincluded, so"unknown": there was no address at all.
:inet.ntoa/1 decides what is an address: it answers {:error, :einval} for a wrong arity and
for out-of-range elements, and never raises. So no element-range guard is duplicated here. The
previous copies guarded on tuple_size/1 alone and then called to_string/1 on the result, which
raised Protocol.UndefinedError for a correctly sized tuple holding out-of-range elements.
The tuple test lives in the clause head rather than after the :inet.ntoa/1 call because dialyzer
narrows the argument to t:inet.ip_address/0 once the call is made, which would make a later
is_tuple/1 test look unreachable.
What this does not do
It does not decide which address a request came from. There is no forwarded-header handling
here: the peer address is the socket's own, and the trusted-proxy check in
Malachi.Auth.SessionManager compares that against configured CIDR ranges.
Summary
Types
What a caller may hand to format/1: an address tuple, an already formatted binary, or anything
else (which is answered with a sentinel rather than a crash).
Functions
The canonical string form of ip.
The client address behind socket, in canonical form, for either transport.
The address tuple behind ip, for callers that need to compare rather than record it.
Types
@type formattable() :: :inet.ip_address() | String.t() | nil | term()
What a caller may hand to format/1: an address tuple, an already formatted binary, or anything
else (which is answered with a sentinel rather than a crash).
Functions
@spec format(formattable()) :: String.t()
The canonical string form of ip.
Examples
iex> Malachi.IPAddress.format({127, 0, 0, 1})
"127.0.0.1"
iex> Malachi.IPAddress.format({0, 0, 0, 0, 0, 0, 0, 1})
"::1"An IPv4 client reaching a listener bound to :: arrives as an IPv4-mapped eight-element tuple,
which is where the old implementations diverged most confusingly:
iex> Malachi.IPAddress.format({0, 0, 0, 0, 0, 0xFFFF, 0x7F00, 1})
"::ffff:127.0.0.1"A binary is passed through, so formatting twice is the same as formatting once:
iex> Malachi.IPAddress.format("10.0.0.7")
"10.0.0.7"The sentinels tell a malformed address apart from no address:
iex> Malachi.IPAddress.format({999, 0, 0, 1})
"invalid"
iex> Malachi.IPAddress.format(nil)
"unknown"
@spec from_socket(:gen_tcp.socket() | :ssl.sslsocket(), :gen_tcp | :ssl) :: String.t()
The client address behind socket, in canonical form, for either transport.
Answers "unknown" when the peer cannot be read, which is the ordinary outcome when the client
closed the connection between the accept and this call.
@spec parse(formattable()) :: {:ok, :inet.ip_address()} | :error
The address tuple behind ip, for callers that need to compare rather than record it.
Accepts either representation, because the two listeners produce different ones: the TCP acceptor
formats at the edge and carries a binary, while a tuple arrives straight from :inet.peername/1.
Examples
iex> Malachi.IPAddress.parse("::1")
{:ok, {0, 0, 0, 0, 0, 0, 0, 1}}
iex> Malachi.IPAddress.parse({10, 0, 0, 7})
{:ok, {10, 0, 0, 7}}
iex> Malachi.IPAddress.parse("not an address")
:error