#!/usr/bin/env pwsh
# Windows smoke test for the nebula tun + UDP + NLM code paths.
#
# Topology:
#   - lighthouse runs natively on the Windows host (wintun + windows UDP)
#   - peer runs inside WSL2 (Linux build of nebula, /dev/net/tun)
#
# WSL2 gives us a real netns boundary so the loopback fast-path on Windows
# does not short-circuit the overlay -- when WSL pings the lighthouse VPN IP,
# Linux has no idea that IP is local to the Windows host, so the packet is
# forced through nebula. Same in reverse.

$ErrorActionPreference = 'Stop'

# wsl.exe emits UTF-16 LE by default which PowerShell reads as bytes, mangling
# every captured string. WSL_UTF8 makes wsl.exe emit UTF-8 instead.
$env:WSL_UTF8 = '1'

$RepoRoot = Resolve-Path "$PSScriptRoot\..\..\.."
$Nebula = Join-Path $RepoRoot 'nebula.exe'
$NebulaCert = Join-Path $RepoRoot 'nebula-cert.exe'
$NebulaLinux = Join-Path $RepoRoot 'build\linux-amd64\nebula'

if (-not (Test-Path $Nebula)) { throw "missing $Nebula; run 'make bin-windows' first" }
if (-not (Test-Path $NebulaCert)) { throw "missing $NebulaCert; run 'make bin-windows' first" }
if (-not (Test-Path $NebulaLinux)) { throw "missing $NebulaLinux; build the linux nebula first" }

# Matches the distro installed by Vampire/setup-wsl in smoke-extra.yml.
$Distro = 'Ubuntu-24.04'
$listed = (wsl --list --quiet 2>$null) -join "`n"
if ($listed -notmatch [regex]::Escape($Distro)) {
    throw "WSL distro $Distro not registered. Got: $listed"
}
Write-Host "Using WSL distro: $Distro"

# Windows host as seen from inside WSL: WSL's default-route gateway. We extract
# it with a regex rather than awk fields so PowerShell does not eat any '$N'
# tokens, and tabs/double-spaces in `ip route` output do not confuse a cut.
$ipCmd = 'ip route show default | grep -oE "([0-9]+\.){3}[0-9]+" | head -1'
$WindowsIp = (wsl -d $Distro -- bash -c $ipCmd).Trim()
if (-not $WindowsIp) { throw "could not determine Windows host IP from WSL" }
Write-Host "Windows host IP from WSL: $WindowsIp"

$WorkDir = Join-Path $env:TEMP 'nebula-smoke-windows'
if (Test-Path $WorkDir) { Remove-Item -Recurse -Force $WorkDir }
New-Item -ItemType Directory -Path $WorkDir | Out-Null

$WslDir = '/tmp/nebula-smoke'
wsl -d $Distro -- bash -c "rm -rf $WslDir && mkdir -p $WslDir" | Out-Null

$DevName = 'nebula-smoke'
$Ip1 = '192.168.241.1'
$Ip2 = '192.168.241.2'
$Port = 4242

& $NebulaCert ca -name 'smoke-ca' -out-crt "$WorkDir\ca.crt" -out-key "$WorkDir\ca.key"
if ($LASTEXITCODE -ne 0) { throw "nebula-cert ca failed (exit $LASTEXITCODE)" }

& $NebulaCert sign -name 'lighthouse' -networks "$Ip1/24" -ca-crt "$WorkDir\ca.crt" -ca-key "$WorkDir\ca.key" -out-crt "$WorkDir\lighthouse.crt" -out-key "$WorkDir\lighthouse.key"
if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign lighthouse failed (exit $LASTEXITCODE)" }

& $NebulaCert sign -name 'peer' -networks "$Ip2/24" -ca-crt "$WorkDir\ca.crt" -ca-key "$WorkDir\ca.key" -out-crt "$WorkDir\peer.crt" -out-key "$WorkDir\peer.key"
if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign peer failed (exit $LASTEXITCODE)" }

# Windows lighthouse config.
@"
pki:
  ca: $WorkDir\ca.crt
  cert: $WorkDir\lighthouse.crt
  key: $WorkDir\lighthouse.key
static_host_map: {}
lighthouse:
  am_lighthouse: true
  interval: 60
  hosts: []
listen:
  host: 0.0.0.0
  port: $Port
tun:
  disabled: false
  dev: $DevName
  drop_local_broadcast: false
  drop_multicast: false
  tx_queue: 500
  mtu: 1300
  network_category: private
logging:
  level: info
  format: text
firewall:
  outbound_action: drop
  inbound_action: drop
  conntrack:
    tcp_timeout: 12m
    udp_timeout: 3m
    default_timeout: 10m
  outbound:
    - port: any
      proto: any
      host: any
  inbound:
    - port: any
      proto: any
      host: any
"@ | Out-File -FilePath "$WorkDir\lighthouse.yml" -Encoding utf8

# WSL peer config (paths are POSIX, deliberately).
@"
pki:
  ca: $WslDir/ca.crt
  cert: $WslDir/peer.crt
  key: $WslDir/peer.key
static_host_map:
  "${Ip1}": ["${WindowsIp}:$Port"]
lighthouse:
  am_lighthouse: false
  interval: 60
  hosts:
    - "${Ip1}"
listen:
  host: 0.0.0.0
  port: 0
tun:
  disabled: false
  dev: nebula1
  drop_local_broadcast: false
  drop_multicast: false
  tx_queue: 500
  mtu: 1300
logging:
  level: info
  format: text
firewall:
  outbound_action: drop
  inbound_action: drop
  conntrack:
    tcp_timeout: 12m
    udp_timeout: 3m
    default_timeout: 10m
  outbound:
    - port: any
      proto: any
      host: any
  inbound:
    - port: any
      proto: any
      host: any
"@ | Out-File -FilePath "$WorkDir\peer.yml" -Encoding utf8

# Stage WSL artifacts. Convert Windows paths to WSL paths ourselves rather than
# calling `wslpath`, because PowerShell's argument-passing to external EXEs
# strips backslashes from path arguments in ways that are hard to escape around.
function ConvertTo-WslPath {
    param([string]$WindowsPath)
    if ($WindowsPath -notmatch '^([A-Za-z]):\\(.*)$') {
        throw "cannot convert path to WSL: $WindowsPath"
    }
    return "/mnt/$($matches[1].ToLower())/$($matches[2].Replace('\','/'))"
}

$WslWorkDir = ConvertTo-WslPath $WorkDir
$WslNebulaPath = ConvertTo-WslPath $NebulaLinux
wsl -d $Distro -- bash -c "cp '$WslWorkDir/ca.crt' '$WslWorkDir/peer.crt' '$WslWorkDir/peer.key' '$WslWorkDir/peer.yml' $WslDir/ && cp '$WslNebulaPath' $WslDir/nebula && chmod +x $WslDir/nebula"

# Make sure WSL has tun support and /dev/net/tun is usable before starting
# nebula. Diagnostics first so a fail here points at the real problem (e.g.
# WSL1 distros do not have a real kernel and will not have tun).
Write-Host '=== WSL diagnostic ==='
wsl --version 2>&1 | Out-Host
wsl --list --verbose 2>&1 | Out-Host
wsl -d $Distro -u root -- uname -a | Out-Host
wsl -d $Distro -u root -- bash -c "modprobe tun 2>&1 || true; mkdir -p /dev/net; [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200; chmod 600 /dev/net/tun; ls -l /dev/net/tun"
if ($LASTEXITCODE -ne 0) { throw "failed to prepare /dev/net/tun in WSL (TUN support missing?)" }

# Deliberately no New-NetFirewallRule calls here -- nebula's windows_bypass_wdf
# feature is supposed to install WFP permit filters that let inbound traffic
# through Windows Defender Firewall on its own. If this smoke regresses, that
# feature regressed.

$lhOut = Join-Path $WorkDir 'lighthouse.out.log'
$lhErr = Join-Path $WorkDir 'lighthouse.err.log'
$lhProc = Start-Process -FilePath $Nebula -ArgumentList @('-config', "$WorkDir\lighthouse.yml") `
    -PassThru -NoNewWindow `
    -RedirectStandardOutput $lhOut `
    -RedirectStandardError $lhErr

# Run nebula in WSL as root with no sudo + no shell wrapper. PowerShell's
# Start-Process arg quoting mangles `bash -c "..."` strings that contain
# spaces/redirections, so we skip bash entirely and let Start-Process do the
# stdout/stderr capture itself.
$peerOut = Join-Path $WorkDir 'peer.out.log'
$peerErr = Join-Path $WorkDir 'peer.err.log'
$peerProc = Start-Process -FilePath 'wsl' `
    -ArgumentList @('-d', $Distro, '-u', 'root', '--', "$WslDir/nebula", '-config', "$WslDir/peer.yml") `
    -PassThru -NoNewWindow `
    -RedirectStandardOutput $peerOut `
    -RedirectStandardError $peerErr

function Wait-Until {
    param([scriptblock]$Predicate, [int]$TimeoutSec, [string]$What)
    $deadline = (Get-Date).AddSeconds($TimeoutSec)
    while ((Get-Date) -lt $deadline) {
        if (& $Predicate) { return }
        Start-Sleep -Milliseconds 500
    }
    throw "timed out waiting for: $What"
}

try {
    Wait-Until -TimeoutSec 30 -What "windows wintun adapter $DevName with NetworkCategory=Private" -Predicate {
        if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before tun was ready" }
        $p = Get-NetConnectionProfile -InterfaceAlias $DevName -ErrorAction SilentlyContinue
        $p -and ("$($p.NetworkCategory)" -ieq 'Private')
    }
    Write-Host "OK: $DevName NetworkCategory=Private"

    Wait-Until -TimeoutSec 30 -What "WSL nebula1 with $Ip2" -Predicate {
        if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before tun was ready" }
        $r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet $Ip2' && echo yes"
        ("$r").Trim() -eq 'yes'
    }
    Write-Host "OK: WSL nebula1 has $Ip2"

    Wait-Until -TimeoutSec 30 -What "ping from WSL peer to windows lighthouse ($Ip1)" -Predicate {
        if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before ping succeeded" }
        $r = wsl -d $Distro -u root -- bash -c "ping -c1 -W1 $Ip1 >/dev/null 2>&1 && echo OK"
        ("$r").Trim() -eq 'OK'
    }
    Write-Host "OK: WSL peer -> windows lighthouse"

    Wait-Until -TimeoutSec 30 -What "ping from windows lighthouse to WSL peer ($Ip2)" -Predicate {
        $null = & ping.exe -n 1 -w 1000 $Ip2
        $LASTEXITCODE -eq 0
    }
    Write-Host "OK: windows lighthouse -> WSL peer"

    Write-Host ''
    Write-Host 'All smoke checks passed.'
}
catch {
    Write-Host ''
    Write-Host '=== lighthouse stdout ==='
    Get-Content $lhOut -ErrorAction SilentlyContinue | Out-Host
    Write-Host '=== lighthouse stderr ==='
    Get-Content $lhErr -ErrorAction SilentlyContinue | Out-Host
    Write-Host '=== peer stdout ==='
    Get-Content $peerOut -ErrorAction SilentlyContinue | Out-Host
    Write-Host '=== peer stderr ==='
    Get-Content $peerErr -ErrorAction SilentlyContinue | Out-Host
    Write-Host '=== nebula WFP filters ==='
    # Dump nebula-installed filters so we can verify they got registered with
    # the conditions we expect.
    $wfpDump = Join-Path $WorkDir 'wfp.xml'
    netsh wfp show filters file=$wfpDump 2>&1 | Out-Null
    if (Test-Path $wfpDump) {
        Select-String -Path $wfpDump -Pattern 'Nebula' -Context 0,80 -ErrorAction SilentlyContinue | Out-Host
    }
    throw
}
finally {
    if (-not $lhProc.HasExited) {
        Stop-Process -Id $lhProc.Id -Force -ErrorAction SilentlyContinue
        $lhProc.WaitForExit(5000) | Out-Null
    }
    wsl -d $Distro -u root -- bash -c "pkill -f $WslDir/nebula 2>/dev/null; true" | Out-Null
    # pkill returns 1 when no match and wsl propagates that; the smoke is done
    # so we don't want it to leak into the script's exit code.
    $global:LASTEXITCODE = 0
    if ($peerProc -and -not $peerProc.HasExited) {
        Stop-Process -Id $peerProc.Id -Force -ErrorAction SilentlyContinue
    }
}
