@samitouri / QOSAMI-WSL / commits / 96b220c7

Track implicit autobind ports from listen() in Consomme networking (#41125)

* Track implicit autobind ports from listen() in Consomme networking WSL2's Consomme networking mode only forwards TCP/UDP ports that were explicitly bind()'d in the guest. If an application calls listen() without first calling bind() (relying on the kernel's implicit autobind to an ephemeral port), the port tracker never sees it, so the port is never forwarded to the host and the socket is unreachable via 127.0.0.1:<port> from Windows. The seccomp filter that traps socket syscalls for port tracking only ever trapped bind(); this extends it to also trap listen() across all supported architectures (x86_64, x86 compat via socketcall, and ARM/ARM64), and dispatches it through the existing DeferredPortLookup/ResolvePortZeroBind mechanism already used for explicit bind(port=0). To avoid adding latency to the overwhelmingly common bind()+listen() sequence (where the port is already known), ParseListen first checks getsockname() synchronously and only falls back to the deferred resolution path for genuine implicit-autobind-via-listen() calls (no prior bind()). Adds ListenWithoutBindIsTracked test coverage in both Consomme and Mirrored NetworkTests suites. Fixes #41117 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b * Fix clang-format violation in NetworkTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b * Fix stale/inaccurate comments per Copilot review - Update RegisterSeccompHook doc comment to reference SIOCSIFFLAGS (matching the actual BPF filter) instead of the incorrect TUNSETIFF. - Update ParseListen's comment to reflect the getsockname() fast-path added before deferring resolution, instead of describing it as always deferring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b * Clarify RegisterSeccompHook doc comment scope per Copilot review ioctl(*, SIOCSIFFLAGS, *) is only trapped in the native 64-bit BPF block, not the 32-bit compat blocks (socketcall/ARMV7), which only trap bind()/listen(). Clarify the routine description accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b * Fix potential port truncation race in ListenWithoutBindIsTracked test Only parse the PORT= value once the line is terminated by a newline, instead of stopping at the first non-digit character seen in a partial ReadFile() buffer. This avoids truncating the port number (e.g. reading "123" as "12") when the perl helper's output is split across multiple reads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85780ea9-1bf4-4997-8732-b33920e7d6b8 * Move network namespace read_symlink() into try block in ParseListen If the target process exits between the seccomp trap and this call, read_symlink() could throw outside of the try block, routing through the outer GetCallInfo catch/log path with a bind()-focused error message instead of the intended 'not an IP socket, let it through' handling used by the rest of ParseListen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 10ce6d20-f8ca-4204-92a2-48e23bb6ee66 --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b Copilot-Session: 85780ea9-1bf4-4997-8732-b33920e7d6b8 Copilot-Session: 10ce6d20-f8ca-4204-92a2-48e23bb6ee66

Ben Hillis committed Jul 23, 2026 at 17:54 UTC 96b220c785f6ed90e103813cdfc66a723224c657
5 files changed +274 -13
src/linux/inc/seccomp_defs.h
+1
@@ -11,3 +11,4 @@
11 // You will not get this value with the standard defines.
12 #define I386_NR_socketcall (102)
13 #define ARMV7_NR_bind (282)
14 +#define ARMV7_NR_listen (284)
src/linux/init/GnsPortTracker.cpp
+102 -3
@@ -13,6 +13,7 @@
13 #include "NetlinkTransactionError.h"
14 #include "GnsPortTracker.h"
15 #include "lxinitshared.h"
16 +#include "seccomp_defs.h"
17
18 constexpr size_t c_bind_timeout_seconds = 60;
19 constexpr auto c_sock_diag_refresh_delay = std::chrono::milliseconds(500);
@@ -453,19 +454,105 @@ std::optional<GnsPortTracker::BindCall> GnsPortTracker::GetCallInfo(
454
455 return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, {}, CallId}};
456 };
457 +
458 + // listen() can trigger an implicit autobind (assigning an ephemeral port) on a socket that
459 + // was never explicitly bind()'d. There's no sockaddr to inspect here (listen() only takes a
460 + // socket fd and a backlog), and we can't tell in advance whether the socket is already bound,
461 + // so duplicate the fd and check getsockname() immediately: if it's already bound (the common
462 + // bind()+listen() case), resolve the port synchronously here. Otherwise defer resolution to
463 + // ResolvePortZeroBind(), the same as a bind(port=0) call, since the port isn't assigned until
464 + // the listen() syscall (which performs the implicit autobind) actually completes in-kernel.
465 + auto ParseListen = [&](int Socket) -> std::optional<BindCall> {
466 + try
467 + {
468 + auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string();
469 + if (networkNamespace != m_networkNamespace)
470 + {
471 + GNS_LOG_INFO("Skipping listen() call for pid {} in network namespace {}", Pid, networkNamespace.c_str());
472 + return {{{}, {}, CallId}}; // Different network namespace. Let it go through.
473 + }
474 +
475 + const int protocol = GetSocketProtocol(Pid, Socket);
476 + auto dupFd = DuplicateSocketFd(Pid, Socket);
477 + if (!dupFd)
478 + {
479 + return {{{}, {}, CallId}};
480 + }
481 + if (!m_seccompDispatcher->ValidateCookie(CallId))
482 + {
483 + return {{{}, {}, CallId}};
484 + }
485 +
486 + // If the socket was already explicitly bind()'d - the common case of a normal
487 + // bind() followed by listen() - its port is already known right now, before the
488 + // listen() syscall even runs, so resolve it immediately instead of deferring
489 + // through the post-completion polling path. That path is reserved for the case
490 + // where listen() itself is what triggers the kernel's implicit autobind (i.e. no
491 + // prior bind() call), which can only be observed after listen() has completed.
492 + sockaddr_storage storage{};
493 + socklen_t addressLength = sizeof(storage);
494 + if (getsockname(dupFd.get(), reinterpret_cast<sockaddr*>(&storage), &addressLength) == 0)
495 + {
496 + in_port_t port = 0;
497 + in6_addr address = {};
498 + if (storage.ss_family == AF_INET)
499 + {
500 + const auto* sin = reinterpret_cast<const sockaddr_in*>(&storage);
501 + port = ntohs(sin->sin_port);
502 + address.s6_addr32[0] = sin->sin_addr.s_addr;
503 + }
504 + else if (storage.ss_family == AF_INET6)
505 + {
506 + const auto* sin6 = reinterpret_cast<const sockaddr_in6*>(&storage);
507 + port = ntohs(sin6->sin6_port);
508 + memcpy(address.s6_addr32, sin6->sin6_addr.s6_addr32, sizeof(address.s6_addr32));
509 + }
510 +
511 + if (port != 0)
512 + {
513 + return {{{PortAllocation(port, static_cast<int>(storage.ss_family), protocol, address)}, {}, CallId}};
514 + }
515 + }
516 +
517 + return {{{}, DeferredPortLookup{Pid, std::move(dupFd), protocol}, CallId}};
518 + }
519 + catch (const std::exception&)
520 + {
521 + return {{{}, {}, CallId}}; // Not an IP socket (or can't determine its protocol), just let it through
522 + }
523 + };
524 +
525 #ifdef __x86_64__
526 if (Arch & __AUDIT_ARCH_64BIT)
527 {
528 + if (SysCallNumber == __NR_listen)
529 + {
530 + return ParseListen(Arguments[0]);
531 + }
532 +
533 return ParseSocket(Arguments[0], Arguments[1], Arguments[2]);
534 }
535 // Note: 32bit on x86_64 uses the __NR_socketcall with the first argument
462 - // set to SYS_BIND to make bind system call and the second argument is
463 - // a pointer to a block of memory containing the original arguments.
536 + // set to SYS_BIND/SYS_LISTEN to make bind()/listen() system calls and the
537 + // second argument is a pointer to a block of memory containing the original arguments.
538 else
539 {
540 + if (Arguments[0] == SYS_LISTEN)
541 + {
542 + // Grab the first parameter (the socket fd).
543 + auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t));
544 + if (!processMemory.has_value())
545 + {
546 + throw RuntimeErrorWithSourceLocation("Failed to read process memory");
547 + }
548 +
549 + const uint32_t* CopiedArguments = reinterpret_cast<uint32_t*>(processMemory->data());
550 + return ParseListen(CopiedArguments[0]);
551 + }
552 +
553 if (Arguments[0] != SYS_BIND)
554 {
468 - return {{{}, {}, CallId}}; // Not a bind call, just let the call go through
555 + return {{{}, {}, CallId}}; // Not a bind or listen call, just let the call go through
556 }
557 // Grab the first 3 parameters
558 auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t) * 3);
@@ -478,6 +565,14 @@ std::optional<GnsPortTracker::BindCall> GnsPortTracker::GetCallInfo(
565 return ParseSocket(CopiedArguments[0], CopiedArguments[1], CopiedArguments[2]);
566 }
567 #else
568 + // Both native 64-bit listen() (trapped via __NR_listen, e.g. on aarch64) and 32-bit ARM
569 + // compat listen() (trapped via the hardcoded ARMV7_NR_listen syscall number) land here, so
570 + // both syscall numbers must be checked.
571 + if (SysCallNumber == __NR_listen || SysCallNumber == ARMV7_NR_listen)
572 + {
573 + return ParseListen(Arguments[0]);
574 + }
575 +
576 return ParseSocket(Arguments[0], Arguments[1], Arguments[2]);
577 #endif
578 }
@@ -558,6 +653,10 @@ catch (const std::exception& e)
653
654 std::optional<GnsPortTracker::PortAllocation> GnsPortTracker::ResolvePortZeroBind(DeferredPortLookup lookup)
655 {
656 + // This resolves the port for both an explicit bind(port=0) and an implicit
657 + // autobind triggered by listen(), since neither can be known until after the
658 + // syscall has actually completed in-kernel.
659 + //
660 // The socket fd was already duplicated (via pidfd_getfd) while the target process
661 // was stopped by seccomp, so it remains valid even if the process has closed or
662 // reused the original fd number.
src/linux/init/localhost.cpp
+9
@@ -485,6 +485,12 @@ int RunPortTracker(int Argc, char** Argv)
485 seccompDispatcher->RegisterHandler(
486 __NR_bind, [&portTracker](seccomp_notif* notification) { return portTracker.ProcessSecCompNotification(notification); });
487
488 + // listen() can perform an implicit autobind (assigning an ephemeral port) when called on a
489 + // socket that was never explicitly bind()'d. That autobind is otherwise invisible to the
490 + // port tracker, so listen() needs to be intercepted the same way bind() is.
491 + seccompDispatcher->RegisterHandler(
492 + __NR_listen, [&portTracker](seccomp_notif* notification) { return portTracker.ProcessSecCompNotification(notification); });
493 +
494 #ifdef __x86_64__
495 seccompDispatcher->RegisterHandler(I386_NR_socketcall, [&portTracker](seccomp_notif* notification) {
496 return portTracker.ProcessSecCompNotification(notification);
@@ -493,6 +499,9 @@ int RunPortTracker(int Argc, char** Argv)
499 seccompDispatcher->RegisterHandler(ARMV7_NR_bind, [&portTracker](seccomp_notif* notification) {
500 return portTracker.ProcessSecCompNotification(notification);
501 });
502 + seccompDispatcher->RegisterHandler(ARMV7_NR_listen, [&portTracker](seccomp_notif* notification) {
503 + return portTracker.ProcessSecCompNotification(notification);
504 + });
505 #endif
506
507 seccompDispatcher->RegisterHandler(__NR_ioctl, [hvSocketChannel, seccompDispatcher](auto notification) -> int {
src/linux/init/main.cpp
+21 -10
@@ -3336,7 +3336,12 @@ wil::unique_fd RegisterSeccompHook()
3336
3337 Routine Description:
3338
3339 - Register a seccomp notification for bind() & ioctl(*, TUNSETIFF, *) calls.
3339 + Register a seccomp notification for bind() & listen() calls (both the native and 32-bit
3340 + compat ABIs), plus ioctl(*, SIOCSIFFLAGS, *) calls on the native 64-bit ABI only.
3341 +
3342 + listen() is intercepted in addition to bind() because it can perform an implicit
3343 + autobind (assigning an ephemeral port) on a socket that was never explicitly bind()'d;
3344 + that autobind would otherwise be invisible to the port tracker.
3345
3346 Arguments:
3347
@@ -3360,11 +3365,13 @@ Return Value:
3365 // If syscall_arch & __AUDIT_ARCH_64BIT then continue else goto :32bit
3366 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_arch),
3367 // For now, notify on all non-native arch
3363 - BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 7),
3368 + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 8),
3369 // If syscall_nr == __NR_bind then goto user_notify: else continue
3370 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
3366 - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_bind, 3, 0),
3367 - // if (syscall_nr == __NR_bind) then continue else goto allow:
3371 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_bind, 4, 0),
3372 + // if (syscall_nr == __NR_listen) then goto user_notify: else continue
3373 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_listen, 3, 0),
3374 + // if (syscall_nr == __NR_ioctl) then continue else goto allow:
3375 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_ioctl, 0, 3),
3376 // if (syscall arg1 == SIOCSIFFLAGS) goto user_notify else goto allow:
3377 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_arg(1)),
@@ -3377,15 +3384,17 @@ Return Value:
3384 BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
3385
3386 // Note: 32bit on x86_64 uses the __NR_socketcall with the first argument
3380 - // set to SYS_BIND to make bind system call.
3387 + // set to SYS_BIND/SYS_LISTEN to make bind()/listen() system calls.
3388 #ifdef __x86_64__
3389 // 32bit:
3390 // If syscall_nr == __NR_socketcall then continue else goto allow:
3391 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
3385 - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, I386_NR_socketcall, 0, 3),
3386 - // if syscall arg0 == SYS_BIND then goto user_notify: else goto allow:
3392 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, I386_NR_socketcall, 0, 4),
3393 + // if syscall arg0 == SYS_BIND then goto user_notify: else continue
3394 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_arg(0)),
3388 - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, SYS_BIND, 0, 1),
3395 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, SYS_BIND, 1, 0),
3396 + // if syscall arg0 == SYS_LISTEN then continue else goto allow:
3397 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, SYS_LISTEN, 0, 1),
3398 // user_notify:
3399 // return SECCOMP_RET_USER_NOTIF;
3400 BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_USER_NOTIF),
@@ -3394,9 +3403,11 @@ Return Value:
3403 BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
3404 #else
3405 // 32bit:
3397 - // If syscall_nr == __NR_bind then goto user_notify: else goto allow:
3406 + // If syscall_nr == __NR_bind then goto user_notify: else continue
3407 BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
3399 - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARMV7_NR_bind, 0, 1),
3408 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARMV7_NR_bind, 1, 0),
3409 + // if (syscall_nr == __NR_listen) then goto user_notify: else goto allow:
3410 + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARMV7_NR_listen, 0, 1),
3411 // user_notify:
3412 // return SECCOMP_RET_USER_NOTIF;
3413 BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_USER_NOTIF),
test/windows/NetworkTests.cpp
+141
@@ -2125,6 +2125,127 @@ class NetworkTests
2125 return {std::move(process), assignedPort};
2126 }
2127
2128 + // Create a TCP listening socket in the guest via listen() WITHOUT ever calling bind() first
2129 + // (implicit autobind to an ephemeral port on the wildcard address, e.g. INADDR_ANY:0).
2130 + // This exercises the seccomp listen() trap added for the implicit-autobind port tracking fix,
2131 + // as opposed to BindGuestPortZero() which exercises the pre-existing explicit bind(0) path.
2132 + static std::tuple<unique_kill_process, uint16_t> BindGuestPortViaListenOnly()
2133 + {
2134 + auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true);
2135 +
2136 + // Perl one-liner: socket() + listen() with no bind(), print the kernel-assigned
2137 + // port via getsockname(), then accept() (blocking) to keep the socket alive.
2138 + const std::wstring wslCmd =
2139 + L"perl -MSocket -e '"
2140 + L"$|=1;"
2141 + L"socket(S,AF_INET,SOCK_STREAM,0) or die;"
2142 + L"listen(S,5) or die;"
2143 + L"my $port=(sockaddr_in(getsockname(S)))[0];"
2144 + L"print \"PORT=$port\\n\";"
2145 + L"accept(C,S);"
2146 + L"'";
2147 + auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
2148 +
2149 + auto process = LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), nullptr);
2150 + stdOutWrite.reset();
2151 +
2152 + std::string output(256, '\0');
2153 + DWORD writeOffset = 0;
2154 + uint16_t assignedPort = 0;
2155 + bool found = false;
2156 +
2157 + while (!found)
2158 + {
2159 + if (writeOffset == output.size())
2160 + {
2161 + output.resize(output.size() * 2);
2162 + }
2163 +
2164 + DWORD bytesRead = 0;
2165 + if (!ReadFile(stdOutRead.get(), output.data() + writeOffset, static_cast<DWORD>(output.size() - writeOffset), &bytesRead, nullptr) ||
2166 + bytesRead == 0)
2167 + {
2168 + break;
2169 + }
2170 +
2171 + writeOffset += bytesRead;
2172 + LogInfo("output %hs", output.c_str());
2173 + std::string_view outputView(output.data(), writeOffset);
2174 + auto pos = outputView.find("PORT=");
2175 + if (pos != std::string_view::npos)
2176 + {
2177 + auto portStr = outputView.substr(pos + 5);
2178 +
2179 + // Only parse once the line is fully read (terminated by '\n'); otherwise a
2180 + // partial read could truncate the port digits (e.g. "123" read as "12").
2181 + auto newlinePos = portStr.find('\n');
2182 + if (newlinePos != std::string_view::npos)
2183 + {
2184 + portStr = portStr.substr(0, newlinePos);
2185 + if (!portStr.empty())
2186 + {
2187 + assignedPort = static_cast<uint16_t>(std::stoi(std::string(portStr)));
2188 + found = true;
2189 + }
2190 + }
2191 + }
2192 + }
2193 +
2194 + VERIFY_IS_TRUE(found);
2195 + VERIFY_IS_TRUE(assignedPort > 0);
2196 + LogInfo("listen()-only autobind resolved to port %u", assignedPort);
2197 +
2198 + return {std::move(process), assignedPort};
2199 + }
2200 +
2201 + // Verifies that a listen() call with no preceding bind() (implicit autobind) is tracked
2202 + // by the host port tracker, mirroring VerifyPortZeroBindIsTracked's coverage of the
2203 + // pre-existing explicit bind(0) path.
2204 + static void VerifyListenWithoutBindIsTracked(bool verifyRelease = true)
2205 + {
2206 + WslKeepAlive keepAlive;
2207 +
2208 + auto [guestProcess, assignedPort] = BindGuestPortViaListenOnly();
2209 +
2210 + // Port resolution is asynchronous (deferred to a background thread) for the case where
2211 + // the socket wasn't already bound. Retry until the host port tracker registers the port,
2212 + // blocking the host bind.
2213 + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
2214 + [&assignedPort]() {
2215 + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
2216 + THROW_LAST_ERROR_IF(!sock);
2217 +
2218 + SOCKADDR_IN addr{};
2219 + addr.sin_family = AF_INET;
2220 + addr.sin_port = htons(assignedPort);
2221 + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) != SOCKET_ERROR);
2222 + },
2223 + std::chrono::seconds(1),
2224 + std::chrono::seconds(30)));
2225 +
2226 + if (!verifyRelease)
2227 + {
2228 + return;
2229 + }
2230 +
2231 + // Kill the guest process so the port tracker releases the port.
2232 + guestProcess.reset();
2233 +
2234 + // Retry until the host can bind the port again, confirming it was released.
2235 + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
2236 + [&assignedPort]() {
2237 + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
2238 + THROW_LAST_ERROR_IF(!sock);
2239 +
2240 + SOCKADDR_IN addr{};
2241 + addr.sin_family = AF_INET;
2242 + addr.sin_port = htons(assignedPort);
2243 + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
2244 + },
2245 + std::chrono::seconds(1),
2246 + std::chrono::minutes(2)));
2247 + }
2248 +
2249 static void VerifyPortZeroBindIsTracked(bool verifyRelease = true)
2250 {
2251 // Make sure the VM doesn't time out while we wait for async port resolution
@@ -4176,6 +4297,17 @@ class MirroredTests
4297 NetworkTests::VerifyPortZeroBindIsTracked(false);
4298 }
4299
4300 + WSL2_TEST_METHOD(ListenWithoutBindIsTracked)
4301 + {
4302 + MIRRORED_NETWORKING_TEST_ONLY();
4303 +
4304 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
4305 + WaitForMirroredStateInLinux();
4306 +
4307 + // See PortZeroBindIsTracked above for why release verification is skipped in mirrored mode.
4308 + NetworkTests::VerifyListenWithoutBindIsTracked(false);
4309 + }
4310 +
4311 WSL2_TEST_METHOD(AcceptedConnectionPortTracking)
4312 {
4313 MIRRORED_NETWORKING_TEST_ONLY();
@@ -5133,6 +5265,15 @@ class ConsommeTests
5265 NetworkTests::VerifyPortZeroBindIsTracked();
5266 }
5267
5268 + WSL2_TEST_METHOD(ListenWithoutBindIsTracked)
5269 + {
5270 + CONSOMME_TEST_ONLY();
5271 +
5272 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Consomme}));
5273 +
5274 + NetworkTests::VerifyListenWithoutBindIsTracked();
5275 + }
5276 +
5277 WSL2_TEST_METHOD(PortZeroRebindSucceeds)
5278 {
5279 CONSOMME_TEST_ONLY();