Fix mirrored mode port tracking for implicit binds resulting from accept() calls (#40287)

* wip * works * edit comments * add test * use unique_handle * fix lookup to be efficient * simplify ListAllocatedPorts * remove include * edits * pass networking mode to port tracker * add keepalive * add bound check after parsing * default to none before parsing * exit early on invalid mode --------- Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com>

FetoiuCatalin committed May 12, 2026 at 09:06 UTC ee92475a9b7da2b8f5f8d94e1f783f6b416d1743
6 files changed +215 -22
src/linux/init/GnsPortTracker.cpp
+36 -9
@@ -20,10 +20,17 @@ constexpr auto c_sock_diag_poll_timeout = std::chrono::milliseconds(10);
20 constexpr auto c_bpf_poll_timeout = std::chrono::milliseconds(500);
21
22 GnsPortTracker::GnsPortTracker(
23 - std::shared_ptr<wsl::shared::SocketChannel> hvSocketChannel, NetlinkChannel&& netlinkChannel, std::shared_ptr<SecCompDispatcher> seccompDispatcher) :
24 - m_hvSocketChannel(std::move(hvSocketChannel)), m_channel(std::move(netlinkChannel)), m_seccompDispatcher(seccompDispatcher)
23 + std::shared_ptr<wsl::shared::SocketChannel> hvSocketChannel,
24 + NetlinkChannel&& netlinkChannel,
25 + std::shared_ptr<SecCompDispatcher> seccompDispatcher,
26 + LX_MINI_INIT_NETWORKING_MODE networkingMode) :
27 + m_hvSocketChannel(std::move(hvSocketChannel)),
28 + m_channel(std::move(netlinkChannel)),
29 + m_seccompDispatcher(seccompDispatcher),
30 + m_networkingMode(networkingMode)
31 {
32 m_networkNamespace = std::filesystem::read_symlink("/proc/self/ns/net").string();
33 + GNS_LOG_INFO("GnsPortTracker initialized with networking mode ({})", static_cast<int>(m_networkingMode));
34 }
35
36 void GnsPortTracker::RunPortRefresh()
@@ -175,9 +182,9 @@ void GnsPortTracker::Run()
182 }
183 }
184
178 -std::set<GnsPortTracker::PortAllocation> GnsPortTracker::ListAllocatedPorts()
185 +GnsPortTracker::ActivePorts GnsPortTracker::ListAllocatedPorts()
186 {
180 - std::set<PortAllocation> ports;
187 + ActivePorts ports;
188
189 inet_diag_req_v2 message{};
190 message.sdiag_family = AF_INET;
@@ -188,8 +195,9 @@ std::set<GnsPortTracker::PortAllocation> GnsPortTracker::ListAllocatedPorts()
195 for (const auto& e : response.Messages<inet_diag_msg>(SOCK_DIAG_BY_FAMILY))
196 {
197 const auto* payload = e.Payload();
191 - in6_addr address = {};
198 + ports.PortProtocolPairs.emplace(ntohs(payload->id.idiag_sport), static_cast<int>(message.sdiag_protocol));
199
200 + in6_addr address = {};
201 if (payload->idiag_family == AF_INET6)
202 {
203 static_assert(sizeof(address.s6_addr32) == 16);
@@ -201,7 +209,8 @@ std::set<GnsPortTracker::PortAllocation> GnsPortTracker::ListAllocatedPorts()
209 address.s6_addr32[0] = payload->id.idiag_src[0];
210 }
211
204 - ports.emplace(ntohs(payload->id.idiag_sport), static_cast<int>(payload->idiag_family), static_cast<int>(message.sdiag_protocol), address);
212 + ports.FullAllocations.emplace(
213 + ntohs(payload->id.idiag_sport), static_cast<int>(payload->idiag_family), static_cast<int>(message.sdiag_protocol), address);
214 }
215 };
216
@@ -232,7 +241,7 @@ std::set<GnsPortTracker::PortAllocation> GnsPortTracker::ListAllocatedPorts()
241 return ports;
242 }
243
235 -void GnsPortTracker::OnRefreshAllocatedPorts(const std::set<PortAllocation>& Ports, time_t Timestamp)
244 +void GnsPortTracker::OnRefreshAllocatedPorts(const ActivePorts& Ports, time_t Timestamp)
245 {
246 // Because there's no way to get notified when the bind() call actually completes, it' possible
247 // that this method is called before the bind() completion and so the port allocation may not be visible yet.
@@ -244,7 +253,25 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set<PortAllocation>& Por
253
254 for (auto it = m_allocatedPorts.begin(); it != m_allocatedPorts.end();)
255 {
247 - if (Ports.find(it->first) == Ports.end())
256 + bool portStillActive = false;
257 + if (IsMirroredMode())
258 + {
259 + // In mirrored mode, match by port+protocol only. Sockets can use a source port even though an explicit
260 + // bind() was not made for that port. As long as there is a socket using the source port, we should not
261 + // deallocate it yet.
262 + //
263 + // For example, a listening socket on port X and a connection socket accepted from that listening socket
264 + // will both use port X. Even if the listening socket is closed the connection socket may still be using
265 + // the same port.
266 + portStillActive = Ports.PortProtocolPairs.contains({it->first.Port, it->first.Protocol});
267 + }
268 + else
269 + {
270 + // In other modes, match by full allocation (port+family+protocol+address).
271 + portStillActive = Ports.FullAllocations.contains(it->first);
272 + }
273 +
274 + if (!portStillActive)
275 {
276 if (!it->second.has_value() || it->second.value() < Timestamp)
277 {
@@ -269,7 +296,7 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set<PortAllocation>& Por
296 it->second.reset(); // The port is known to be allocated, remove the timeout
297 }
298
272 - it++;
299 + ++it;
300 }
301 }
302
src/linux/init/GnsPortTracker.h
+25 -5
@@ -15,11 +15,16 @@
15 #include "waitablevalue.h"
16 #include "SecCompDispatcher.h"
17 #include "SocketChannel.h"
18 +#include "lxinitshared.h"
19
20 class GnsPortTracker
21 {
22 public:
22 - GnsPortTracker(std::shared_ptr<wsl::shared::SocketChannel> hvSocketChannel, NetlinkChannel&& netlinkChannel, std::shared_ptr<SecCompDispatcher> seccompDispatcher);
23 + GnsPortTracker(
24 + std::shared_ptr<wsl::shared::SocketChannel> hvSocketChannel,
25 + NetlinkChannel&& netlinkChannel,
26 + std::shared_ptr<SecCompDispatcher> seccompDispatcher,
27 + LX_MINI_INIT_NETWORKING_MODE networkingMode);
28
29 GnsPortTracker(const GnsPortTracker&) = delete;
30 GnsPortTracker(GnsPortTracker&&) = delete;
@@ -116,19 +121,32 @@ public:
121 std::uint64_t CallId;
122 };
123
124 +private:
125 + using ActivePortSet = std::set<std::pair<std::uint16_t, int>>;
126 +
127 + struct ActivePorts
128 + {
129 + std::set<PortAllocation> FullAllocations;
130 + ActivePortSet PortProtocolPairs; // Always populated, but only used in mirrored mode
131 + };
132 +
133 struct PortRefreshResult
134 {
121 - std::set<PortAllocation> Ports;
135 + ActivePorts Ports;
136 time_t Timestamp;
137 std::function<void()> Resume;
138 };
139
126 -private:
127 - void OnRefreshAllocatedPorts(const std::set<PortAllocation>& Ports, time_t Timestamp);
140 + bool IsMirroredMode() const
141 + {
142 + return m_networkingMode == LxMiniInitNetworkingModeMirrored;
143 + }
144 +
145 + void OnRefreshAllocatedPorts(const ActivePorts& Ports, time_t Timestamp);
146
147 void RunPortRefresh();
148
131 - std::set<PortAllocation> ListAllocatedPorts();
149 + ActivePorts ListAllocatedPorts();
150
151 std::optional<BindCall> ReadNextRequest();
152
@@ -158,6 +176,8 @@ private:
176
177 std::shared_ptr<SecCompDispatcher> m_seccompDispatcher;
178
179 + LX_MINI_INIT_NETWORKING_MODE m_networkingMode;
180 +
181 std::string m_networkNamespace;
182 };
183
src/linux/init/localhost.cpp
+12 -2
@@ -408,7 +408,9 @@ int RunPortTracker(int Argc, char** Argv)
408 " fd]"
409 " [" INIT_NETLINK_FD_ARG
410 " fd]"
411 - " [" INIT_PORT_TRACKER_LOCALHOST_RELAY " fd]\n";
411 + " [" INIT_PORT_TRACKER_LOCALHOST_RELAY
412 + " fd]"
413 + " [" INIT_PORT_TRACKER_NETWORKING_MODE_ARG " mode]\n";
414
415 // This is only supported on VM mode.
416 if (!UtilIsUtilityVm())
@@ -423,12 +425,14 @@ int RunPortTracker(int Argc, char** Argv)
425 int PortTrackerFd = -1;
426 int NetlinkSocketFd = -1;
427 int GuestRelayFd = -1;
428 + int NetworkingMode = static_cast<int>(LxMiniInitNetworkingModeNone);
429
430 ArgumentParser parser(Argc, Argv);
431 parser.AddArgument(Integer{BpfFd}, INIT_BPF_FD_ARG);
432 parser.AddArgument(Integer{PortTrackerFd}, INIT_PORT_TRACKER_FD_ARG);
433 parser.AddArgument(Integer{NetlinkSocketFd}, INIT_NETLINK_FD_ARG);
434 parser.AddArgument(Integer{GuestRelayFd}, INIT_PORT_TRACKER_LOCALHOST_RELAY);
435 + parser.AddArgument(Integer{NetworkingMode}, INIT_PORT_TRACKER_NETWORKING_MODE_ARG);
436
437 try
438 {
@@ -440,6 +444,12 @@ int RunPortTracker(int Argc, char** Argv)
444 return 1;
445 }
446
447 + if (NetworkingMode < LxMiniInitNetworkingModeNone || NetworkingMode > LxMiniInitNetworkingModeVirtioProxy)
448 + {
449 + std::cerr << "Invalid networking mode (" << NetworkingMode << ")\n";
450 + return 1;
451 + }
452 +
453 const bool synchronousMode = BpfFd != -1 && NetlinkSocketFd != -1;
454 const bool localhostRelay = GuestRelayFd != -1;
455 auto hvSocketChannel = std::make_shared<wsl::shared::SocketChannel>(wil::unique_fd{PortTrackerFd}, "localhost");
@@ -469,7 +479,7 @@ int RunPortTracker(int Argc, char** Argv)
479
480 auto seccompDispatcher = std::make_shared<SecCompDispatcher>(BpfFd);
481
472 - GnsPortTracker portTracker(hvSocketChannel, std::move(channel), seccompDispatcher);
482 + GnsPortTracker portTracker(hvSocketChannel, std::move(channel), seccompDispatcher, static_cast<LX_MINI_INIT_NETWORKING_MODE>(NetworkingMode));
483
484 seccompDispatcher->RegisterHandler(
485 __NR_bind, [&portTracker](seccomp_notif* notification) { return portTracker.ProcessSecCompNotification(notification); });
src/linux/init/main.cpp
+9 -4
@@ -211,7 +211,7 @@ int StartDhcpClient(int DhcpTimeout);
211
212 int StartGuestNetworkService(int GnsFd, wil::unique_fd&& DnsTunnelingFd, uint32_t DnsTunnelingIpAddress);
213
214 -void StartPortTracker(LX_MINI_INIT_PORT_TRACKER_TYPE Type);
214 +void StartPortTracker(LX_MINI_INIT_PORT_TRACKER_TYPE Type, LX_MINI_INIT_NETWORKING_MODE NetworkingMode);
215
216 void StartTimeSyncAgent(void);
217
@@ -1301,7 +1301,7 @@ Return Value:
1301 return (ChildPid < 0) ? -1 : 0;
1302 }
1303
1304 -void StartPortTracker(LX_MINI_INIT_PORT_TRACKER_TYPE Type)
1304 +void StartPortTracker(LX_MINI_INIT_PORT_TRACKER_TYPE Type, LX_MINI_INIT_NETWORKING_MODE NetworkingMode)
1305
1306 /*++
1307
@@ -1313,6 +1313,8 @@ Arguments:
1313
1314 Type - specifies the type of port tracker (localhost relay or mirrored).
1315
1316 + NetworkingMode - specifies the networking mode (mirrored, virtio, etc.).
1317 +
1318 Return Value:
1319
1320 None.
@@ -1374,7 +1376,8 @@ Return Value:
1376 [PortTrackerFd = std::move(PortTrackerFd),
1377 NetlinkSocket = std::move(NetlinkSocket),
1378 BpfFd = std::move(BpfFd),
1377 - GuestRelayFd = std::move(GuestRelayFd)]() {
1379 + GuestRelayFd = std::move(GuestRelayFd),
1380 + NetworkingMode]() {
1381 execl(
1382 LX_INIT_PATH,
1383 LX_INIT_LOCALHOST_RELAY,
@@ -1386,6 +1389,8 @@ Return Value:
1389 std::format("{}", NetlinkSocket.get()).c_str(),
1390 INIT_PORT_TRACKER_LOCALHOST_RELAY,
1391 std::format("{}", GuestRelayFd.get()).c_str(),
1392 + INIT_PORT_TRACKER_NETWORKING_MODE_ARG,
1393 + std::format("{}", static_cast<int>(NetworkingMode)).c_str(),
1394 NULL);
1395
1396 LOG_ERROR("execl failed {}", errno);
@@ -3390,7 +3395,7 @@ try
3395 Config.NetworkingMode = NetworkingConfiguration->NetworkingMode;
3396 if (NetworkingConfiguration->PortTrackerType != LxMiniInitPortTrackerTypeNone)
3397 {
3393 - StartPortTracker(NetworkingConfiguration->PortTrackerType);
3398 + StartPortTracker(NetworkingConfiguration->PortTrackerType, NetworkingConfiguration->NetworkingMode);
3399 }
3400
3401 if (NetworkingConfiguration->DisableIpv6)
src/shared/inc/lxinitshared.h
+1
@@ -275,6 +275,7 @@ Abstract:
275 #define INIT_BPF_FD_ARG "--bpf-fd"
276 #define INIT_NETLINK_FD_ARG "--netlink-fd"
277 #define INIT_PORT_TRACKER_LOCALHOST_RELAY "--localhost-relay"
278 +#define INIT_PORT_TRACKER_NETWORKING_MODE_ARG "--networking-mode"
279
280 #define DECLARE_MESSAGE_CTOR(Name) \
281 Name() \
test/windows/NetworkTests.cpp
+132 -2
@@ -2154,6 +2154,78 @@ class NetworkTests
2154 0L);
2155 }
2156
2157 + // Verifies that after a listen socket is closed, the port remains allocated to the guest
2158 + // as long as an accepted connection in Linux is still using it.
2159 + static void VerifyAcceptedConnectionPortTracking()
2160 + {
2161 + WslKeepAlive keepAlive;
2162 +
2163 + // Perl server: listen on port 1234, print "listening", accept one connection,
2164 + // close the listen socket, print "ready", then wait forever to keep the accepted connection alive.
2165 + auto serverCmd = LxssGenerateWslCommandLine(
2166 + L"perl -MSocket -e '"
2167 + L"$|=1;"
2168 + L"socket(S,AF_INET,SOCK_STREAM,0) or die;"
2169 + L"bind(S,sockaddr_in(1234,INADDR_ANY)) or die;"
2170 + L"listen(S,1) or die;"
2171 + L"print \"listening\\n\";"
2172 + L"accept(C,S) or die;"
2173 + L"close(S);"
2174 + L"print \"ready\\n\";"
2175 + L"while(1){sleep 1000}"
2176 + L"'");
2177 +
2178 + wil::unique_handle serverOutRead;
2179 + wil::unique_handle serverOutWrite;
2180 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&serverOutRead, &serverOutWrite, nullptr, 0));
2181 + VERIFY_WIN32_BOOL_SUCCEEDED(SetHandleInformation(serverOutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
2182 +
2183 + unique_kill_process serverProcess(LxsstuStartProcess(serverCmd.data(), nullptr, serverOutWrite.get()));
2184 + serverOutWrite.reset();
2185 +
2186 + // Wait for the server to be listening
2187 + std::string output;
2188 + VERIFY_IS_TRUE(FindSubstring(serverOutRead, "listening", output));
2189 +
2190 + // Perl client: connect to 127.0.0.1:1234, then wait forever to keep the connection alive.
2191 + auto clientCmd = LxssGenerateWslCommandLine(
2192 + L"perl -MSocket -e '"
2193 + L"socket(S,AF_INET,SOCK_STREAM,0) or die;"
2194 + L"connect(S,sockaddr_in(1234,inet_aton(\"127.0.0.1\"))) or die;"
2195 + L"while(1){sleep 1000}"
2196 + L"'");
2197 +
2198 + unique_kill_process guestClientProcess(LxsstuStartProcess(clientCmd.data()));
2199 +
2200 + // Wait for the server to accept the connection and close the listen socket
2201 + VERIFY_IS_TRUE(FindSubstring(serverOutRead, "ready", output));
2202 +
2203 + // We need to wait > 60 seconds so that the port tracker's deallocation logic kicks in for this port.
2204 + // See c_bind_timeout_seconds in GnsPortTracker.cpp
2205 + std::this_thread::sleep_for(std::chrono::seconds(90));
2206 +
2207 + // Verify the port is still allocated to the guest — host bind should fail
2208 + BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
2209 +
2210 + // stop server and client processes
2211 + serverProcess.reset();
2212 + guestClientProcess.reset();
2213 +
2214 + // Verify the port is eventually released and host is able to bind to it
2215 + wsl::shared::retry::RetryWithTimeout<void>(
2216 + [&]() {
2217 + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
2218 + THROW_LAST_ERROR_IF(!sock);
2219 +
2220 + SOCKADDR_IN addr{};
2221 + addr.sin_family = AF_INET;
2222 + addr.sin_port = htons(1234);
2223 + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
2224 + },
2225 + std::chrono::seconds(1),
2226 + std::chrono::minutes(2));
2227 + }
2228 +
2229 template <typename T>
2230 static void VerifyNotBound(T& Address, int AddressFamily, int Protocol)
2231 {
@@ -3203,7 +3275,8 @@ class NetworkTests
3275 {
3276 char buffer[256];
3277 DWORD bytesRead;
3206 - const HANDLE readFileThread = OpenThread(THREAD_ALL_ACCESS, false, GetCurrentThreadId());
3278 + const wil::unique_handle readFileThread(OpenThread(THREAD_ALL_ACCESS, false, GetCurrentThreadId()));
3279 + VERIFY_IS_NOT_NULL(readFileThread.get());
3280 const wil::unique_handle event(CreateEvent(nullptr, FALSE, FALSE, nullptr));
3281 VERIFY_ARE_NOT_EQUAL(event.get(), INVALID_HANDLE_VALUE);
3282
@@ -3212,7 +3285,7 @@ class NetworkTests
3285 if (WaitForSingleObject(event.get(), 30000) == WAIT_TIMEOUT)
3286 {
3287 LogInfo("Canceling synchronous IO", GetTickCount());
3215 - CancelSynchronousIo(readFileThread);
3288 + CancelSynchronousIo(readFileThread.get());
3289 }
3290 });
3291
@@ -4056,6 +4129,63 @@ class MirroredTests
4129 NetworkTests::VerifyPortZeroBindIsTracked(false);
4130 }
4131
4132 + WSL2_TEST_METHOD(AcceptedConnectionPortTracking)
4133 + {
4134 + MIRRORED_NETWORKING_TEST_ONLY();
4135 +
4136 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
4137 + WaitForMirroredStateInLinux();
4138 +
4139 + NetworkTests::VerifyAcceptedConnectionPortTracking();
4140 + }
4141 +
4142 + WSL2_TEST_METHOD(MirroredReusePortOnGuest)
4143 + {
4144 + MIRRORED_NETWORKING_TEST_ONLY();
4145 +
4146 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
4147 + WaitForMirroredStateInLinux();
4148 +
4149 + WslKeepAlive keepAlive;
4150 +
4151 + // Verify that when guest has two binds on the same port (with reuseport) and the first
4152 + // bind is released, the port remains allocated to the guest because the second bind is
4153 + // still active. This validates the relaxed port+protocol matching in mirrored mode.
4154 + {
4155 + auto [guestLocal, read1] = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234,bind=127.0.0.1,reuseport", true);
4156 +
4157 + auto guestWild = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234,bind=0.0.0.0,reuseport", true);
4158 +
4159 + // Release the first bind (127.0.0.1)
4160 + guestLocal.reset();
4161 + read1.reset();
4162 +
4163 + // Wait > 60 seconds so that the port tracker's deallocation logic kicks in.
4164 + // See c_bind_timeout_seconds in GnsPortTracker.cpp
4165 + std::this_thread::sleep_for(std::chrono::seconds(90));
4166 +
4167 + // The host tries to bind on 127.0.0.1 (matching the released guest bind). This should
4168 + // still fail because the second guest bind (0.0.0.0) is still active and the mirrored
4169 + // mode port tracker matches by port+protocol, not the full allocation tuple.
4170 + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false, false, true);
4171 + }
4172 +
4173 + // Both binds are now released. Verify the port is eventually released.
4174 + wsl::shared::retry::RetryWithTimeout<void>(
4175 + [&]() {
4176 + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
4177 + THROW_LAST_ERROR_IF(!sock);
4178 +
4179 + SOCKADDR_IN addr{};
4180 + addr.sin_family = AF_INET;
4181 + addr.sin_port = htons(1234);
4182 + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
4183 + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
4184 + },
4185 + std::chrono::seconds(1),
4186 + std::chrono::minutes(2));
4187 + }
4188 +
4189 WSL2_TEST_METHOD(PortZeroRebindSucceeds)
4190 {
4191 MIRRORED_NETWORKING_TEST_ONLY();