Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
- Content-Security-Policy: nonce-based per-request plug replacing unsafe-inline scripts - RemoteIp: CIDR-based trust gating via InetCidr, skips forwarded headers from untrusted peers - Rate limiting: auth pipeline (20/min), LiveView event handlers, existing mobile channel limits - NetworkPolicy: 4 k8s policies (web ingress, cluster, metrics, egress) for least-privilege networking - PartitionManager: deadlock retry with exponential backoff in drop_partition - Tests: reduced parallelism (max_cases 4), packets_test async:false to prevent trigger contention - k8s: APRS_PASSWORD -> APRS_PASSCODE secretRef, vendor/aprs submodule hardened
124 lines
3 KiB
Elixir
124 lines
3 KiB
Elixir
defmodule AprsmeWeb.Plugs.RemoteIp do
|
|
@moduledoc """
|
|
Plug that extracts the real client IP from Cloudflare or proxy headers
|
|
and sets `conn.remote_ip` so downstream logging shows the actual client address.
|
|
|
|
Header priority:
|
|
1. `CF-Connecting-IP` (Cloudflare)
|
|
2. `X-Forwarded-For` (first IP in the chain)
|
|
|
|
## Trust model
|
|
|
|
Forwarded headers are only respected when the **TCP peer IP** (the actual
|
|
connecting IP, before any header rewriting) is within a configured list of
|
|
trusted proxy CIDRs. If the connecting peer is not trusted, the forwarded
|
|
headers are ignored and the TCP peer IP is used as-is.
|
|
|
|
Configure via Application environment or plug options:
|
|
|
|
config :aprsme, AprsmeWeb.Plugs.RemoteIp,
|
|
trusted_proxies: ["10.0.0.0/8", "172.16.0.0/12"]
|
|
|
|
Or inline in the endpoint pipeline:
|
|
|
|
plug AprsmeWeb.Plugs.RemoteIp, trusted_proxies: ["103.21.244.0/22"]
|
|
|
|
When no trusted proxies are configured, **no peer is trusted** and all
|
|
forwarded headers are ignored — the safest default.
|
|
"""
|
|
@behaviour Plug
|
|
|
|
import Plug.Conn
|
|
|
|
require Logger
|
|
|
|
@default_trusted_proxies []
|
|
|
|
@impl true
|
|
def init(opts) do
|
|
trusted_proxies =
|
|
Keyword.get(opts, :trusted_proxies) ||
|
|
Application.get_env(:aprsme, __MODULE__, [])[:trusted_proxies] ||
|
|
@default_trusted_proxies
|
|
|
|
cidrs =
|
|
trusted_proxies
|
|
|> Enum.map(&InetCidr.parse_cidr/1)
|
|
|> Enum.filter(fn
|
|
{:ok, _cidr} ->
|
|
true
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Invalid CIDR in RemoteIp trusted_proxies: #{inspect(reason)}")
|
|
false
|
|
end)
|
|
|> Enum.map(fn {:ok, cidr} -> cidr end)
|
|
|
|
%{trusted_proxies: cidrs}
|
|
end
|
|
|
|
@impl true
|
|
def call(conn, %{trusted_proxies: cidrs}) do
|
|
peer_ip = conn.remote_ip
|
|
|
|
conn =
|
|
if trusted_peer?(peer_ip, cidrs) do
|
|
case get_client_ip(conn) do
|
|
{:ok, ip} -> %{conn | remote_ip: ip}
|
|
:error -> conn
|
|
end
|
|
else
|
|
conn
|
|
end
|
|
|
|
user_agent =
|
|
case get_req_header(conn, "user-agent") do
|
|
[ua | _] -> ua
|
|
[] -> "-"
|
|
end
|
|
|
|
Logger.metadata(
|
|
remote_ip: conn.remote_ip |> :inet.ntoa() |> to_string(),
|
|
user_agent: user_agent
|
|
)
|
|
|
|
conn
|
|
end
|
|
|
|
defp trusted_peer?(peer_ip, cidrs) do
|
|
Enum.any?(cidrs, fn cidr -> InetCidr.contains?(cidr, peer_ip) end)
|
|
end
|
|
|
|
defp get_client_ip(conn) do
|
|
with :error <- parse_cf_header(conn) do
|
|
parse_forwarded_for(conn)
|
|
end
|
|
end
|
|
|
|
defp parse_cf_header(conn) do
|
|
case get_req_header(conn, "cf-connecting-ip") do
|
|
[ip_str | _] -> parse_ip(ip_str)
|
|
[] -> :error
|
|
end
|
|
end
|
|
|
|
defp parse_forwarded_for(conn) do
|
|
case get_req_header(conn, "x-forwarded-for") do
|
|
[value | _] ->
|
|
value
|
|
|> String.split(",")
|
|
|> List.first()
|
|
|> parse_ip()
|
|
|
|
[] ->
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp parse_ip(ip_str) do
|
|
case ip_str |> String.trim() |> String.to_charlist() |> :inet.parse_address() do
|
|
{:ok, ip} -> {:ok, ip}
|
|
{:error, _} -> :error
|
|
end
|
|
end
|
|
end
|