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:
parent
78853033e8
commit
d6f8f4edcb
5 changed files with 235 additions and 40 deletions
143
tests/ShouldBindLoopbackEphemeral/main.cpp
Normal file
143
tests/ShouldBindLoopbackEphemeral/main.cpp
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// ListenerTCP can be told the address it binds and take port 0, with Port()
|
||||
// reporting what the kernel picked. Before, it always bound 0.0.0.0 at a port
|
||||
// the caller chose: a helper meant for this machine's own processes — a broker
|
||||
// that hands out database access, say — was reachable from the network, and
|
||||
// the caller had to guess a free port and hope.
|
||||
//
|
||||
// Also: a host name is refused rather than bound to whichever address the
|
||||
// resolver ranks first, and the port-only constructor keeps listening on every
|
||||
// IPv4 interface.
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
import Crafter.Network;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
// An IPv4 address of this host that is not loopback, if it has one.
|
||||
std::optional<std::string> NonLoopbackIPv4() {
|
||||
ifaddrs* interfaces = nullptr;
|
||||
if (getifaddrs(&interfaces) != 0) return std::nullopt;
|
||||
std::optional<std::string> found;
|
||||
for (const ifaddrs* entry = interfaces; entry != nullptr && !found; entry = entry->ifa_next) {
|
||||
if (entry->ifa_addr == nullptr || entry->ifa_addr->sa_family != AF_INET) continue;
|
||||
const auto* v4 = reinterpret_cast<const sockaddr_in*>(entry->ifa_addr);
|
||||
if (ntohl(v4->sin_addr.s_addr) >> 24 == 127) continue;
|
||||
char text[INET_ADDRSTRLEN] = {};
|
||||
inet_ntop(AF_INET, &v4->sin_addr, text, sizeof(text));
|
||||
found = std::string(text);
|
||||
}
|
||||
freeifaddrs(interfaces);
|
||||
return found;
|
||||
}
|
||||
|
||||
// Runs a listener's accept loop on a thread of its own and counts the
|
||||
// connections it was handed.
|
||||
struct Serving {
|
||||
ListenerTCP& listener;
|
||||
std::atomic<int> accepted = 0;
|
||||
std::thread loop;
|
||||
|
||||
explicit Serving(ListenerTCP& listener) : listener(listener) {
|
||||
listener.connectCallback = [this](ClientTCP* client) {
|
||||
++accepted;
|
||||
delete client;
|
||||
};
|
||||
loop = std::thread([this] { this->listener.ListenSyncSync(); });
|
||||
}
|
||||
~Serving() {
|
||||
listener.Stop();
|
||||
loop.join();
|
||||
}
|
||||
bool WaitFor(int count) {
|
||||
for (int i = 0; i < 200 && accepted.load() < count; ++i) std::this_thread::sleep_for(10ms);
|
||||
return accepted.load() >= count;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
{
|
||||
ListenerTCP listener("127.0.0.1", 0, nullptr);
|
||||
const std::uint16_t port = listener.Port();
|
||||
Check(port != 0, std::format("port 0 gets a port from the kernel, which Port() reports ({})", port));
|
||||
Serving serving(listener);
|
||||
{
|
||||
ClientTCP client("127.0.0.1", port, 2s);
|
||||
}
|
||||
Check(serving.WaitFor(1), "a client on loopback reaches it");
|
||||
|
||||
if (const std::optional<std::string> outside = NonLoopbackIPv4()) {
|
||||
bool refused = false;
|
||||
try {
|
||||
ClientTCP client(*outside, port, 2s);
|
||||
} catch (const SocketErrorException& e) {
|
||||
refused = e.code == ECONNREFUSED;
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
Check(refused, std::format("through {} it is refused: it listens on 127.0.0.1 alone", *outside));
|
||||
} else {
|
||||
std::println("SKIP no non-loopback IPv4 address to try from");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::optional<ListenerTCP> v6;
|
||||
try {
|
||||
v6.emplace("::1", 0, nullptr);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
if (v6) {
|
||||
Serving serving(*v6);
|
||||
{
|
||||
ClientTCP client("::1", v6->Port(), 2s);
|
||||
}
|
||||
Check(serving.WaitFor(1), "an IPv6 loopback listener takes a client on ::1");
|
||||
} else {
|
||||
std::println("SKIP no IPv6 loopback on this host");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
bool refused = false;
|
||||
try {
|
||||
ListenerTCP named("localhost", 0, nullptr);
|
||||
} catch (const std::runtime_error&) {
|
||||
refused = true;
|
||||
}
|
||||
Check(refused, "a host name is refused: the address to bind is spelled out");
|
||||
}
|
||||
|
||||
{
|
||||
ListenerTCP any(0, nullptr);
|
||||
Check(any.Port() != 0, "the port-only constructor takes port 0 as well");
|
||||
Serving serving(any);
|
||||
{
|
||||
ClientTCP client("127.0.0.1", any.Port(), 2s);
|
||||
}
|
||||
Check(serving.WaitFor(1), "and still listens on every IPv4 interface, loopback included");
|
||||
}
|
||||
|
||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Loading…
Reference in a new issue