From 7dace8bb03181ba438d61893557c3f9b84d18ca8 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 1 Nov 2026 01:45:31 +0100 Subject: [PATCH] feat(listener): ListenerQUIC::Port() reads back the port it bound Port 0 asks msquic for an ephemeral port, but nothing read that port back, so a QUIC server had to pick a fixed one and hope nothing else held it, and this suite has to hand every QUIC test a port of its own. ListenerStart writes the port of the UDP socket it bound into the listener's local address; the constructor now reads it with QUIC_PARAM_LISTENER_LOCAL_ADDRESS right after the start, and Port() returns it. It is valid as soon as the constructor returns and still after Stop(), and 0 on a moved-from listener. ListenerTCP has had the same since d6f8f4e. Co-Authored-By: Claude Opus 5.5 --- .../Crafter.Network-ListenerQUIC.cpp | 25 +++++++++++++++++++ interfaces/Crafter.Network-ListenerQUIC.cppm | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/implementations/Crafter.Network-ListenerQUIC.cpp b/implementations/Crafter.Network-ListenerQUIC.cpp index e3c43fe..25f8da3 100644 --- a/implementations/Crafter.Network-ListenerQUIC.cpp +++ b/implementations/Crafter.Network-ListenerQUIC.cpp @@ -150,6 +150,10 @@ struct ListenerQUIC::Impl { HQUIC listener = nullptr; HQUIC configuration = nullptr; std::vector alpnBuf; + // The port msquic bound, read back once ListenerStart returned: a port + // of 0 asks msquic for an ephemeral one, and only the listener knows + // which it got. + std::uint16_t port = 0; ListenerQUIC* outer = nullptr; @@ -301,6 +305,27 @@ ListenerQUIC::ListenerQUIC(std::uint16_t port, Runtime().api->ConfigurationClose(impl->configuration); throw QUICException(std::format("ListenerStart failed: 0x{:x}", static_cast(s))); } + + // ListenerStart writes the port of the UDP socket it bound into the + // listener's local address when it was asked for port 0, and only + // then; read it back rather than trusting `port`. The UNSPEC address + // still carries it: the port sits at the same offset in sockaddr_in + // and sockaddr_in6, and QuicAddrGetPort reads the latter for anything + // that is not INET. + QUIC_ADDR bound{}; + std::uint32_t boundSize = sizeof(bound); + s = Runtime().api->GetParam(impl->listener, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, &boundSize, &bound); + if (QUIC_FAILED(s)) { + Runtime().api->ListenerClose(impl->listener); + Runtime().api->ConfigurationClose(impl->configuration); + throw QUICException(std::format("GetParam(QUIC_PARAM_LISTENER_LOCAL_ADDRESS) failed: 0x{:x}", + static_cast(s))); + } + impl->port = QuicAddrGetPort(&bound); +} + +std::uint16_t ListenerQUIC::Port() const { + return impl ? impl->port : 0; } ListenerQUIC::ListenerQUIC(ListenerQUIC&& other) noexcept diff --git a/interfaces/Crafter.Network-ListenerQUIC.cppm b/interfaces/Crafter.Network-ListenerQUIC.cppm index a9ab802..8db379c 100644 --- a/interfaces/Crafter.Network-ListenerQUIC.cppm +++ b/interfaces/Crafter.Network-ListenerQUIC.cppm @@ -38,6 +38,12 @@ namespace Crafter { ListenerQUIC(const ListenerQUIC&) = delete; ListenerQUIC(ListenerQUIC&&) noexcept; + // The UDP port the listener is bound to: the one asked for, or the + // one the system picked when that was 0. Read from msquic once the + // listener has started, so it is valid as soon as the constructor + // returns, and still after Stop(). 0 on a moved-from listener. + std::uint16_t Port() const; + void Stop(); // Block on this thread, dispatch each accepted connection on this