ganl: canonicalize IPv4-mapped IPv6 source addresses (site-ban bypass, #800)

The GANL listeners are dual-stack (IPV6_V6ONLY=0), so an inbound IPv4
connection is delivered as a sockaddr_in6 holding ::ffff:a.b.c.d.
ApplyNetworkAddressToDesc memcpy'd that verbatim into d->address, leaving
it AF_INET6, and compare_to(MUX_SOCKADDR*) compared it as a pure v6
address -- so an IPv4 `forbid a.b.c.d` rule (stored AF_INET) never matched
it and the ban was bypassed simply by connecting IPv4 to the v6 socket.
isRegistered/isSuspect/sitemon inherited the same blind spot.

Add CanonicalizeMappedV4(): when d->address is an IN6_IS_ADDR_V4MAPPED
sockaddr_in6, rewrite it in place to the native AF_INET sockaddr (embedded
v4 = trailing 4 bytes, same port). Called once in onConnectionOpen after
the address is populated -- covering both the GANL sockaddr and the
parsed-host fallback -- and before the address is used for access control,
display, or logging. So a single IPv4 rule now covers both wire forms, and
the logged/displayed address is the natural a.b.c.d rather than ::ffff:.
Genuine IPv6 addresses (not v4-mapped) and native IPv4 are untouched, so
the change is a no-op except for the mapped form.

Uses saro() + reinterpret_cast (the declared sai6ro() has no definition)
and copies the raw network-order sin6_port to avoid byte-order juggling;
reassigns from a fresh sockaddr_in so no stale sockaddr_in6 bytes linger.

Verified: clean build; full smoke 1115/0/0; live netmux serves clean QUIT,
concurrent, and abrupt-RST native-IPv4 connections with no regression (the
canonicalizer is a no-op there).  NOTE: a positive end-to-end demonstration
(forbid an IPv4, connect as ::ffff:<ip> over a dual-stack listener, observe
the refusal) could not be produced in this sandbox -- the box binds the
listener v4-only by default and an `ip_address ::` override did not take
effect here -- so the v4-mapped rewrite itself is verified by code
reasoning rather than live; on a normal dual-stack deployment the bug bites
and this corrects it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-06-10 17:03:45 -06:00
parent 01d4cafb4a
commit d841f7a366

View file

@ -183,6 +183,44 @@ namespace
return true;
}
// Canonicalize an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to its native
// AF_INET form. The GANL listeners are dual-stack (IPV6_V6ONLY=0), so an
// inbound IPv4 connection is delivered as a sockaddr_in6 holding
// ::ffff:a.b.c.d. Left as-is, d->address stays AF_INET6 and an IPv4
// `forbid a.b.c.d` rule (stored AF_INET) never matches it -- the ban is
// bypassed simply by connecting IPv4 to the v6 socket (#800). It also
// makes the displayed/logged address the natural a.b.c.d rather than the
// ::ffff: form. Genuine IPv6 addresses are untouched.
void CanonicalizeMappedV4(MUX_SOCKADDR& addr)
{
#if defined(HAVE_SOCKADDR_IN6) && defined(HAVE_SOCKADDR_IN)
if (AF_INET6 != addr.Family())
{
return;
}
const struct sockaddr_in6* sin6 =
reinterpret_cast<const struct sockaddr_in6*>(addr.saro());
if (nullptr == sin6 || !IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr))
{
return;
}
struct sockaddr_in sin;
std::memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
// The embedded IPv4 is the trailing 4 bytes of the v4-mapped address.
std::memcpy(&sin.sin_addr, sin6->sin6_addr.s6_addr + 12,
sizeof(sin.sin_addr));
sin.sin_port = sin6->sin6_port;
// Reassign from the fresh sockaddr_in (the ctor zero-inits its union),
// so no stale sockaddr_in6 bytes linger behind the AF_INET form.
addr = MUX_SOCKADDR(reinterpret_cast<const struct sockaddr*>(&sin));
#else
UNUSED_PARAMETER(addr);
#endif
}
int MapGanlReasonToMux(ganl::DisconnectReason reason)
{
switch (reason)
@ -593,6 +631,14 @@ public:
std::memset(d->address.sa(), 0, d->address.maxaddrlen());
}
// Normalize an IPv4-mapped IPv6 source to native AF_INET before the
// address is used for access control (isForbid below), display, or
// logging (#800). Covers both the GANL sockaddr and the parsed-host
// fallback above.
if (haveSockAddr) {
CanonicalizeMappedV4(d->address);
}
if (haveSockAddr) {
d->address.ntop(d->addr, sizeof(d->addr));
} else if (!endpoint.host.empty()) {