IPv6 segments were being printed in decimal (e.g. 10772:1985:1024:45::1 for 2a14:7c1:400:2d::1), which made request_id logs unreadable.
288 lines
8.1 KiB
Elixir
288 lines
8.1 KiB
Elixir
defmodule MicrowavepropWeb.Plugs.RemoteIpTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
import Plug.Conn
|
|
import Plug.Test
|
|
|
|
alias MicrowavepropWeb.Plugs.RemoteIp
|
|
|
|
# A trusted peer that looks like a k8s cluster pod or ingress.
|
|
@trusted_peer {10, 42, 0, 1}
|
|
# An external peer: must NOT be able to spoof headers.
|
|
@untrusted_peer {8, 8, 8, 8}
|
|
|
|
defp call_plug(conn, opts \\ []) do
|
|
RemoteIp.call(conn, RemoteIp.init(opts))
|
|
end
|
|
|
|
defp with_peer(conn, ip) do
|
|
Map.put(conn, :remote_ip, ip)
|
|
end
|
|
|
|
describe "call/2 with trusted peer" do
|
|
test "uses cf-connecting-ip when immediate peer is trusted" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "falls back to x-forwarded-for when no cf-connecting-ip (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {5, 6, 7, 8}
|
|
end
|
|
|
|
test "prefers cf-connecting-ip over x-forwarded-for (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "takes first IP from comma-separated x-forwarded-for (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("x-forwarded-for", "10.0.0.1, 10.0.0.2, 10.0.0.3")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {10, 0, 0, 1}
|
|
end
|
|
|
|
test "preserves original remote_ip when no headers present (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @trusted_peer
|
|
end
|
|
|
|
test "handles IPv6 header value (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "2001:db8::1")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 1}
|
|
end
|
|
|
|
test "handles loopback peer (IPv4)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({127, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "handles loopback peer (IPv6 ::1)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({0, 0, 0, 0, 0, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "ignores malformed IP in header (trusted peer)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@trusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "not-an-ip")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @trusted_peer
|
|
end
|
|
end
|
|
|
|
describe "call/2 with untrusted peer" do
|
|
test "ignores cf-connecting-ip from untrusted peer (spoof attempt)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
|
|
test "ignores x-forwarded-for from untrusted peer (spoof attempt)" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("x-forwarded-for", "1.2.3.4, 5.6.7.8")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
|
|
test "ignores both spoofed headers from untrusted peer" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(@untrusted_peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> put_req_header("x-forwarded-for", "9.9.9.9")
|
|
|> call_plug()
|
|
|
|
assert conn.remote_ip == @untrusted_peer
|
|
end
|
|
end
|
|
|
|
describe "call/2 with IPv6 peers" do
|
|
test "trusts IPv6 ULA peer when configured" do
|
|
# fc00::/7 ULA range — included in default trusted list.
|
|
peer = {0xFC00, 0, 0, 0, 0, 0, 0, 1}
|
|
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["fc00::/7"])
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
end
|
|
|
|
test "does not trust a public IPv6 peer when only ULA is trusted" do
|
|
peer = {0x2001, 0xDB8, 0, 0, 0, 0, 0, 1}
|
|
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(peer)
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["fc00::/7"])
|
|
|
|
assert conn.remote_ip == peer
|
|
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 =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({127, 0, 0, 1})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: [])
|
|
|
|
assert conn.remote_ip == {127, 0, 0, 1}
|
|
end
|
|
|
|
test "logs IPv6 peer in canonical hex form, not decimal" do
|
|
# Regression: a public IPv6 client like 2a14:7c1:400:2d::1 was being
|
|
# logged as "10772:1985:1024:45:0:0:0:1" because we stringified the
|
|
# 8-tuple in decimal. Logger metadata should hold the canonical hex.
|
|
peer = {0x2A14, 0x07C1, 0x0400, 0x002D, 0, 0, 0, 1}
|
|
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer(peer)
|
|
|> call_plug()
|
|
|
|
assert {:remote_ip, ip_str} = List.keyfind(Logger.metadata(), :remote_ip, 0)
|
|
assert ip_str =~ "2a14"
|
|
refute ip_str =~ "10772"
|
|
end
|
|
|
|
test "single-host CIDR (/32) trusts only that exact IP" do
|
|
conn =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({203, 0, 113, 5})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["203.0.113.5/32"])
|
|
|
|
assert conn.remote_ip == {1, 2, 3, 4}
|
|
|
|
conn2 =
|
|
:get
|
|
|> conn("/")
|
|
|> with_peer({203, 0, 113, 6})
|
|
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
|
|> call_plug(trusted_proxies: ["203.0.113.5/32"])
|
|
|
|
assert conn2.remote_ip == {203, 0, 113, 6}
|
|
end
|
|
end
|
|
end
|