smoke-windows.ps1 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #!/usr/bin/env pwsh
  2. # Windows smoke test for the nebula tun + UDP + NLM code paths.
  3. #
  4. # Topology:
  5. # - lighthouse runs natively on the Windows host (wintun + windows UDP)
  6. # - peer runs inside WSL2 (Linux build of nebula, /dev/net/tun)
  7. #
  8. # WSL2 gives us a real netns boundary so the loopback fast-path on Windows
  9. # does not short-circuit the overlay -- when WSL pings the lighthouse VPN IP,
  10. # Linux has no idea that IP is local to the Windows host, so the packet is
  11. # forced through nebula. Same in reverse.
  12. $ErrorActionPreference = 'Stop'
  13. # wsl.exe emits UTF-16 LE by default which PowerShell reads as bytes, mangling
  14. # every captured string. WSL_UTF8 makes wsl.exe emit UTF-8 instead.
  15. $env:WSL_UTF8 = '1'
  16. $RepoRoot = Resolve-Path "$PSScriptRoot\..\..\.."
  17. $Nebula = Join-Path $RepoRoot 'nebula.exe'
  18. $NebulaCert = Join-Path $RepoRoot 'nebula-cert.exe'
  19. $NebulaLinux = Join-Path $RepoRoot 'build\linux-amd64\nebula'
  20. if (-not (Test-Path $Nebula)) { throw "missing $Nebula; run 'make bin-windows' first" }
  21. if (-not (Test-Path $NebulaCert)) { throw "missing $NebulaCert; run 'make bin-windows' first" }
  22. if (-not (Test-Path $NebulaLinux)) { throw "missing $NebulaLinux; build the linux nebula first" }
  23. # Matches the distro installed by Vampire/setup-wsl in smoke-extra.yml.
  24. $Distro = 'Ubuntu-24.04'
  25. $listed = (wsl --list --quiet 2>$null) -join "`n"
  26. if ($listed -notmatch [regex]::Escape($Distro)) {
  27. throw "WSL distro $Distro not registered. Got: $listed"
  28. }
  29. Write-Host "Using WSL distro: $Distro"
  30. # Windows host as seen from inside WSL: WSL's default-route gateway. We extract
  31. # it with a regex rather than awk fields so PowerShell does not eat any '$N'
  32. # tokens, and tabs/double-spaces in `ip route` output do not confuse a cut.
  33. $ipCmd = 'ip route show default | grep -oE "([0-9]+\.){3}[0-9]+" | head -1'
  34. $WindowsIp = (wsl -d $Distro -- bash -c $ipCmd).Trim()
  35. if (-not $WindowsIp) { throw "could not determine Windows host IP from WSL" }
  36. Write-Host "Windows host IP from WSL: $WindowsIp"
  37. $WorkDir = Join-Path $env:TEMP 'nebula-smoke-windows'
  38. if (Test-Path $WorkDir) { Remove-Item -Recurse -Force $WorkDir }
  39. New-Item -ItemType Directory -Path $WorkDir | Out-Null
  40. $WslDir = '/tmp/nebula-smoke'
  41. wsl -d $Distro -- bash -c "rm -rf $WslDir && mkdir -p $WslDir" | Out-Null
  42. $DevName = 'nebula-smoke'
  43. $Ip1 = '192.168.241.1'
  44. $Ip2 = '192.168.241.2'
  45. $Port = 4242
  46. & $NebulaCert ca -name 'smoke-ca' -out-crt "$WorkDir\ca.crt" -out-key "$WorkDir\ca.key"
  47. if ($LASTEXITCODE -ne 0) { throw "nebula-cert ca failed (exit $LASTEXITCODE)" }
  48. & $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"
  49. if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign lighthouse failed (exit $LASTEXITCODE)" }
  50. & $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"
  51. if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign peer failed (exit $LASTEXITCODE)" }
  52. # Windows lighthouse config.
  53. @"
  54. pki:
  55. ca: $WorkDir\ca.crt
  56. cert: $WorkDir\lighthouse.crt
  57. key: $WorkDir\lighthouse.key
  58. static_host_map: {}
  59. lighthouse:
  60. am_lighthouse: true
  61. interval: 60
  62. hosts: []
  63. listen:
  64. host: 0.0.0.0
  65. port: $Port
  66. tun:
  67. disabled: false
  68. dev: $DevName
  69. drop_local_broadcast: false
  70. drop_multicast: false
  71. tx_queue: 500
  72. mtu: 1300
  73. network_category: private
  74. logging:
  75. level: info
  76. format: text
  77. firewall:
  78. outbound_action: drop
  79. inbound_action: drop
  80. conntrack:
  81. tcp_timeout: 12m
  82. udp_timeout: 3m
  83. default_timeout: 10m
  84. outbound:
  85. - port: any
  86. proto: any
  87. host: any
  88. inbound:
  89. - port: any
  90. proto: any
  91. host: any
  92. "@ | Out-File -FilePath "$WorkDir\lighthouse.yml" -Encoding utf8
  93. # WSL peer config (paths are POSIX, deliberately).
  94. @"
  95. pki:
  96. ca: $WslDir/ca.crt
  97. cert: $WslDir/peer.crt
  98. key: $WslDir/peer.key
  99. static_host_map:
  100. "${Ip1}": ["${WindowsIp}:$Port"]
  101. lighthouse:
  102. am_lighthouse: false
  103. interval: 60
  104. hosts:
  105. - "${Ip1}"
  106. listen:
  107. host: 0.0.0.0
  108. port: 0
  109. tun:
  110. disabled: false
  111. dev: nebula1
  112. drop_local_broadcast: false
  113. drop_multicast: false
  114. tx_queue: 500
  115. mtu: 1300
  116. logging:
  117. level: info
  118. format: text
  119. firewall:
  120. outbound_action: drop
  121. inbound_action: drop
  122. conntrack:
  123. tcp_timeout: 12m
  124. udp_timeout: 3m
  125. default_timeout: 10m
  126. outbound:
  127. - port: any
  128. proto: any
  129. host: any
  130. inbound:
  131. - port: any
  132. proto: any
  133. host: any
  134. "@ | Out-File -FilePath "$WorkDir\peer.yml" -Encoding utf8
  135. # Stage WSL artifacts. Convert Windows paths to WSL paths ourselves rather than
  136. # calling `wslpath`, because PowerShell's argument-passing to external EXEs
  137. # strips backslashes from path arguments in ways that are hard to escape around.
  138. function ConvertTo-WslPath {
  139. param([string]$WindowsPath)
  140. if ($WindowsPath -notmatch '^([A-Za-z]):\\(.*)$') {
  141. throw "cannot convert path to WSL: $WindowsPath"
  142. }
  143. return "/mnt/$($matches[1].ToLower())/$($matches[2].Replace('\','/'))"
  144. }
  145. $WslWorkDir = ConvertTo-WslPath $WorkDir
  146. $WslNebulaPath = ConvertTo-WslPath $NebulaLinux
  147. 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"
  148. # Make sure WSL has tun support and /dev/net/tun is usable before starting
  149. # nebula. Diagnostics first so a fail here points at the real problem (e.g.
  150. # WSL1 distros do not have a real kernel and will not have tun).
  151. Write-Host '=== WSL diagnostic ==='
  152. wsl --version 2>&1 | Out-Host
  153. wsl --list --verbose 2>&1 | Out-Host
  154. wsl -d $Distro -u root -- uname -a | Out-Host
  155. 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"
  156. if ($LASTEXITCODE -ne 0) { throw "failed to prepare /dev/net/tun in WSL (TUN support missing?)" }
  157. # Deliberately no New-NetFirewallRule calls here -- nebula's windows_bypass_wdf
  158. # feature is supposed to install WFP permit filters that let inbound traffic
  159. # through Windows Defender Firewall on its own. If this smoke regresses, that
  160. # feature regressed.
  161. $lhOut = Join-Path $WorkDir 'lighthouse.out.log'
  162. $lhErr = Join-Path $WorkDir 'lighthouse.err.log'
  163. $lhProc = Start-Process -FilePath $Nebula -ArgumentList @('-config', "$WorkDir\lighthouse.yml") `
  164. -PassThru -NoNewWindow `
  165. -RedirectStandardOutput $lhOut `
  166. -RedirectStandardError $lhErr
  167. # Run nebula in WSL as root with no sudo + no shell wrapper. PowerShell's
  168. # Start-Process arg quoting mangles `bash -c "..."` strings that contain
  169. # spaces/redirections, so we skip bash entirely and let Start-Process do the
  170. # stdout/stderr capture itself.
  171. $peerOut = Join-Path $WorkDir 'peer.out.log'
  172. $peerErr = Join-Path $WorkDir 'peer.err.log'
  173. $peerProc = Start-Process -FilePath 'wsl' `
  174. -ArgumentList @('-d', $Distro, '-u', 'root', '--', "$WslDir/nebula", '-config', "$WslDir/peer.yml") `
  175. -PassThru -NoNewWindow `
  176. -RedirectStandardOutput $peerOut `
  177. -RedirectStandardError $peerErr
  178. function Wait-Until {
  179. param([scriptblock]$Predicate, [int]$TimeoutSec, [string]$What)
  180. $deadline = (Get-Date).AddSeconds($TimeoutSec)
  181. while ((Get-Date) -lt $deadline) {
  182. if (& $Predicate) { return }
  183. Start-Sleep -Milliseconds 500
  184. }
  185. throw "timed out waiting for: $What"
  186. }
  187. try {
  188. Wait-Until -TimeoutSec 30 -What "windows wintun adapter $DevName with NetworkCategory=Private" -Predicate {
  189. if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before tun was ready" }
  190. $p = Get-NetConnectionProfile -InterfaceAlias $DevName -ErrorAction SilentlyContinue
  191. $p -and ("$($p.NetworkCategory)" -ieq 'Private')
  192. }
  193. Write-Host "OK: $DevName NetworkCategory=Private"
  194. Wait-Until -TimeoutSec 30 -What "WSL nebula1 with $Ip2" -Predicate {
  195. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before tun was ready" }
  196. $r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet $Ip2' && echo yes"
  197. ("$r").Trim() -eq 'yes'
  198. }
  199. Write-Host "OK: WSL nebula1 has $Ip2"
  200. Wait-Until -TimeoutSec 30 -What "ping from WSL peer to windows lighthouse ($Ip1)" -Predicate {
  201. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before ping succeeded" }
  202. $r = wsl -d $Distro -u root -- bash -c "ping -c1 -W1 $Ip1 >/dev/null 2>&1 && echo OK"
  203. ("$r").Trim() -eq 'OK'
  204. }
  205. Write-Host "OK: WSL peer -> windows lighthouse"
  206. Wait-Until -TimeoutSec 30 -What "ping from windows lighthouse to WSL peer ($Ip2)" -Predicate {
  207. $null = & ping.exe -n 1 -w 1000 $Ip2
  208. $LASTEXITCODE -eq 0
  209. }
  210. Write-Host "OK: windows lighthouse -> WSL peer"
  211. Write-Host ''
  212. Write-Host 'All smoke checks passed.'
  213. }
  214. catch {
  215. Write-Host ''
  216. Write-Host '=== lighthouse stdout ==='
  217. Get-Content $lhOut -ErrorAction SilentlyContinue | Out-Host
  218. Write-Host '=== lighthouse stderr ==='
  219. Get-Content $lhErr -ErrorAction SilentlyContinue | Out-Host
  220. Write-Host '=== peer stdout ==='
  221. Get-Content $peerOut -ErrorAction SilentlyContinue | Out-Host
  222. Write-Host '=== peer stderr ==='
  223. Get-Content $peerErr -ErrorAction SilentlyContinue | Out-Host
  224. Write-Host '=== nebula WFP filters ==='
  225. # Dump nebula-installed filters so we can verify they got registered with
  226. # the conditions we expect.
  227. $wfpDump = Join-Path $WorkDir 'wfp.xml'
  228. netsh wfp show filters file=$wfpDump 2>&1 | Out-Null
  229. if (Test-Path $wfpDump) {
  230. Select-String -Path $wfpDump -Pattern 'Nebula' -Context 0,80 -ErrorAction SilentlyContinue | Out-Host
  231. }
  232. throw
  233. }
  234. finally {
  235. if (-not $lhProc.HasExited) {
  236. Stop-Process -Id $lhProc.Id -Force -ErrorAction SilentlyContinue
  237. $lhProc.WaitForExit(5000) | Out-Null
  238. }
  239. wsl -d $Distro -u root -- bash -c "pkill -f $WslDir/nebula 2>/dev/null; true" | Out-Null
  240. # pkill returns 1 when no match and wsl propagates that; the smoke is done
  241. # so we don't want it to leak into the script's exit code.
  242. $global:LASTEXITCODE = 0
  243. if ($peerProc -and -not $peerProc.HasExited) {
  244. Stop-Process -Id $peerProc.Id -Force -ErrorAction SilentlyContinue
  245. }
  246. }