Skip to content

net, http, rpc: dual-stack listeners (IPV6_V6ONLY, IPv4-mapped peers) - #5

Open
david-yu wants to merge 2 commits into
ipv6/03-dns-literals-errorsfrom
ipv6/04-dual-stack-listeners
Open

david-yu wants to merge 2 commits into
ipv6/03-dns-literals-errorsfrom
ipv6/04-dual-stack-listeners

Conversation

@david-yu

@david-yu david-yu commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Part 4 of 10 of the IPv6 series, split out of #1 and rebased on current scylladb/seastar master. Based on #4, so the diff here is only this PR's 2 commits.

Why

On a dual-stack host a [::] listener's IPv4 clients arrive as ::ffff:a.b.c.d, which does not compare, hash or test as loopback like a.b.c.d. http_server and rpc::server handed that form to handlers and filter_connection, so address-keyed policies written for IPv4 stopped matching once a listener moved to [::]. Whether a [::] listener accepts IPv4 at all also followed the host's net.ipv6.bindv6only, with no way to ask.

What

  • net: let listen_options pin IPV6_V6ONLY (372fb6ffd) — listen_options::ipv6_only (std::optional<bool>; unset keeps today's behaviour).
  • net, http, rpc: treat IPv4-mapped IPv6 peers as IPv4 (a788a963a) — inet_address::is_ipv4_mapped()/unmapped() and socket_address::unmapped(); http_server and rpc::server record unmapped peer and local addresses.

Builds on

inet_address's in_addr conversion already unwrapped the mapped form; unmapped() is that logic, made reusable. Sockets keep reporting what the kernel returns, as datagram channels do since 731ac075f (scylladb/seastar#3700); only the two servers that hand the address to application code normalise it.

Testing

A [::]:0 listener reached from 127.0.0.1 with ipv6_only false (accepted, peer ::ffff:127.0.0.1) and true (ECONNREFUSED); an http_server on [::] reports its IPv4 client as 127.0.0.1. unmapped() is the identity for anything that is not ::ffff:a.b.c.d.

On the core-aws VM (Ubuntu 24.04, GCC 14, C++23, RelWithDebInfo, GnuTLS and OpenSSL in one tree), this PR's head builds and passes dns, ipv6, socket, rpc, httpd, httpd_openssl, tls, tls_openssl, network_interface, websocket and unix_domain with SEASTAR_TEST_REQUIRE_IPV6=1, so none of their IPv6 cases skipped.

Stack

  1. tests, ci: fail instead of skip when IPv6 is expected #2 tests, ci: fail instead of skip when IPv6 is expected
  2. dns: reach nameservers over IPv6 #3 dns: reach nameservers over IPv6
  3. dns: literals for any family, c-ares error text, link-local nameservers #4 dns: literals for any family, c-ares error text, link-local nameservers
  4. net, http, rpc: dual-stack listeners (IPV6_V6ONLY, IPv4-mapped peers) #5 net, http, rpc: dual-stack listeners (IPV6_V6ONLY, IPv4-mapped peers) ← this PR
  5. net: IPv6 zones and strict literal parsing #6 net: IPv6 zones and strict literal parsing
  6. tls: do not send IP literals in the SNI extension #7 tls: do not send IP literals in the SNI extension
  7. tls: verify server_name on OpenSSL, opt-in #8 tls: verify server_name on OpenSSL, opt-in
  8. net: ipv6_addr equality and hash, ipv4_addr(socket_address) throws instead of terminating #9 net: ipv6_addr equality and hash, ipv4_addr(socket_address) throws instead of terminating
  9. net: listen on an unspecified address binds any family #10 net: listen on an unspecified address binds any family
  10. http: build the Host header from an address in authority form #11 http: build the Host header from an address in authority form

Whether a wildcard [::] listener also accepts IPv4 clients (as IPv4-mapped
IPv6 peers) has so far followed the host's net.ipv6.bindv6only default,
and a [::]:P listener could never share a port with a 0.0.0.0:P one.

Add listen_options::ipv6_only. When set, posix_listen applies it with
IPV6_V6ONLY before bind on AF_INET6 sockets; other families are untouched
and the default (unset) keeps today's behaviour.

Tested by a listener on [::]:0 reached from 127.0.0.1 with the option
false (accepted, peer ::ffff:127.0.0.1) and true (ECONNREFUSED).
An IPv4 client of a dual-stack [::] listener is reported by the kernel as
::ffff:a.b.c.d. inet_address already unwrapped that form in its in_addr
conversion but nowhere else: operator==, hashing and is_loopback() treated
it as a different address from a.b.c.d, and http_server / rpc::server
handed the mapped form to handlers and filter_connection, so address-keyed
policies written as IPv4 never matched.

Add inet_address::is_ipv4_mapped() and unmapped() (the in_addr conversion
now reuses them), socket_address::unmapped(), and normalise the peer and
local addresses http_server and rpc::server record. Non-mapped addresses
of either family are returned unchanged. Sockets keep reporting what the
kernel returns, as datagram channels do since 731ac07; only the two
servers that hand the address to application policy normalise it.
@david-yu
david-yu force-pushed the ipv6/04-dual-stack-listeners branch from 55a3348 to a788a96 Compare September 25, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant