Skip to content

Windows CLI: WSL2 镜像网络预留范围内的 45278 导致服务启动失败,端口查询无占用记录 #55918

Closed

Description

@zhpengsy

Description

Windows 原生 CLI 启动失败,提示 127.0.0.1:45278 被其他进程占用,但 Windows 和 WSL 均查不到该端口的连接/监听记录。实测该端口位于正在运行的 WSL2 镜像网络的临时端口范围 44620–48715 内。

Starting background server...
Error: Server process exited with code 1
Error: Managed service port 45278 on 127.0.0.1 is already in use by another process.
Configure another port with `opencode service set port <port>` and start the service again.

OpenCode version

2.0.30,Windows 原生 npm 包 @opencode/cli。opencode service get 返回 {},opencode service status 返回 stopped。

Operating System

Windows 11,build 26200.9711;WSL 2.7.3.0,Ubuntu 正在运行。

相关 .wslconfig 配置:

[wsl2]
networkingMode=Mirrored
[experimental]
hostAddressLoopback=true

Terminal

PowerShell

Steps to reproduce

  1. 在启用了 WSL2 镜像网络的 Windows 上运行 Ubuntu。
  2. 查询 WSL 临时端口范围:
    wsl -d Ubuntu -- cat /proc/sys/net/ipv4/ip_local_port_range
    本机结果为 44620 48715。
  3. 在 Windows PowerShell 执行 opencode,服务尝试监听 45278,报上述错误。

排查结果:

  • Get-NetTCPConnection -LocalPort 45278 无匹配记录。
  • netstat -anoq 无 45278 记录;WSL 的 ss -tanp 也无该端口记录。
  • IPv4/IPv6 的 netsh interface ... show excludedportrange protocol=tcp 仅列出 3336 和 50000–50059,均不包含 45278。
  • 独立于 OpenCode 的 .NET TCP 监听测试也失败,返回 SocketException 10048 / AddressAlreadyInUse。
  • 对照测试:45279、45000、46000、47000 同样失败;43000、44000、49374、18080 均可监听。

最小端口测试(Windows PowerShell):

$listener = [System.Net.Sockets.TcpListener]::new(
    [System.Net.IPAddress]::Loopback, 45278
)
try { $listener.Start(); 'bind succeeded' }
finally { $listener.Stop() }

Expected behavior

端口不可绑定时,提示应涵盖系统/WSL 预留的可能性,而不直接断言存在占用进程。对于未显式配置的服务端口,建议尝试可用端口并保存发现信息,避免一直重试不可用端口。

Additional context

上述证据与 WSL 镜像模式的 HNS 临时端口预留行为一致:microsoft/WSL#40169。未通过关闭 WSL 做对照实验,因此这是根据端口范围和独立绑定测试得出的判断。

可尝试 opencode service set port 18080 绕过。本报告只验证了 18080 能正常绑定,尚未修改服务配置或验证 OpenCode 在该端口完整启动。

关联 #53351(显式 excluded TCP range)和 #55907(进程竞争)。此处补充的区别是:端口不在 netsh 排除列表内,且没有服务进程时独立绑定测试仍失败。

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed investigation. I can't recreate the WSL2 mirrored-mode reservation in our Linux sandbox, but I could confirm the OpenCode side:

    • In 2.0.30, the Windows default service port is computed as 0xB000 | (port & 0xFFF), which gives 45278 (from fix(cli): default the Windows service to port 45278 #55547). That's inside your WSL mirrored ephemeral range (44620–48715).
    • With no port configured (opencode service get → {}) and the default port blocked, opencode service start fails with Managed service port … is already in use by another process and never tries another port. opencode service status stays stopped. I simulated the blocked port with a plain listener.
    • The same thing still happens on the latest dev (e25b7ab).

    The script below reproduces it on Linux.

    Related:

    Until this is fixed, opencode service set port <port> with a port outside both your WSL range and the Windows excluded ranges should work around it.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #55918's OpenCode-side behaviour on Linux: when the default managed-service
    # port cannot be bound and no port is configured, `opencode service start` fails with
    # "already in use by another process" and never falls back to another port.
    # On Windows (2.0.30+) the default port is 45278 (0xB0DE), which falls inside the WSL2
    # mirrored-mode ephemeral reservation (e.g. 44620-48715). Here we make the Linux default
    # port (49374) unbindable to stand in for that reservation.
    # Usage: ./repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/workspace/repo ./repro.sh   (runs the CLI from a source checkout)
    set -u
    # 49374 = default for release channels, 49375 = default for a source checkout ('local' channel)
    H=$(mktemp -d)
    export HOME=$H XDG_CONFIG_HOME=$H/.config XDG_STATE_HOME=$H/.state XDG_DATA_HOME=$H/.data
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else
      OC=(opencode)
    fi
    "${OC[@]}" --version
    python3 -c "
    import socket,time
    ss=[]
    for p in (49374,49375):
        s=socket.socket();s.bind(('127.0.0.1',p));s.listen();ss.append(s)
    time.sleep(90)" &
    BLOCKER=$!
    sleep 1
    echo "--- service get (no port configured):"; "${OC[@]}" service get
    echo "--- service start:"
    if [ -n "${OC_DEV:-}" ]; then cd "$OC_DEV/packages/cli"; else cd "$H"; fi; timeout 60 "${OC[@]}" service start 2>&1 | head -3
    echo "--- service status:"; "${OC[@]}" service status 2>&1 | head -3
    kill $BLOCKER 2>/dev/null

    Run it: bash repro.sh

  2. Mr-YW commented on Oct 16, 2026

    @Mr-YW

    因为这个桌面端打不开,转用DeepSeek Hermes了,意外的是还挺好用的

  3. deadczarvc commented on Oct 17, 2026

    @deadczarvc

    Independent confirmation from a second machine (Windows 11, Desktop 2.0.32, bundled CLI 2.0.32) — same signature, plus the mechanism and a durable workaround.

    Confirming evidence

    The port is held by an HNS/WSL dynamic reservation, not by a process. All of these are empty while bind() returns WSAEADDRINUSE (10048):

    • netstat -ano | findstr 45278 — empty
    • Get-NetTCPConnection -LocalPort 45278 (including -State Bound) — empty
    • a native GetExtendedTcpTable(..., TCP_TABLE_OWNER_PID_ALL) probe (C/WinAPI, no shell tooling) — 0 rows

    Binding with SO_REUSEADDR fails with 10013 (WSAEACCES) — the documented signature of a reserved/excluded range.

    Bisection distinguishes the two cases. Probing neighbours shows a wide block (~4,000 ports), not a single busy port:

    40000-45277 OK
    45278-45278 EADDRINUSE
    45279-46000 OK          <- this run: only one port blocked
    

    On earlier runs the whole band 41900–45989 failed. So the reserved window moves:

    • 2026-08-31: 41890–45999 (after wsl --shutdown) → 37795–41890
    • 2026-10-17: the band was free again; only 45278 was blocked, and it was blocked by the (already immunized) port itself

    netsh int ipv4 show excludedportrange protocol=tcp does not list the reserved window — only the ports that were manually immunized.

    Trigger

    networkingMode=mirrored in %USERPROFILE%\.wslconfig with WSL2 running. The mirrored mode makes HNS reserve a large ephemeral window through sockets that never appear in any user-visible socket table. net stop/start winnat does not free it; wsl --shutdown does, but the window is re-drawn on the next start/reboot, so it is not a durable fix.

    Durable workaround (verified here)

    Reserve the canonical port so it survives the drift — admin shell:

    netsh int ipv4 add excludedportrange protocol=tcp startport=45278 numberofports=1
    

    After this, 45278 stays bindable even when WSL re-draws its window, and the managed service starts normally on it. (Observed working across a WSL restart.)

    WSL-side lever worth documenting too: ignoredPorts (experimental section of .wslconfig) controls which ports Linux may bind even when Windows uses them.

    Suggestions

    1. Error text: the current message ("already in use by another process") sends users hunting for a process that does not exist. Distinguishing "a process holds this port" from "the OS refuses this port" (e.g. by counting how many neighbouring ports also fail to bind) would point at the real cause in one step.
    2. Default port: on Windows with WSL2 present, either pick a default outside the Linux ephemeral range or probe and report before failing.
    3. Docs: mention the excludedportrange immunization and the mirrored-mode interaction in the service/port troubleshooting page.

    Related: #55907 (same class, desktop), #52234 (hardcoded default port without fallback), #54267 (different cause — a previous service not exiting). A comparable case in another Electron app: httptoolkit/httptoolkit#901.

  4. BigBear0099 commented on Oct 19, 2026

    @BigBear0099

    Confirmed on Windows Desktop 2.0.32 with its bundled CLI 2.0.32 after an in-app update. The desktop failed with Desktop IPC handler failed and the nested error Managed service port 45278 on 127.0.0.1 is already in use by another process.

    Environment: Windows 11 build 26200.8737; WSL 3.0.3.0, kernel 6.18.54.1-microsoft-standard-WSL2; Ubuntu running with networkingMode=mirrored.

    wsl -d Ubuntu --exec cat /proc/sys/net/ipv4/ip_local_port_range returns 44620 48715. Windows TCP tables and WSL ss show no socket on 45278. Neither the IPv4 nor IPv6 excluded-port list includes this range.

    Independent Windows .NET socket bind tests, closing each socket immediately:

    Port Result
    44619 Success
    44620, 44621 WSAEADDRINUSE / 10048
    45278 WSAEADDRINUSE / 10048
    48714, 48715 WSAEADDRINUSE / 10048
    48717, 48718, 49374 Success

    45278 also fails on IPv4 wildcard, IPv6 loopback, and a local NIC address. This matches the mirrored-mode reservation described by Microsoft in microsoft/WSL#12228 and microsoft/WSL#40169.

    Verified recovery: using the desktop-bundled CLI, service set port 48716 followed by service start succeeds. Restarting Desktop restores the existing session; its main log reports v2 CLI background service ready, version 2.0.32, URL http://127.0.0.1:48716. The model configuration's SHA-256 is unchanged. No system networking settings were changed.

    I did not stop/restart WSL or reboot Windows as a control, so this confirms recovery for the current reservation, not durability across WSL restarts.

    This extends the report to the desktop's current bundled service. Please consider a bind-tested fallback for an unconfigured/default service port, preserving service discovery and explicit user port settings, plus an error that includes OS/WSL reservation as a possible cause. Moving the default below Windows' dynamic range alone (#55547) does not avoid the WSL mirrored range.

  5. SHJordan commented on Oct 19, 2026

    @SHJordan

    Can reproduce on OpenCode v2.0.32 (Windows 11 build 26300, PowerShell 7.6.7). Running plugin update fails on 45279 and 45280 despite no active listener in Get-NetTCPConnection and neither port being listed in netsh excludedportrange. Workaround: 'opencode service set port 12580' resolved the issue immediately.

  6. jiem88 commented on Oct 20, 2026

    @jiem88

    Additional confirmation on Desktop 2.0.32 (bundled CLI 2.0.32, Windows 11 10.0.26300), including the exact port-derivation and a workaround that survives the reserved window moving.

    Port derivation (extracted from the 2.0.32 opencode-cli.exe bundle):

    function A(e) {                      // default per channel
      if (e === "latest" || e === "dev" || e === "beta" || e === "next") return 49374;
      if (e === "local") return 49375;
      return 1e4 + parseInt(hash(e).slice(0, 8), 16) % 50000;
    }
    function DDe(e, r = "win32") {       // win32 remap
      const l = A(e);
      if (r === "win32" && l >= 49152) return 45056 | (l & 4095);
      return l;
    }

    45056 | (49374 & 4095) = 45278. So this is not a random collision: every Windows default is parked in 45056–49151, while WSL mirrored mode reserves the guest ephemeral range (44620–48715 on this machine, exactly /proc/sys/net/ipv4/ip_local_port_range). Overlap: 45056–48715 = 3660 of 4096 ports (89.4%). Roughly 9 out of 10 mappings are unusable whenever WSL2 mirrored networking is running.

    Evidence (same signature as reported above):

    • netstat -ano | findstr :45278 → empty; Get-NetTCPConnection -LocalPort 45278 → 0 rows
    • netsh int ipv4/ipv6 show excludedportrange → only 5357 and 50000–50059; 45278 is not listed
    • bind tests: 45278 → 10048 (no SO_REUSEADDR) / 10013 (with SO_REUSEADDR); the whole 44620–48715 block behaves identically; 30000, 40000, 48716, 49374 bind fine
    • a sweep of 44000–49200 yields a blocked window exactly 44620–48715, matching the distro's ephemeral range (WSL 2.6.3.0, networkingMode=mirrored)
    • WSL started at 08:51; Desktop failed at 08:54. On 10-17 the same port failed immediately after the 2.0.28→2.0.32 update (reason: 'version-mismatch', previousVersion: '2.0.28'), then succeeded at 21:50 when the reservation was not active.

    Workaround robust to the window moving: pick a port below 32768 (outside both the WSL range and the Windows dynamic range):

    opencode-cli.exe service set port 31415
    opencode-cli.exe service restart

    Verified on Desktop 2.0.32: service.json → {"port":31415}, service ready at http://127.0.0.1:31415. Ports near the band edges (44619, 48716) work today but are only one port away from the reservation and will break when the window shifts.

    Suggestion: since 89.4% of the current Windows remap band is unusable under WSL mirrored mode, either remap below 32768, or bind-test the selected default and fall back to a registered ephemeral port. Also, "already in use by another process" should distinguish WSAEACCES/reserved ranges from a real listener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions