defmodule ToweropsWeb.RemoteIp do @moduledoc """ Shared helpers for extracting and formatting remote IP addresses. """ import Plug.Conn, only: [get_req_header: 2] @spec from_conn(Plug.Conn.t()) :: String.t() | nil def from_conn(conn) do if trusted_proxy?(conn.remote_ip) do case get_req_header(conn, "x-forwarded-for") do [forwarded | _] -> forwarded |> String.split(",") |> List.first() |> String.trim() [] -> from_x_real_ip_or_tuple(conn) end else format(conn.remote_ip) end end @spec from_socket(Phoenix.Socket.t()) :: String.t() | nil def from_socket(socket) do # Use peer_data stashed into socket assigns at connect time via # `connect_info: [:peer_data]`. This is the documented Phoenix API # and is adapter-agnostic (works with both Bandit and Cowboy). case socket.assigns[:peer_data] do %{address: ip} when is_tuple(ip) -> format(ip) _ -> nil end end @spec format(tuple() | nil) :: String.t() | nil def format({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}" def format({a, b, c, d, e, f, g, h}) do Enum.map_join([a, b, c, d, e, f, g, h], ":", &(&1 |> Integer.to_string(16) |> String.upcase())) end def format(_), do: nil defp from_x_real_ip_or_tuple(conn) do case get_req_header(conn, "x-real-ip") do [real_ip | _] -> real_ip [] -> format(conn.remote_ip) end end # Only trust forwarding headers from known proxy/RFC 1918 addresses. # Direct internet clients can spoof x-forwarded-for, so we must not # trust it unless the immediate peer is a known reverse proxy. defp trusted_proxy?({10, _b, _c, _d}), do: true defp trusted_proxy?({172, b, _c, _d}) when b >= 16 and b <= 31, do: true defp trusted_proxy?({192, 168, _c, _d}), do: true defp trusted_proxy?({127, _b, _c, _d}), do: true defp trusted_proxy?({0xFD00, _, _, _, _, _, _, _}), do: true defp trusted_proxy?(_), do: false end