You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
drogon::plugin::RealIpResolver only handles IPv4. This is documented in the header:
/** * @note This plugin currently supports only ipv4 address or cidr.*/
Two distinct problems appear once a deployment goes dual-stack — and neither of them fails loudly.
1. IPv6 in trust_ips fails with a misleading error
CIDR::CIDR builds a trantor address with the default ipv6 = false:
// lib/src/RealIpResolver.cc
trantor::InetAddress addr(ipv4, 0); // ipv6 defaults to falseif (addr.isIpV6())
{
throwstd::runtime_error("Ipv6 is not supported by RealIpResolver.");
}
if (addr.isUnspecified())
{
throwstd::runtime_error("Bad ipv4 address: " + ipv4);
}
But trantor's string constructor does not auto-detect the address family — isIpV6_ is taken straight from the argument:
// trantor/net/InetAddress.ccInetAddress::InetAddress(const std::string &ip, uint16_t port, bool ipv6)
: isIpV6_(ipv6) // <-- comes from the parameter, not from the string
{
...
if (::inet_pton(AF_INET, ip.c_str(), &addr_.sin_addr) <= 0)
{
return; // leaves isUnspecified_ == true
}
isUnspecified_ = false;
}
So with "trust_ips": ["2001:db8::1"], isIpV6() is false, the dedicated "Ipv6 is not supported" branch is never taken, and the user gets:
Bad ipv4 address: 2001:db8::1
The message says the address is malformed, when it is in fact valid IPv6 that the plugin simply does not support. (The trantor header even documents that constructor as @param ip A IPv4 or IPv6 address., which makes the auto-detect assumption easy to make.)
2. IPv6 entries in X-Forwarded-For are silently discarded
parseAddress() splits host and port with find(':'), which collides with IPv6 syntax:
InetAddress("2001", 0) fails inet_pton(AF_INET, ...) and stays isUnspecified() == true. Back in the parsing loop:
while (!(ip = parser.getNext()).empty())
{
trantor::InetAddress addr = parseAddress(ip);
if (addr.isUnspecified() || matchCidr(addr, trustCIDRs_))
{
continue; // <-- IPv6 entries are dropped here
}
req->attributes()->insert(attributeKey_, addr);
return;
}
// No match, use peerAddr
req->attributes()->insert(attributeKey_, peerAddr);
Every IPv6 entry falls through to the "no match" path, and the plugin stores the TCP peer address — i.e. the reverse proxy's address.
The impact has the same shape as a misconfigured trust_ips: behind a reverse proxy all IPv6 clients collapse onto a single key. Any per-IP limiting built on GetRealAddr() (registration / login throttling, for example) then treats the entire IPv6 population as one client, while the logs look completely normal. In a dual-stack deployment that is a large share of real traffic, not a corner case.
matchCidr() is IPv4-only as well — addr.ipNetEndian() returns a 32-bit in_addr_t — so a v6 peer can never match a trusted CIDR even before parsing is considered.
Suggested direction
trantor already exposes what is needed:
boolisIpV6() const;
uint32_tipNetEndian() const; // v4constuint32_t *ip6NetEndian() const; // v6: 4 x uint32 = 16 bytes, net endian
so the change stays contained:
CIDR — store the address as trantor::InetAddress (or std::array<uint8_t, 16> + uint8_t prefixLen); allow prefixes up to 128 for v6 and 32 for v4.
matchCidr() — branch on addr.isIpV6(), compare 16 bytes through ip6NetEndian() for v6, and leave the existing 32-bit path byte-for-byte identical for v4.
parseAddress() — handle the shapes that actually occur in XFF: 1.2.3.4, 1.2.3.4:5678, [2001:db8::1]:5678, and bare 2001:db8::1.
XForwardedForParser — no change needed; it only splits on space and comma, so v6 addresses already come through intact.
One constraint worth flagging: friend class Hodor means Hodor uses CIDR(const std::string &), the CIDRs type and RealIpResolver::matchCidr. Keep those three public signatures as they are and Hodor compiles unchanged. (Its own trust_ips handling inherits the same v4-only limitation.)
If this direction looks acceptable, I'd be glad to prepare a PR — including RealIpResolverTest cases for v6 CIDR matching, v6 parsing, mixed v4/v6 trust_ips, and a v4 regression run. Or would you rather see it approached differently, e.g. dropping in_addr_t altogether in favour of InetAddress?
Notes
Reporting this as a feature gap rather than a regression: nothing here has changed since the plugin was merged in Resolve real ip from HttpRequest. #1321 (2022-07). v1.9.10 and master are identical in this area.
I checked for existing work first — no open PR or issue covers IPv6 for this plugin.
Everything above comes from reading lib/src/RealIpResolver.cc and trantor/net/InetAddress.{h,cc}. I have not run a reproduction, so please treat the exact error strings as derived rather than observed. Happy to attach a failing test if that would help.
Summary
drogon::plugin::RealIpResolveronly handles IPv4. This is documented in the header:Two distinct problems appear once a deployment goes dual-stack — and neither of them fails loudly.
1. IPv6 in
trust_ipsfails with a misleading errorCIDR::CIDRbuilds a trantor address with the defaultipv6 = false:But trantor's string constructor does not auto-detect the address family —
isIpV6_is taken straight from the argument:So with
"trust_ips": ["2001:db8::1"],isIpV6()isfalse, the dedicated "Ipv6 is not supported" branch is never taken, and the user gets:The message says the address is malformed, when it is in fact valid IPv6 that the plugin simply does not support. (The trantor header even documents that constructor as
@param ip A IPv4 or IPv6 address., which makes the auto-detect assumption easy to make.)2. IPv6 entries in
X-Forwarded-Forare silently discardedparseAddress()splits host and port withfind(':'), which collides with IPv6 syntax:InetAddress("2001", 0)failsinet_pton(AF_INET, ...)and staysisUnspecified() == true. Back in the parsing loop:Every IPv6 entry falls through to the "no match" path, and the plugin stores the TCP peer address — i.e. the reverse proxy's address.
The impact has the same shape as a misconfigured
trust_ips: behind a reverse proxy all IPv6 clients collapse onto a single key. Any per-IP limiting built onGetRealAddr()(registration / login throttling, for example) then treats the entire IPv6 population as one client, while the logs look completely normal. In a dual-stack deployment that is a large share of real traffic, not a corner case.matchCidr()is IPv4-only as well —addr.ipNetEndian()returns a 32-bitin_addr_t— so a v6 peer can never match a trusted CIDR even before parsing is considered.Suggested direction
trantor already exposes what is needed:
so the change stays contained:
CIDR— store the address astrantor::InetAddress(orstd::array<uint8_t, 16>+uint8_t prefixLen); allow prefixes up to 128 for v6 and 32 for v4.matchCidr()— branch onaddr.isIpV6(), compare 16 bytes throughip6NetEndian()for v6, and leave the existing 32-bit path byte-for-byte identical for v4.parseAddress()— handle the shapes that actually occur in XFF:1.2.3.4,1.2.3.4:5678,[2001:db8::1]:5678, and bare2001:db8::1.XForwardedForParser— no change needed; it only splits on space and comma, so v6 addresses already come through intact.One constraint worth flagging:
friend class HodormeansHodorusesCIDR(const std::string &), theCIDRstype andRealIpResolver::matchCidr. Keep those three public signatures as they are andHodorcompiles unchanged. (Its owntrust_ipshandling inherits the same v4-only limitation.)If this direction looks acceptable, I'd be glad to prepare a PR — including
RealIpResolverTestcases for v6 CIDR matching, v6 parsing, mixed v4/v6trust_ips, and a v4 regression run. Or would you rather see it approached differently, e.g. droppingin_addr_taltogether in favour ofInetAddress?Notes
v1.9.10andmasterare identical in this area.lib/src/RealIpResolver.ccandtrantor/net/InetAddress.{h,cc}. I have not run a reproduction, so please treat the exact error strings as derived rather than observed. Happy to attach a failing test if that would help.