| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- #!/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'
- # Dual stack on purpose: a v4-only overlay never exercises the v6 side of tun.mtu.
- $Ip6_1 = 'fd42:4242:241::1'
- $Ip6_2 = 'fd42:4242:241::2'
- $Mtu = 1300
- $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,$Ip6_1/64" -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,$Ip6_2/64" -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: $Mtu
- 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: $Mtu
- 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; { echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6; echo 0 > /proc/sys/net/ipv6/conf/default/disable_ipv6; } 2>/dev/null || true; 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"
- # v6 silently kept the adapter default of 65535 while v4 was correct.
- foreach ($family in @('IPv4', 'IPv6')) {
- Wait-Until -TimeoutSec 30 -What "$DevName $family NlMtu=$Mtu" -Predicate {
- if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before $family mtu was set" }
- $rows = @(Get-NetIPInterface -InterfaceAlias $DevName -AddressFamily $family -ErrorAction SilentlyContinue)
- $rows.Count -gt 0 -and -not ($rows | Where-Object { $_.NlMtu -ne $Mtu })
- }
- Write-Host "OK: $DevName $family NlMtu=$Mtu"
- }
- 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 "WSL nebula1 with $Ip6_2" -Predicate {
- if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the v6 address was up" }
- $r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet6 $Ip6_2' && echo yes"
- ("$r").Trim() -eq 'yes'
- }
- Write-Host "OK: WSL nebula1 has $Ip6_2"
- 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"
- # Otherwise the v6 networks only prove the interface exists, not that it forwards.
- Wait-Until -TimeoutSec 30 -What "v6 ping from WSL peer to windows lighthouse ($Ip6_1)" -Predicate {
- if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the v6 ping succeeded" }
- $r = wsl -d $Distro -u root -- bash -c "ping -6 -c1 -W1 $Ip6_1 >/dev/null 2>&1 && echo OK"
- ("$r").Trim() -eq 'OK'
- }
- Write-Host "OK: WSL peer -> windows lighthouse over v6"
- # The NlMtu check above proves the value was written, not that windows honors it. A payload this size only
- # crosses the tunnel as fragments cut at NlMtu on the way out of the adapter. Left at the adapter default it
- # reaches nebula whole, overflows the udp.MTU (9001) write buffer and is dropped, so the ping never answers.
- # Anything smaller would ride out as one oversized datagram and survive on IP fragmentation of the underlay.
- $BigPayload = 9000
- foreach ($target in @(@{ Family = 'v4'; Ip = $Ip2 }, @{ Family = 'v6'; Ip = $Ip6_2 })) {
- Wait-Until -TimeoutSec 30 -What "$($target.Family) ping with a $BigPayload byte payload from windows lighthouse to WSL peer ($($target.Ip))" -Predicate {
- if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the large $($target.Family) ping succeeded" }
- $null = & ping.exe -n 1 -w 1000 -l $BigPayload $target.Ip
- $LASTEXITCODE -eq 0
- }
- Write-Host "OK: windows lighthouse -> WSL peer, $BigPayload byte $($target.Family) payload"
- }
- 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
- }
- }
|