WSL 2: Multiple SSH tunnels on port 443 using different loopback addresses

This article describes a way to expose several remote HTTPS services to a Windows browser through SSH local forwards. Giving each tunnel a different loopback address means that every service can use local port 443:

127.0.0.201:443 -> upstream A:443
127.0.0.202:443 -> upstream B:443

The article separates the documented WSL networking behavior from a workaround observed in one WSL 2 setup.

TL;DR

WSL 2 mirrored networking does not officially promise forwarding arbitrary 127.0.0.X addresses to Windows. In one tested setup, starting a temporary listener on 127.0.0.1:443 made already-running listeners on several other loopback addresses reachable from Windows, even after the temporary listener stopped. This is undocumented behavior: verify it locally before using it for SSH tunnels.

Why WSL 1 and WSL 2 behave differently

WSL 1 is not simply “Windows with a Linux shell.” Microsoft describes WSL 1 as using a Linux system-call translation layer, without a managed VM or a full Linux kernel. WSL 2 instead runs a real Linux kernel inside a lightweight managed virtual machine, with Linux distributions running as isolated containers in that VM. See Microsoft’s comparison of WSL versions and the WSL overview.

That architectural difference matters for networking. Microsoft documents that WSL 2 normally uses a virtualized network adapter and NAT. In the default NAT mode, Windows can normally reach a service in WSL through localhost, but accessing the WSL 2 instance as a LAN service requires additional configuration such as a port proxy. WSL 1 does not have the same WSL 2 virtual machine boundary; Microsoft’s documentation notes that, when the computer is configured for LAN access, applications running in WSL 1 can be accessed on the LAN. It is therefore reasonable that Windows and WSL 1 expose loopback listeners differently, but this is not a claim that WSL 1 and Windows share one Linux/Windows socket table.

What mirrored networking officially promises

On Windows 11 22H2 and later, WSL can be configured with networkingMode=mirrored under [wsl2] in the global %UserProfile%\\.wslconfig file. Microsoft describes mirrored mode as a new networking architecture intended to mirror Windows network interfaces into Linux. Its documented benefits include IPv6, better VPN compatibility, multicast, LAN access to WSL, and connecting from Linux to Windows services using the IPv4 localhost address 127.0.0.1 (Microsoft Learn: Mirrored mode networking). The same page specifically notes that IPv6 localhost ::1 is not supported for that connection.

The tested configuration was:

[wsl2]
networkingMode=Mirrored

[experimental]
hostAddressLoopback=true

The hostAddressLoopback configuration reference says that this option allows connections using IPv4 addresses assigned to the Windows host. It does not say that it enables arbitrary addresses in 127.0.0.0/8; 127.0.0.1 remains the documented localhost path.

The documentation does not promise that a listener on every address in 127.0.0.0/8 is mirrored to Windows. That distinction is important for SSH tunnels bound to 127.0.0.201, 127.0.0.202, and similar addresses.

What issue #11766 says today

WSL issue #11766 reports the limitation “Mirrored networking does not loopback the entire 127.0.0.0/8 range.” The original report used WSL 2.2.4.0, kernel 5.15.153.1-2, Windows 10.0.22635.3858, and Fedora 40. It showed that Windows could connect to a WSL listener on 127.0.0.1, but not one on 127.0.0.2, despite mirrored networking and hostAddressLoopback=true.

The issue has a bydesign label. A WSL contributor wrote that only 127.0.0.1 can be used for communication between Windows and Linux. The issue was later automatically closed on August 7, 2025 after a year without activity and marked completed. That status is not a statement that arbitrary loopback addresses were added or that the limitation was fixed. The issue therefore describes a limitation involving access to other loopback addresses; it does not document the procedure below.

The observed WSL 2 behavior

The following results were collected by the author on November 10, 2026. They were not independently tested on another computer. The test used the UbuntuOld distribution, confirmed as WSL 2, and the following environment:

Windows: Windows 11 Enterprise, DisplayVersion 26H2, build 26300.9711, x64
WSL:     3.0.3.0
Kernel:  6.18.54.1-1
Distro:  Ubuntu 24.04.5 LTS (Noble Numbat)
Runtime: 6.18.54.1-microsoft-standard-WSL2

The machine also had an UbuntuOld1 distribution running WSL 1, but that distribution was not used for this test. No Windows netsh interface portproxy rules were configured.

The result was:

  1. An HTTP server bound to 127.0.0.210:443 was initially inaccessible from Windows.
  2. While it remained running, a second server was started on 127.0.0.1:443; Windows could then reach both addresses and received HTTP 200 responses.
  3. After the 127.0.0.1 server was stopped, Windows could still reach 127.0.0.210:443 and received HTTP 200.
  4. netsh interface portproxy show all returned no rules. The requests were made from Windows with curl.exe, using --noproxy "*"; the Python server logged the successful GET requests.

This differs from the documented mirrored-mode description and from the original issue report. It demonstrates an observed behavior on this setup and port; it does not establish a supported contract or explain what WSL does internally. In particular, this article does not claim that WSL creates a port relay, registers the address, caches a mapping, or uses any other specific mechanism.

Microsoft’s WSL localhost technical documentation provides useful implementation context: in mirrored mode, WSL uses a BPF program to intercept bind() calls and communicates the information to Windows through wslservice.exe. That documentation does not establish why the sequence observed here—binding 127.0.0.1 after the custom listener already exists—makes the other address reachable, so this is context rather than a confirmed explanation.

State Windows result for 127.0.0.210:443
Custom listener before priming Failed
After starting 127.0.0.1:443 Succeeded with HTTP 200
After stopping 127.0.0.1:443 Custom listener still succeeded with HTTP 200

The same behavior was also observed with multiple custom loopback listeners running at the same time. Listeners on 127.0.0.201:443, 127.0.0.202:443, and 127.0.0.203:443 became reachable from Windows after one listener on 127.0.0.1:443 was started and stopped. All target listeners remained reachable after the helper stopped. This supports the multi-address use case, while remaining an observation from the tested setup rather than an official WSL guarantee.

Reproduce the experiment

Use a trusted local test server only. The test uses TCP port 443 and serves plain HTTP for reachability testing. The following procedure uses three simultaneous listeners and therefore covers both the single-address observation and the extended multi-address test.

1. Start three non-standard loopback servers in WSL

Create a dedicated test directory, and ensure it contains no sensitive files, so the temporary HTTP servers cannot expose files from your working directory:

mkdir -p /tmp/wsl-loopback-test

In three separate Ubuntu/WSL terminals, run one command in each terminal:

sudo python3 -m http.server 443 --bind 127.0.0.201 --directory /tmp/wsl-loopback-test
sudo python3 -m http.server 443 --bind 127.0.0.202 --directory /tmp/wsl-loopback-test
sudo python3 -m http.server 443 --bind 127.0.0.203 --directory /tmp/wsl-loopback-test

Leave all three servers running. From a Windows PowerShell prompt, not from the WSL terminal, test them:

curl.exe --noproxy "*" -v http://127.0.0.201:443/
curl.exe --noproxy "*" -v http://127.0.0.202:443/
curl.exe --noproxy "*" -v http://127.0.0.203:443/

On the setup described here, these Windows requests initially fail. A request made inside WSL is not a substitute for this test: it only proves that Linux can reach its own listeners.

2. Briefly start a listener on ordinary localhost

Open another WSL terminal and run:

sudo python3 -m http.server 443 --bind 127.0.0.1 --directory /tmp/wsl-loopback-test

While all four servers are running, test the helper and all three custom addresses from Windows:

curl.exe --noproxy "*" -v http://127.0.0.1:443/
curl.exe --noproxy "*" -v http://127.0.0.201:443/
curl.exe --noproxy "*" -v http://127.0.0.202:443/
curl.exe --noproxy "*" -v http://127.0.0.203:443/

In the reported test, all four requests returned HTTP 200. The first request reaches the helper server; the other three reach the servers on the custom loopback addresses.

Binding a port below 1024 may require elevated privileges in Linux, which is why the example uses sudo. If the bind fails with Address already in use, identify the existing listener and choose another test port or stop the conflicting service:

ss -ltnp

The helper server may temporarily respond on the test port, so do not use an untrusted or production service for this step.

3. Stop only the helper server

Press Ctrl-C in the terminal running the 127.0.0.1 server. Keep all three custom loopback servers running.

4. Test again from Windows

Run the Windows test again:

curl.exe --noproxy "*" -v http://127.0.0.201:443/
curl.exe --noproxy "*" -v http://127.0.0.202:443/
curl.exe --noproxy "*" -v http://127.0.0.203:443/

In the reported experiment, all three requests still succeeded even though the 127.0.0.1 helper had stopped. Again, this is reproduced behavior on the stated WSL/Windows combination, not a documented guarantee. Stop the three remaining test servers with Ctrl-C when finished.

Two SSH tunnels using the same local port

Once the behavior has been verified for your setup, the same pattern can be used for separate SSH local forwards. For example, use different local loopback addresses while keeping local port 443 for both tunnels:

ssh -o ExitOnForwardFailure=yes -N \\
  -L 127.0.0.201:443:internal-a.example.com:443 \\
  user@jump.example.com

ssh -o ExitOnForwardFailure=yes -N \\
  -L 127.0.0.202:443:internal-b.example.com:443 \\
  user@jump.example.com

ExitOnForwardFailure=yes makes SSH exit if it cannot create a requested local forward, rather than leaving a session running without the expected listener.

The upstream names are resolved from the SSH server side according to SSH local-forwarding rules. The example.com names and user@jump.example.com are documentation-only values; replace them with values appropriate for your environment and do not put real internal names or credentials in a public article or shell history.

On Windows, map separate browser names to the corresponding loopback addresses in the Windows hosts file, located at C:\Windows\System32\drivers\etc\hosts:

127.0.0.201 service-a.example.test
127.0.0.202 service-b.example.test

Then browse to https://service-a.example.test/ and https://service-b.example.test/, or test with:

curl.exe -vk https://service-a.example.test/
curl.exe -vk https://service-b.example.test/

The -k option is only for a test where the upstream certificate is not trusted for the test name. An SSH local forward carries TCP traffic; it does not make the HTTPS certificate match the hostname in the Windows hosts file. Prefer proper certificate validation in normal use.

The Python server in this experiment speaks plain HTTP even though it uses TCP port 443. This verifies Windows-to-WSL reachability for the port; it does not validate HTTPS certificates or the complete SSH-tunnel setup. Port 443 may already be occupied by another process.

For a WSL 2 distribution using mirrored networking, the only sequence verified by the experiment is:

  1. Verify the WSL version and mirrored-networking configuration.
  2. Start the target listener on 127.0.0.X using the desired local port.
  3. From a second WSL terminal, briefly start a trusted helper listener on 127.0.0.1 using that same port.
  4. Test both addresses from Windows, stop the helper, and test the target listener again.
  5. The tested multi-address case used listeners on 127.0.0.201, 127.0.0.202, and 127.0.0.203, followed by one helper on 127.0.0.1. For SSH tunnels, use the same order: start the tunnels first, then briefly start and stop the helper listener. Both the multi-address HTTP listener behavior and the SSH tunnel workflow were tested successfully. The behavior still relies on an undocumented WSL implementation rather than an official guarantee.

Whether this priming step must be repeated after wsl --shutdown, a Windows restart, a WSL update, or a networking configuration change remains to be tested. Do not assume that it persists across those events. Also remember that while the helper is running it may answer requests on the port.

Security and operational notes

  • Bind test services only to loopback and use trusted local test content.
  • Stop the helper service after the priming test; while it is running, it temporarily serves HTTP on 127.0.0.1:<port>.
  • Check listeners with ss -ltnp before changing ports or killing a process.
  • Mirrored networking has its own firewall and networking settings. If a documented localhost connection does not work, check the Hyper-V firewall guidance rather than assuming the loopback workaround applies.
  • Treat this as a local compatibility workaround, not as a security boundary or a replacement for SSH host-key checking, TLS validation, or access control.

Conclusion

WSL 1 can be the simpler choice when this exact multi-address loopback behavior is essential and the rest of the workload does not require WSL 2’s full Linux kernel and system-call compatibility. Microsoft recommends WSL 2 for most workloads, and mirrored networking officially improves Windows/Linux connectivity through 127.0.0.1.

For WSL 2, issue #11766 shows that access to arbitrary 127.0.0.X addresses is not the documented behavior. The localhost-priming sequence described here was reproduced on one newer setup and successfully used with multiple SSH tunnels, but it remains undocumented behavior. Try it only after testing your own WSL version, Windows build, distribution, and port—and keep WSL 1 when predictable multi-address loopback access matters more than the benefits of WSL 2.

Sources