Skip to content

Instantly share code, notes, and snippets.

@mdeweerd
Last active September 21, 2026 17:00
Show Gist options
  • Select an option

  • Save mdeweerd/21b20b83c69dfbded4a855a77ede59f1 to your computer and use it in GitHub Desktop.

Select an option

Save mdeweerd/21b20b83c69dfbded4a855a77ede59f1 to your computer and use it in GitHub Desktop.
WSL2 Port Forwarding to Windows localhost - Persistent rules for LmStudio, MySQL, etc. Tags: wsl2, port-forwarding, powershell, windows, lmstudio, firewall, netsh, localhost, networking, docker-alternative

AllowLmStudio.ps1 - WSL2 Port Forwarding Setup

Overview

This PowerShell script configures firewall and port forwarding rules to allow WSL2 to access services running on the Windows host's 127.0.0.1 (localhost), such as LmStudio, MySQL, or other services.

Problem: By default, WSL2 cannot access services bound to 127.0.0.1 on the Windows host. This script creates the necessary port forwarding rules to enable this access.

Important: The port forwarding rules created by netsh interface portproxy are not persistent across Windows restarts. This script provides solutions to make them permanent.


Quick Start

One-Time Setup (Non-Persistent)

Run the script as Administrator to configure rules immediately:

# From Windows PowerShell (as Administrator):
Set-ExecutionPolicy Bypass -Scope Process -Force
.\AllowLmStudio.ps1

This will:

  • Detect your WSL2 gateway IP automatically
  • Create port forwarding rules for ports 1234 (LmStudio) and 3306 (MySQL)
  • Add firewall rules to allow traffic from WSL2
  • Restart the IP Helper Service to apply changes

Note: These rules will be lost when you restart Windows.


Making Rules Permanent

To make the rules persist across Windows restarts, use the built-in install:

# From Windows PowerShell (as Administrator):
.\AllowLmStudio.ps1 -Install

This creates a scheduled task named "WSL2 PortProxy Setup" that:

  • Runs at user logon (with 1-minute delay for network readiness)
  • Runs when the WSL service starts (with 30-second delay for WSL VM initialization)
  • Executes with SYSTEM privileges (highest level)
  • Runs silently (with -Silent flag, but still logs to file)
  • Automatically reconfigures all rules

To verify the scheduled task was created:

schtasks /query /tn "WSL2 PortProxy Setup"

Manual Creation (From WSL)

The recommended -Install flag creates a scheduled task with dual triggers (logon + WSL service start) using PowerShell's ScheduledTask module. However, if you're working from WSL, you can create a basic scheduled task directly:

# From WSL (as your user):
schtasks.exe /create /tn "WSL2 PortProxy Setup" \
  /tr "powershell.exe -ExecutionPolicy Bypass -File 'C:\Users\<your-user>\path\to\AllowLmStudio.ps1' -Silent" \
  /sc onstart /ru SYSTEM /rl HIGHEST /f

# Verify it was created:
schtasks.exe /query /tn "WSL2 PortProxy Setup"

Note: This creates a single trigger (Windows startup). The built-in -Install flag provides better reliability with dual triggers.

Note: Adjust the path to match your actual Windows path to the script.

WSL2 Auto-Run (Belt-and-Suspenders)

For additional reliability, configure WSL2 to also run the script when it starts:

# From WSL:
sudo bash -c 'cat > /etc/wsl.conf << "EOF"
[boot]
command = "powershell.exe -ExecutionPolicy Bypass -File /mnt/c/Users/<your-user>/path/to/AllowLmStudio.ps1 -Silent"
EOF'

# Restart WSL2 for changes to take effect:
wsl.exe --shutdown

Note: This only runs when WSL2 starts, not when Windows starts. Use this in combination with the scheduled task for maximum reliability.


Usage

Command Line Parameters

Parameter Description
-Install Create a scheduled task to run this script automatically (logon + WSL service start triggers)
-Uninstall Remove the scheduled task and clean up all rules
-Test Display current configuration without making changes
-Silent Run with minimal output (still logs to file; useful for scheduled tasks)
-Port <ports> Specify custom ports instead of defaults (1234, 3306)

Examples

# Configure rules now with default ports
.\AllowLmStudio.ps1

# Configure rules for custom ports
.\AllowLmStudio.ps1 -Port 1234,8080,5432

# Install as scheduled task (persistent)
.\AllowLmStudio.ps1 -Install

# Check current configuration
.\AllowLmStudio.ps1 -Test

# Remove scheduled task and all rules
.\AllowLmStudio.ps1 -Uninstall

# Run silently (for scheduled tasks)
.\AllowLmStudio.ps1 -Silent

How It Works

1. WSL2 Gateway Detection

The script automatically detects the WSL2 gateway IP address (the Windows-side IP of the virtual network interface). This is typically in the 172.x.x.x range.

The detection now works in two ways:

  • Primary (reliable under SYSTEM account): Reads the WSL virtual adapter directly from Windows (Get-NetAdapter looking for vEthernet (WSL*)
  • Fallback: Queries inside WSL using ip route

This ensures detection works even when running as a scheduled task under the SYSTEM account.

2. Port Forwarding Rules

For each configured port, the script creates a rule that forwards traffic from the WSL2 gateway IP to 127.0.0.1:

WSL2_Gateway_IP:1234 -> 127.0.0.1:1234
WSL2_Gateway_IP:3306 -> 127.0.0.1:3306

3. Firewall Rules

The script adds Windows Firewall rules to allow incoming traffic from the WSL2 subnet to the specified ports.

4. Service Restart

The IP Helper Service (iphlpsvc) is restarted to ensure the port forwarding rules take effect immediately.


Default Ports

  • 1234 - LmStudio default port
  • 3306 - MySQL default port

Customize using the -Port parameter:

.\AllowLmStudio.ps1 -Port 8000,8080,3000

Verification

Check Port Forwarding Rules

netsh interface portproxy show all

Check Firewall Rules

netsh advfirewall firewall show rule name=all | findstr "WSL2"

Test Connectivity from WSL

# For a service on port 1234:
curl http://127.0.0.1:1234

# For MySQL:
mysql -h 127.0.0.1 -P 3306 -u username -p

Uninstallation

# Remove scheduled task and all rules
.\AllowLmStudio.ps1 -Uninstall

This will:

  • Delete the scheduled task (if it exists)
  • Remove all port forwarding rules for default ports
  • Remove all firewall rules for WSL2

Technical Details

WSL2 Network Architecture

WSL2 uses a virtualized network with a lightweight VM. The WSL2 gateway IP is the Windows-side IP address assigned to the virtual adapter (typically 172.x.x.x range) and remains constant across reboots.

Why PortProxy Rules Are Not Persistent

Windows netsh interface portproxy rules are stored in memory by the IP Helper Service (iphlpsvc). They are not written to disk, so they are lost on Windows restart. This is a Windows limitation.

Logging

The script now maintains a log file (AllowLmStudio.log in the same directory) that captures all operations, including:

  • Startup and configuration attempts
  • WSL2 gateway detection attempts and results
  • Port forwarding and firewall rule creation status
  • Error messages with context

The log file is automatically rotated (renamed to .old) when it exceeds ~200 KB to prevent unbounded growth.

Retry Logic

The script includes retry logic to handle cases where WSL2 is still initializing:

  • Windows-side detection: 20 attempts, 3 seconds between each
  • WSL-side fallback: 10 attempts, 3 seconds between each
  • Only the first 3 attempts are logged to avoid noise

Tags

wsl2 port-forwarding powershell windows lmsudio firewall netsh localhost networking docker-alternative


License

This project is licensed under the MIT License.

Copyright (c) 2026 mdeweerd

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Changelog

  • v2.1 - Enhanced scheduled task with dual triggers (logon + WSL service start), added file-based logging with rotation, improved vEthernet adapter detection for WSL, better error handling for portproxy and firewall commands
  • v2.0 - Added persistence support via scheduled task, retry logic, multiple installation options
  • v1.0 - Initial version with basic port forwarding setup
# Script: AllowLmStudio.ps1
# Description: Configures firewall and port forwarding rules to allow WSL2 to access services bound to 127.0.0.1 on the Windows host.
# Purpose: Allows WSL2 to reach services (like LmStudio) listening on localhost by forwarding through the WSL2 gateway IP.
# WARNING: Requires Administrator privileges to run successfully due to network configuration changes (netsh).
#
# Usage:
# .\AllowLmStudio.ps1 - Configure rules now (requires admin)
# .\AllowLmStudio.ps1 -Install - Create scheduled task for auto-startup
# .\AllowLmStudio.ps1 -Uninstall - Remove scheduled task and rules
# .\AllowLmStudio.ps1 -Test - Show current configuration
# .\AllowLmStudio.ps1 -Silent - Run with minimal output
#
# To make rules permanent, run: .\AllowLmStudio.ps1 -Install
#
param(
[switch]$Install,
[switch]$Uninstall,
[switch]$Silent,
[switch]$Test,
[string[]]$Port = @()
)
# Set to $false to suppress output (for scheduled task runs)
$verboseOutput = -not $Silent
function Write-OutputIfNotSilent {
param([string]$Message)
if ($verboseOutput) {
Write-Host $Message
}
}
function Write-ErrorIfNotSilent {
param([string]$Message)
if ($verboseOutput) {
Write-Error $Message
}
}
# Administrator check
function Test-IsAdmin {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-IsAdmin)) {
Write-ErrorIfNotSilent "This script requires Administrator privileges. Please run as Administrator."
exit 1
}
# Scheduled task management
$taskName = "WSL2 PortProxy Setup"
function Install-ScheduledTask {
param(
[string]$ScriptPath
)
# Check if task already exists
$taskExists = schtasks /query /tn $taskName 2>$null
if ($LASTEXITCODE -eq 0) {
if ($verboseOutput) {
Write-Host "Scheduled task '$taskName' already exists."
}
return
}
# Escape backslashes for schtasks
$escapedScriptPath = $ScriptPath -replace '\\', '\\\\'
$psCommand = "powershell.exe -ExecutionPolicy Bypass -File `"$escapedScriptPath`" -Silent"
Write-OutputIfNotSilent "Creating scheduled task '$taskName'..."
schtasks /create /tn $taskName /tr "$psCommand" /sc onstart /ru SYSTEM /rl HIGHEST /f
if ($LASTEXITCODE -ne 0) {
Write-ErrorIfNotSilent "Failed to create scheduled task. Error code: $LASTEXITCODE"
exit 1
}
Write-OutputIfNotSilent "Scheduled task '$taskName' created successfully."
}
function Uninstall-ScheduledTask {
# Delete the scheduled task if it exists
$taskExists = schtasks /query /tn $taskName 2>$null
if ($LASTEXITCODE -eq 0) {
Write-OutputIfNotSilent "Removing scheduled task '$taskName'..."
schtasks /delete /tn $taskName /f 2>$null
Write-OutputIfNotSilent "Scheduled task '$taskName' removed."
} else {
Write-OutputIfNotSilent "Scheduled task '$taskName' not found."
}
}
# WSL2 detection with retry logic
function Get-WslGatewayIP {
param(
[int]$MaxRetries = 20,
[int]$RetryInterval = 3
)
for ($i = 1; $i -le $MaxRetries; $i++) {
try {
$routeOutput = wsl bash -c "ip route | grep default" 2>$null
if ($LASTEXITCODE -eq 0 -and $routeOutput) {
$wslGateway = ($routeOutput -split '\s+') | Where-Object { $_ -ne "" } | Select-Object -Index 2
if ($wslGateway -and ($wslGateway -match '\d+\.\d+\.\d+\.\d+')) {
return $wslGateway
}
}
} catch {
# WSL not ready yet, continue retrying
}
if ($verboseOutput -and $i -le 3) {
Write-Host "Waiting for WSL2 to initialize... (attempt $i/$MaxRetries)"
}
Start-Sleep -Seconds $RetryInterval
}
return $null
}
function Get-WslSubnet {
param(
[int]$MaxRetries = 20,
[int]$RetryInterval = 3
)
for ($i = 1; $i -le $MaxRetries; $i++) {
try {
$addrOutput = wsl bash -c "ip -4 addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+'" 2>$null
if ($LASTEXITCODE -eq 0 -and $addrOutput) {
$subnet = $addrOutput.Trim()
if ($subnet -match '\d+\.\d+\.\d+\.\d+/\d+') {
return $subnet
}
}
} catch {
# WSL not ready yet
}
if ($verboseOutput -and $i -le 3) {
Write-Host "Waiting for WSL2 network... (attempt $i/$MaxRetries)"
}
Start-Sleep -Seconds $RetryInterval
}
return $null
}
# Alternative: Get WSL2 gateway from Windows side (more reliable on startup)
function Get-WslGatewayFromWindows {
param(
[int]$MaxRetries = 20,
[int]$RetryInterval = 3
)
for ($i = 1; $i -le $MaxRetries; $i++) {
try {
# Get the WSL2 virtual NIC adapter
$adapter = Get-NetAdapter -Name "vEthernet (WSL)" -ErrorAction SilentlyContinue
if (-not $adapter -or $adapter.Status -ne "Up") {
# Also try without the space
$adapter = Get-NetAdapter -Name "vEthernet(WSL)" -ErrorAction SilentlyContinue
}
if (-not $adapter -or $adapter.Status -ne "Up") {
# Try Hyper-V firewall variant
$adapter = Get-NetAdapter -Name "vEthernet (WSL (Hyper-V firewall))" -ErrorAction SilentlyContinue
}
if ($adapter -and $adapter.Status -eq "Up") {
$ipConfig = Get-NetIPConfiguration -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue
if ($ipConfig -and $ipConfig.IPv4Address) {
foreach ($ip in $ipConfig.IPv4Address) {
# WSL2 typically uses 172.x.x.x range
if ($ip.IPAddress -match '^172\.\d+\.\d+\.\d+$') {
return $ip.IPAddress
}
}
}
}
} catch {
# Network not ready yet
}
if ($verboseOutput -and $i -le 3) {
Write-Host "Waiting for WSL2 virtual NIC... (attempt $i/$MaxRetries)"
}
Start-Sleep -Seconds $RetryInterval
}
return $null
}
# Verify WSL2 is available
function Test-WslAvailable {
try {
wsl --version 2>$null
return ($LASTEXITCODE -eq 0)
} catch {
return $false
}
}
# ============ MAIN SCRIPT LOGIC ============
# Handle -Install flag
if ($Install) {
$scriptPath = $MyInvocation.MyCommand.Path
Install-ScheduledTask -ScriptPath $scriptPath
if (-not $Silent) {
Write-Host ""
Write-Host "=== Installation Complete ==="
Write-Host "Rules will be applied automatically on next Windows startup."
Write-Host "To apply immediately, run this script without the -Install flag."
Write-Host ""
}
exit 0
}
# Handle -Uninstall flag
if ($Uninstall) {
Uninstall-ScheduledTask
# Clean up portproxy rules
$portsToClean = @("1234", "3306")
if ($Port -and $Port.Count -gt 0) {
$portsToClean = $Port
}
foreach ($port in $portsToClean) {
netsh interface portproxy delete v4tov4 listenport=$port 2>$null
netsh advfirewall firewall delete rule name="Allow $port from WSL2" protocol=TCP localport=$port 2>$null
}
if (-not $Silent) {
Write-Host ""
Write-Host "=== Uninstallation Complete ==="
Write-Host "Scheduled task and rules have been removed."
Write-Host ""
}
exit 0
}
# Handle -Test flag
if ($Test) {
Write-Host "=== Current WSL2 Network Configuration ==="
Write-Host ""
Write-Host "Portproxy Rules:"
netsh interface portproxy show all
if ($LASTEXITCODE -ne 0) {
Write-Host "No portproxy rules found or error querying."
}
Write-Host ""
Write-Host "Firewall Rules (WSL2):"
netsh advfirewall firewall show rule name=all | findstr "WSL2"
if ($LASTEXITCODE -ne 0) {
Write-Host "No WSL2 firewall rules found or error querying."
}
Write-Host ""
# Try to show current WSL2 info
Write-Host "WSL2 Status:"
wsl --version 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "WSL2 is installed."
} else {
Write-Host "WSL2 may not be installed or running."
}
Write-Host ""
exit 0
}
# Check if WSL2 is available
if (-not (Test-WslAvailable)) {
Write-ErrorIfNotSilent "WSL2 is not installed or not available. Cannot configure port forwarding."
exit 1
}
Write-OutputIfNotSilent "=== Starting WSL2 Network Configuration ==="
# Define ports - allow custom ports via -Port parameter
$ports = @("1234", "3306")
if ($Port -and $Port.Count -gt 0) {
$ports = $Port
}
# Reset all portproxy rules to clear any malformed entries
Write-OutputIfNotSilent "Resetting all portproxy rules..."
netsh interface portproxy reset
# Get WSL2 gateway IP with retry logic
Write-OutputIfNotSilent "Retrieving WSL2 gateway IP..."
$wslGateway = Get-WslGatewayIP -MaxRetries 20 -RetryInterval 3
if (-not $wslGateway) {
Write-OutputIfNotSilent "Trying Windows-side detection..."
$wslGateway = Get-WslGatewayFromWindows -MaxRetries 20 -RetryInterval 3
}
if (-not $wslGateway) {
Write-ErrorIfNotSilent "Could not determine the WSL2 default gateway IP address after multiple attempts."
Write-ErrorIfNotSilent "WSL2 may not be fully initialized yet. Try running the script again in a few moments."
exit 1
}
Write-OutputIfNotSilent "WSL2 Gateway IP: $wslGateway"
# Get WSL2 subnet for firewall rules
Write-OutputIfNotSilent "Retrieving WSL2 subnet..."
$wslSubnet = Get-WslSubnet -MaxRetries 20 -RetryInterval 3
if (-not $wslSubnet) {
# Fallback: derive subnet from gateway
Write-OutputIfNotSilent "Using derived subnet from gateway IP..."
$gatewayParts = $wslGateway -split '\.'
$wslSubnet = "$($gatewayParts[0]).$($gatewayParts[1]).0.0/16"
Write-OutputIfNotSilent "Derived WSL2 Subnet: $wslSubnet"
}
Write-OutputIfNotSilent "WSL2 Subnet: $wslSubnet"
Write-OutputIfNotSilent "Configuring rules for ports: $($ports -join ', ')"
# Configure each port
foreach ($port in $ports) {
Write-OutputIfNotSilent "`n--- Processing port $port ---"
# Cleanup existing rules for idempotency
Write-OutputIfNotSilent "Cleaning up existing rules for port $port..."
netsh interface portproxy delete v4tov4 listenport=$port 2>$null
netsh advfirewall firewall delete rule name="Allow $port from WSL2" protocol=TCP localport=$port 2>$null
# Forward from WSL2 gateway IP to localhost
Write-OutputIfNotSilent "Adding portproxy rule: ${wslGateway}:$port -> 127.0.0.1:$port"
netsh interface portproxy add v4tov4 listenport=$port listenaddress=$wslGateway connectport=$port connectaddress=127.0.0.1
# Allow traffic only from WSL2 subnet
Write-OutputIfNotSilent "Adding firewall rule for port $port from subnet $wslSubnet..."
netsh advfirewall firewall add rule name="Allow $port from WSL2" dir=in action=allow protocol=TCP localport=$port remoteip=$wslSubnet
}
# Restart IP Helper Service to apply network changes
Write-OutputIfNotSilent "`nRestarting IP Helper Service (iphlpsvc) to apply network changes..."
net stop iphlpsvc 2>$null
net start iphlpsvc
# Display final configuration if not silent
if ($verboseOutput) {
Write-Host "`n=== Configuration Summary ==="
Write-Host "WSL2 Gateway: $wslGateway"
Write-Host "WSL2 Subnet: $wslSubnet"
Write-Host "`nPortproxy Rules:"
foreach ($port in $ports) {
netsh interface portproxy show all | findstr "$port"
}
Write-Host "`nFirewall Rules:"
foreach ($port in $ports) {
netsh advfirewall firewall show rule name=all | findstr "$port"
}
Write-Host "`n=== Configuration Complete ==="
Write-Host ""
Write-Host "To make these rules persistent across restarts, run:"
Write-Host " .\AllowLmStudio.ps1 -Install"
}
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment