Public/Firewall/Set-RouterSshPortProxyFirewall.ps1

<#
.NOTES
    Do not run this file directly. Dot-sourced by provision.ps1.
#>


# ---------------------------------------------------------------------------
# Set-RouterSshPortProxyFirewall
# Idempotent firewall companion to Set-RouterSshPortProxy.
# Without an inbound allow the portproxy listens on 0.0.0.0:<port>
# but the firewall silently drops inbound TCP from WSL, yielding the
# "Connection timed out during banner exchange" symptom Ansible
# surfaces as UNREACHABLE.
#
# Two firewall engines, one per WSL networking generation - we ensure
# a rule in BOTH because which one is live depends on the host's WSL
# build and the cost of the wrong guess is the same opaque timeout:
#
# 1. Windows Defender Firewall (New-NetFirewallRule), scoped to the
# WSL vEthernet adapter. Governs WSL in the older NAT networking
# mode.
# 2. Hyper-V Firewall (New-NetFirewallHyperVRule), scoped to WSL's
# VM-creator id. On Windows 11 builds where WSL runs in
# "Hyper-V firewall" mode (adapter name
# 'vEthernet (WSL (Hyper-V firewall))'), inbound from the WSL VM
# to the host is filtered HERE, not by engine 1, and the WSL
# VM-creator's DefaultInboundAction is Block. An adapter-scoped
# Defender rule does nothing for that traffic - the segment-1 TCP
# timeout in _assert-router-reachable.sh is exactly this gap.
#
# Tight scoping on both: engine 1 binds to the WSL vEthernet adapter,
# engine 2 to the WSL VM-creator id. The host's WiFi, Ethernet, and
# ICS adapters keep the OS-default deny posture - a coffee-shop WiFi
# cannot reach the router VM through either rule.
#
# No-op on hosts without a WSL adapter (engine 1) or without the
# Hyper-V firewall feature / a WSL VM-creator setting (engine 2), so
# the rest of the provisioner stays usable on Linux/Mac developer
# boxes that exercise these helpers via Pester.
# ---------------------------------------------------------------------------

# WSL's well-known Hyper-V VM-creator id. The Hyper-V Firewall scopes
# rules per VM-creator rather than per host adapter; this GUID is the
# stable identifier the WSL platform registers under. Gating engine 2
# on the presence of a setting for this id means we never strand an
# allow rule against a creator the host does not have.
$script:WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'

function Set-RouterSshPortProxyFirewall {
    [CmdletBinding()]
    param(
        # Listen port the inbound rule covers. Must match the
        # Set-RouterSshPortProxy listen port - same default.
        [int] $ListenPort = 2222
    )

    $ruleName = "Vm-Provisioner: WSL -> router SSH portproxy (TCP/$ListenPort)"

    Set-WslDefenderFirewallAllow   -ListenPort $ListenPort -RuleName $ruleName
    Set-WslHyperVFirewallAllow     -ListenPort $ListenPort -RuleName $ruleName
}

# Engine 1: standard Windows Defender Firewall, scoped to the WSL
# vEthernet adapter. Governs WSL in the older NAT networking mode.
function Set-WslDefenderFirewallAllow {
    [CmdletBinding()]
    param(
        [int]    $ListenPort,
        [string] $RuleName
    )

    # Discover the WSL vEthernet adapter (if any). Get-NetAdapter
    # returns nothing on hosts without WSL installed.
    $wslAdapter = Get-NetAdapter -ErrorAction SilentlyContinue |
                  Where-Object { $_.Name -like 'vEthernet (WSL*' } |
                  Select-Object -First 1

    if (-not $wslAdapter) {
        Write-Host " [firewall] no vEthernet (WSL*) adapter found; skipping Defender rule (WSL probably not installed)."
        return
    }

    $existing = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host " [firewall] inbound rule '$RuleName' already present on '$($wslAdapter.Name)', skipping."
        return
    }

    Write-Host " [firewall] adding inbound TCP/$ListenPort allow on '$($wslAdapter.Name)' (WSL-only scope)"
    New-NetFirewallRule `
        -DisplayName    $RuleName `
        -Direction      Inbound `
        -LocalPort      $ListenPort `
        -Protocol       TCP `
        -Action         Allow `
        -InterfaceAlias $wslAdapter.Name | Out-Null
}

# Engine 2: Hyper-V Firewall, scoped to WSL's VM-creator id. Governs
# WSL in the Windows 11 "Hyper-V firewall" networking mode where
# engine 1 has no effect on WSL-to-host traffic.
function Set-WslHyperVFirewallAllow {
    [CmdletBinding()]
    param(
        [int]    $ListenPort,
        [string] $RuleName
    )

    # The Hyper-V Firewall cmdlets ship only on builds that have the
    # feature; older hosts lack them entirely. Probe before use so
    # this stays a silent no-op rather than a hard error on down-level
    # Windows and on Linux/Mac Pester runs.
    if (-not (Get-Command New-NetFirewallHyperVRule -ErrorAction SilentlyContinue)) {
        Write-Host " [hyperv-firewall] New-NetFirewallHyperVRule unavailable; skipping (no Hyper-V firewall feature)."
        return
    }

    # Confirm the host actually registers a WSL VM-creator setting
    # before adding a rule, so a non-WSL Hyper-V host (or WSL still in
    # NAT mode) does not get an orphan allow rule.
    $vmSetting = Get-NetFirewallHyperVVMSetting -Name $script:WslVmCreatorId `
                    -ErrorAction SilentlyContinue
    if (-not $vmSetting) {
        Write-Host " [hyperv-firewall] no WSL VM-creator setting ($script:WslVmCreatorId); skipping (WSL not in Hyper-V firewall mode)."
        return
    }

    # Hyper-V rules carry a -Name (identity key) distinct from the
    # human-facing -DisplayName, so idempotency keys off -Name.
    $hyperVRuleName = "VmProvisioner-WSL-RouterSshPortproxy-$ListenPort"
    $existing = Get-NetFirewallHyperVRule -Name $hyperVRuleName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host " [hyperv-firewall] rule '$hyperVRuleName' already present, skipping."
        return
    }

    Write-Host " [hyperv-firewall] adding inbound TCP/$ListenPort allow for WSL VM-creator $script:WslVmCreatorId"
    New-NetFirewallHyperVRule `
        -Name        $hyperVRuleName `
        -DisplayName $RuleName `
        -Direction   Inbound `
        -VMCreatorId $script:WslVmCreatorId `
        -Protocol    TCP `
        -LocalPorts  $ListenPort `
        -Action      Allow | Out-Null
}