fix(plugs): normalize IPv4-mapped IPv6 peers so cf-connecting-ip wins
Bandit's dual-stack listener delivers IPv4 connections to the app as IPv4-mapped IPv6 tuples (`::ffff:a.b.c.d`). The trust check compared that 128-bit form against our IPv4 trusted_proxies CIDRs and never matched, so cf-connecting-ip was ignored on every cloudflared-relayed request — every visitor logged as the cloudflared sidecar pod IP (rendered as `0:0:0:0:0:65535:2804:101` etc.). Collapses the mapped tuple to plain IPv4 before the trust check and records it that way on the conn, so logs and session storage see the real client IP that Cloudflare set in cf-connecting-ip.
This commit is contained in:
parent
b525a4c34e
commit
938af4d6f2
2 changed files with 66 additions and 0 deletions
|
|
@ -60,6 +60,12 @@ defmodule MicrowavepropWeb.Plugs.RemoteIp do
|
|||
|
||||
@impl true
|
||||
def call(conn, %{trusted_proxies: trusted_proxies}) do
|
||||
# Bandit's dual-stack listener serves IPv4 connections as IPv4-mapped
|
||||
# IPv6 tuples (`::ffff:a.b.c.d`). Normalize those to plain IPv4 so
|
||||
# the trust check matches our IPv4 CIDRs and downstream logs see
|
||||
# consistent IPv4 tuples instead of `0:0:0:0:0:65535:...` forms.
|
||||
conn = %{conn | remote_ip: normalize_ip(conn.remote_ip)}
|
||||
|
||||
conn =
|
||||
if peer_trusted?(conn.remote_ip, trusted_proxies) do
|
||||
case extract_forwarded_ip(conn) do
|
||||
|
|
@ -79,6 +85,14 @@ defmodule MicrowavepropWeb.Plugs.RemoteIp do
|
|||
conn
|
||||
end
|
||||
|
||||
# Collapse an IPv4-mapped IPv6 tuple (`::ffff:a.b.c.d`) to a plain IPv4
|
||||
# tuple so the rest of the pipeline sees one canonical form.
|
||||
defp normalize_ip({0, 0, 0, 0, 0, 0xFFFF, hi, lo}) do
|
||||
{hi >>> 8, hi &&& 0xFF, lo >>> 8, lo &&& 0xFF}
|
||||
end
|
||||
|
||||
defp normalize_ip(ip), do: ip
|
||||
|
||||
defp extract_forwarded_ip(conn) do
|
||||
Enum.find_value(@ip_headers, fn header ->
|
||||
case Plug.Conn.get_req_header(conn, header) do
|
||||
|
|
|
|||
|
|
@ -185,6 +185,58 @@ defmodule MicrowavepropWeb.Plugs.RemoteIpTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "call/2 with IPv4-mapped IPv6 peers (Bandit dual-stack listener)" do
|
||||
# Bandit serves IPv4 connections on a dual-stack socket as IPv4-mapped
|
||||
# IPv6 tuples, e.g. cloudflared pod 10.244.0.101 → {0,0,0,0,0,0xffff,
|
||||
# 0x0AF4, 0x0065}. Without normalization the trust check sees an IPv6
|
||||
# peer and never matches the IPv4 trusted_proxies CIDRs, so
|
||||
# cf-connecting-ip stays unread and every request looks like the
|
||||
# cloudflared sidecar.
|
||||
|
||||
test "treats ::ffff:10.244.0.101 as the IPv4 cluster pod and trusts it" do
|
||||
mapped_peer = {0, 0, 0, 0, 0, 0xFFFF, 0x0AF4, 0x0065}
|
||||
|
||||
conn =
|
||||
:get
|
||||
|> conn("/")
|
||||
|> with_peer(mapped_peer)
|
||||
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {1, 2, 3, 4}
|
||||
end
|
||||
|
||||
test "normalizes a remaining IPv4-mapped peer to plain IPv4 in the conn" do
|
||||
# No forwarded headers — the conn should still walk away with a
|
||||
# plain IPv4 remote_ip rather than the mapped IPv6 form so logs
|
||||
# and downstream code see consistent IPv4 tuples.
|
||||
mapped_peer = {0, 0, 0, 0, 0, 0xFFFF, 0x0AF4, 0x0065}
|
||||
|
||||
conn =
|
||||
:get
|
||||
|> conn("/")
|
||||
|> with_peer(mapped_peer)
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {10, 244, 0, 101}
|
||||
end
|
||||
|
||||
test "an IPv4-mapped public peer is NOT trusted (no header spoofing)" do
|
||||
# ::ffff:8.8.8.8 — public IPv4 reached us directly somehow.
|
||||
mapped_public = {0, 0, 0, 0, 0, 0xFFFF, 0x0808, 0x0808}
|
||||
|
||||
conn =
|
||||
:get
|
||||
|> conn("/")
|
||||
|> with_peer(mapped_public)
|
||||
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
||||
|> call_plug()
|
||||
|
||||
# The spoof attempt is rejected; the normalized IPv4 peer is used.
|
||||
assert conn.remote_ip == {8, 8, 8, 8}
|
||||
end
|
||||
end
|
||||
|
||||
describe "call/2 with custom trusted_proxies option" do
|
||||
test "explicit empty list trusts nothing — even loopback is ignored" do
|
||||
conn =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue