Skip to content

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

Merged
Ben Hillis (benhillis) merged 6 commits into
masterfrom
user/benhill/consomme-listen-autobind
Jul 24, 2026
Merged

Ben Hillis (benhillis) merged 6 commits into
masterfrom
user/benhill/consomme-listen-autobind

Conversation

@benhillis

Copy link
Copy Markdown
Member

Summary

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 on the wildcard address), the port tracker never observes the bind, so the port is never registered for host forwarding and the socket is unreachable via 127.0.0.1: from Windows.

Fixes #41117

Root cause

The seccomp filter installed by RegisterSeccompHook() in src/linux/init/main.cpp only ever trapped bind(). listen() was never trapped, so a socket() + listen() sequence with no prior bind() call was invisible to GnsPortTracker.

What this changes

  • Extends the seccomp BPF filter to also trap listen() across all supported architectures (native 64-bit __NR_listen, x86 compat via socketcall(SYS_LISTEN, ...), and 32-bit ARM EABI via a new ARMV7_NR_listen constant).
  • GnsPortTracker::GetCallInfo dispatches listen() calls through a new ParseListen path, reusing 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 before listen() runs), ParseListen first checks getsockname() synchronously and only falls back to the deferred/polling resolution path for genuine implicit-autobind-via-listen() calls (no prior bind()).

Testing

  • Added ListenWithoutBindIsTracked to both NetworkTests::ConsommeTests and NetworkTests::MirroredTests, mirroring the existing PortZeroBindIsTracked coverage but exercising listen() with no preceding bind().
  • Verified with a real before/after run against a full local build+deploy:
    • Pre-fix: ConsommeTests::ListenWithoutBindIsTracked fails (host can bind the port — Consomme never tracked it).
    • Post-fix: passes.
    • MirroredTests::ListenWithoutBindIsTracked passes both before and after — mirrored mode already has an independent port+protocol refresh mechanism (OnRefreshAllocatedPorts, added in Fix mirrored mode port tracking for implicit binds resulting from accept() calls #40287) that covers this case, so this change doesn't affect mirrored mode's behavior.
  • Also validated the raw BPF filter bytes directly against the running kernel via a standalone seccomp-unotify harness, confirming the old filter never traps listen() and the new filter does (__NR_listen, syscall nr 50 on x86_64).

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: 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
Copilot AI review requested due to automatic review settings July 20, 2026 23:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Consomme-mode port-forwarding gap where TCP/UDP listeners created via listen() without a preceding explicit bind() (kernel implicit autobind to an ephemeral port) were not observed by the guest port tracker, leaving the socket unreachable from Windows via 127.0.0.1:.

Changes:

  • Extend the seccomp BPF filter and dispatcher registration to intercept listen() in addition to bind() (including x86 compat socketcall(SYS_LISTEN, ...) and 32-bit ARM compat via ARMV7_NR_listen).
  • Teach GnsPortTracker::GetCallInfo() to recognize listen() notifications and resolve the effective port via getsockname() (fast-path when already bound) or the existing deferred ResolvePortZeroBind() mechanism (implicit autobind case).
  • Add new NetworkTests coverage for the listen()-without-bind() scenario in both Consomme and Mirrored suites.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/windows/NetworkTests.cpp Adds ListenWithoutBindIsTracked test coverage and a guest helper that creates a listener via listen() without bind().
src/linux/init/main.cpp Updates seccomp BPF filter to trap listen() alongside bind()/ioctl(...) across supported ABIs.
src/linux/init/localhost.cpp Registers a seccomp handler for __NR_listen and for ARMV7_NR_listen in compat mode.
src/linux/init/GnsPortTracker.cpp Adds parsing/dispatch for listen() notifications and reuses deferred port resolution for implicit autobinds.
src/linux/inc/seccomp_defs.h Introduces ARMV7_NR_listen constant for 32-bit ARM compat syscall trapping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b
Copilot AI review requested due to automatic review settings July 20, 2026 23:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/linux/init/GnsPortTracker.cpp Outdated
Comment thread src/linux/init/main.cpp Outdated
- 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
Copilot AI review requested due to automatic review settings July 20, 2026 23:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/linux/init/main.cpp Outdated
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
Copilot AI review requested due to automatic review settings July 21, 2026 00:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@benhillis
Ben Hillis (benhillis) marked this pull request as ready for review July 21, 2026 21:08
@benhillis
Ben Hillis (benhillis) requested a review from a team as a code owner July 21, 2026 21:08
Comment thread test/windows/NetworkTests.cpp Outdated
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
Copilot AI review requested due to automatic review settings July 23, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/linux/init/main.cpp:3368

  • The comment above the __AUDIT_ARCH_64BIT check is misleading: the filter does not “notify on all non-native arch”; it jumps to the compat block and applies a narrower match there. Updating the comment will help future edits avoid incorrect assumptions about the control flow.
        // For now, notify on all non-native arch
        BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 8),

Comment on lines +465 to +475
auto ParseListen = [&](int Socket) -> std::optional {
auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string();
if (networkNamespace != m_networkNamespace)
{
GNS_LOG_INFO("Skipping listen() call for pid {} in network namespace {}", Pid, networkNamespace.c_str());
return {{{}, {}, CallId}}; // Different network namespace. Let it go through.
}

try
{
const int protocol = GetSocketProtocol(Pid, Socket);
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
Copilot AI review requested due to automatic review settings July 23, 2026 21:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/linux/init/main.cpp:3368

  • This BPF comment is misleading: the code does not "notify on all non-native arch"; it jumps to the compat blocks below which then selectively notify on bind/listen. Clarify the intent so future edits don’t misinterpret the control flow.
        // For now, notify on all non-native arch
        BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 8),

L"listen(S,5) or die;"
L"my $port=(sockaddr_in(getsockname(S)))[0];"
L"print \"PORT=$port\\n\";"
L"accept(C,S);"
@benhillis
Ben Hillis (benhillis) merged commit 96b220c into master Jul 24, 2026
12 checks passed
@benhillis
Ben Hillis (benhillis) deleted the user/benhill/consomme-listen-autobind branch July 24, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Consomme] TCP listener using implicit listen() autobind is unreachable via 127.0.0.1

3 participants