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
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
eth0interface 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
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"
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
Duplicate Address Detection kicked in. Why? Because:
- WSL VM’s eth0 has 192.168.33.10
- Windows adapter also has 192.168.33.10
- Same L2 network segment
- Two different MAC addresses claiming the same IP
- 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
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
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
Ubuntu netplan config:
network:
version: 2
ethernets:
eth0:
dhcp4: true
addresses:
- 192.168.33.10/24
- 1.2.3.4/24
Debian systemd-networkd config:
[Match]
Name=eth0
[Network]
DHCP=yes
Address=192.168.33.10/24
Address=1.2.3.4/24
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!
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
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
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
From vm2:
vagrant ssh vm2
curl 192.168.50.10 # Works! Returns directory listing
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
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
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)