mirror of
https://github.com/slackhq/nebula.git
synced 2026-10-08 10:57:54 +02:00
Follow-up to #1871. The Get-NetIPInterface check proves the value was written, not that windows fragments to it. A 9000 byte payload 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, which is why a payload that is merely larger than tun.mtu would not catch a regression. Both families are pinged from the windows side so the v4 NlMtu, which was always right, gets the same guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J34ExJUUPKWUvr2g5GMREi
316 lines
13 KiB
PowerShell
316 lines
13 KiB
PowerShell
#!/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
|
|
}
|
|
}
|