feat(tcp): ListenerTCP binds the address it is given, at port 0 if asked

A new constructor takes a numeric bind address (127.0.0.1, ::1, 0.0.0.0,
::) and accepts port 0, and Port() reports the port the listener got.
Until now every listener bound 0.0.0.0 at a port the caller chose, so a
helper meant for this machine's own processes was reachable from the
network, and the caller had to guess a free port. A name such as
`localhost` is refused instead of bound to whichever address the
resolver ranks first. The port-only constructor still listens on every
IPv4 interface.

`running` is atomic now: Stop() runs on another thread than the accept
loop that reads it. The accept loops take a sockaddr_storage, so an IPv6
peer's address is no longer cut short.

New: ShouldBindLoopbackEphemeral.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:06:41 +01:00
commit d6f8f4edcb
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
5 changed files with 235 additions and 40 deletions

View file

@ -12,6 +12,7 @@ module;
#include <sys/time.h>
#include <sys/wait.h>
#include <strings.h>
#include <netdb.h>
#include <cerrno>
module Crafter.Network:ListenerTCP_impl;
@ -21,14 +22,30 @@ import Crafter.Thread;
using namespace Crafter;
ListenerTCP::ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit, std::uint32_t totalClientLimit) : connectCallback(connectCallback), concurrentClientLimit(concurrentClientLimit), totalClientLimit(totalClientLimit) {
sockaddr_in servAddr;
bzero((char*)&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(port);
ListenerTCP::ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit, std::uint32_t totalClientLimit)
: ListenerTCP("0.0.0.0", port, std::move(connectCallback), concurrentClientLimit, totalClientLimit) {}
s = socket(AF_INET, SOCK_STREAM, 0);
ListenerTCP::ListenerTCP(std::string_view bindAddress, std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit, std::uint32_t totalClientLimit) : connectCallback(connectCallback), concurrentClientLimit(concurrentClientLimit), totalClientLimit(totalClientLimit) {
// AI_NUMERICHOST: no lookup, so `localhost` is refused here rather than
// bound to whichever of ::1 and 127.0.0.1 the resolver ranks first.
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST | AI_NUMERICSERV;
const std::string host(bindAddress);
const std::string service = std::to_string(port);
addrinfo* resolved = nullptr;
const int status = getaddrinfo(host.c_str(), service.c_str(), &hints, &resolved);
if (status != 0 || resolved == nullptr) {
throw std::runtime_error("Could not bind " + host + ": not a numeric IPv4 or IPv6 address (" + gai_strerror(status) + ")");
}
sockaddr_storage servAddr {};
std::memcpy(&servAddr, resolved->ai_addr, resolved->ai_addrlen);
const socklen_t servAddrLength = resolved->ai_addrlen;
const int family = resolved->ai_family;
freeaddrinfo(resolved);
s = socket(family, SOCK_STREAM, 0);
if(s < 0)
{
throw std::runtime_error("Error establishing the server socket");
@ -39,7 +56,7 @@ ListenerTCP::ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> con
int reuse = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
if(bind(s, (struct sockaddr*) &servAddr, sizeof(servAddr)) < 0) {
if(bind(s, reinterpret_cast<const sockaddr*>(&servAddr), servAddrLength) < 0) {
// Ignoring this left the listener silently accepting nothing at
// all, with no hint as to why.
const std::string reason = std::strerror(errno);
@ -55,6 +72,18 @@ ListenerTCP::ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> con
}
}
std::uint16_t ListenerTCP::Port() const {
sockaddr_storage address {};
socklen_t length = sizeof(address);
if (getsockname(s, reinterpret_cast<sockaddr*>(&address), &length) == -1) {
throw std::runtime_error(std::string("Could not read the listening port: ") + std::strerror(errno));
}
if (address.ss_family == AF_INET6) {
return ntohs(reinterpret_cast<const sockaddr_in6*>(&address)->sin6_port);
}
return ntohs(reinterpret_cast<const sockaddr_in*>(&address)->sin_port);
}
void ListenerTCP::Stop() {
running = false;
shutdown(s, SHUT_RDWR);
@ -64,7 +93,7 @@ void ListenerTCP::Stop() {
void ListenerTCP::ListenSyncSync() {
while (running && totalClientCounter < totalClientLimit) {
sockaddr_in newSockAddr;
sockaddr_storage newSockAddr;
socklen_t newSockAddrSize = sizeof(newSockAddr);
int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize);
if (client > 0) {
@ -81,7 +110,7 @@ void ListenerTCP::ListenSyncSync() {
void ListenerTCP::ListenSyncAsync() {
while (running && totalClientCounter < totalClientLimit) {
sockaddr_in newSockAddr;
sockaddr_storage newSockAddr;
socklen_t newSockAddrSize = sizeof(newSockAddr);
int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize);
if (client > 0) {
@ -97,7 +126,7 @@ void ListenerTCP::ListenSyncAsync() {
void ListenerTCP::ListenAsyncSync() {
ThreadPool::Enqueue([this]() {
while (running && totalClientCounter < totalClientLimit) {
sockaddr_in newSockAddr;
sockaddr_storage newSockAddr;
socklen_t newSockAddrSize = sizeof(newSockAddr);
int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize);
if (client > 0) {
@ -114,7 +143,7 @@ void ListenerTCP::ListenAsyncSync() {
void ListenerTCP::ListenAsyncAsync() {
ThreadPool::Enqueue([this]() {
while (running && totalClientCounter < totalClientLimit) {
sockaddr_in newSockAddr;
sockaddr_storage newSockAddr;
socklen_t newSockAddrSize = sizeof(newSockAddr);
int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize);
if (client > 0) {