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();