//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // ClientTCP resolves a host for IPv4 and IPv6 alike and tries every address // the name resolves to, in order, the way Python's socket.create_connection // does. Before, it asked getaddrinfo for AF_INET only and kept the first // answer, so a server listening on ::1 alone — a dev server on a recent Node // binds `localhost` that way — could not be reached by name, while every // Python client reached it. // // The listeners are raw sockets, so nothing here depends on ListenerTCP: one // IPv6-only on ::1, one on 127.0.0.1. Skips (77) on a host without IPv6 // loopback. #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; } // A socket listening on the loopback address of `family`, on a port the // kernel picks; fd stays -1 when the host cannot open one. struct LoopbackListener { int fd = -1; std::uint16_t port = 0; explicit LoopbackListener(int family) { fd = socket(family, SOCK_STREAM, 0); if (fd == -1) return; sockaddr_storage address {}; socklen_t length = 0; if (family == AF_INET6) { // v6 only, or the socket would also take IPv4 connects and the // checks below would not tell the two apart. int only = 1; setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &only, sizeof(only)); auto* v6 = reinterpret_cast(&address); v6->sin6_family = AF_INET6; v6->sin6_addr = in6addr_loopback; length = sizeof(sockaddr_in6); } else { auto* v4 = reinterpret_cast(&address); v4->sin_family = AF_INET; v4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); length = sizeof(sockaddr_in); } if (bind(fd, reinterpret_cast(&address), length) != 0 || listen(fd, 8) != 0) { close(fd); fd = -1; return; } getsockname(fd, reinterpret_cast(&address), &length); port = family == AF_INET6 ? ntohs(reinterpret_cast(&address)->sin6_port) : ntohs(reinterpret_cast(&address)->sin_port); } ~LoopbackListener() { if (fd != -1) close(fd); } }; // The address family of the peer a connected client reached. int PeerFamily(const ClientTCP& client) { sockaddr_storage address {}; socklen_t length = sizeof(address); getpeername(client.socketid, reinterpret_cast(&address), &length); return address.ss_family; } // Whether `localhost` resolves to ::1 here; /etc/hosts decides. bool LocalhostResolvesToIPv6() { addrinfo hints {}; hints.ai_family = AF_INET6; hints.ai_socktype = SOCK_STREAM; addrinfo* resolved = nullptr; if (getaddrinfo("localhost", nullptr, &hints, &resolved) != 0) return false; freeaddrinfo(resolved); return true; } } // namespace int main() { LoopbackListener v6(AF_INET6); if (v6.fd == -1) { std::println("SKIP no IPv6 loopback on this host"); return 77; } { ClientTCP client("::1", v6.port, 2s); Check(PeerFamily(client) == AF_INET6, "an IPv6 literal connects over IPv6"); } if (LocalhostResolvesToIPv6()) { try { ClientTCP client("localhost", v6.port, 2s); Check(PeerFamily(client) == AF_INET6, "localhost reaches a server that listens on ::1 only"); } catch (const std::exception& e) { Check(false, std::format("localhost reaches a server that listens on ::1 only (threw: {})", e.what())); } } else { std::println("SKIP localhost does not resolve to ::1 on this host"); } { // Whichever address the resolver ranks first, a refused one is not // the end: the next is tried. LoopbackListener v4(AF_INET); try { ClientTCP client("localhost", v4.port, 2s); Check(PeerFamily(client) == AF_INET, "localhost reaches a server that listens on 127.0.0.1 only"); } catch (const std::exception& e) { Check(false, std::format("localhost reaches a server that listens on 127.0.0.1 only (threw: {})", e.what())); } } { std::uint16_t closedPort = 0; { LoopbackListener gone(AF_INET); closedPort = gone.port; } bool refused = false; try { ClientTCP client("localhost", closedPort, 2s); } catch (const SocketErrorException& e) { refused = e.phase == SocketPhase::Connect && e.code() == std::errc::connection_refused; } catch (const std::exception&) { } Check(refused, "when no address answers, the last failure is thrown, with its errno"); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }