Add RemoteIp plug to show real client IP in K8s logs
Extracts client IP from CF-Connecting-IP or X-Forwarded-For headers and sets conn.remote_ip before request logging, so K8s logs show the actual client address instead of the internal cluster/ingress IP.
This commit is contained in:
parent
17bef39d51
commit
bb6c24856d
3 changed files with 139 additions and 0 deletions
|
|
@ -48,6 +48,7 @@ defmodule AprsmeWeb.Endpoint do
|
|||
param_key: "request_logger",
|
||||
cookie_key: "request_logger"
|
||||
|
||||
plug AprsmeWeb.Plugs.RemoteIp
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint], log: {AprsmeWeb.Plugs.LogFilter, :log_level, []}
|
||||
|
||||
|
|
|
|||
57
lib/aprsme_web/plugs/remote_ip.ex
Normal file
57
lib/aprsme_web/plugs/remote_ip.ex
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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)
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@impl true
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
case get_client_ip(conn) do
|
||||
{:ok, ip} -> %{conn | remote_ip: ip}
|
||||
:error -> conn
|
||||
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
|
||||
81
test/aprsme_web/plugs/remote_ip_test.exs
Normal file
81
test/aprsme_web/plugs/remote_ip_test.exs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
defmodule AprsmeWeb.Plugs.RemoteIpTest do
|
||||
use AprsmeWeb.ConnCase
|
||||
|
||||
alias AprsmeWeb.Plugs.RemoteIp
|
||||
|
||||
describe "call/2" do
|
||||
test "sets remote_ip from CF-Connecting-IP header", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {203, 0, 113, 50}
|
||||
end
|
||||
|
||||
test "sets remote_ip from X-Forwarded-For when CF header missing", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("x-forwarded-for", "198.51.100.25, 10.0.0.1")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {198, 51, 100, 25}
|
||||
end
|
||||
|
||||
test "prefers CF-Connecting-IP over X-Forwarded-For", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("cf-connecting-ip", "203.0.113.50")
|
||||
|> put_req_header("x-forwarded-for", "198.51.100.25")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {203, 0, 113, 50}
|
||||
end
|
||||
|
||||
test "handles IPv6 addresses", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("cf-connecting-ip", "2001:db8::1")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 1}
|
||||
end
|
||||
|
||||
test "leaves remote_ip unchanged when no proxy headers present", %{conn: conn} do
|
||||
original_ip = conn.remote_ip
|
||||
|
||||
conn = RemoteIp.call(conn, [])
|
||||
|
||||
assert conn.remote_ip == original_ip
|
||||
end
|
||||
|
||||
test "leaves remote_ip unchanged when header contains invalid IP", %{conn: conn} do
|
||||
original_ip = conn.remote_ip
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("cf-connecting-ip", "not-an-ip")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == original_ip
|
||||
end
|
||||
|
||||
test "trims whitespace from IP addresses", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("cf-connecting-ip", " 203.0.113.50 ")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {203, 0, 113, 50}
|
||||
end
|
||||
|
||||
test "takes first IP from X-Forwarded-For chain", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("x-forwarded-for", "203.0.113.50, 10.0.0.1, 172.16.0.1")
|
||||
|> RemoteIp.call([])
|
||||
|
||||
assert conn.remote_ip == {203, 0, 113, 50}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue