Public/Firewall/Set-RouterSshPortProxyFirewall.ps1

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


# ---------------------------------------------------------------------------
# Set-RouterSshPortProxyFirewall
# Idempotent Windows Firewall companion to Set-RouterSshPortProxy.
# Without an allow rule 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 regimes, both handled:
#
# 1. Standard Windows Defender Firewall (Win10 WSL, or Win11 WSL with
# the Hyper-V Firewall feature off). Inbound is filtered per host
# interface, so an interface-scoped allow on the WSL vEthernet
# adapter opens the path. Tight scoping: the rule applies ONLY to
# the WSL vEthernet adapter (alias starts with "vEthernet (WSL");
# the host's WiFi, Ethernet, and ICS adapters keep the OS-default
# deny posture - a coffee-shop WiFi cannot reach the router VM.
#
# 2. Hyper-V Firewall (Win11 WSL2 default). WSL runs behind a separate
# packet filter keyed by the WSL VM creator id, NOT the per-
# interface Defender rules - so the regime-1 rule is bypassed and
# its DefaultInboundAction (Block) drops WSL's SYN before it ever
# reaches the portproxy listener. This is the exact failure the
# interface rule alone could not fix. When that regime is present
# we add the matching Hyper-V allow rule keyed to the WSL creator.
#
# Both rules are additive and idempotent; on a given host only the
# regime in force actually governs traffic, and adding the inert one
# is harmless. No-op on hosts without a WSL adapter installed; the
# rest of the provisioner stays usable on Linux/Mac developer boxes
# that exercise these helpers via Pester.
# ---------------------------------------------------------------------------

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

    # 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 firewall rule (WSL probably not installed)."
        return
    }

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

    # -----------------------------------------------------------------
    # Regime 1: standard Defender interface-scoped allow. Governs WSL
    # on Win10 and on Win11 hosts with the Hyper-V Firewall feature off.
    # -----------------------------------------------------------------
    $existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host " [firewall] inbound rule '$ruleName' already present on '$($wslAdapter.Name)', skipping."
    }
    else {
        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
    }

    # -----------------------------------------------------------------
    # Regime 2: Hyper-V Firewall allow keyed to the WSL VM creator.
    # Inert (early-returns) wherever the regime is not in force, guarded
    # three ways: the Hyper-V cmdlets must exist (older Windows lacks
    # them), a WSL VM creator must be registered (else WSL is not behind
    # the Hyper-V Firewall and regime 1 already governs it), and a
    # matching rule must not already exist (idempotency).
    # -----------------------------------------------------------------
    if (-not (Get-Command New-NetFirewallHyperVRule -ErrorAction SilentlyContinue)) {
        return
    }

    $wslCreator = Get-NetFirewallHyperVVMCreator -ErrorAction SilentlyContinue |
                  Where-Object { $_.FriendlyName -eq 'WSL' } |
                  Select-Object -First 1
    if (-not $wslCreator) {
        return
    }

    # Stable Name (not just DisplayName) so the idempotency lookup is
    # exact. DisplayName mirrors the Defender rule for operator parity.
    $hvRuleName = 'VmProvisioner-WSL-RouterSshPortproxy'

    $hvExisting = Get-NetFirewallHyperVRule -Name $hvRuleName -ErrorAction SilentlyContinue
    if ($hvExisting) {
        Write-Host " [firewall] Hyper-V allow rule '$hvRuleName' already present, skipping."
        return
    }

    Write-Host " [firewall] adding Hyper-V inbound TCP/$ListenPort allow for WSL creator '$($wslCreator.VMCreatorId)'"
    New-NetFirewallHyperVRule `
        -Name        $hvRuleName `
        -DisplayName $ruleName `
        -Direction   Inbound `
        -VMCreatorId $wslCreator.VMCreatorId `
        -Protocol    TCP `
        -LocalPorts  $ListenPort `
        -Action      Allow | Out-Null
}