diff --git a/implementations/Crafter.Network-ListenerQUIC.cpp b/implementations/Crafter.Network-ListenerQUIC.cpp index 25f8da3..a8be0e8 100644 --- a/implementations/Crafter.Network-ListenerQUIC.cpp +++ b/implementations/Crafter.Network-ListenerQUIC.cpp @@ -6,6 +6,8 @@ module; #include #include #include +#include +#include #include module Crafter.Network:ListenerQUIC_impl; import :ListenerQUIC; @@ -62,6 +64,33 @@ namespace { return r; } + // `bindAddress` at `port`, for ListenerStart. AI_NUMERICHOST, as + // ListenerTCP resolves its bind address: `localhost` is refused here + // rather than bound to whichever of ::1 and 127.0.0.1 the resolver + // ranks first, since the address decides who can reach the listener. + // QUIC_ADDR is the sockaddr_in/sockaddr_in6 union on POSIX, so the + // resolver's answer copies straight in. + QUIC_ADDR NumericAddress(std::string_view bindAddress, std::uint16_t port) { + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST; + const std::string host(bindAddress); + addrinfo* resolved = nullptr; + const int status = getaddrinfo(host.c_str(), nullptr, &hints, &resolved); + if (status != 0 || resolved == nullptr) { + throw QUICException(std::format( + "could not listen on '{}': not a numeric IPv4 or IPv6 address ({})", + host, gai_strerror(status))); + } + QUIC_ADDR address{}; + std::memcpy(&address, resolved->ai_addr, + std::min(resolved->ai_addrlen, sizeof(address))); + freeaddrinfo(resolved); + QuicAddrSetPort(&address, port); + return address; + } + // Lazily generate (and cache) a self-signed cert + key pair on disk by // shelling out to the openssl CLI. Used by ListenerQUIC when the caller // passes QUICServerCredentials{selfSigned=true}. The pair lives in a @@ -171,6 +200,10 @@ struct ListenerQUIC::Impl { // by raw pointer; running it past the destructor is a use-after-free). std::thread acceptLoop; + // Open the configuration and the listener and start it on `address`, + // or throw with nothing left open. Shared by both constructors. + void Start(const QUIC_ADDR& address, const QUICServerCredentials& creds); + static QUIC_STATUS QUIC_API Callback(HQUIC, void* ctx, QUIC_LISTENER_EVENT* ev) { auto* self = static_cast(ctx); switch (ev->Type) { @@ -268,6 +301,54 @@ static HQUIC OpenServerConfiguration(const std::string& alpn, return cfg; } +void ListenerQUIC::Impl::Start(const QUIC_ADDR& address, const QUICServerCredentials& creds) { + configuration = OpenServerConfiguration(outer->alpn, creds, alpnBuf); + + QUIC_STATUS s = Runtime().api->ListenerOpen(Runtime().registration, + reinterpret_cast(&Impl::Callback), + this, &listener); + if (QUIC_FAILED(s)) { + Runtime().api->ConfigurationClose(configuration); + configuration = nullptr; + throw QUICException(std::format("ListenerOpen failed: 0x{:x}", static_cast(s))); + } + + QUIC_BUFFER alpnBuffer{}; + alpnBuffer.Length = static_cast(alpnBuf.size()); + alpnBuffer.Buffer = alpnBuf.data(); + + // Whatever the address, msquic binds the dual-stack wildcard socket for + // the port and matches a new connection's local address against this + // one itself (listener.c, QuicBindingGetListener). + s = Runtime().api->ListenerStart(listener, &alpnBuffer, 1, &address); + if (QUIC_FAILED(s)) { + Runtime().api->ListenerClose(listener); + Runtime().api->ConfigurationClose(configuration); + listener = nullptr; + configuration = nullptr; + 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 the argument. 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(listener, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, &boundSize, &bound); + if (QUIC_FAILED(s)) { + Runtime().api->ListenerClose(listener); + Runtime().api->ConfigurationClose(configuration); + listener = nullptr; + configuration = nullptr; + throw QUICException(std::format("GetParam(QUIC_PARAM_LISTENER_LOCAL_ADDRESS) failed: 0x{:x}", + static_cast(s))); + } + port = QuicAddrGetPort(&bound); +} + ListenerQUIC::ListenerQUIC(std::uint16_t port, std::string alpnIn, QUICServerCredentials creds, @@ -281,47 +362,28 @@ ListenerQUIC::ListenerQUIC(std::uint16_t port, , impl(std::make_unique()) { impl->outer = this; - impl->configuration = OpenServerConfiguration(alpn, creds, impl->alpnBuf); - - QUIC_STATUS s = Runtime().api->ListenerOpen(Runtime().registration, - reinterpret_cast(&Impl::Callback), - impl.get(), &impl->listener); - if (QUIC_FAILED(s)) { - Runtime().api->ConfigurationClose(impl->configuration); - throw QUICException(std::format("ListenerOpen failed: 0x{:x}", static_cast(s))); - } - - QUIC_BUFFER alpnBuffer{}; - alpnBuffer.Length = static_cast(impl->alpnBuf.size()); - alpnBuffer.Buffer = impl->alpnBuf.data(); - + // UNSPEC: every address of both families. QUIC_ADDR addr{}; QuicAddrSetFamily(&addr, QUIC_ADDRESS_FAMILY_UNSPEC); QuicAddrSetPort(&addr, port); + impl->Start(addr, creds); +} - s = Runtime().api->ListenerStart(impl->listener, &alpnBuffer, 1, &addr); - if (QUIC_FAILED(s)) { - Runtime().api->ListenerClose(impl->listener); - 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); +ListenerQUIC::ListenerQUIC(std::string_view bindAddress, + std::uint16_t port, + std::string alpnIn, + QUICServerCredentials creds, + std::function cb, + std::uint32_t concurrentClientLimit, + std::uint32_t totalClientLimit) + : connectCallback(std::move(cb)) + , concurrentClientLimit(concurrentClientLimit) + , totalClientLimit(totalClientLimit) + , alpn(std::move(alpnIn)) + , impl(std::make_unique()) +{ + impl->outer = this; + impl->Start(NumericAddress(bindAddress, port), creds); } std::uint16_t ListenerQUIC::Port() const { diff --git a/interfaces/Crafter.Network-ListenerQUIC.cppm b/interfaces/Crafter.Network-ListenerQUIC.cppm index 8db379c..f059d51 100644 --- a/interfaces/Crafter.Network-ListenerQUIC.cppm +++ b/interfaces/Crafter.Network-ListenerQUIC.cppm @@ -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 connectCallback, std::uint32_t concurrentClientLimit = std::numeric_limits::max(), std::uint32_t totalClientLimit = std::numeric_limits::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 connectCallback, + std::uint32_t concurrentClientLimit = std::numeric_limits::max(), + std::uint32_t totalClientLimit = std::numeric_limits::max()); ~ListenerQUIC(); ListenerQUIC(const ListenerQUIC&) = delete;