'Connecting to WSL2 server via local network

I'm developing a rails app using WSL2/Ubuntu on my Windows 10 machine, which is great! The problem is I can't connect to my server from another computer in the same network.

For further clarity, I am running a Puma server on localhost:3000

I have tried the following:

  1. Directly connecting to the IP address assigned to Ethernet adapter vEthernet (WSL) -> 172.26.208.1:3000
  2. Directly connecting to the host machine's IPv4 address -> 192.168.0.115
  3. Adding a firewall exception (using Bitdefender)
  4. Binding the IPs above rails s -b 172.26.208.1 -p 3000

None of the above have worked thus far... What I'd like to do is:

  • Test the website on another laptop/tablet/phone
  • Use VScode from another computer

Is there anything I'm missing to at least see the website correctly? (and any comments on the VScode part would be appreciated)



Solution 1:[1]

See this video, it helped me:

https://www.youtube.com/watch?v=yCK3easuYm4

netsh interface portproxy add v4tov4 listenport=<port-to-listen> listenaddress=0.0.0.0 connectport=<port-to-forward> connectaddress=<forward-to-this-IP-address>

for example

netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=172.30.16.3

Microsoft has published a little bit of information about this on their WSL1 to WSL2 comparison page

Solution 2:[2]

Option 1: Use port forwarding

This one worked for me:

  1. Run the port forwarding by a script from xmeng1: https://gist.github.com/xmeng1/aae4b223e9ccc089911ee764928f5486

The firewall commands in that script didn't work on my system. So I deactivated the Windows firewall completely and use the following stripped version. (The final users will use a 3d party firewall anyway, so that's ok).

$remoteport = bash.exe -c "ifconfig eth0 | grep 'inet '"
$found = $remoteport -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';

if ($found)
{
  $remoteport = $matches[0];
} else {
  echo "The Script Exited, the ip address of WSL 2 cannot be found";
  exit;
}

#[Ports]

#All the ports you want to forward separated by coma
$ports=@(123,456);

#[Static ip]
#You can change the addr to your ip config to listen to a specific address
$addr='0.0.0.0';
$ports_a = $ports -join ",";

for ($i = 0; $i -lt $ports.length; $i++)
{
  $port = $ports[$i];
  iex "netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$addr";
  iex "netsh interface portproxy add v4tov4 listenport=$port listenaddress=$addr connectport=$port connectaddress=$remoteport";
}

#Then so something, e.g.
#bash.exe -c "cd /g-inverter; ./start_docker.sh"

You may need to "apt install net-tools" for ifconfig in the script. There's also a solution with "ip addr" in the internet somewhere that does not need ifconfig" in a great thread, I haven't a link for here and now.

Caveat

This works only for TCP traffic. netsh interface portproxy does not support port forwaarding of UDP traffic.

Option 2: Bridge mode

Solution: Switch from NAT to Bridge mode

WSL2 comes by default in NAT mode. There the wsl2 system has another ip in another subnet than the host. The PC is from external peers only visible by the windows IP and the wsl2 ip/net is hidden/internal. So all traffic would need to be accepted by the windows IP and then forwarded to the wsl2 ip (port forwarding).

There is another mode called bridge mode. In bridge mode your network interface card will be shared to the wsl2 system, and it will get its own IP/Net in wsl2. So in effect your network card is shared to both systems (windows / wsl2) and will have two IPs, as if you'd have two systems with its own network card each. Cool thing: You will never have port conflicts when Windows uses the same port as well, as your wsl2 app (like 111).

Enable bridge mode

Open Hyper-V Manager as administrator

Select your pc, open Virtual Switch Manager

Select WSL

Set to external network

Select the network card the traffic runs through

Then login to wsl2 terminal and configure an IP address. E.g.

sudo ip addr add 192.168.0.116/24 dev eth0

You need to use another free IP (not your Windows IP). If your network has a DHCP server your wsl can get one by:

sudo ip addr flush dev eth0
sudo dhclient eth0

Caveat

I haven't elaborated yet, how to get DNS working in this scenario in case you want to still be able to access the internet (apt etc.). There's some documentation from MS written in /etc/resolv.conf and maybe executing what's written there and installing resolvconf (prior to all steps above, as you have no internet once you start to bridge) might do the trick.

Solution 3:[3]

First, you will need to open a port in your machine to be able to access it from your network.

netsh advfirewall firewall add rule name="Allowing LAN connections" dir=in action=allow protocol=TCP localport=5000

After you open the port (5000 in my case) you will need to make port forwarding from this port to the port that your app is listening on in the WSL.

netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=5000 connectaddress=localhost connectport=<the port that your app is listening on>

Notice: I set the connectaddress to localhost not to the IP address of the WSL because by default the requests that go to localhost are forwarded to the WSL. By doing this you won't need to set the port forwarding every time you restart your machine because the IP address of the WSL is dynamic.

Solution 4:[4]

WSL2 exposes ports on the local interface (which is why in Windows you can access localhost:8080 when your 8080 service is running in WSL2), but they listen on 127.0.0.1 (which is why you can't access yourhostname:8080 on other computers your LAN).

There's a tool to fix this called WSLHostPatcher, which you can find here (download via the Releases section): https://github.com/CzBiX/WSLHostPatcher

WSLHostPatcher changes the behaviour to listen on all IPs, exposing any WSL2 services to all computers on your network.

  1. Download the latest release from https://github.com/CzBiX/WSLHostPatcher/releases (I tested v0.1.0)
  2. Unzip the file
  3. Run the WSLHostPatcher.exe
  4. (Re)start your service in WSL2

The service should now be accessible via other computers on your network.

Solution 5:[5]

Getting your WSL2 distro to behave like any other client on your network

Building on Roelofs suggestion no.2, here's what made everything tick in my case. I'm too fresh to just leave a comment unfortunately.

My starting point:
Win 10 Pro
Ubuntu under WSL2
(Docker with Linux containers)

My goal:
Getting an rtmp stream from a client on the network into and back out of an nginx server running on the Ubuntu machine.

Building the bridge

In my case, I could not get Hyper-V to set the bridge up properly. After selecting External network for the WSL switch in the Virtual switch section of Hyper-V Manager and hitting apply, it eventually failed with error 0x80070490. Don't know why and didn't have the time to investigate. WSL was not running and neither was the Docker service.

Instead, I just left the setting on Internal network and bridged the interfaces the manual way, under Network Connections (run->ncpa.cpl). In my case, the WiFi connection and vEthernet (WSL). Immediately after doing this, I lost internet connectivity and it took me an embarrassingly long time to find out that a reboot was needed. (Windows for once did not ask me to!)

Getting the VM in shape

After the reboot, I now had internet access from the host, the bridge was set to DHCP and had inherited the IP of the WiFi interface (192.168.1.246). Great.

The VM however was still getting the IP of the virtual switch (or however you want to view it, the random 172.x.x.x address that windows seems to assign to the switch as well as the VM).

Firing up Ubuntu, I decided to do a:

sudo ip addr flush dev eth0

Continuing with:

sudo dhclient eth0

threw a handful of errors at me since I was using the vanilla Ubuntu distro from Windows store, no systemd, no fun. Despite that, it did manage to add the IP of the bridge to eth0. As this was not very handy, I got rid of that with:

sudo ip addr del 192.168.1.248/24 dev eth0

but not before taking a sneak peek at the routing table:

user@vm:~$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         192.168.1.1     0.0.0.0         UG    0      0        0 eth0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0

After deleting the old IP, I added a unique one from outside of my DHCP range:

sudo ip addr add 192.168.1.50/24 dev eth0

I checked the routing table again and the first entry was gone. At this stage I could ping LAN but not WAN.

Added the entry back with:

sudo ip route add default via 192.168.1.1 dev eth0

Pinging WAN IPs was now possible, but no DNS resolution. LMGTFM: Adding permanent DNS In case the solution goes missing, here it is, credit to non-static:

Create a file: /etc/wsl.conf.
Put the following lines in the file

[network]
generateResolvConf = false

In a cmd window, run wsl --shutdown
Restart WSL2
Create a file: /etc/resolv.conf. If it exists, replace existing one with this new file.
Put the following lines in the file

nameserver 8.8.8.8

Or the IP of whatever DNS server you want to use, repeat step 3 and 4.

So, to conclude, check your routing and setup your DNS-conf properly. Unfortunately, the IP settings are reverted every time you restart WSL. If you are not ok with doing this manually every time there are discussions on how to automate it here and here. I haven't had the time to find my favorite.

Best regards Alexander

Solution 6:[6]

Taking into consideration the above (correct) solutions this is a simplified one liner version that works for me:

  1. Use "ifconfig" to find your wsl2 public IP address. Have in mind that The Hyper-V Switch IP change everytime when the Windows reboot, so the Public IP in wsl2 also changes. enter image description here
  1. Run the following command using the IP found in the previous step in connectaddress parameter:

    netsh interface portproxy add v4tov4 listenport= listenaddress=0.0.0.0 connectport= connectaddress=172.24.26.277

Parameters explanation:

  • listenport: the port that Windows will listen

  • listenaddress: the address that your Windows will listen. Usually 0.0.0.0 should do.

  • connectport: the port that your Linux Distro through wsl2 will listen.

  • connectaddress: the public IP of your Linux wsl2 instance (found in step 1)

Use cmd or PowerShell as Administrator to run the above command.

Solution 7:[7]

  1. install the ssh server on WSL

  2. Find some ssh client on windows

  3. Execute on windows: (Windows's IP with 192.168.x.x and WSL ip with 172.28.x.x)

    ssh -L 192.168.x.x:3000:172.28.x.x:3000 [email protected]

if it doesn't work, try to using another local port such as (192.168.x.x:3001:172.28.x.x:3000)

Solution 8:[8]

Here's the full three steps I found worked for LAN development access to something running in WSL2 (Ubuntu 20 on Windows 10 20H2):

1. One time at the start: open Expo ports inbound in Windows Firewall

Windows firewall should be on — don't turn it off! —, and it should block inbound attempts by default.

The following will open the ports 19000–19001, inbound, but only on a network that you have configured as "private" (that's the -Profile Private part, replace with Domain if you're on a domain):

  • Replace the 19000-19001 with the port(s) you need.
  • Replace the DisplayName with one that works for you. It's handy for finding the rule later.

(powershell as Admin)

New-NetFireWallRule -Profile Private -DisplayName 'Expo ports for LAN development' `
    -Direction Inbound -LocalPort 19000-19001 -Action Allow -Protocol TCP

(You can check it after with Get-NetFirewallRule |Where-Object {$_.DisplayName -Match "Expo.*"} — replace the -Match arg with the DisplayName you chose)

2. Point portproxy to WSL; Re-run "Each time WSL has a new IP address"

(I'm not sure yet how often the WSL IP address changes, but I suspect only a reboot would)

I saw stuff on the web, including other answers here, saying portproxy to connectaddress=127.0.0.1 but it did not work for me (WSL2, Windows 10 20H2).
I can't say why others found it worked, I can only say that repeated testing confirmed for me that 127.0.0.1 did not work, but the WSL IP address did work.

So here's a reusable command to auto set the connectaddress to the right WSL address:
(powershell — just for the easy inline Trim() — as Admin)

netsh interface portproxy add v4tov4 listenport=19000 listenaddress=0.0.0.0 `
    connectport=19000 connectaddress=$($(wsl hostname -I).Trim());

netsh interface portproxy add v4tov4 listenport=19001 listenaddress=0.0.0.0 `
    connectport=19001 connectaddress=$($(wsl hostname -I).Trim());

3. (Optional) If your WSL needs to know your dev machine LAN IP Address

(e.g. QR-Code mobile apps like Expo)

You'll need to re-run the following inside WSL "Each time dev host has a new IP address"

  • This is the one that probably changes most often. Your laptop local network IP certainly changes when you change networks (e.g. home/office) — and can change at other times too.

Fortunately it's also pastable / aliasable:
WSL2 shell

netsh.exe interface ip show address "Wi-Fi" | grep 'IP Address' | sed -r 's/^.*IP Address:\W*//'

# e.g. for Expo, I used:
export REACT_NATIVE_PACKAGER_HOSTNAME=$(netsh.exe interface ip show address "Wi-Fi" | grep 'IP Address' | sed -r 's/^.*IP Address:\W*//')

echo Meteor will use dev machine IP address: $REACT_NATIVE_PACKAGER_HOSTNAME

I "wish I didn't have to re-run things and it could all be automated",
but that same laziness makes me happy to at least have command 2 (and 3) easy to "rerun" and consistently get the LAN access I need to my WSL2-hosted service.

Solution 9:[9]

Another solution is to bridge WSL2 directly to the relevant network adapter. I tried doing it within Hyper-V, but couldn't get it to work. However, what did work was going to Control Panel\Network and Internet\Network Connections, selecting the NIC (Ethernet 3 in my case) and vEthernet (WSL) and bridging them by right clicking and selecting "Bridge Connections":

Bridge Connections Menu Item

You should end up with something like this:

Bridged Connections

After the bridge is set up let Windows do its thing for a minute or so (seriously!) before continuing.

Next open WSL and have it run dhcp to get a new address:

sudo ip addr flush dev eth0 && sudo dhclient eth0

At this point, both WSL and Windows should have connectivity. Check by pinging some IP address like 1.1.1.1 or 8.8.8.8. If that doesn't work, dismantle the bridge and try again. Remember to wait for a minute or two while Windows configures everything.

Additionally, make sure that eth0 has an address from your LAN (ip addr sh eth0).

The problem now is that WSL probably can't resolve domain names since WSL creates /etc/resolv.conf on startup which points to the virtual gateway which is no longer there. This can be solved by asking wsl not to create /etc/resolv.conf by appending the following to /etc/wsl.conf (create the file if it doesn't exist):

[network]
generateResolvConf=false

Then restart wsl by restarting the LxssManager windows services. The only thing that remains is setting up alternative DNS servers within WSL. Instead of the generated /etc/resolv.conf, just create a /etc/resolv.conf with something like:

nameserver 1.1.1.1
nameserver 1.0.0.1

That's it! WSL should now be bridged to your network and have its own unique address on it.

Solution 10:[10]

Regarding bridge mode - Windows would prevent of modifying WSL virtual switch (access denied error) when any Linux distribution is running. From the other hand WSL switch is created when first Linux distro is started. I did as follows:

  1. Started wsl
  2. Left wsl shell and check that all distros are stopped ( wsl -l -v)
  3. Modified WSL virtual switch
  4. Started wsl again

Solution 11:[11]

Finally I've got a solution that has to be run just once after the Windows host boot, works with multiple WSL2 instances at the same time, and does not have to be re-run when a WSL2 instance restarts!

Drawback: cannot listen on 0.0.0.0; you have to specify IPs or adapters for listening explicitly. (This version listens only on one IP address, but can be easily extended to use a list).

$ports = @(8000,8001);     # ports to forward
$iface = "Ethernet";       # alias of the Adapter to listen on

# if your host has a fixed IP, you can specify it here explicitly
# and remove the ping-and-if safeguard;
# but 0.0.0.0 won't work!
$external_ip = (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias $iface).IPv4Address;

ping -n 1 "$external_ip" > $null

if( !$?  ) {
    echo "error: cannot determine the external IP address";
    exit 1;
}

$firewall_rule_name = "WSL2 Firewall Hole"
Remove-NetFireWallRule -DisplayName "$firewall_rule_name"

New-NetFireWallRule -DisplayName "$firewall_rule_name" -Direction Outbound -LocalPort $ports -Action Allow -Protocol TCP
New-NetFireWallRule -DisplayName "$firewall_rule_name" -Direction Inbound -LocalPort $ports -Action Allow -Protocol TCP

ForEach( $port in $ports ) {
    netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$external_ip
    netsh interface portproxy add v4tov4 listenport=$port listenaddress=$external_ip connectport=$port connectaddress=127.0.0.1
}

Credits:

  • The starting point was this script by edwindijas on WSL Github issue #4150.
  • The idea of using localhost instead of obtaining the IP address of a running WSL2 instance was gleaned from this answer by Charl Botha here.

The original edwindijas's script didn't work for me because I run WSL2 under a limited user, the script has to run as admin, and running bash or wsl -e as admin gets a new WSL2 instance with a different IP address.

Solution 12:[12]

WSL2-PortForward.cmd

@ECHO OFF
REM ** Set WSL Distro, Distro Port, Host Port
SET LXDISTRO=WinKDE
SET WSL2PORT=3389
SET HOSTPORT=13389
REM ** Reset any existing port proxies and delete any stale firewall rule
NETSH INTERFACE PORTPROXY RESET & NETSH AdvFirewall Firewall delete rule name="%LXDISTRO% Port Forward" > NUL
REM ** Get IP from WSL2 instance and write it out to IP.tmp
WSL -d %LXDISTRO% -- ip addr show eth0 ^| grep -oP '(?^<=inet\s)\d+(\.\d+){3}' > IP.TMP
REM ** Read IP.tmp back into batch variable
SET /p IP=<IP.TMP
REM ** Create the port proxy
NETSH INTERFACE PORTPROXY ADD v4tov4 listenport=%HOSTPORT% listenaddress=0.0.0.0 connectport=%WSL2PORT% connectaddress=%IP% 
REM ** Create a new firewall rule for the port listening on the LAN
NETSH AdvFirewall Firewall add rule name="%LXDISTRO% Port Forward" dir=in action=allow protocol=TCP localport=%HOSTPORT% > NUL
REM ** Summary
ECHO WSL2 Virtual Machine %IP%:%WSL2PORT%now accepting traffic on %COMPUTERNAME%:%HOSTPORT%

Output::

WSL2 Virtual Machine 172.17.109.95:3389 now accepting traffic on MYCOMPUTER:13389

Solution 13:[13]

Here's a solution that is perhaps simpler than many of the others that have been offered. You need a WSL1 instance with socat installed in addition to the WSL2 instance that you're using.

On the WSL1 instance, start socat like this:

socat -d TCP-LISTEN:3000,reuseaddr,fork TCP:localhost:3000

When you start socat like this, Windows will ask if you want to allow network access to the port. Confirm.

Make sure your puma binds to port 3000 on all interfaces.

You will now be able to access your dev server from the LAN. What's happening here is that socat is forwarding requests to 3000 on your ethernet port to port 3000 on localhost, which will magically get forwarded by Windows to WSL2.

(I'm currently using this exact setup to access my Django dev server on WSL2 from an iOS app on my LAN.)

Solution 14:[14]

Unfortunately, for me, this is a simplified case because my Home network is defined by my Router. Since my home devices don't communicate using the back-to-back connection and my network is protected by the Router's firewall I have to respect the Router's configuration.

  1. There is a list of DNS's - 8.8.8.8, 8.8.4.4, 9.9.9.9 which back my ISP DNS. It means that resolv.conf has to be crafted. nslookup reflects the correct configuration.
  2. It is possible to configure DHCP relay to external DHCP
  3. I can define and config how devices obtain dynamic IP including the static lease. It assumes that the device uses the correct DHCP IP address.
  4. I can configure port forwarding for known to the router devices, of course.

Solution 15:[15]

So, I struggled with this for a while the last two days, nothing seemed to work, in this thread or elsewhere. I finally got it worked out though by piecing together a few things I've seen, and I figured I'd post my solution here in case it helps anyone. A few caveats though. First, I'm definitely no Linux guru, so "it works" is just about my real only consideration :) Second, this is on a home dev server, which means that security isn't my biggest concern (the server isn't exposed outside my LAN in any way), so I did some things that I wouldn't do on a real, important machine. Finally, I didn't need this to work all the time, meaning I'm okay with having to do something myself after a server reboot to get it all working.

With all that in mind, the first step was, as malt above describes, bridging the WSL adapter with the server's physical adapter. In my case, the server has a static IP, so I configured the Network Bridge adapter that results with a static IP, gateway and DNS servers for IPv4.

After that, I wrote the following script named startup_tasks.sh in my home directory:

#!/bin/bash

clear
echo Running startup tasks...

# Clear existing IP config
sudo ip addr flush eth0
echo .
sleep 2
# Add static IP
sudo ip addr add 192.168.1.100/24 dev eth0
echo .
sleep 2
# Remove existing default gateway
sudo ip route del default
echo .
sleep 2
# Add default gateway
sudo ip route add default via 192.168.1.1
echo .
sleep 2
# Remove existing name resolution config file
sudo rm /etc/resolv.conf
echo .
sleep 2
# Create name resolution config file
sudo touch /etc/resolv.conf
echo .
sleep 2
# Update permissions on file so we can edit it
sudo chmod 777 /etc/resolv.conf
echo .
sleep 2
# Add name servers
sudo echo "nameserver 8.8.8.8" >> /etc/resolv.conf
echo .
sleep 2
sudo echo "nameserver 1.1.1.1" >> /etc/resolv.conf
echo .
sleep 2

echo - Static IP set
echo .
echo ...done!

For this to work, I also had to ensure my user account has sudo rights without a password, so this was added to the sudoer's file:

devuser=(ALL) NOPASSWD:ALL

I simply run that script after a server reboot and the result is WSL gets a static IP address and all the servers I run (all in Docker containers, though I removed all that from the script above) are accessible.

For me, it's that easy.

I've also considered writing a simple .vbs file and dropping it in my startup folder to run that script after a reboot automatically. That should work fine and make it all completely automatic, but I just haven't done it yet.