From cbb93a342bdf586fb25bd103da35a56a5c05216e Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Thu, 23 Apr 2026 10:24:14 -0700 Subject: [PATCH 1/3] wip --- src/linux/init/GnsPortTracker.cpp | 15 +++- test/windows/NetworkTests.cpp | 121 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/linux/init/GnsPortTracker.cpp b/src/linux/init/GnsPortTracker.cpp index 92e4beff6..c1f3e8f0f 100644 --- a/src/linux/init/GnsPortTracker.cpp +++ b/src/linux/init/GnsPortTracker.cpp @@ -244,7 +244,20 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set& Por for (auto it = m_allocatedPorts.begin(); it != m_allocatedPorts.end();) { - if (Ports.find(it->first) == Ports.end()) + // Ports contains the list of all Linux sockets currently active and stores the source port used by each socket + // Sockets can use a source port even though an explicit bind() was not made for that port. As long as there + // is a socket using the source port, we should not deallocate it yet. + // + // For example, a listening socket on port X and a socket accepted from that listening socket will both use port X. + // Even if the listening socket is closed the accepted socket may still be using the same port. + // + // Port allocations are done based on protocol+port so we don't need the socket to explicitly match the address or family + // of the bind request that is tracked in m_allocatedPorts, it just needs to match the port number and protocol. + const auto matchCondition = [&](const PortAllocation& p) { + return p.Port == it->first.Port && p.Protocol == it->first.Protocol; + }; + + if (std::find_if(Ports.begin(), Ports.end(), matchCondition) == Ports.end()) { if (!it->second.has_value() || it->second.value() < Timestamp) { diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 8d42277df..7bac9290f 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -4027,6 +4027,127 @@ class MirroredTests } } + WSL2_TEST_METHOD(AcceptedConnectionKeepsPortReservedGuestClient) + { + MIRRORED_NETWORKING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); + WaitForMirroredStateInLinux(); + + // Make sure the VM doesn't time out + WslKeepAlive keepAlive; + + // Start a guest process that listens, accepts a single connection, closes the listen socket, + // then keeps the accepted connection open by echoing data back (via cat). + // socat TCP-LISTEN closes the listener after accepting one connection, which is exactly the + // scenario being tested: the port tracker must not release the port while the accepted + // connection is still alive. + auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); + auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true); + + const std::wstring wslCmd = L"socat -dd TCP4-LISTEN:1234,reuseaddr EXEC:cat"; + auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); + auto guestProcess = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), stdErrWrite.get())); + stdErrWrite.reset(); + stdOutWrite.reset(); + + // Wait for socat to start listening + std::string output; + VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "listening on", output)); + + // Connect from a guest client that keeps the connection alive + auto [clientStdErrRead, clientStdErrWrite] = CreateSubprocessPipe(false, true); + const std::wstring clientCmd = L"socat -dd TCP4-CONNECT:127.0.0.1:1234 EXEC:'sleep 60'"; + auto clientCmdLine = LxssGenerateWslCommandLine(clientCmd.data()); + auto clientProcess = unique_kill_process(LxsstuStartProcess(clientCmdLine.data(), nullptr, nullptr, clientStdErrWrite.get())); + clientStdErrWrite.reset(); + + // Wait for the server to accept the connection (it closes the listen socket after accepting) + VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "starting data transfer loop", output)); + + // Verify the listen socket is actually closed — no LISTEN state socket on port 1234 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ss -tln sport = :1234 | grep -q LISTEN"), static_cast(1)); + + // The listen socket is now closed but the accepted connection is still alive. + // Verify the port tracker has NOT released the port — host should not be able to bind it. + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::chrono::steady_clock::now() < timeout) + { + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + + WSL2_TEST_METHOD(AcceptedConnectionKeepsPortReservedHostClient) + { + MIRRORED_NETWORKING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); + WaitForMirroredStateInLinux(); + + // Make sure the VM doesn't time out + WslKeepAlive keepAlive; + + // Start a guest process that listens, accepts a single connection, closes the listen socket, + // then keeps the accepted connection open by echoing data back (via cat). + // socat TCP-LISTEN closes the listener after accepting one connection, which is exactly the + // scenario being tested: the port tracker must not release the port while the accepted + // connection is still alive. + auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); + auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true); + + const std::wstring wslCmd = L"socat -dd TCP4-LISTEN:1234,reuseaddr EXEC:cat"; + auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); + auto guestProcess = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), stdErrWrite.get())); + stdErrWrite.reset(); + stdOutWrite.reset(); + + // Wait for socat to start listening + std::string output; + VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "listening on", output)); + + // Connect from the host + const wil::unique_socket clientSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + VERIFY_ARE_NOT_EQUAL(clientSocket.get(), INVALID_SOCKET); + + SOCKADDR_IN addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(1234); + + auto connectPred = [&]() { + THROW_HR_IF(E_FAIL, connect(clientSocket.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR); + }; + + wsl::shared::retry::RetryWithTimeout(connectPred, std::chrono::seconds(1), std::chrono::minutes(2)); + + // Wait for socat to accept the connection (it closes the listen socket after accepting) + VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "starting data transfer loop", output)); + + // Verify the listen socket is actually closed — no LISTEN state socket on port 1234 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ss -tln sport = :1234 | grep -q LISTEN"), static_cast(1)); + + // Send data and verify it echoes back through the accepted connection + const char sendBuf[] = "hello"; + VERIFY_ARE_NOT_EQUAL(send(clientSocket.get(), sendBuf, sizeof(sendBuf), 0), SOCKET_ERROR); + + char recvBuf[64] = {}; + int Timeout = 5000; + VERIFY_ARE_NOT_EQUAL(setsockopt(clientSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&Timeout), sizeof(Timeout)), SOCKET_ERROR); + const auto bytesReceived = recv(clientSocket.get(), recvBuf, sizeof(recvBuf), 0); + VERIFY_IS_TRUE(bytesReceived > 0); + VERIFY_ARE_EQUAL(std::string_view(recvBuf, bytesReceived), std::string_view(sendBuf, sizeof(sendBuf))); + + // The listen socket is now closed but the accepted connection is still alive. + // Verify the port tracker has NOT released the port — host should not be able to bind it. + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::chrono::steady_clock::now() < timeout) + { + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + WSL2_TEST_METHOD(EphemeralBind) { MIRRORED_NETWORKING_TEST_ONLY(); From 73cbb2568a38d60e5996682750193cd48cb1550f Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Thu, 23 Apr 2026 10:44:15 -0700 Subject: [PATCH 2/3] works --- test/windows/NetworkTests.cpp | 121 ---------------------------------- 1 file changed, 121 deletions(-) diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 7bac9290f..8d42277df 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -4027,127 +4027,6 @@ class MirroredTests } } - WSL2_TEST_METHOD(AcceptedConnectionKeepsPortReservedGuestClient) - { - MIRRORED_NETWORKING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); - WaitForMirroredStateInLinux(); - - // Make sure the VM doesn't time out - WslKeepAlive keepAlive; - - // Start a guest process that listens, accepts a single connection, closes the listen socket, - // then keeps the accepted connection open by echoing data back (via cat). - // socat TCP-LISTEN closes the listener after accepting one connection, which is exactly the - // scenario being tested: the port tracker must not release the port while the accepted - // connection is still alive. - auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); - auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true); - - const std::wstring wslCmd = L"socat -dd TCP4-LISTEN:1234,reuseaddr EXEC:cat"; - auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); - auto guestProcess = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), stdErrWrite.get())); - stdErrWrite.reset(); - stdOutWrite.reset(); - - // Wait for socat to start listening - std::string output; - VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "listening on", output)); - - // Connect from a guest client that keeps the connection alive - auto [clientStdErrRead, clientStdErrWrite] = CreateSubprocessPipe(false, true); - const std::wstring clientCmd = L"socat -dd TCP4-CONNECT:127.0.0.1:1234 EXEC:'sleep 60'"; - auto clientCmdLine = LxssGenerateWslCommandLine(clientCmd.data()); - auto clientProcess = unique_kill_process(LxsstuStartProcess(clientCmdLine.data(), nullptr, nullptr, clientStdErrWrite.get())); - clientStdErrWrite.reset(); - - // Wait for the server to accept the connection (it closes the listen socket after accepting) - VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "starting data transfer loop", output)); - - // Verify the listen socket is actually closed — no LISTEN state socket on port 1234 - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ss -tln sport = :1234 | grep -q LISTEN"), static_cast(1)); - - // The listen socket is now closed but the accepted connection is still alive. - // Verify the port tracker has NOT released the port — host should not be able to bind it. - const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); - while (std::chrono::steady_clock::now() < timeout) - { - NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - } - - WSL2_TEST_METHOD(AcceptedConnectionKeepsPortReservedHostClient) - { - MIRRORED_NETWORKING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); - WaitForMirroredStateInLinux(); - - // Make sure the VM doesn't time out - WslKeepAlive keepAlive; - - // Start a guest process that listens, accepts a single connection, closes the listen socket, - // then keeps the accepted connection open by echoing data back (via cat). - // socat TCP-LISTEN closes the listener after accepting one connection, which is exactly the - // scenario being tested: the port tracker must not release the port while the accepted - // connection is still alive. - auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); - auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true); - - const std::wstring wslCmd = L"socat -dd TCP4-LISTEN:1234,reuseaddr EXEC:cat"; - auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); - auto guestProcess = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), stdErrWrite.get())); - stdErrWrite.reset(); - stdOutWrite.reset(); - - // Wait for socat to start listening - std::string output; - VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "listening on", output)); - - // Connect from the host - const wil::unique_socket clientSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); - VERIFY_ARE_NOT_EQUAL(clientSocket.get(), INVALID_SOCKET); - - SOCKADDR_IN addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = htons(1234); - - auto connectPred = [&]() { - THROW_HR_IF(E_FAIL, connect(clientSocket.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR); - }; - - wsl::shared::retry::RetryWithTimeout(connectPred, std::chrono::seconds(1), std::chrono::minutes(2)); - - // Wait for socat to accept the connection (it closes the listen socket after accepting) - VERIFY_IS_TRUE(NetworkTests::FindSubstring(stdErrRead, "starting data transfer loop", output)); - - // Verify the listen socket is actually closed — no LISTEN state socket on port 1234 - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ss -tln sport = :1234 | grep -q LISTEN"), static_cast(1)); - - // Send data and verify it echoes back through the accepted connection - const char sendBuf[] = "hello"; - VERIFY_ARE_NOT_EQUAL(send(clientSocket.get(), sendBuf, sizeof(sendBuf), 0), SOCKET_ERROR); - - char recvBuf[64] = {}; - int Timeout = 5000; - VERIFY_ARE_NOT_EQUAL(setsockopt(clientSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&Timeout), sizeof(Timeout)), SOCKET_ERROR); - const auto bytesReceived = recv(clientSocket.get(), recvBuf, sizeof(recvBuf), 0); - VERIFY_IS_TRUE(bytesReceived > 0); - VERIFY_ARE_EQUAL(std::string_view(recvBuf, bytesReceived), std::string_view(sendBuf, sizeof(sendBuf))); - - // The listen socket is now closed but the accepted connection is still alive. - // Verify the port tracker has NOT released the port — host should not be able to bind it. - const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); - while (std::chrono::steady_clock::now() < timeout) - { - NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - } - WSL2_TEST_METHOD(EphemeralBind) { MIRRORED_NETWORKING_TEST_ONLY(); From debcaeb432e1923c666c32975a16388d0e0aea12 Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Thu, 23 Apr 2026 10:53:51 -0700 Subject: [PATCH 3/3] edit comments --- src/linux/init/GnsPortTracker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linux/init/GnsPortTracker.cpp b/src/linux/init/GnsPortTracker.cpp index c1f3e8f0f..ea69d6737 100644 --- a/src/linux/init/GnsPortTracker.cpp +++ b/src/linux/init/GnsPortTracker.cpp @@ -248,8 +248,8 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set& Por // Sockets can use a source port even though an explicit bind() was not made for that port. As long as there // is a socket using the source port, we should not deallocate it yet. // - // For example, a listening socket on port X and a socket accepted from that listening socket will both use port X. - // Even if the listening socket is closed the accepted socket may still be using the same port. + // For example, a listening socket on port X and a connection socket accepted from that listening socket will both use port X. + // Even if the listening socket is closed the connection socket may still be using the same port. // // Port allocations are done based on protocol+port so we don't need the socket to explicitly match the address or family // of the bind request that is tracked in m_allocatedPorts, it just needs to match the port number and protocol.