WSL2 NAT 端口转发管理工具

WSL2 NAT 端口转发管理工具 使用说明

1. 工具简介

本工具是一个 PowerShell 脚本,用于在 WSL2 NAT 网络模式 下管理 Windows 到 WSL2 的端口转发规则。它可以帮助您:

  • 查看当前已配置的 TCP 端口转发规则和 UDP 防火墙规则
  • 添加新的 TCP 端口转发(自动配置 Windows 防火墙)
  • 添加 UDP 端口防火墙规则(提示 netsh 不支持 UDP 转发)
  • 删除不再需要的 TCP/UDP 规则(基于序号操作)

注意:由于 Windows 的 netsh interface portproxy 命令不支持 UDP 转发,本工具对于 UDP 协议仅能添加/删除 Windows 防火墙入站规则,实际的 UDP 端口转发需要采用其他方案(如 WSL2 镜像网络模式或在 WSL2 内部使用 socat)。

2. 使用前提

  • 操作系统:Windows 10/11 且已安装 WSL2
  • 权限:需要以管理员身份运行脚本(脚本会自动请求提权)
  • PowerShell:建议使用 PowerShell 5.1 或更高版本
  • WSL2:目标 WSL2 发行版必须处于运行状态(可通过 wsl 命令启动)

3. 安装与首次运行

3.1 下载脚本

将以下脚本内容保存为 wsl_nat_port_manager.ps1 文件(推荐放在一个单独的文件夹,例如 C:\Users\你的用户名\Documents\WSL_tools\):

# 此处粘贴完整脚本代码(见前文最终版)

3.2 首次运行

由于 Windows 默认禁止运行 PowerShell 脚本,您可以采用以下任一方式运行:

方法一:使用命令行绕过执行策略(推荐)

  • 管理员身份打开 PowerShell
  • 切换到脚本所在目录,执行:
    powershell -ExecutionPolicy Bypass -File .\wsl_nat_port_manager.ps1

方法二:创建批处理启动器

  • 在脚本同目录下新建一个文本文件,命名为 启动端口管理.bat
  • 写入以下内容:
    @echo off
    powershell -ExecutionPolicy Bypass -File "%~dp0wsl_nat_port_manager.ps1"
    pause
  • 以后右键.bat 文件 → 以管理员身份运行

方法三:永久修改执行策略(仅限个人开发机)

  • 以管理员身份打开 PowerShell,执行:
    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
  • 输入 Y 确认。之后可以直接双击 .ps1 文件运行。

4. 界面与操作指南

运行脚本后,会显示主菜单:

======================================
   WSL2 NAT 端口转发管理工具
======================================
当前 WSL2 IP: 172.18.87.79

已转发的 TCP 端口:
   1996 (BT-Panel)  3390 (xrdp)  9659 (自定义)

已添加的 UDP 端口 (仅防火墙):
   无

请选择操作:
  1. 查看详细规则
  2. 添加端口 (TCP/UDP)
  3. 删除端口 (输入序号)
  0. 退出

4.1 查看详细规则 (选项 1)

显示所有 TCP 转发规则(包含目标 IP 和注释)以及 UDP 防火墙规则列表。

4.2 添加端口 (选项 2)

  • 输入要添加的端口号(支持多个,用英文逗号分隔,例如 1996,3390,8080)。
  • 选择协议:1 = TCP,2 = UDP。
    • 若选择 UDP,工具会提示 netsh 不支持 UDP 转发,询问是否仅添加防火墙规则。选择 y 后,会创建 Windows 防火墙入站规则(允许 UDP),但不会执行端口转发。
  • 确认后,工具会自动:
    • 删除可能存在的旧 TCP 转发规则(避免冲突)
    • 添加新的 TCP 转发规则(指向当前 WSL2 IP)
    • 创建/确认防火墙规则(TCP/UDP 分别命名:WSL TCP Port xxxx / WSL UDP Port xxxx

4.3 删除端口 (选项 3)

  • 工具会列出所有已存在的规则(TCP 转发 + UDP 防火墙),并统一编号。
  • 输入要删除的序号(例如 12,31-3 等)。
  • 确认后,工具会:
    • 删除 TCP 端口转发规则(执行 netsh interface portproxy delete
    • 删除 UDP 防火墙规则(执行 Remove-NetFirewallRule

提示:删除操作不会自动删除对应的防火墙规则(TCP 防火墙规则会保留,因为可能被其他程序使用)。如需彻底清理,可手动运行 Remove-NetFirewallRule -DisplayName "WSL TCP Port xxxx"

4.4 退出 (选项 0)

退出脚本。

5. 注意事项

  1. WSL2 IP 会变化
    每次重启 WSL2 或 Windows 后,WSL2 的虚拟 IP 地址可能会改变。建议每次 WSL2 重启后重新运行本工具,更新端口转发规则(添加时会自动使用最新 IP)。

  2. UDP 转发的限制
    Windows 的 netsh interface portproxy 不支持 UDP。如果您的应用确实需要 UDP 转发,请考虑以下替代方案:

    • 方案一(推荐):使用 WSL2 的镜像网络模式。编辑 %UserProfile%\.wslconfig,设置 networkingMode=Mirrored,然后执行 wsl --shutdown 重启 WSL。之后 WSL 与主机共享网络,UDP 端口自动可用。
    • 方案二:在 WSL2 内部安装 socat,手动进行 UDP 转发。例如:
      sudo apt install socat
      socat UDP-LISTEN:1996,fork UDP:172.18.87.79:1996

      (将 172.18.87.79 替换为你的 WSL2 IP)

  3. 防火墙规则清理
    本工具添加的防火墙规则名称格式为 WSL TCP Port xxxxWSL UDP Port xxxx。如果需要手动删除,可以在管理员 PowerShell 中执行:

    Remove-NetFirewallRule -DisplayName "WSL TCP Port 1996"
    Remove-NetFirewallRule -DisplayName "WSL UDP Port 1996"
  4. 脚本自动提权
    脚本开头包含了自动请求管理员权限的代码。如果您双击运行,会弹出 UAC 提示,请点击“是”。

  5. WSL2 未启动时的处理
    如果 WSL2 未运行,脚本会提示“无法获取 WSL IP”并退出。请先启动 WSL(例如在终端中执行 wsl 或打开一个 WSL 窗口)。

6. 常见问题 (FAQ)

Q1:运行脚本时提示“无法加载文件,因为在此系统上禁止运行脚本”
A:请参考第 3.2 节“首次运行”中的方法,使用 -ExecutionPolicy Bypass 参数运行,或修改执行策略。

Q2:添加 TCP 端口后,从 Windows 访问 localhost:端口 仍然失败
A:请按以下步骤排查:

  1. 确认 WSL2 内部的服务确实在监听 0.0.0.0 而不是 127.0.0.1(在 WSL2 中运行 ss -tlnp | grep 端口 检查)。
  2. 检查 Windows 防火墙是否允许该端口(脚本会自动添加规则,但某些安全软件可能阻止)。
  3. 确认 WSL2 IP 是否发生变化(运行 wsl hostname -I 获取最新 IP,并与转发规则中的 IP 比对)。

Q3:删除端口后,端口仍然可以访问?
A:可能是防火墙规则未删除。脚本目前只删除 TCP 转发规则和 UDP 防火墙规则,但保留了 TCP 防火墙规则(避免误删)。您可以手动删除对应的防火墙规则(见第 5 节)。

Q4:如何批量添加端口?
A:在添加端口时,输入用逗号分隔的端口号即可,例如 1996,3390,8080

Q5:为什么 UDP 端口不能转发?
A:这是 Windows netsh portproxy 的限制。建议改用 WSL2 镜像网络模式(见第 5 节)。

7. 卸载

本工具是一个独立的 PowerShell 脚本,不会在系统中安装任何文件。如需“卸载”,直接删除脚本文件即可。防火墙规则和端口转发规则可通过脚本的删除功能清除,也可手动清理(参见第 5 节)。

8. 更新日志

  • v1.0(2026-04-07)
    • 初始版本
    • 支持 TCP 端口转发添加/删除/查看
    • 支持 UDP 防火墙规则添加/删除
    • 自动获取 WSL2 IP 并配置转发
    • 交互式菜单,实时显示当前端口

如有任何问题或建议,欢迎反馈。

源码

# ============================================
# WSL2 NAT 端口转发管理工具 (支持 TCP/UDP 完整管理)
# 功能:查看/添加/删除 TCP 转发 + UDP 防火墙规则
# ============================================

# ---------- 自动请求管理员权限 ----------
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    $arguments = "-ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`""
    Start-Process powershell -Verb RunAs -ArgumentList $arguments
    exit
}

# ---------- 常用端口注释 ----------
$portComments = @{
    22   = "SSH"
    80   = "HTTP"
    443  = "HTTPS"
    3306 = "MySQL"
    5432 = "PostgreSQL"
    6379 = "Redis"
    27017 = "MongoDB"
    8080 = "Web备用"
    8888 = "Jupyter"
    3389 = "RDP"
    5900 = "VNC"
}

# ---------- 获取 WSL IP ----------
function Get-WslIp {
    $ip = (wsl hostname -I).Trim()
    if (-not $ip) {
        Write-Error "无法获取 WSL IP,请确保 WSL 正在运行"
        pause
        exit 1
    }
    return $ip
}

# ---------- 获取当前 TCP 转发规则(端口 -> 目标IP)----------
function Get-TcpRules {
    $output = netsh interface portproxy show v4tov4 2>$null
    $rules = @{}
    if ([string]::IsNullOrWhiteSpace($output)) { return $rules }
    $lines = $output -split "`r`n"
    foreach ($line in $lines) {
        if ($line -match '0\.0\.0\.0\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+\d+') {
            $port = [int]$Matches[1]
            $target = $Matches[2]
            $rules[$port] = $target
        }
    }
    return $rules
}

# ---------- 获取所有已添加的 UDP 防火墙规则(端口列表)----------
function Get-UdpFirewallPorts {
    $rules = Get-NetFirewallRule -DisplayName "WSL UDP Port *" -ErrorAction SilentlyContinue
    $ports = @()
    foreach ($rule in $rules) {
        # 从规则名称中提取端口号,例如 "WSL UDP Port 1996"
        if ($rule.DisplayName -match 'WSL UDP Port (\d+)') {
            $ports += [int]$Matches[1]
        }
    }
    return ($ports | Sort-Object -Unique)
}

# ---------- 显示当前 TCP 端口列表(主菜单用)----------
function Show-TcpPortList {
    $rules = Get-TcpRules
    if ($rules.Count -eq 0) {
        Write-Host "   (无)" -ForegroundColor DarkGray
        return
    }
    $sorted = $rules.Keys | Sort-Object
    $output = @()
    foreach ($port in $sorted) {
        $comment = if ($portComments.ContainsKey($port)) { "($($portComments[$port]))" } else { "" }
        $output += "$port $comment"
    }
    Write-Host ("   " + ($output -join "  ")) -ForegroundColor Green
}

# ---------- 显示当前 UDP 防火墙端口列表(主菜单用)----------
function Show-UdpPortList {
    $udpPorts = Get-UdpFirewallPorts
    if ($udpPorts.Count -eq 0) {
        Write-Host "   (无)" -ForegroundColor DarkGray
        return
    }
    $output = @()
    foreach ($port in $udpPorts) {
        $comment = if ($portComments.ContainsKey($port)) { "($($portComments[$port]))" } else { "" }
        $output += "$port $comment"
    }
    Write-Host ("   " + ($output -join "  ")) -ForegroundColor Green
}

# ---------- 显示详细规则表格(TCP)----------
function Show-DetailRules {
    $rules = Get-TcpRules
    if ($rules.Count -eq 0) {
        Write-Host "`n当前没有任何 TCP 端口转发规则。" -ForegroundColor Yellow
    } else {
        Write-Host "`n当前 TCP 端口转发规则:" -ForegroundColor Cyan
        Write-Host ("{0,-8} {1,-20} {2,-15}" -f "端口", "目标地址", "注释")
        Write-Host ("{0,-8} {1,-20} {2,-15}" -f "----", "--------", "----")
        foreach ($port in ($rules.Keys | Sort-Object)) {
            $target = $rules[$port]
            $comment = if ($portComments.ContainsKey($port)) { $portComments[$port] } else { "" }
            Write-Host ("{0,-8} {1,-20} {2,-15}" -f $port, $target, $comment)
        }
    }

    $udpPorts = Get-UdpFirewallPorts
    if ($udpPorts.Count -eq 0) {
        Write-Host "`n当前没有任何 UDP 防火墙规则。" -ForegroundColor Yellow
    } else {
        Write-Host "`n当前 UDP 防火墙规则(仅入站允许,无转发):" -ForegroundColor Cyan
        foreach ($port in $udpPorts) {
            $comment = if ($portComments.ContainsKey($port)) { "($($portComments[$port]))" } else { "" }
            Write-Host "  $port $comment" -ForegroundColor Green
        }
    }
    Write-Host ""
}

# ---------- 添加端口(TCP/UDP)----------
function Add-Ports {
    param($wslIp)
    # 显示当前已有端口
    $existingTcp = Get-TcpRules
    $existingUdp = Get-UdpFirewallPorts
    Write-Host "`n当前已转发的 TCP 端口: " -ForegroundColor Cyan -NoNewline
    if ($existingTcp.Count -gt 0) { Write-Host ($existingTcp.Keys | Sort-Object) -ForegroundColor Green } else { Write-Host "无" -ForegroundColor DarkGray }
    Write-Host "当前 UDP 防火墙端口: " -ForegroundColor Cyan -NoNewline
    if ($existingUdp.Count -gt 0) { Write-Host $existingUdp -ForegroundColor Green } else { Write-Host "无" -ForegroundColor DarkGray }

    Write-Host "`n请输入要添加的端口号(多个用逗号分隔,例如 1996,3390,8080):" -ForegroundColor Cyan
    $input = Read-Host "端口列表"
    if ([string]::IsNullOrWhiteSpace($input)) {
        Write-Host "未输入端口,返回主菜单。" -ForegroundColor Yellow
        return
    }
    $ports = $input -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ }
    if ($ports.Count -eq 0) {
        Write-Host "没有有效的端口号。" -ForegroundColor Red
        return
    }

    # 选择协议
    Write-Host "请选择协议: 1 = TCP, 2 = UDP" -ForegroundColor Cyan
    $protoChoice = Read-Host "请输入数字"
    $protocol = if ($protoChoice -eq '1') { "TCP" } elseif ($protoChoice -eq '2') { "UDP" } else { "TCP" }

    if ($protocol -eq "UDP") {
        Write-Host "⚠  netsh portproxy 不支持 UDP 转发。" -ForegroundColor Red
        Write-Host "   将仅为这些端口添加 Windows 防火墙规则(允许入站 UDP)。" -ForegroundColor Yellow
        Write-Host "   实际的 UDP 端口转发需要在 WSL2 内自行配置,例如:" -ForegroundColor Gray
        Write-Host "     - 使用镜像网络模式 (networkingMode=Mirrored)" -ForegroundColor Gray
        Write-Host "     - 在 WSL2 内运行 socat UDP-LISTEN:端口,fork UDP:目标IP:端口" -ForegroundColor Gray
        $continue = Read-Host "是否继续添加 UDP 防火墙规则?(y/N)"
        if ($continue -ne 'y' -and $continue -ne 'Y') { return }
    }

    # 确认添加
    Write-Host "将添加端口: $($ports -join ', ') 协议: $protocol" -ForegroundColor Yellow
    $confirm = Read-Host "确认操作?(Y/N)"
    if ($confirm -ne 'Y' -and $confirm -ne 'y') { return }

    foreach ($port in $ports) {
        if ($protocol -eq "TCP") {
            # 删除旧规则(避免冲突)
            netsh interface portproxy delete v4tov4 listenport=$port listenaddress=0.0.0.0 2>$null
            # 添加新规则
            netsh interface portproxy add v4tov4 listenport=$port listenaddress=0.0.0.0 connectport=$port connectaddress=$wslIp 2>&1 | Out-Null
            if ($LASTEXITCODE -eq 0) {
                Write-Host "✓ TCP 端口 $port 已转发至 $wslIp" -ForegroundColor Green
            } else {
                Write-Host "✗ TCP 端口 $port 转发失败" -ForegroundColor Red
            }
        }
        # 防火墙规则(TCP 和 UDP 都添加)
        $ruleName = "WSL $protocol Port $port"
        $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
        if (-not $existingRule) {
            New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol $protocol -LocalPort $port | Out-Null
            Write-Host "✓ 防火墙规则 ($protocol) $ruleName 已创建" -ForegroundColor Green
        } else {
            Write-Host "✓ 防火墙规则 ($protocol) $ruleName 已存在" -ForegroundColor Gray
        }
    }
    Write-Host "添加完成。" -ForegroundColor Green
    pause "按 Enter 继续..."
}

# ---------- 删除端口(支持 TCP 规则和 UDP 防火墙规则)----------
function Remove-Ports {
    $tcpRules = Get-TcpRules
    $udpPorts = Get-UdpFirewallPorts
    if ($tcpRules.Count -eq 0 -and $udpPorts.Count -eq 0) {
        Write-Host "没有可删除的规则。" -ForegroundColor Yellow
        pause
        return
    }

    # 显示 TCP 规则
    if ($tcpRules.Count -gt 0) {
        Write-Host "`n当前 TCP 端口转发规则:" -ForegroundColor Cyan
        $tcpList = @($tcpRules.Keys | Sort-Object)
        for ($i = 0; $i -lt $tcpList.Count; $i++) {
            $port = $tcpList[$i]
            $comment = if ($portComments.ContainsKey($port)) { " ($($portComments[$port]))" } else { "" }
            Write-Host "$($i+1). TCP $port$comment"
        }
    }
    # 显示 UDP 规则
    if ($udpPorts.Count -gt 0) {
        Write-Host "`n当前 UDP 防火墙规则 (仅允许入站,无转发):" -ForegroundColor Cyan
        $udpList = $udpPorts | Sort-Object
        $startIdx = $tcpRules.Count
        for ($i = 0; $i -lt $udpList.Count; $i++) {
            $port = $udpList[$i]
            $comment = if ($portComments.ContainsKey($port)) { " ($($portComments[$port]))" } else { "" }
            Write-Host "$($startIdx + $i + 1). UDP $port$comment"
        }
    }

    Write-Host "`n请输入要删除的规则序号(例如 1,3)或直接输入端口号(例如 1996,3390):" -ForegroundColor Cyan
    $input = Read-Host "选择"
    if ([string]::IsNullOrWhiteSpace($input)) { return }

    $toDeleteTcp = @()
    $toDeleteUdp = @()
    $items = $input -split ','
    foreach ($item in $items) {
        $item = $item.Trim()
        if ($item -match '^\d+$') {
            $num = [int]$item
            if ($num -le $tcpRules.Count) {
                $port = ($tcpRules.Keys | Sort-Object)[$num-1]
                $toDeleteTcp += $port
            } elseif ($num -le ($tcpRules.Count + $udpPorts.Count)) {
                $udpIndex = $num - $tcpRules.Count - 1
                if ($udpIndex -ge 0 -and $udpIndex -lt $udpPorts.Count) {
                    $port = ($udpPorts | Sort-Object)[$udpIndex]
                    $toDeleteUdp += $port
                } else {
                    Write-Host "无效序号: $num" -ForegroundColor Red
                }
            } else {
                Write-Host "无效序号: $num" -ForegroundColor Red
            }
        } elseif ($item -match '^\d+$') { # 直接端口号
            $port = [int]$item
            if ($tcpRules.ContainsKey($port)) {
                $toDeleteTcp += $port
            } elseif ($udpPorts -contains $port) {
                $toDeleteUdp += $port
            } else {
                Write-Host "端口 $port 未找到任何规则。" -ForegroundColor Red
            }
        } else {
            Write-Host "无效输入: $item" -ForegroundColor Red
        }
    }

    if ($toDeleteTcp.Count -eq 0 -and $toDeleteUdp.Count -eq 0) { return }

    Write-Host "将删除 TCP 端口: $($toDeleteTcp -join ', ')" -ForegroundColor Yellow
    Write-Host "将删除 UDP 端口: $($toDeleteUdp -join ', ')" -ForegroundColor Yellow
    $confirm = Read-Host "确认删除?(Y/N)"
    if ($confirm -ne 'Y' -and $confirm -ne 'y') { return }

    foreach ($port in $toDeleteTcp) {
        netsh interface portproxy delete v4tov4 listenport=$port listenaddress=0.0.0.0
        Write-Host "✓ 已删除 TCP 端口 $port 的转发规则" -ForegroundColor Green
        # 可选:同时删除对应的防火墙规则(但保留以免影响其他)
        # Remove-NetFirewallRule -DisplayName "WSL TCP Port $port" -ErrorAction SilentlyContinue
    }
    foreach ($port in $toDeleteUdp) {
        Remove-NetFirewallRule -DisplayName "WSL UDP Port $port" -ErrorAction SilentlyContinue
        Write-Host "✓ 已删除 UDP 端口 $port 的防火墙规则" -ForegroundColor Green
    }
    Write-Host "删除完成。" -ForegroundColor Green
    pause "按 Enter 继续..."
}

# ---------- 主菜单(实时显示端口列表)----------
function MainMenu {
    Clear-Host
    $wslIp = Get-WslIp
    Write-Host "======================================" -ForegroundColor Cyan
    Write-Host "   WSL2 NAT 端口转发管理工具" -ForegroundColor White
    Write-Host "======================================" -ForegroundColor Cyan
    Write-Host "当前 WSL2 IP 地址: $wslIp" -ForegroundColor Green
    Write-Host ""
    Write-Host "已转发的 TCP 端口 (转发规则):" -ForegroundColor Yellow
    Show-TcpPortList
    Write-Host ""
    Write-Host "已添加的 UDP 端口 (仅防火墙):" -ForegroundColor Yellow
    Show-UdpPortList
    Write-Host ""
    Write-Host "请选择操作:" -ForegroundColor Yellow
    Write-Host "  1. 查看详细规则 (TCP + UDP)"
    Write-Host "  2. 添加端口转发 (TCP/UDP)"
    Write-Host "  3. 删除端口转发/防火墙规则 (TCP/UDP)"
    Write-Host "  0. 退出"
    Write-Host ""
    $choice = Read-Host "请输入数字"
    switch ($choice) {
        '1' { Show-DetailRules; pause "按 Enter 返回菜单..."; MainMenu }
        '2' { Add-Ports -wslIp $wslIp; MainMenu }
        '3' { Remove-Ports; MainMenu }
        '0' { Write-Host "退出脚本。"; exit 0 }
        default { Write-Host "无效选项"; pause "按 Enter 返回菜单..."; MainMenu }
    }
}

# ---------- 入口 ----------
MainMenu