smoke-windows.ps1 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. # Dual stack on purpose: a v4-only overlay never exercises the v6 side of tun.mtu.
  46. $Ip6_1 = 'fd42:4242:241::1'
  47. $Ip6_2 = 'fd42:4242:241::2'
  48. $Mtu = 1300
  49. $Port = 4242
  50. & $NebulaCert ca -name 'smoke-ca' -out-crt "$WorkDir\ca.crt" -out-key "$WorkDir\ca.key"
  51. if ($LASTEXITCODE -ne 0) { throw "nebula-cert ca failed (exit $LASTEXITCODE)" }
  52. & $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"
  53. if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign lighthouse failed (exit $LASTEXITCODE)" }
  54. & $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"
  55. if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign peer failed (exit $LASTEXITCODE)" }
  56. # Windows lighthouse config.
  57. @"
  58. pki:
  59. ca: $WorkDir\ca.crt
  60. cert: $WorkDir\lighthouse.crt
  61. key: $WorkDir\lighthouse.key
  62. static_host_map: {}
  63. lighthouse:
  64. am_lighthouse: true
  65. interval: 60
  66. hosts: []
  67. listen:
  68. host: 0.0.0.0
  69. port: $Port
  70. tun:
  71. disabled: false
  72. dev: $DevName
  73. drop_local_broadcast: false
  74. drop_multicast: false
  75. tx_queue: 500
  76. mtu: $Mtu
  77. network_category: private
  78. logging:
  79. level: info
  80. format: text
  81. firewall:
  82. outbound_action: drop
  83. inbound_action: drop
  84. conntrack:
  85. tcp_timeout: 12m
  86. udp_timeout: 3m
  87. default_timeout: 10m
  88. outbound:
  89. - port: any
  90. proto: any
  91. host: any
  92. inbound:
  93. - port: any
  94. proto: any
  95. host: any
  96. "@ | Out-File -FilePath "$WorkDir\lighthouse.yml" -Encoding utf8
  97. # WSL peer config (paths are POSIX, deliberately).
  98. @"
  99. pki:
  100. ca: $WslDir/ca.crt
  101. cert: $WslDir/peer.crt
  102. key: $WslDir/peer.key
  103. static_host_map:
  104. "${Ip1}": ["${WindowsIp}:$Port"]
  105. lighthouse:
  106. am_lighthouse: false
  107. interval: 60
  108. hosts:
  109. - "${Ip1}"
  110. listen:
  111. host: 0.0.0.0
  112. port: 0
  113. tun:
  114. disabled: false
  115. dev: nebula1
  116. drop_local_broadcast: false
  117. drop_multicast: false
  118. tx_queue: 500
  119. mtu: $Mtu
  120. logging:
  121. level: info
  122. format: text
  123. firewall:
  124. outbound_action: drop
  125. inbound_action: drop
  126. conntrack:
  127. tcp_timeout: 12m
  128. udp_timeout: 3m
  129. default_timeout: 10m
  130. outbound:
  131. - port: any
  132. proto: any
  133. host: any
  134. inbound:
  135. - port: any
  136. proto: any
  137. host: any
  138. "@ | Out-File -FilePath "$WorkDir\peer.yml" -Encoding utf8
  139. # Stage WSL artifacts. Convert Windows paths to WSL paths ourselves rather than
  140. # calling `wslpath`, because PowerShell's argument-passing to external EXEs
  141. # strips backslashes from path arguments in ways that are hard to escape around.
  142. function ConvertTo-WslPath {
  143. param([string]$WindowsPath)
  144. if ($WindowsPath -notmatch '^([A-Za-z]):\\(.*)$') {
  145. throw "cannot convert path to WSL: $WindowsPath"
  146. }
  147. return "/mnt/$($matches[1].ToLower())/$($matches[2].Replace('\','/'))"
  148. }
  149. $WslWorkDir = ConvertTo-WslPath $WorkDir
  150. $WslNebulaPath = ConvertTo-WslPath $NebulaLinux
  151. 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"
  152. # Make sure WSL has tun support and /dev/net/tun is usable before starting
  153. # nebula. Diagnostics first so a fail here points at the real problem (e.g.
  154. # WSL1 distros do not have a real kernel and will not have tun).
  155. Write-Host '=== WSL diagnostic ==='
  156. wsl --version 2>&1 | Out-Host
  157. wsl --list --verbose 2>&1 | Out-Host
  158. wsl -d $Distro -u root -- uname -a | Out-Host
  159. 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"
  160. if ($LASTEXITCODE -ne 0) { throw "failed to prepare /dev/net/tun in WSL (TUN support missing?)" }
  161. # Deliberately no New-NetFirewallRule calls here -- nebula's windows_bypass_wdf
  162. # feature is supposed to install WFP permit filters that let inbound traffic
  163. # through Windows Defender Firewall on its own. If this smoke regresses, that
  164. # feature regressed.
  165. $lhOut = Join-Path $WorkDir 'lighthouse.out.log'
  166. $lhErr = Join-Path $WorkDir 'lighthouse.err.log'
  167. $lhProc = Start-Process -FilePath $Nebula -ArgumentList @('-config', "$WorkDir\lighthouse.yml") `
  168. -PassThru -NoNewWindow `
  169. -RedirectStandardOutput $lhOut `
  170. -RedirectStandardError $lhErr
  171. # Run nebula in WSL as root with no sudo + no shell wrapper. PowerShell's
  172. # Start-Process arg quoting mangles `bash -c "..."` strings that contain
  173. # spaces/redirections, so we skip bash entirely and let Start-Process do the
  174. # stdout/stderr capture itself.
  175. $peerOut = Join-Path $WorkDir 'peer.out.log'
  176. $peerErr = Join-Path $WorkDir 'peer.err.log'
  177. $peerProc = Start-Process -FilePath 'wsl' `
  178. -ArgumentList @('-d', $Distro, '-u', 'root', '--', "$WslDir/nebula", '-config', "$WslDir/peer.yml") `
  179. -PassThru -NoNewWindow `
  180. -RedirectStandardOutput $peerOut `
  181. -RedirectStandardError $peerErr
  182. function Wait-Until {
  183. param([scriptblock]$Predicate, [int]$TimeoutSec, [string]$What)
  184. $deadline = (Get-Date).AddSeconds($TimeoutSec)
  185. while ((Get-Date) -lt $deadline) {
  186. if (& $Predicate) { return }
  187. Start-Sleep -Milliseconds 500
  188. }
  189. throw "timed out waiting for: $What"
  190. }
  191. try {
  192. Wait-Until -TimeoutSec 30 -What "windows wintun adapter $DevName with NetworkCategory=Private" -Predicate {
  193. if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before tun was ready" }
  194. $p = Get-NetConnectionProfile -InterfaceAlias $DevName -ErrorAction SilentlyContinue
  195. $p -and ("$($p.NetworkCategory)" -ieq 'Private')
  196. }
  197. Write-Host "OK: $DevName NetworkCategory=Private"
  198. # v6 silently kept the adapter default of 65535 while v4 was correct.
  199. foreach ($family in @('IPv4', 'IPv6')) {
  200. Wait-Until -TimeoutSec 30 -What "$DevName $family NlMtu=$Mtu" -Predicate {
  201. if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before $family mtu was set" }
  202. $rows = @(Get-NetIPInterface -InterfaceAlias $DevName -AddressFamily $family -ErrorAction SilentlyContinue)
  203. $rows.Count -gt 0 -and -not ($rows | Where-Object { $_.NlMtu -ne $Mtu })
  204. }
  205. Write-Host "OK: $DevName $family NlMtu=$Mtu"
  206. }
  207. Wait-Until -TimeoutSec 30 -What "WSL nebula1 with $Ip2" -Predicate {
  208. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before tun was ready" }
  209. $r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet $Ip2' && echo yes"
  210. ("$r").Trim() -eq 'yes'
  211. }
  212. Write-Host "OK: WSL nebula1 has $Ip2"
  213. Wait-Until -TimeoutSec 30 -What "WSL nebula1 with $Ip6_2" -Predicate {
  214. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the v6 address was up" }
  215. $r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet6 $Ip6_2' && echo yes"
  216. ("$r").Trim() -eq 'yes'
  217. }
  218. Write-Host "OK: WSL nebula1 has $Ip6_2"
  219. Wait-Until -TimeoutSec 30 -What "ping from WSL peer to windows lighthouse ($Ip1)" -Predicate {
  220. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before ping succeeded" }
  221. $r = wsl -d $Distro -u root -- bash -c "ping -c1 -W1 $Ip1 >/dev/null 2>&1 && echo OK"
  222. ("$r").Trim() -eq 'OK'
  223. }
  224. Write-Host "OK: WSL peer -> windows lighthouse"
  225. Wait-Until -TimeoutSec 30 -What "ping from windows lighthouse to WSL peer ($Ip2)" -Predicate {
  226. $null = & ping.exe -n 1 -w 1000 $Ip2
  227. $LASTEXITCODE -eq 0
  228. }
  229. Write-Host "OK: windows lighthouse -> WSL peer"
  230. # Otherwise the v6 networks only prove the interface exists, not that it forwards.
  231. Wait-Until -TimeoutSec 30 -What "v6 ping from WSL peer to windows lighthouse ($Ip6_1)" -Predicate {
  232. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the v6 ping succeeded" }
  233. $r = wsl -d $Distro -u root -- bash -c "ping -6 -c1 -W1 $Ip6_1 >/dev/null 2>&1 && echo OK"
  234. ("$r").Trim() -eq 'OK'
  235. }
  236. Write-Host "OK: WSL peer -> windows lighthouse over v6"
  237. # The NlMtu check above proves the value was written, not that windows honors it. A payload this size only
  238. # crosses the tunnel as fragments cut at NlMtu on the way out of the adapter. Left at the adapter default it
  239. # reaches nebula whole, overflows the udp.MTU (9001) write buffer and is dropped, so the ping never answers.
  240. # Anything smaller would ride out as one oversized datagram and survive on IP fragmentation of the underlay.
  241. $BigPayload = 9000
  242. foreach ($target in @(@{ Family = 'v4'; Ip = $Ip2 }, @{ Family = 'v6'; Ip = $Ip6_2 })) {
  243. Wait-Until -TimeoutSec 30 -What "$($target.Family) ping with a $BigPayload byte payload from windows lighthouse to WSL peer ($($target.Ip))" -Predicate {
  244. if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before the large $($target.Family) ping succeeded" }
  245. $null = & ping.exe -n 1 -w 1000 -l $BigPayload $target.Ip
  246. $LASTEXITCODE -eq 0
  247. }
  248. Write-Host "OK: windows lighthouse -> WSL peer, $BigPayload byte $($target.Family) payload"
  249. }
  250. Write-Host ''
  251. Write-Host 'All smoke checks passed.'
  252. }
  253. catch {
  254. Write-Host ''
  255. Write-Host '=== lighthouse stdout ==='
  256. Get-Content $lhOut -ErrorAction SilentlyContinue | Out-Host
  257. Write-Host '=== lighthouse stderr ==='
  258. Get-Content $lhErr -ErrorAction SilentlyContinue | Out-Host
  259. Write-Host '=== peer stdout ==='
  260. Get-Content $peerOut -ErrorAction SilentlyContinue | Out-Host
  261. Write-Host '=== peer stderr ==='
  262. Get-Content $peerErr -ErrorAction SilentlyContinue | Out-Host
  263. Write-Host '=== nebula WFP filters ==='
  264. # Dump nebula-installed filters so we can verify they got registered with
  265. # the conditions we expect.
  266. $wfpDump = Join-Path $WorkDir 'wfp.xml'
  267. netsh wfp show filters file=$wfpDump 2>&1 | Out-Null
  268. if (Test-Path $wfpDump) {
  269. Select-String -Path $wfpDump -Pattern 'Nebula' -Context 0,80 -ErrorAction SilentlyContinue | Out-Host
  270. }
  271. throw
  272. }
  273. finally {
  274. if (-not $lhProc.HasExited) {
  275. Stop-Process -Id $lhProc.Id -Force -ErrorAction SilentlyContinue
  276. $lhProc.WaitForExit(5000) | Out-Null
  277. }
  278. wsl -d $Distro -u root -- bash -c "pkill -f $WslDir/nebula 2>/dev/null; true" | Out-Null
  279. # pkill returns 1 when no match and wsl propagates that; the smoke is done
  280. # so we don't want it to leak into the script's exit code.
  281. $global:LASTEXITCODE = 0
  282. if ($peerProc -and -not $peerProc.HasExited) {
  283. Stop-Process -Id $peerProc.Id -Force -ErrorAction SilentlyContinue
  284. }
  285. }