You spun up a perfectly good service in WSL2—an API, a Prometheus endpoint, a dev database—and it works from the Windows host.
Then someone on the LAN tries to hit it and gets nothing but a timeout and a vague sense of betrayal.
This isn’t a Linux problem. It’s not even really a Windows problem. It’s an expectation management problem with a NAT’d VM
that changes IPs, plus a firewall that treats you like a stranger until you prove otherwise.
The mental model: what “WSL2 networking” actually is
WSL2 is not “Linux processes on Windows” in the way WSL1 was. WSL2 is a lightweight VM running a real Linux kernel.
That VM sits behind a virtual switch and typically a NAT. Translation: your WSL2 instance has its own IP address, usually on a private
Hyper-V network, and that address can change whenever WSL restarts.
When you hit localhost on Windows, WSL2 tries to be helpful. There’s a feature commonly referred to as
“localhost forwarding” that makes ports reachable from the Windows host without you doing explicit routing.
But “reachable from the Windows host” is not the same as “reachable from the rest of your network.”
For the LAN to reach a service inside WSL2, you need an ingress point on the Windows host (which does have a LAN-reachable IP),
and then a forward or proxy from that host into the VM’s private IP/port. That’s the whole game.
Think of Windows as your edge node, and WSL2 as a backend service in a private subnet. Suddenly it looks like any other environment
you’ve supported in production. Congratulations: you’re running a tiny NAT’d datacenter on your laptop.
Interesting facts and quick history
- WSL1 vs WSL2 networking is fundamentally different. WSL1 shared the host network stack; WSL2 does not. That’s why port exposure got “harder.”
- WSL2 uses a real Linux kernel. This is why iptables, sysctl, and “normal Linux” debugging tools are relevant again.
- Hyper-V’s virtual switch is the plumbing. Even on Windows editions where you never opened Hyper-V Manager, WSL2 still relies on Hyper-V components.
- The WSL2 VM IP is usually ephemeral. It’s allocated on startup; shutdown/restart often changes it, which breaks static forwards if you hardcode it.
netsh interface portproxypredates WSL2 by years. It’s an old-but-effective TCP forwarding tool that became popular again because it works.- Portproxy is TCP-only. If you need UDP, you’ll use a different approach (or a different design).
- Windows Firewall treats forwarded ports like real inbound services. If the firewall blocks the port on Windows, your forwarding plan is dead on arrival.
- “It works on localhost” is a trap. Localhost success often proves only that Windows-to-WSL forwarding is active—not that your service is listening on the right interface.
Fast diagnosis playbook
When someone says “I can’t reach my WSL2 service from the LAN,” the fastest route is to identify the first hop where packets stop.
Don’t guess. Don’t reboot. Measure.
1) First check: is the service listening in Linux on the right interface?
If it only listens on 127.0.0.1 inside WSL, forwarding from Windows won’t help. You’ll be forwarding into a brick wall.
2) Second check: can Windows reach the service by WSL IP?
If Windows can’t connect to WSL_IP:PORT, your issue is not the LAN. It’s service binding, Linux firewall, or routing between host and VM.
3) Third check: is Windows listening on the LAN IP/port and is the firewall open?
If Windows isn’t listening (or firewall is blocking), the LAN will time out. If Windows is listening but forwarding is wrong, you’ll see quick failures or resets.
4) Fourth check: does it fail only from some LAN clients?
That’s a clue for client-side firewall/VPN split tunneling, wrong subnet profiles (Public vs Private), or a corporate endpoint policy.
Baseline tasks: prove where the packets die
These are the tasks I run in order. Each one has: command, example output, what it means, and what decision you make next.
The goal is to stop arguing with your future self.
Task 1: Find your Windows LAN IP (the real ingress point)
cr0x@server:~$ ipconfig
Windows IP Configuration
Ethernet adapter Ethernet:
IPv4 Address. . . . . . . . . . . : 192.168.1.40
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 192.168.1.1
Meaning: LAN clients must target 192.168.1.40 (or your Wi-Fi IP) to reach anything on this machine.
Decision: Use this IP in your test from another computer. Don’t use the WSL IP; the LAN can’t route to it by default.
Task 2: Find the WSL2 VM IP (the backend target)
cr0x@server:~$ wsl.exe -e bash -lc "ip -4 addr show eth0 | sed -n 's/.*inet \\([0-9.]*\\).*/\\1/p'"
172.24.181.166
Meaning: This is the address Windows must forward to.
Decision: If you implement portproxy, you’ll point it at this IP. Also: expect it to change later.
Task 3: Confirm the service is listening in WSL2, and where
cr0x@server:~$ wsl.exe -e bash -lc "ss -lntp | head -n 20"
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:8000 0.0.0.0:* users:(("python3",pid=2217,fd=3))
LISTEN 0 4096 0.0.0.0:9090 0.0.0.0:* users:(("prometheus",pid=1022,fd=7))
Meaning: Port 8000 is bound to 127.0.0.1 only (loopback). Port 9090 is bound to 0.0.0.0 (all interfaces).
Decision: Anything bound to 127.0.0.1 will not be reachable through a forward to eth0. Fix the service config first.
Task 4: Test from Windows to the WSL IP directly (bypasses portproxy)
cr0x@server:~$ powershell.exe -NoProfile -Command "Test-NetConnection -ComputerName 172.24.181.166 -Port 9090"
ComputerName : 172.24.181.166
RemoteAddress : 172.24.181.166
RemotePort : 9090
InterfaceAlias : vEthernet (WSL)
SourceAddress : 172.24.176.1
TcpTestSucceeded : True
Meaning: Windows can reach the service inside WSL. That’s good.
Decision: Now you only need to solve LAN-to-Windows ingress + forward.
Task 5: If the Windows-to-WSL test fails, check Linux firewall inside WSL
cr0x@server:~$ wsl.exe -e bash -lc "sudo iptables -S | head -n 30"
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
Meaning: No iptables blocking here (policy ACCEPT).
Decision: If your policy is DROP or you have explicit rejects, fix that before touching Windows.
Task 6: Check whether Windows is already listening on the target port
cr0x@server:~$ powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort 9090 -State Listen | Format-Table -AutoSize"
LocalAddress LocalPort RemoteAddress RemotePort State OwningProcess
----------- --------- ------------- ---------- ----- -------------
0.0.0.0 9090 0.0.0.0 0 Listen 1234
Meaning: Something is already listening on Windows on port 9090. Portproxy cannot bind to a port that’s taken.
Decision: Choose a different external port, or stop the conflicting listener.
Task 7: Check existing portproxy rules (because ghosts exist)
cr0x@server:~$ powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov4"
Listen on ipv4: Connect to ipv4:
Address Port Address Port
--------------- ---------- --------------- ----------
0.0.0.0 8080 172.24.181.166 8000
Meaning: Windows listens on 0.0.0.0:8080 and forwards to WSL 172.24.181.166:8000.
Decision: Validate that WSL is actually listening on 8000 on a non-loopback interface, or this forward is lying to you.
Task 8: Confirm Windows is listening after you add portproxy
cr0x@server:~$ powershell.exe -NoProfile -Command "netstat -ano | findstr :8080"
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 4
Meaning: PID 4 is the System process hosting the portproxy listener. That’s normal.
Decision: If you don’t see LISTENING, portproxy isn’t active or the port is blocked/occupied.
Task 9: Test from a different LAN machine (the only test that matters)
cr0x@server:~$ curl -I http://192.168.1.40:8080
HTTP/1.1 200 OK
Server: uvicorn
Date: Tue, 06 Feb 2026 11:20:14 GMT
Content-Type: text/html; charset=utf-8
Meaning: LAN can reach Windows, Windows forwards to WSL, and the app responds.
Decision: Ship it. Then make it persistent, because WSL IPs are fickle.
Task 10: If LAN test times out, check Windows firewall rule state
cr0x@server:~$ powershell.exe -NoProfile -Command "Get-NetFirewallRule -DisplayName 'WSL2 8080 Inbound' -ErrorAction SilentlyContinue | Select-Object DisplayName, Enabled, Profile, Direction, Action"
DisplayName Enabled Profile Direction Action
----------- ------- ------- --------- ------
WSL2 8080 Inbound True Private Inbound Allow
Meaning: Rule exists, enabled, applies to Private profile, allows inbound.
Decision: If your network is set to Public, this rule won’t match. Fix the profile or add Public (carefully).
Task 11: Verify the Windows network profile (Public vs Private)
cr0x@server:~$ powershell.exe -NoProfile -Command "Get-NetConnectionProfile | Format-Table -AutoSize"
Name InterfaceAlias NetworkCategory IPv4Connectivity
---- -------------- --------------- ----------------
OfficeLAN Ethernet Public Internet
Meaning: You’re on a Public profile, so Private-only firewall rules won’t help.
Decision: Either switch the profile to Private (if appropriate) or allow the port on Public for this interface only.
Task 12: Confirm traffic is arriving on Windows (packet capture, quick and honest)
cr0x@server:~$ powershell.exe -NoProfile -Command "pktmon filter remove; pktmon filter add -p 8080; pktmon start --etw -m real-time"
PktMon started with Real Time display.
Meaning: You’re now watching for inbound packets on port 8080.
Decision: If you see nothing during a client test, the problem is upstream (wrong IP, VLAN, client route, or network policy). If you see traffic, focus on firewall/portproxy/service.
Joke #1: Port forwarding is like office politics—everything works until you assume the other side can “just reach it.”
Three ways to expose WSL2 services to the LAN (and when to use each)
Approach A: Windows portproxy (simple, works, TCP-only)
This is the go-to for “I need my WSL2 TCP service reachable from my LAN today.” You bind a listener on Windows, forward to WSL’s IP.
It’s boring. Boring is good.
Use it when:
- You need TCP (HTTP, gRPC, SSH, Prometheus, databases over TCP).
- You can tolerate a small amount of Windows-side configuration.
- You want LAN reachability without changing the WSL2 networking mode.
Approach B: Reverse proxy on Windows (IIS/NGINX/Caddy on the host)
If you want TLS termination, host-based auth, nicer logs, or multiple backends with sane routing, run a proper reverse proxy on Windows
and point it at WSL. This is cleaner than stacking multiple portproxy rules, and it’s easier to reason about when you run more than one service.
Use it when:
- You need HTTPS and don’t want your dev certs living inside WSL only.
- You want path-based routing (
/api,/grafana), or multiple apps on port 443. - You need real logs and rate limiting.
Approach C: Bridging / mirrored networking modes (tempting, but environment-specific)
Depending on your Windows and WSL version, there are newer networking modes that can change how WSL is connected (including “mirrored” behavior).
This can reduce the need for explicit port forwards, but it’s not a universal fix—especially in corporate environments with endpoint controls,
VPN clients, and managed firewall policies.
Use it when:
- You’re standardizing a dev fleet and can validate behavior across identical builds.
- You want fewer moving parts than portproxy + scheduled tasks.
- You understand that “it works on my laptop” is not a deployment strategy.
Method 1 (most common): Windows netsh portproxy + firewall
Step 1: Make sure your service listens on something reachable from WSL’s eth0
If your app binds to 127.0.0.1 inside WSL, it will only accept connections originating inside that same WSL instance.
That’s fine for local dev. It’s useless for LAN exposure.
Example: Uvicorn/FastAPI. Don’t do this:
cr0x@server:~$ wsl.exe -e bash -lc "python3 -m uvicorn app:app --host 127.0.0.1 --port 8000"
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Do this:
cr0x@server:~$ wsl.exe -e bash -lc "python3 -m uvicorn app:app --host 0.0.0.0 --port 8000"
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Decision: Before touching Windows networking, get ss -lntp to show 0.0.0.0:8000 (or the WSL eth0 IP).
That’s the contract you’re going to forward to.
Step 2: Add a portproxy rule (Windows listens, forwards to WSL)
Pick an external listening port on Windows. Often you’ll keep it the same as the internal port.
Here, we’ll forward Windows port 8080 to WSL port 8000.
cr0x@server:~$ powershell.exe -NoProfile -Command "netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8080 connectaddress=172.24.181.166 connectport=8000"
Ok.
Meaning: Windows now accepts TCP connections on all interfaces (including LAN) on port 8080 and forwards to WSL.
Decision: If you only want LAN exposure on one interface/IP, set listenaddress to your LAN IP instead of 0.0.0.0.
Step 3: Verify the rule and listener
cr0x@server:~$ powershell.exe -NoProfile -Command "netsh interface portproxy show v4tov4"
Listen on ipv4: Connect to ipv4:
Address Port Address Port
--------------- ---------- --------------- ----------
0.0.0.0 8080 172.24.181.166 8000
cr0x@server:~$ powershell.exe -NoProfile -Command "netstat -ano | findstr :8080"
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 4
Meaning: The portproxy mapping exists and Windows is listening.
Decision: If netstat shows nothing, you have a conflict (port in use), insufficient privileges, or a policy blocking it.
Step 4: Test from Windows, then from LAN
cr0x@server:~$ powershell.exe -NoProfile -Command "curl.exe -I http://127.0.0.1:8080"
HTTP/1.1 200 OK
Server: uvicorn
Date: Tue, 06 Feb 2026 11:22:40 GMT
Content-Type: text/html; charset=utf-8
Meaning: Windows can hit its own listener and get forwarded into WSL.
Decision: If localhost works but LAN doesn’t, your issue is almost always Windows Firewall profile/rules or upstream network.
Windows Firewall rules that don’t make you hate your life
Windows Firewall is not your enemy. It’s a bouncer. You just need to put your name on the list.
The subtle part is the profile: Domain, Private, Public. Your “OfficeLAN” might be categorized as Public, and then your carefully crafted
Private-only rule does exactly nothing.
Create an inbound allow rule for the listening port
cr0x@server:~$ powershell.exe -NoProfile -Command "New-NetFirewallRule -DisplayName 'WSL2 8080 Inbound' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8080 -Profile Private"
Name : {2d4e3f2b-41c0-4b84-9c85-1c09f3324c7f}
DisplayName : WSL2 8080 Inbound
Enabled : True
Direction : Inbound
Profiles : Private
Meaning: TCP/8080 inbound is allowed when your interface is in the Private profile.
Decision: If you’re on Public (Task 11), either change the network category or add Public. Don’t blindly open ports on Public on a laptop you roam with.
Limit scope: allow only your LAN subnet (recommended)
cr0x@server:~$ powershell.exe -NoProfile -Command "Set-NetFirewallRule -DisplayName 'WSL2 8080 Inbound' -RemoteAddress 192.168.1.0/24"
Meaning: Only clients in 192.168.1.0/24 can connect.
Decision: This is the grown-up move for home labs and office subnets. You’ll thank yourself later.
Confirm the rule applies and is enabled
cr0x@server:~$ powershell.exe -NoProfile -Command "Get-NetFirewallRule -DisplayName 'WSL2 8080 Inbound' | Get-NetFirewallPortFilter | Format-Table -AutoSize"
Protocol LocalPort RemotePort IcmpType LocalAddress RemoteAddress
-------- --------- ---------- -------- ------------ -------------
TCP 8080 Any Any Any 192.168.1.0/24
Meaning: The port filter is correct and scoped.
Decision: If RemoteAddress shows “Any” and you didn’t intend that, tighten it. Security incidents often begin with “It’s just my dev box.”
Make it persistent across WSL restarts
WSL2’s IP changes. Portproxy rules don’t magically follow it. If you hardcode yesterday’s WSL IP, you will be
“mysteriously down” after a reboot, a WSL shutdown, or sometimes after the machine wakes up cranky.
There are two sane options:
- Automate updating portproxy whenever WSL starts (or at user logon).
- Stop using portproxy for dynamic backends and put a reverse proxy on Windows that targets
localhostvia WSL localhost forwarding (where appropriate).
Option 1: Script portproxy update at logon (simple, effective)
This PowerShell one-liner gets the current WSL IP and updates a portproxy mapping.
cr0x@server:~$ powershell.exe -NoProfile -Command "$wslip = (wsl.exe -e bash -lc `"ip -4 addr show eth0 | sed -n 's/.*inet \\([0-9.]*\\).*/\\1/p'`").Trim(); netsh interface portproxy delete v4tov4 listenport=8080 listenaddress=0.0.0.0 2>$null; netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8080 connectaddress=$wslip connectport=8000; netsh interface portproxy show v4tov4"
Listen on ipv4: Connect to ipv4:
Address Port Address Port
--------------- ---------- --------------- ----------
0.0.0.0 8080 172.24.181.166 8000
Meaning: The rule is now aligned with today’s WSL IP.
Decision: Put this in a Scheduled Task “At log on” or “At startup” with highest privileges.
If you don’t run it elevated, portproxy changes can fail silently or half-work.
Option 2: Confirm WSL isn’t being shut down unexpectedly
cr0x@server:~$ wsl.exe --status
Default Distribution: Ubuntu
Default Version: 2
WSL version: 2.1.5.0
Kernel version: 5.15.146.1
Meaning: WSL is installed and active; version details help when behavior differs across machines.
Decision: If your org has mixed versions, standardize or expect inconsistent port/localhost behavior.
Make the service listen correctly (the part everyone forgets)
Most “WSL2 forwarding” failures are not forwarding failures. They’re binding failures.
Your service is listening only on loopback, so it works from within WSL and maybe from Windows via special localhost forwarding,
but it won’t accept connections coming in on eth0.
Diagnose binding: loopback vs all interfaces
cr0x@server:~$ wsl.exe -e bash -lc "ss -lnt | awk 'NR==1 || /:8000|:9090/'"
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:8000 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:9090 0.0.0.0:*
Meaning: 8000 is loopback-only; 9090 is reachable from the WSL VM network.
Decision: For LAN exposure, bind to 0.0.0.0 or the WSL IP, then lock it down with firewall/proxy if needed.
Check what address your app thinks it should bind to
cr0x@server:~$ wsl.exe -e bash -lc "grep -R --line-number 'listen\\|bind\\|host' /etc/nginx 2>/dev/null | head"
Meaning: No nginx config in this example. On real systems, you’ll find listen 127.0.0.1:... surprises.
Decision: If you see loopback binds, decide whether you want portproxy to forward to loopback (not typical) or change the service to listen on eth0.
Joke #2: If you bind to 127.0.0.1 and expect the LAN to reach it, you’re basically mailing a package to yourself and blaming the postal service.
Docker/Desktop/containers inside WSL2: the extra layer cake
Containers don’t remove networking complexity; they concentrate it. If you run Docker inside WSL2 (or Docker Desktop using WSL2),
you may have three networking layers: LAN → Windows → WSL2 VM → container.
The right move depends on where the container port is published.
Task: Check container port publishing inside WSL2
cr0x@server:~$ wsl.exe -e bash -lc "docker ps --format 'table {{.Names}}\t{{.Ports}}' | head"
NAMES PORTS
api-dev 0.0.0.0:8000->8000/tcp
grafana 127.0.0.1:3000->3000/tcp
Meaning: api-dev is published on all interfaces in WSL2; grafana is loopback-only inside WSL2.
Decision: For LAN exposure, publish the container port on 0.0.0.0 in WSL2 (or use a WSL-side reverse proxy), then forward from Windows.
Task: Verify from Windows to WSL IP for a containerized service
cr0x@server:~$ powershell.exe -NoProfile -Command "Test-NetConnection -ComputerName 172.24.181.166 -Port 8000"
ComputerName : 172.24.181.166
RemoteAddress : 172.24.181.166
RemotePort : 8000
InterfaceAlias : vEthernet (WSL)
SourceAddress : 172.24.176.1
TcpTestSucceeded : True
Meaning: The container-published port is reachable from Windows through the vEthernet interface.
Decision: Now portproxy that Windows port to WSL:8000, and handle firewall.
Three corporate-world mini-stories (so you don’t repeat them)
1) The incident caused by a wrong assumption
A team built an internal toolchain: a small API in WSL2, a UI on another developer’s machine, and some automation hitting the API from a CI runner on the LAN.
The developer demoed it: works on their Windows laptop using http://localhost:5000. Everyone nodded. They merged.
When the CI runner tried to call it, timeouts. The assumption was “localhost means reachable,” which is the kind of lie that survives just long enough to ship.
They had confused Windows-host localhost forwarding with actual LAN reachability.
The debugging started in the wrong place. People stared at application logs and blame-shifted to HTTP clients. Meanwhile, packets never made it to Windows at all.
The firewall profile was Public on the laptop because the network was treated as untrusted by policy. Inbound was blocked.
The fix was straightforward: a Windows inbound firewall rule scoped to the CI subnet, plus a portproxy mapping that updated on logon.
The important part wasn’t the commands. It was the change in mental model: WSL2 is a backend network segment, and Windows is the edge.
Afterward they wrote a one-page runbook: “If it only works on localhost, it’s not a service. It’s a local demo.” That saved them from repeating the same mistake later.
2) The optimization that backfired
Another team had multiple WSL2 services and got tired of managing portproxy rules. Someone suggested a “cleaner” approach:
bind everything inside WSL2 to 0.0.0.0, then open a broad inbound firewall rule on Windows for a port range.
Less admin overhead, faster onboarding. It sounded efficient.
Then a security scan flagged the machine as exposing unexpected services on the corporate network.
Some of those ports were dev databases without auth (because “it’s just on my laptop”), and one was a debugging endpoint that happily spilled environment variables.
The team argued it was a false positive. It wasn’t. They had created a “soft target” workstation that behaved like a lightly defended server.
Even if nobody exploited it, the exposure alone was enough to trigger incident response and policy scrutiny.
The rollback was painful but educational: they moved to a Windows reverse proxy on 443 with authentication, and only forwarded specific ports.
They also restricted firewall rules to known subnets. The onboarding got slightly more complex, but the security posture stopped being embarrassing.
Optimization isn’t just about speed. In networking, “fewer rules” often means “bigger blast radius.”
3) The boring but correct practice that saved the day
A platform team had a predictable problem: WSL2 IP changes broke local integrations for a handful of engineers every week.
Instead of heroic debugging sessions, they wrote a dull script and standardized it across machines via endpoint management.
The script did three things: query current WSL IP, rewrite portproxy mappings for a short list of required ports, and validate listeners with netstat.
If validation failed, it logged an error and stopped. No guessing, no partial state.
They also created firewall rules with explicit scopes (office subnets only) and locked them to Private/Domain profiles.
On Public networks (coffee shops), the services simply weren’t reachable. That was a feature, not a bug.
Months later, when a Windows update changed behavior on a subset of machines, their validation step caught it early.
They didn’t discover the break during a demo. They discovered it during a login script run, with a clear failure point.
The most valuable SRE work often looks like paperwork: standardization, idempotent scripts, and checks that fail loudly.
Common mistakes: symptom → root cause → fix
1) Symptom: works in WSL with curl, fails from LAN with timeout
Root cause: Windows Firewall blocks inbound on the listening port (often wrong network profile: Public vs Private).
Fix: Create/enable an inbound allow rule for the port and correct profile, scope to your LAN subnet.
2) Symptom: works from Windows localhost, fails from Windows to WSL_IP
Root cause: Service in WSL binds to 127.0.0.1 only, or Linux firewall blocks non-loopback.
Fix: Bind service to 0.0.0.0 (or WSL eth0 IP). Re-check with ss -lntp.
3) Symptom: LAN sees “connection refused” immediately
Root cause: Windows is reachable, but nothing is listening (portproxy missing, wrong listenport, or port conflict prevented binding).
Fix: Check netstat -ano for LISTENING. Fix port conflicts or recreate portproxy mapping.
4) Symptom: it worked yesterday; today it times out
Root cause: WSL2 IP changed; portproxy still points to the old IP.
Fix: Re-query WSL IP and update portproxy. Automate it via Scheduled Task.
5) Symptom: only some LAN clients can connect
Root cause: Firewall rule scoped too tightly, client on different subnet/VLAN, or client is on VPN with different routing.
Fix: Validate client source IP/subnet. Expand RemoteAddress scope intentionally, or route appropriately.
6) Symptom: HTTPS/TLS errors or wrong Host header behavior
Root cause: Using raw portproxy for multiple HTTP apps without a reverse proxy; backend expects specific Host/SNI.
Fix: Put a reverse proxy on Windows, terminate TLS there, route by host/path, and forward to WSL.
7) Symptom: UDP service never works through portproxy
Root cause: Portproxy is TCP-only.
Fix: Redesign (use TCP), run a different proxy that supports UDP, or use a networking mode that provides direct addressing.
8) Symptom: portproxy exists, firewall open, still nothing
Root cause: You’re listening on 0.0.0.0 but the Windows NIC is on Public profile with inbound restrictions or corporate endpoint rules override local firewall.
Fix: Check network profile, endpoint security policies, and validate with packet capture (pktmon). If policy blocks inbound, stop fighting it and use an approved reverse proxy/ingress solution.
Checklists / step-by-step plan
Checklist A: One service, one port, needs LAN access (TCP)
-
Confirm service binds correctly in WSL2.
Run
ss -lntp. You want0.0.0.0:PORTorWSL_IP:PORT, not127.0.0.1:PORT. -
Get the WSL IP.
Use
wsl.exe -e bash -lc "ip -4 addr show eth0 ...". Write it down; it’s your connectaddress. -
Test Windows to WSL directly.
Test-NetConnection -ComputerName WSL_IP -Port PORT. If this fails, don’t touch the LAN yet. -
Create portproxy mapping.
Bind on Windows
listenaddress=0.0.0.0and forward to WSL. -
Open Windows Firewall for the listening port.
Create an inbound allow rule on Private/Domain; scope RemoteAddress to your LAN subnet.
-
Test from another LAN machine.
Use
curlor a browser. Don’t call it done until a second machine succeeds. -
Make it persistent.
Schedule a script to update portproxy mapping with the current WSL IP at startup/logon.
Checklist B: Multiple services (HTTP) and you want it clean
- Run a reverse proxy on Windows on 443 (single inbound hole).
- Forward to WSL backends on their internal ports.
- Keep WSL ports closed to the LAN; only Windows proxy is exposed.
- Use firewall scopes to limit who can hit the proxy.
- Log requests on the Windows side; debugging becomes survivable.
Checklist C: You keep breaking after reboot
- Assume the WSL IP changed until proven otherwise.
- Update portproxy mapping programmatically.
- Validate:
netsh ... showandnetstat -ano. - If it still fails, run the fast diagnosis playbook again. Don’t “just reboot.”
One quote worth keeping on your monitor
“Everything fails, all the time.” — Werner Vogels
It’s not pessimism. It’s a design constraint. Your port forwarding should be built like it’ll be rebooted, renumbered, and mis-profiled—because it will.
FAQ
1) Why can I reach the service from Windows localhost but not from other LAN machines?
Because Windows-to-WSL localhost forwarding is not LAN routing. LAN machines hit your Windows LAN IP, and Windows must accept inbound traffic
(firewall) and forward it (portproxy or proxy) to WSL.
2) Do I always need netsh portproxy?
No. It’s the simplest for TCP. If you want TLS, routing, and logs, run a reverse proxy on Windows instead.
If you need UDP, portproxy won’t help.
3) Why does portproxy break after a reboot or wsl --shutdown?
The WSL2 VM gets a new IP. Portproxy still points at the old one. Automate updating the mapping with the current WSL IP at startup/logon.
4) Can I forward to 127.0.0.1 inside WSL?
Portproxy forwards to an IP that Windows can route to. Typically you forward to the WSL eth0 IP.
If your service is loopback-only, change it to listen on 0.0.0.0 (and then use firewall/proxy to control exposure).
5) Is it safe to set listenaddress=0.0.0.0?
It’s safe only if your firewall scopes are sane. If you expose a port on all interfaces and allow it from “Any,” you are publishing a service.
Restrict by profile and RemoteAddress, especially on laptops.
6) Does portproxy support UDP?
No. It’s TCP-only. If you need UDP services (some games, DNS, certain telemetry), use a different proxy mechanism or redesign toward TCP.
7) Why do I see LISTENING PID 4 for my forwarded port?
That’s expected. The System process hosts the listener for portproxy rules.
Validate the mapping with netsh interface portproxy show v4tov4 and end-to-end testing.
8) Can I bind the forward to only my LAN IP instead of all interfaces?
Yes. Set listenaddress to your LAN IP (like 192.168.1.40) and keep the firewall rule scoped too.
This reduces accidental exposure on VPN adapters or other interfaces.
9) What’s the quickest way to tell if the LAN request even reaches my Windows machine?
Use pktmon filtered on the listening port while you run a LAN client request. If no packets arrive, stop blaming WSL.
Fix IP targeting, VLAN routing, or client VPN behavior.
Conclusion: next steps that actually work
Make this boring. Boring scales.
- Fix binding in WSL first: ensure your service listens on
0.0.0.0(or WSL eth0 IP), not just loopback. - Prove Windows can reach WSL by IP with
Test-NetConnection. If this fails, don’t involve the LAN. - Use portproxy for simple TCP exposure, then open Windows Firewall narrowly (profile + subnet scope).
- Automate portproxy updates so WSL IP churn doesn’t page you (or ruin your demo) after every reboot.
- When complexity grows (TLS, multiple apps, auth), stop stacking hacks and run a proper Windows reverse proxy.
If you treat your laptop like a tiny edge server—with explicit ingress, explicit policy, and repeatable config—you’ll stop “debugging networking”
and start shipping services.