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

@ -1,28 +1,40 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Network:ListenerTCP;
import std;
import :ClientTCP;
#ifndef CRAFTER_NETWORK_BROWSER
namespace Crafter {
export class ListenerTCP {
public:
bool running = true;
std::function<void(ClientTCP*)> connectCallback;
std::uint32_t concurrentClientLimit;
std::uint32_t totalClientLimit;
ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(), std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::max());
~ListenerTCP();
void Stop();
void ListenSyncSync();
void ListenSyncAsync();
void ListenAsyncSync();
void ListenAsyncAsync();
private:
std::uint32_t totalClientCounter = 0;
int s;
};
}
#endif
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Network:ListenerTCP;
import std;
import :ClientTCP;
#ifndef CRAFTER_NETWORK_BROWSER
namespace Crafter {
export class ListenerTCP {
public:
// Atomic: Stop() is called from another thread than the one running
// an accept loop, which reads this on every turn.
std::atomic<bool> running = true;
std::function<void(ClientTCP*)> connectCallback;
std::uint32_t concurrentClientLimit;
std::uint32_t totalClientLimit;
// Every IPv4 interface (0.0.0.0) at `port`.
ListenerTCP(std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(), std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::max());
// `bindAddress` is a numeric IPv4 or IPv6 address — 127.0.0.1 or ::1
// for this machine alone, 0.0.0.0 or :: for every interface — and a
// name such as `localhost` is refused: the address decides who can
// reach the listener, so it is spelled out rather than looked up. A
// `port` of 0 lets the kernel pick a free one; Port() says which.
ListenerTCP(std::string_view bindAddress, std::uint16_t port, std::function<void(ClientTCP*)> connectCallback, std::uint32_t concurrentClientLimit = std::numeric_limits<std::uint32_t>::max(), std::uint32_t totalClientLimit = std::numeric_limits<std::uint32_t>::max());
~ListenerTCP();
// The port the listener is bound to: the one asked for, or the
// kernel's pick when that was 0.
std::uint16_t Port() const;
void Stop();
void ListenSyncSync();
void ListenSyncAsync();
void ListenAsyncSync();
void ListenAsyncAsync();
private:
std::uint32_t totalClientCounter = 0;
int s;
};
}
#endif