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:
parent
fdb0ac2524
commit
bffb757ab7
2 changed files with 112 additions and 33 deletions
|
|
@ -6,6 +6,8 @@ module;
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
module Crafter.Network:ListenerQUIC_impl;
|
module Crafter.Network:ListenerQUIC_impl;
|
||||||
import :ListenerQUIC;
|
import :ListenerQUIC;
|
||||||
|
|
@ -62,6 +64,33 @@ namespace {
|
||||||
return r;
|
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<std::size_t>(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
|
// 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
|
// shelling out to the openssl CLI. Used by ListenerQUIC when the caller
|
||||||
// passes QUICServerCredentials{selfSigned=true}. The pair lives in a
|
// 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).
|
// by raw pointer; running it past the destructor is a use-after-free).
|
||||||
std::thread acceptLoop;
|
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) {
|
static QUIC_STATUS QUIC_API Callback(HQUIC, void* ctx, QUIC_LISTENER_EVENT* ev) {
|
||||||
auto* self = static_cast<Impl*>(ctx);
|
auto* self = static_cast<Impl*>(ctx);
|
||||||
switch (ev->Type) {
|
switch (ev->Type) {
|
||||||
|
|
@ -268,6 +301,54 @@ static HQUIC OpenServerConfiguration(const std::string& alpn,
|
||||||
return cfg;
|
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<QUIC_LISTENER_CALLBACK_HANDLER>(&Impl::Callback),
|
||||||
|
this, &listener);
|
||||||
|
if (QUIC_FAILED(s)) {
|
||||||
|
Runtime().api->ConfigurationClose(configuration);
|
||||||
|
configuration = nullptr;
|
||||||
|
throw QUICException(std::format("ListenerOpen failed: 0x{:x}", static_cast<unsigned>(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QUIC_BUFFER alpnBuffer{};
|
||||||
|
alpnBuffer.Length = static_cast<std::uint32_t>(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<unsigned>(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<unsigned>(s)));
|
||||||
|
}
|
||||||
|
port = QuicAddrGetPort(&bound);
|
||||||
|
}
|
||||||
|
|
||||||
ListenerQUIC::ListenerQUIC(std::uint16_t port,
|
ListenerQUIC::ListenerQUIC(std::uint16_t port,
|
||||||
std::string alpnIn,
|
std::string alpnIn,
|
||||||
QUICServerCredentials creds,
|
QUICServerCredentials creds,
|
||||||
|
|
@ -281,47 +362,28 @@ ListenerQUIC::ListenerQUIC(std::uint16_t port,
|
||||||
, impl(std::make_unique<Impl>())
|
, impl(std::make_unique<Impl>())
|
||||||
{
|
{
|
||||||
impl->outer = this;
|
impl->outer = this;
|
||||||
impl->configuration = OpenServerConfiguration(alpn, creds, impl->alpnBuf);
|
// UNSPEC: every address of both families.
|
||||||
|
|
||||||
QUIC_STATUS s = Runtime().api->ListenerOpen(Runtime().registration,
|
|
||||||
reinterpret_cast<QUIC_LISTENER_CALLBACK_HANDLER>(&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<unsigned>(s)));
|
|
||||||
}
|
|
||||||
|
|
||||||
QUIC_BUFFER alpnBuffer{};
|
|
||||||
alpnBuffer.Length = static_cast<std::uint32_t>(impl->alpnBuf.size());
|
|
||||||
alpnBuffer.Buffer = impl->alpnBuf.data();
|
|
||||||
|
|
||||||
QUIC_ADDR addr{};
|
QUIC_ADDR addr{};
|
||||||
QuicAddrSetFamily(&addr, QUIC_ADDRESS_FAMILY_UNSPEC);
|
QuicAddrSetFamily(&addr, QUIC_ADDRESS_FAMILY_UNSPEC);
|
||||||
QuicAddrSetPort(&addr, port);
|
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<unsigned>(s)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenerStart writes the port of the UDP socket it bound into the
|
ListenerQUIC::ListenerQUIC(std::string_view bindAddress,
|
||||||
// listener's local address when it was asked for port 0, and only
|
std::uint16_t port,
|
||||||
// then; read it back rather than trusting `port`. The UNSPEC address
|
std::string alpnIn,
|
||||||
// still carries it: the port sits at the same offset in sockaddr_in
|
QUICServerCredentials creds,
|
||||||
// and sockaddr_in6, and QuicAddrGetPort reads the latter for anything
|
std::function<void(ClientQUIC*)> cb,
|
||||||
// that is not INET.
|
std::uint32_t concurrentClientLimit,
|
||||||
QUIC_ADDR bound{};
|
std::uint32_t totalClientLimit)
|
||||||
std::uint32_t boundSize = sizeof(bound);
|
: connectCallback(std::move(cb))
|
||||||
s = Runtime().api->GetParam(impl->listener, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, &boundSize, &bound);
|
, concurrentClientLimit(concurrentClientLimit)
|
||||||
if (QUIC_FAILED(s)) {
|
, totalClientLimit(totalClientLimit)
|
||||||
Runtime().api->ListenerClose(impl->listener);
|
, alpn(std::move(alpnIn))
|
||||||
Runtime().api->ConfigurationClose(impl->configuration);
|
, impl(std::make_unique<Impl>())
|
||||||
throw QUICException(std::format("GetParam(QUIC_PARAM_LISTENER_LOCAL_ADDRESS) failed: 0x{:x}",
|
{
|
||||||
static_cast<unsigned>(s)));
|
impl->outer = this;
|
||||||
}
|
impl->Start(NumericAddress(bindAddress, port), creds);
|
||||||
impl->port = QuicAddrGetPort(&bound);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint16_t ListenerQUIC::Port() const {
|
std::uint16_t ListenerQUIC::Port() const {
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,29 @@ namespace Crafter {
|
||||||
std::uint32_t totalClientLimit;
|
std::uint32_t totalClientLimit;
|
||||||
std::string alpn;
|
std::string alpn;
|
||||||
|
|
||||||
|
// Every address of both families at `port`.
|
||||||
ListenerQUIC(std::uint16_t port,
|
ListenerQUIC(std::uint16_t port,
|
||||||
std::string alpn,
|
std::string alpn,
|
||||||
QUICServerCredentials creds,
|
QUICServerCredentials creds,
|
||||||
std::function<void(ClientQUIC*)> connectCallback,
|
std::function<void(ClientQUIC*)> connectCallback,
|
||||||
std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(),
|
std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(),
|
||||||
std::uint32_t totalClientLimit = 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();
|
||||||
ListenerQUIC(const ListenerQUIC&) = delete;
|
ListenerQUIC(const ListenerQUIC&) = delete;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue