From 501271c63c44842a97da27347f41d7723b5f4ade Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 13 Feb 2026 16:19:26 +0000 Subject: [PATCH 1/6] Experimental support for IPv6 --- src/host/tcp.h | 69 +++++++++++++++++++---------------- tests/e2e_common_endpoints.py | 25 +++++++++++++ tests/e2e_logging.py | 7 ++++ tests/infra/node.py | 15 ++++++-- 4 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/host/tcp.h b/src/host/tcp.h index 5810412a8bb9..6ff5353ecf12 100644 --- a/src/host/tcp.h +++ b/src/host/tcp.h @@ -393,37 +393,8 @@ namespace asynchost return false; } - if (is_client) - { - uv_os_sock_t sock = 0; - if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) - { - LOG_FAIL_FMT( - "socket creation failed: {}", - std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) - return false; - } - - if (connection_timeout.has_value()) - { - const unsigned int ms = connection_timeout->count(); - const auto ret = - setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); - if (ret != 0) - { - LOG_FAIL_FMT( - "Failed to set socket option (TCP_USER_TIMEOUT): {}", - std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) - return false; - } - } - - if ((rc = uv_tcp_open(&uv_handle, sock)) < 0) - { - LOG_FAIL_FMT("uv_tcp_open failed: {}", uv_strerror(rc)); - return false; - } - } + // Client socket creation is deferred to connect_resolved(), where + // the resolved address family (AF_INET or AF_INET6) is known. if ((rc = uv_tcp_keepalive(&uv_handle, 1, 30)) < 0) { @@ -526,6 +497,42 @@ namespace asynchost bool connect_resolved() { + // Create the client socket with the correct address family, but only + // if client_bind() hasn't already created one via uv_tcp_bind(). + if (is_client && !client_host.has_value() && addr_current != nullptr) + { + int rc = 0; + const int family = addr_current->ai_family; + uv_os_sock_t sock = 0; + if ((sock = socket(family, SOCK_STREAM, IPPROTO_TCP)) == -1) + { + LOG_FAIL_FMT( + "socket creation failed: {}", + std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) + return false; + } + + if (connection_timeout.has_value()) + { + const unsigned int ms = connection_timeout->count(); + const auto ret = + setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); + if (ret != 0) + { + LOG_FAIL_FMT( + "Failed to set socket option (TCP_USER_TIMEOUT): {}", + std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) + return false; + } + } + + if ((rc = uv_tcp_open(&uv_handle, sock)) < 0) + { + LOG_FAIL_FMT("uv_tcp_open failed: {}", uv_strerror(rc)); + return false; + } + } + auto* req = new uv_connect_t; // NOLINT(cppcoreguidelines-owning-memory) int rc = 0; diff --git a/tests/e2e_common_endpoints.py b/tests/e2e_common_endpoints.py index cde6ed3b5b3f..4ba94adacad8 100644 --- a/tests/e2e_common_endpoints.py +++ b/tests/e2e_common_endpoints.py @@ -338,3 +338,28 @@ def run(args): test_memory(network, args) test_large_messages(network, args) test_readiness(network, args) + + +def run_ipv6(args): + # Set each RPC interface host to the IPv6 loopback address directly, + # so the setting is isolated to this test (no environment variable). + # Ports are dynamically assigned, so sharing ::1 across nodes is fine. + for host_spec in args.nodes: + for rpc_interface in host_spec.rpc_interfaces.values(): + rpc_interface.host = "::1" + + with infra.network.network( + args.nodes, args.binary_dir, args.debug_nodes, pdb=args.pdb + ) as network: + network.start_and_open(args) + + primary, _ = network.find_primary() + primary_interface = primary.host.rpc_interfaces[ + infra.interfaces.PRIMARY_RPC_INTERFACE + ] + assert ( + ":" in primary_interface.host + ), f"Expected IPv6 address, got {primary_interface.host}" + LOG.info(f"Confirmed primary is using IPv6 address: {primary_interface.host}") + + test_primary(network, args) diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 35c149c676e7..116199bcbe13 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -2371,6 +2371,13 @@ def run_parsing_errors(args): nodes=infra.e2e_args.max_nodes(cr.args, f=0), ) + cr.add( + "common_ipv6", + e2e_common_endpoints.run_ipv6, + package="samples/apps/logging/logging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + ) + # Run illegal traffic tests in separate runners, to reduce total serial runtime cr.add( "js_illegal", diff --git a/tests/infra/node.py b/tests/infra/node.py index 5089d3adfa85..146392a129fd 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -176,10 +176,17 @@ def __init__( if interface_name == infra.interfaces.PRIMARY_RPC_INTERFACE: if rpc_interface.protocol == "local": if not self.major_version or self.major_version > 1: - self.node_client_host = str( - ipaddress.ip_address(BASE_NODE_CLIENT_HOST) - + self.local_node_id - ) + if ":" in rpc_interface.host: + # Pure IPv6 addresses (e.g. ::1) are not + # compatible with the IPv4-based client + # interface used for partition simulation. + # Skip client interface binding for IPv6. + self.node_client_host = None + else: + self.node_client_host = str( + ipaddress.ip_address(BASE_NODE_CLIENT_HOST) + + self.local_node_id + ) else: assert False, f"{rpc_interface.protocol} is not 'local://'" From b889d2b3772f6d5ac1509f0d7fc6a027db3b5ec9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:12:37 +0000 Subject: [PATCH 2/6] Fix common_ipv6 test failures on runners without IPv6 support (#7732) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/node/node_state.h | 8 ++++++-- tests/e2e_common_endpoints.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index d917bff15c92..8e0d803286c6 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -2184,8 +2184,7 @@ namespace ccf // numeric, but at least the final component (TLD) must not be // all-numeric. So this distinguishes "1.2.3.4" (an IP address) from // "1.2.3.c4m" (a DNS name). "1.2.3." is invalid for either, and will - // throw. Attempts to handle IPv6 by also splitting on ':', but this is - // untested. + // throw. Handles IPv6 by splitting on ':' after splitting on '.'. const auto final_component = ccf::nonstd::split(ccf::nonstd::split(hostname, ".").back(), ":") .back(); @@ -2216,6 +2215,11 @@ namespace ccf for (const auto& [_, interface] : config.network.rpc_interfaces) { auto host = split_net_address(interface.published_address).first; + // Strip brackets from IPv6 addresses (e.g. "[::1]" -> "::1") + if (host.size() >= 2 && host.front() == '[' && host.back() == ']') + { + host = host.substr(1, host.size() - 2); + } sans.push_back({host, is_ip(host)}); } return sans; diff --git a/tests/e2e_common_endpoints.py b/tests/e2e_common_endpoints.py index 4ba94adacad8..397f8677c62f 100644 --- a/tests/e2e_common_endpoints.py +++ b/tests/e2e_common_endpoints.py @@ -1,9 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache 2.0 License. import infra.network +import infra.interfaces from ccf.ledger import NodeStatus import http import random +import socket import suite.test_requirements as reqs @@ -341,6 +343,15 @@ def run(args): def run_ipv6(args): + # Check if IPv6 loopback is available before attempting to start nodes. + # Some CI environments disable IPv6, in which case this test is skipped. + try: + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: + s.bind(("::1", 0)) + except (OSError, socket.error): + LOG.warning("IPv6 loopback (::1) is not available, skipping IPv6 test") + return + # Set each RPC interface host to the IPv6 loopback address directly, # so the setting is isolated to this test (no environment variable). # Ports are dynamically assigned, so sharing ::1 across nodes is fine. From 8ce7afab9117b6b5e7eaf39c0035a73ed7b28523 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:42:58 +0000 Subject: [PATCH 3/6] Fix TCP connect retry socket handling for IPv6 path Agent-Logs-Url: https://github.com/microsoft/CCF/sessions/025065b0-abc1-490d-9d50-efba3e4d7373 Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/host/tcp.h | 85 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/src/host/tcp.h b/src/host/tcp.h index 9dbeea1d7dca..e95928feeeee 100644 --- a/src/host/tcp.h +++ b/src/host/tcp.h @@ -12,6 +12,7 @@ #include #include +#include namespace asynchost { @@ -192,6 +193,13 @@ namespace asynchost } else { + if (!set_connection_timeout_on_uv_handle()) + { + assert_status(BINDING, BINDING_FAILED); + behaviour->on_bind_failed(); + return; + } + assert_status(BINDING, CONNECTING_RESOLVING); if (addr_current != nullptr) { @@ -520,34 +528,34 @@ namespace asynchost if (is_client && !client_host.has_value() && addr_current != nullptr) { int rc = 0; - const int family = addr_current->ai_family; - uv_os_sock_t sock = 0; - if ((sock = socket(family, SOCK_STREAM, IPPROTO_TCP)) == -1) - { - LOG_FAIL_FMT( - "socket creation failed: {}", - std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) - return false; - } - - if (connection_timeout.has_value()) + uv_os_fd_t existing_sock = 0; + if ( + uv_fileno( + reinterpret_cast(&uv_handle), &existing_sock) < + 0) { - const unsigned int ms = connection_timeout->count(); - const auto ret = - setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); - if (ret != 0) + const int family = addr_current->ai_family; + uv_os_sock_t sock = 0; + if ((sock = socket(family, SOCK_STREAM, IPPROTO_TCP)) == -1) { LOG_FAIL_FMT( - "Failed to set socket option (TCP_USER_TIMEOUT): {}", + "socket creation failed: {}", std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) return false; } - } - if ((rc = uv_tcp_open(&uv_handle, sock)) < 0) - { - LOG_FAIL_FMT("uv_tcp_open failed: {}", uv_strerror(rc)); - return false; + if (!set_connection_timeout(sock)) + { + close(sock); + return false; + } + + if ((rc = uv_tcp_open(&uv_handle, sock)) < 0) + { + LOG_FAIL_FMT("uv_tcp_open failed: {}", uv_strerror(rc)); + close(sock); + return false; + } } } @@ -580,6 +588,41 @@ namespace asynchost return false; } + bool set_connection_timeout(uv_os_sock_t sock) + { + if (!connection_timeout.has_value()) + { + return true; + } + + const unsigned int ms = connection_timeout->count(); + const auto ret = + setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); + if (ret != 0) + { + LOG_FAIL_FMT( + "Failed to set socket option (TCP_USER_TIMEOUT): {}", + std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) + return false; + } + + return true; + } + + bool set_connection_timeout_on_uv_handle() + { + uv_os_fd_t sock = 0; + const auto rc = + uv_fileno(reinterpret_cast(&uv_handle), &sock); + if (rc < 0) + { + LOG_FAIL_FMT("uv_fileno failed: {}", uv_strerror(rc)); + return false; + } + + return set_connection_timeout(sock); + } + void assert_status(Status from, Status to) { if (status != from) From 1ae1a1809797d72a5c8a44192c70b4e83a8cb163 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:50:05 +0000 Subject: [PATCH 4/6] Handle TCP socket retry and timeout setup for IPv6 connections Agent-Logs-Url: https://github.com/microsoft/CCF/sessions/025065b0-abc1-490d-9d50-efba3e4d7373 Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/host/tcp.h | 73 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/src/host/tcp.h b/src/host/tcp.h index e95928feeeee..4b1c03089d69 100644 --- a/src/host/tcp.h +++ b/src/host/tcp.h @@ -12,7 +12,9 @@ #include #include -#include +#ifndef _WIN32 +# include +#endif namespace asynchost { @@ -528,11 +530,19 @@ namespace asynchost if (is_client && !client_host.has_value() && addr_current != nullptr) { int rc = 0; - uv_os_fd_t existing_sock = 0; - if ( - uv_fileno( - reinterpret_cast(&uv_handle), &existing_sock) < - 0) + uv_os_fd_t existing_fd = {}; + const auto uv_fileno_rc = uv_fileno( + reinterpret_cast(&uv_handle), &existing_fd); + if (uv_fileno_rc < 0 && uv_fileno_rc != UV_EBADF) + { + LOG_FAIL_FMT( + "uv_fileno returned unexpected error while checking TCP handle " + "state: {}", + uv_strerror(uv_fileno_rc)); + return false; + } + + if (uv_fileno_rc == UV_EBADF) { const int family = addr_current->ai_family; uv_os_sock_t sock = 0; @@ -546,14 +556,14 @@ namespace asynchost if (!set_connection_timeout(sock)) { - close(sock); + close_socket_before_uv_ownership(sock); return false; } if ((rc = uv_tcp_open(&uv_handle, sock)) < 0) { LOG_FAIL_FMT("uv_tcp_open failed: {}", uv_strerror(rc)); - close(sock); + close_socket_before_uv_ownership(sock); return false; } } @@ -600,27 +610,62 @@ namespace asynchost setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); if (ret != 0) { +#ifdef _WIN32 + const auto err = WSAGetLastError(); + LOG_FAIL_FMT("Failed to set socket option (TCP_USER_TIMEOUT): {}", err); +#else + const auto err = errno; LOG_FAIL_FMT( "Failed to set socket option (TCP_USER_TIMEOUT): {}", - std::strerror(errno)); // NOLINT(concurrency-mt-unsafe) + std::strerror(err)); // NOLINT(concurrency-mt-unsafe) +#endif return false; } return true; } + static void close_socket_before_uv_ownership(uv_os_sock_t sock) + { + // Socket ownership is transferred to libuv only if uv_tcp_open succeeds. + // Before that, this socket must be closed by the caller. + // This is best-effort cleanup on an existing failure path: we only log + // close() errors (including EINTR). We intentionally do not retry + // close(), since retrying may close a reused fd. +#ifdef _WIN32 + const auto rc = closesocket(sock); +#else + const auto rc = close(sock); +#endif + if (rc != 0) + { +#ifdef _WIN32 + const auto err = WSAGetLastError(); + LOG_FAIL_FMT("Failed to close socket {}: {}", sock, err); +#else + const auto err = errno; + LOG_FAIL_FMT( + "Failed to close socket {}: {}", + sock, + std::strerror(err)); // NOLINT(concurrency-mt-unsafe) +#endif + } + } + bool set_connection_timeout_on_uv_handle() { - uv_os_fd_t sock = 0; - const auto rc = - uv_fileno(reinterpret_cast(&uv_handle), &sock); + uv_os_fd_t existing_fd = {}; + const auto rc = uv_fileno( + reinterpret_cast(&uv_handle), &existing_fd); if (rc < 0) { - LOG_FAIL_FMT("uv_fileno failed: {}", uv_strerror(rc)); + LOG_FAIL_FMT( + "uv_fileno failed while applying TCP_USER_TIMEOUT: {}", + uv_strerror(rc)); return false; } - return set_connection_timeout(sock); + return set_connection_timeout(existing_fd); } void assert_status(Status from, Status to) From 05af854269c051a46ca7ae47de8cea09c0b06ad4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:31:16 +0000 Subject: [PATCH 5/6] Remove WIN32 include guard in tcp header Agent-Logs-Url: https://github.com/microsoft/CCF/sessions/0cf0011c-18a2-4003-951c-55a903a0b6a5 Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/host/tcp.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/host/tcp.h b/src/host/tcp.h index 4b1c03089d69..26f2a98a0dca 100644 --- a/src/host/tcp.h +++ b/src/host/tcp.h @@ -12,9 +12,7 @@ #include #include -#ifndef _WIN32 -# include -#endif +#include namespace asynchost { From 262f5630371fd52b7e46a6dd6c5ffbc65e94574b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:12:57 +0000 Subject: [PATCH 6/6] Remove remaining WIN32 guards in tcp socket paths Agent-Logs-Url: https://github.com/microsoft/CCF/sessions/2caf1046-8ae3-4198-98b2-d97b68196b3f Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/host/tcp.h | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/host/tcp.h b/src/host/tcp.h index 26f2a98a0dca..e0e48d370a74 100644 --- a/src/host/tcp.h +++ b/src/host/tcp.h @@ -608,15 +608,10 @@ namespace asynchost setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &ms, sizeof(ms)); if (ret != 0) { -#ifdef _WIN32 - const auto err = WSAGetLastError(); - LOG_FAIL_FMT("Failed to set socket option (TCP_USER_TIMEOUT): {}", err); -#else const auto err = errno; LOG_FAIL_FMT( "Failed to set socket option (TCP_USER_TIMEOUT): {}", std::strerror(err)); // NOLINT(concurrency-mt-unsafe) -#endif return false; } @@ -630,23 +625,14 @@ namespace asynchost // This is best-effort cleanup on an existing failure path: we only log // close() errors (including EINTR). We intentionally do not retry // close(), since retrying may close a reused fd. -#ifdef _WIN32 - const auto rc = closesocket(sock); -#else - const auto rc = close(sock); -#endif + const auto rc = ::close(sock); if (rc != 0) { -#ifdef _WIN32 - const auto err = WSAGetLastError(); - LOG_FAIL_FMT("Failed to close socket {}: {}", sock, err); -#else const auto err = errno; LOG_FAIL_FMT( "Failed to close socket {}: {}", sock, std::strerror(err)); // NOLINT(concurrency-mt-unsafe) -#endif } }