feat(listener): ListenerQUIC listens on the address it is given

A new constructor takes a numeric bind address first, as ListenerTCP's
does: 127.0.0.1 or ::1 for this machine alone, 0.0.0.0 or :: for every
address of one family. Until now every QUIC listener listened on every
address of both families (QUIC_ADDRESS_FAMILY_UNSPEC), so a server meant
for this machine's own processes, such as a debug server embedded in an
app, was reachable from the network. A name such as `localhost` is
refused (AI_NUMERICHOST) rather than bound to whichever address the
resolver ranks first. The port-only constructor keeps listening on every
address.

msquic binds the dual-stack wildcard UDP socket for the port whatever
address it is given, and filters by the address a connection was sent
to in its own listener lookup (QuicBindingGetListener). A connection to
another address of the machine is closed with no_application_protocol
during its first flight, so the client fails at once
(QUIC_STATUS_ALPN_NEG_FAILURE, 4 ms on loopback here) rather than
waiting out its handshake timeout, and never reaches connectCallback.

Both constructors now share Impl::Start, which leaves nothing open when
it throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:48:30 +01:00
commit bffb757ab7
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 112 additions and 33 deletions

View file

@ -27,12 +27,29 @@ namespace Crafter {
std::uint32_t totalClientLimit;
std::string alpn;
// Every address of both families at `port`.
ListenerQUIC(std::uint16_t port,
std::string alpn,
QUICServerCredentials creds,
std::function<void(ClientQUIC*)> connectCallback,
std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(),
std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::max());
// One local address. `bindAddress` is a numeric IPv4 or IPv6 address
// (127.0.0.1 or ::1 for this machine alone, 0.0.0.0 for every IPv4
// address, :: for every IPv6 one) and a name such as `localhost` is
// refused with QUICException: the address decides who can reach the
// listener, so it is spelled out rather than looked up, as
// ListenerTCP takes it. msquic still binds the UDP port on every
// address and filters by the address a connection was sent to, at
// its own layer: one sent to another address of this machine fails
// its handshake at once, refused as having no application protocol.
ListenerQUIC(std::string_view bindAddress,
std::uint16_t port,
std::string alpn,
QUICServerCredentials creds,
std::function<void(ClientQUIC*)> connectCallback,
std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(),
std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::max());
~ListenerQUIC();
ListenerQUIC(const ListenerQUIC&) = delete;