//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 #include #include #include #include #include #include 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 NonLoopbackIPv4() { ifaddrs* interfaces = nullptr; if (getifaddrs(&interfaces) != 0) return std::nullopt; std::optional 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(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 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 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 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; }