DEV Community

Zoltan Toma
Zoltan Toma

Posted on • Originally published at zoltantoma.com on

Vagrant WSL2 Networking: A Reality Check

Starting with High Hopes

After adding Docker support, snapshots, and data disk management to the Vagrant WSL2 Provider, networking felt like the obvious next step. The goal was simple: make config.vm.network work just like it does in VirtualBox.

config.vm.network "private_network", ip: "192.168.33.10"
config.vm.network "forwarded_port", guest: 80, host: 8080

Enter fullscreen mode Exit fullscreen mode

How hard could it be?

Claude: Narrator: It was harder than expected.

The VirtualBox Mental Model

When you use VirtualBox, each VM gets its own network interfaces. Private networks create host-only adapters, each VM has its own IP, and everything just works. I assumed WSL2 would be similar - after all, it’s running on Hyper-V, right?

Wrong.

Reality: WSL2’s Architecture

Here’s what I learned (painfully) about WSL2 networking:

All WSL2 distributions run on a SINGLE Hyper-V VM.

Not separate VMs - one VM running multiple distributions in separate namespaces. This means:

  • Every distribution shares the same base IP address (something like 172.26.143.58)
  • Each distribution has its own network namespace
  • They all use the same eth0 interface in the WSL utility VM

When I created two test VMs:

config.vm.define "vm1" do |vm1|
  vm1.vm.network "private_network", ip: "192.168.50.10"
end

config.vm.define "vm2" do |vm2|
  vm2.vm.network "private_network", ip: "192.168.50.11"
end

Enter fullscreen mode Exit fullscreen mode

Both got configured with their static IPs. Both showed up in ip addr. But when I checked from Windows - both had the same WSL base IP: 172.26.143.58.

The IP Alias Disaster

My first approach: “I’ll just add the static IP as an alias to the Windows WSL network adapter!”

# Add IP to Windows WSL adapter
cmd = "New-NetIPAddress -InterfaceAlias 'vEthernet (WSL)' " +
      "-IPAddress #{ip_address} -PrefixLength 24"

Enter fullscreen mode Exit fullscreen mode

This kind of worked. The IP showed up on Windows. But then:

PS> Get-NetIPAddress -InterfaceAlias "vEthernet (WSL)"

IPAddress : 192.168.33.10
AddressState : Duplicate # OH NO

Enter fullscreen mode Exit fullscreen mode

Duplicate Address Detection kicked in. Why? Because:

  1. WSL VM’s eth0 has 192.168.33.10
  2. Windows adapter also has 192.168.33.10
  3. Same L2 network segment
  4. Two different MAC addresses claiming the same IP
  5. Windows: “Nope, that’s a duplicate, shutting it down”

The IP went into “Duplicate” state and stopped working.

Windows Routing to the Rescue

Instead of trying to put the same IP in two places, use routing:

def add_windows_route(static_ip, wsl_ip)
  cmd = "route add #{static_ip} mask 255.255.255.255 #{wsl_ip}"
  result = Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
end

Enter fullscreen mode Exit fullscreen mode

This tells Windows: “When you see traffic for 192.168.33.10, send it to 172.26.143.58 (the actual WSL IP).”

No duplicate IPs, no conflicts, just routing.

Admin Privileges Required

Of course, route add requires administrator privileges. So I added a check:

def has_admin_privileges?
  cmd = "([Security.Principal.WindowsPrincipal]" +
        "[Security.Principal.WindowsIdentity]::GetCurrent())" +
        ".IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"

  result = Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
  result.exit_code == 0 && result.stdout.strip.downcase == "true"
end

Enter fullscreen mode Exit fullscreen mode

If you’re not running as admin, you get a clear warning and network configuration is skipped. The VM still starts, you just don’t get the fancy networking features.

Persistence Problem

Network configuration in Linux needs to survive reboots. The ip addr add command is immediate but temporary. I needed to write actual config files.

Plot twist: Different distributions use different network managers.

  • Ubuntu : Uses netplan
  • Debian : Uses systemd-networkd (no netplan)

Solution: Detect which one is available and write the appropriate config:

def write_netplan_config
  has_netplan = @machine.communicate.test("command -v netplan")

  if has_netplan
    write_ubuntu_netplan_config
  else
    write_systemd_networkd_config
  end
end

Enter fullscreen mode Exit fullscreen mode

Ubuntu netplan config:

network:
  version: 2
  ethernets:
    eth0:
      dhcp4: true
      addresses:
        - 192.168.33.10/24
        - 1.2.3.4/24

Enter fullscreen mode Exit fullscreen mode

Debian systemd-networkd config:

[Match]
Name=eth0

[Network]
DHCP=yes
Address=192.168.33.10/24
Address=1.2.3.4/24

Enter fullscreen mode Exit fullscreen mode

The Multi-VM Reality

Here’s the part that took me longest to accept: Windows host cannot distinguish between multiple WSL2 VMs using only their static IPs.

Why? Because they all share the same underlying WSL IP. When you add routes:

route add 192.168.50.10 -> 172.26.143.58 # vm1
route add 192.168.50.11 -> 172.26.143.58 # vm2 - same target!

Enter fullscreen mode Exit fullscreen mode

Both routes point to the same WSL IP. Windows can’t tell them apart.

What DOES work:

  • VM-to-VM communication via static IPs (they’re in separate namespaces)
  • Process isolation (completely separate PID spaces)
  • Different services listening on the same ports in different VMs

What DOESN’T work:

  • Windows host accessing VMs via their static IPs
  • Each VM having a truly independent IP from Windows perspective

The solution: Use port forwarding for Windows host access:

config.vm.define "web" do |web|
  web.vm.network "forwarded_port", guest: 80, host: 8080
end

config.vm.define "api" do |api|
  api.vm.network "forwarded_port", guest: 80, host: 8081
end

Enter fullscreen mode Exit fullscreen mode

Different ports on localhost, clear separation, works perfectly.

Port Forwarding Implementation

Port forwarding uses Windows netsh portproxy:

def setup_port_forward(listen_address, listen_port, connect_address, connect_port)
  listen_addresses = listen_address == "0.0.0.0" ? ["127.0.0.1"] : [listen_address]

  listen_addresses.each do |addr|
    cmd = "netsh interface portproxy add v4tov4 " +
          "listenaddress=#{addr} listenport=#{listen_port} " +
          "connectaddress=#{connect_address} connectport=#{connect_port}"

    Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
  end
end

Enter fullscreen mode Exit fullscreen mode

This creates a proper TCP proxy on Windows that forwards traffic to the WSL VM.

Testing the Reality

I created a multi-VM example to document the behavior:

From vm1:

vagrant ssh vm1
sudo python3 -m http.server 80 --bind 192.168.50.10

Enter fullscreen mode Exit fullscreen mode

From vm2:

vagrant ssh vm2
curl 192.168.50.10 # Works! Returns directory listing

Enter fullscreen mode Exit fullscreen mode

VM-to-VM communication via static IPs works perfectly. Each VM is isolated, has its own processes, its own network namespace.

From Windows:

curl 192.168.50.10 # Timeout - can't distinguish which VM

Enter fullscreen mode Exit fullscreen mode

Doesn’t work. The route points to the shared WSL IP, and Windows can’t tell which VM should handle the request.

Documentation Over Pretending

Instead of trying to make WSL2 behave like VirtualBox (impossible), I documented the reality in the README:

WSL2 has a unique networking architecture that differs from traditional hypervisors. All distributions run on a single Hyper-V VM with shared networking. Static IPs work for inter-VM communication but not for Windows host access. Use port forwarding for host-to-VM connectivity.

No marketing speak. No “it’s a feature not a bug.” Just: here’s how it works, here’s what you can do with it, here are the limitations.

What’s Next

The networking implementation is done and working within WSL2’s constraints:

  • ✅ Static IP configuration (Ubuntu netplan + Debian systemd-networkd)
  • ✅ Port forwarding via netsh portproxy
  • ✅ Admin privilege checking
  • ✅ Multi-VM inter-VM communication
  • ✅ Clear documentation of limitations

Next up: Cleaning up error handling, writing integration tests, and preparing for v0.4.0 release.

Try It Yourself

The networking support is in the feature/networking-support branch:

# Clone the repo
git clone https://github.com/LeeShan87/vagrant-wsl2-provider
cd vagrant-wsl2-provider
git checkout feature/networking-support

# Install locally (requires admin)
rake install_local

# Try the networking example
cd examples/networking
vagrant up # Requires administrator privileges

Enter fullscreen mode Exit fullscreen mode

Check out examples/multi-vm-network/README.md for the full explanation of WSL2 networking behavior and limitations.


Lesson learned: Sometimes you start building a feature and discover the platform doesn’t work the way you thought. Instead of fighting reality, document it honestly and work within the constraints. The result might not match your original vision, but it’s better than pretending limitations don’t exist.

Top comments (0)