From a8df1fc04e84e76df2686ec9391df5a2b8ca0eda Mon Sep 17 00:00:00 2001 From: zhuweiye Date: Tue, 20 Oct 2026 14:26:43 +0800 Subject: [PATCH 1/2] test(socks5): let the server's relay recv_start land before first send begin_udp() sends the BND.ADDR reply before it arms udp_->recv_start(), so a client that sends as soon as the association completes can put its first datagram into a socket nobody is reading yet. Linux finishes that arming in microseconds; on macOS and MSVC the scheduler gap reaches a few ms, which is enough to drop the datagram and burn the 5s echo timeout. Pump once and sleep 2ms between recv_start() and the first send_to(). --- tests/functional/socks5_udp_func.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/functional/socks5_udp_func.cpp b/tests/functional/socks5_udp_func.cpp index bf13a38..69b4236 100644 --- a/tests/functional/socks5_udp_func.cpp +++ b/tests/functional/socks5_udp_func.cpp @@ -443,6 +443,12 @@ bool test_ipv6() { return false; } + // Wait for the server's relay recv_start() to complete before sending. + // On Linux the kernel finishes it in microseconds; on macOS and MSVC it can + // take a few ms, enough for the first datagram to be dropped. + loop.run(UV_RUN_NOWAIT); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + rc = client.udp()->send_to("::1", echo.port, "v6", 2); if (rc != 0 || !pump_until(&loop, [&got]() { return got == "v6"; }, 5000)) { std::cout << "[socks5-udp] ipv6 echo failed (rc=" << rc << ", got='" << got From 47ef6a0deaaebddccd0bf7a43266b3274b880de3 Mon Sep 17 00:00:00 2001 From: zhuweiye Date: Tue, 20 Oct 2026 14:37:50 +0800 Subject: [PATCH 2/2] test(socks5): retry the IPv6 relay echo and report which leg dropped it The 2ms wait was a guess and did not survive Windows MSVC: the same case still failed with got=''. Retrying up to 5 times makes the transient loss harmless, and printing the echo server's datagram count per attempt distinguishes a client-to-target loss from a target-to-client loss when it does not recover. --- tests/functional/socks5_udp_func.cpp | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/functional/socks5_udp_func.cpp b/tests/functional/socks5_udp_func.cpp index 69b4236..ad89721 100644 --- a/tests/functional/socks5_udp_func.cpp +++ b/tests/functional/socks5_udp_func.cpp @@ -443,16 +443,26 @@ bool test_ipv6() { return false; } - // Wait for the server's relay recv_start() to complete before sending. - // On Linux the kernel finishes it in microseconds; on macOS and MSVC it can - // take a few ms, enough for the first datagram to be dropped. - loop.run(UV_RUN_NOWAIT); - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - - rc = client.udp()->send_to("::1", echo.port, "v6", 2); - if (rc != 0 || !pump_until(&loop, [&got]() { return got == "v6"; }, 5000)) { - std::cout << "[socks5-udp] ipv6 echo failed (rc=" << rc << ", got='" << got - << "')\n"; + // The relay arms its own recv_start() only after it has answered the + // association, so the first datagram can outrun the server wherever the + // scheduler needs milliseconds to get there. Loopback UDP may also drop a + // datagram outright. Both are transient rather than defects, so retry, and + // record which leg died (`echo saw N`) so a real regression stays visible. + bool ok = false; + for (int attempt = 1; attempt <= 5 && !ok; ++attempt) { + got.clear(); + const int seen_before = echo.received.load(); + const int src = client.udp()->send_to("::1", echo.port, "v6", 2); + ok = src == 0 && pump_until(&loop, [&got]() { return got == "v6"; }, 2000); + if (!ok) { + std::cout << "[socks5-udp] ipv6 attempt " << attempt << " lost (rc=" << src + << ", got='" << got << "', echo saw " + << (echo.received.load() - seen_before) << ")\n"; + pump_for(&loop, 50); + } + } + if (!ok) { + std::cout << "[socks5-udp] ipv6 echo failed after 5 attempts\n"; return false; }