param( [string]$ListenAddress = "0.0.0.0", [string]$WslIp = "" ) $ErrorActionPreference = "Stop" $knownPorts = @{ 8000 = @{ Name = "hm-biz-process"; RuleName = "HM-BIZ-PROCESS-8000" } 8010 = @{ Name = "my-intranet-app"; RuleName = "MyIntranetApp-8010" } 8020 = @{ Name = "b17301"; RuleName = "B17301-8020" } } function Get-WslListeningPorts { $raw = wsl.exe bash -lc "ss -ltnH 2>/dev/null" if (-not $raw) { return @() } $portMap = @{} foreach ($line in ($raw -split "`r?`n")) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $parts = ($line -split "\s+") | Where-Object { $_ -ne "" } if ($parts.Count -lt 4) { continue } $localAddress = $parts[3].Trim() if ([string]::IsNullOrWhiteSpace($localAddress)) { continue } $hostPart = $localAddress $portPart = "" $lastColon = $localAddress.LastIndexOf(":") if ($lastColon -lt 0) { continue } $hostPart = $localAddress.Substring(0, $lastColon) $portPart = $localAddress.Substring($lastColon + 1) $parsedPort = 0 if (-not [int]::TryParse($portPart, [ref]$parsedPort)) { continue } $port = [int]$parsedPort if ($port -le 0) { continue } $normalizedHost = $hostPart.Trim("[", "]") $isLoopback = ( $normalizedHost -eq "127.0.0.1" -or $normalizedHost -eq "::1" -or $normalizedHost -eq "localhost" ) if (-not $portMap.ContainsKey($port)) { $portMap[$port] = [ordered]@{ Port = $port Hosts = New-Object System.Collections.Generic.List[string] ExternallyReachable = $false } } $entry = $portMap[$port] if (-not $entry.Hosts.Contains($normalizedHost)) { $entry.Hosts.Add($normalizedHost) } if (-not $isLoopback) { $entry.ExternallyReachable = $true } } return $portMap.Values | Sort-Object Port } if ([string]::IsNullOrWhiteSpace($WslIp)) { $detected = wsl.exe hostname -I 2>$null if (-not $detected) { throw "WSL IP를 자동으로 찾지 못했습니다. -WslIp 옵션으로 직접 지정해 주세요." } $WslIp = (($detected -split "\s+") | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1) if (-not $WslIp) { throw "WSL IP를 자동으로 파싱하지 못했습니다. -WslIp 옵션으로 직접 지정해 주세요." } } $windowsIps = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike '127.*' ` -and $_.IPAddress -notlike '169.254*' ` -and $_.InterfaceAlias -notlike '*WSL*' } | Select-Object -ExpandProperty IPAddress $listenAddresses = @($ListenAddress) if ($ListenAddress -eq "0.0.0.0") { $listenAddresses += $windowsIps } $listenAddresses = $listenAddresses | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique Write-Host "Setting Windows portproxy for all active WSL TCP ports..." -ForegroundColor Cyan Write-Host "WSL IP: $WslIp" $portEntries = @(Get-WslListeningPorts) $knownPortEntries = @() foreach ($knownPort in ($knownPorts.Keys | Sort-Object)) { $existingEntry = @($portEntries | Where-Object { [int]$_.Port -eq [int]$knownPort } | Select-Object -First 1) if ($existingEntry -and $existingEntry.Count -gt 0) { continue } $hosts = New-Object System.Collections.Generic.List[string] $hosts.Add("not-listening-yet") $knownPortEntries += [ordered]@{ Port = [int]$knownPort Hosts = $hosts ExternallyReachable = $true Assumed = $true } } $portEntries = @($portEntries + $knownPortEntries | Sort-Object Port) if (-not $portEntries -or $portEntries.Count -eq 0) { throw "등록할 WSL TCP 포트를 찾지 못했습니다." } Write-Host "Detected ports: $((@($portEntries | ForEach-Object { $_.Port })) -join ', ')" $reachableEntries = @($portEntries | Where-Object { $_.ExternallyReachable }) $loopbackOnlyEntries = @($portEntries | Where-Object { -not $_.ExternallyReachable }) if (-not $reachableEntries -or $reachableEntries.Count -eq 0) { throw "현재 WSL에서 내부망에 공개 가능한 포트를 찾지 못했습니다. 앱이 0.0.0.0 또는 WSL IP에 바인딩되어 있는지 확인해 주세요." } $configuredPorts = New-Object System.Collections.Generic.List[int] foreach ($entry in $reachableEntries) { $port = [int]$entry.Port $appInfo = $knownPorts[$port] $displayName = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" } $ruleName = if ($appInfo) { [string]$appInfo.RuleName } else { "WSL-PORT-$port" } $assumed = $false if ($entry.Contains("Assumed")) { $assumed = [bool]$entry.Assumed } Write-Host "" Write-Host "$displayName : $port ($($entry.Hosts -join ', '))" -ForegroundColor Cyan if ($assumed) { Write-Host " Known service port is not listening yet; portproxy will still be refreshed to the current WSL IP." -ForegroundColor Yellow } foreach ($address in $listenAddresses) { netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$address | Out-Null netsh interface portproxy add v4tov4 listenport=$port listenaddress=$address connectport=$port connectaddress=$WslIp } $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue if ($existingRule) { Remove-NetFirewallRule -DisplayName $ruleName | Out-Null } New-NetFirewallRule ` -DisplayName $ruleName ` -Direction Inbound ` -Action Allow ` -Protocol TCP ` -LocalPort $port | Out-Null if (-not $configuredPorts.Contains($port)) { $configuredPorts.Add($port) } } Write-Host "" Write-Host "Completed." -ForegroundColor Green Write-Host "Use these URLs from another PC on the intranet:" if ($windowsIps) { foreach ($ip in $windowsIps) { foreach ($port in $configuredPorts) { $appInfo = $knownPorts[$port] $label = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" } Write-Host ("{0,-15}: http://{1}:{2}" -f $label, $ip, $port) } } } else { foreach ($port in $configuredPorts) { $appInfo = $knownPorts[$port] $label = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" } Write-Host ("{0,-15}: http://:{1}" -f $label, $port) } } if ($loopbackOnlyEntries.Count -gt 0) { Write-Host "" Write-Host "Loopback-only ports (not published to intranet):" -ForegroundColor Yellow foreach ($entry in $loopbackOnlyEntries) { Write-Host ("- {0}: {1}" -f $entry.Port, ($entry.Hosts -join ', ')) } Write-Host "These ports are bound only to 127.0.0.1/::1 inside WSL. Run those apps on 0.0.0.0 (or the WSL IP) to expose them to the intranet." -ForegroundColor Yellow } Write-Host "" Write-Host "Tip: if WSL restarts and its IP changes, run this script again."