mirror of
https://github.com/slackhq/nebula.git
synced 2026-09-30 12:26:38 +02:00
Follow-up to #1871, which put a handle on the v6 interface for the first time. Nothing answers on the far side of the adapter but nebula, and nebula drops the probes. Duplicate address detection only holds the overlay address tentative for a round it can never lose, and router solicitations leave for a multicast group nebula has no host for. Both come off on the v6 handle, the same treatment wireguard-windows gives its adapter. The v4 handle is untouched: the adapter has no link layer for ARP-based conflict detection, and #1871 deliberately left the v4-only path alone. Smoke asserts DadTransmits=0 and RouterDiscovery=Disabled on the v6 interface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J34ExJUUPKWUvr2g5GMREi
310 lines
12 KiB
PowerShell
310 lines
12 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"
|
|
}
|
|
|
|
# Both are set on the same handle as the v6 NlMtu, so by now they are either applied or never will be.
|
|
Wait-Until -TimeoutSec 30 -What "$DevName IPv6 DadTransmits=0 RouterDiscovery=Disabled" -Predicate {
|
|
if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before the v6 interface was configured" }
|
|
$rows = @(Get-NetIPInterface -InterfaceAlias $DevName -AddressFamily IPv6 -ErrorAction SilentlyContinue)
|
|
$rows.Count -gt 0 -and -not ($rows | Where-Object { $_.DadTransmits -ne 0 -or "$($_.RouterDiscovery)" -ne 'Disabled' })
|
|
}
|
|
Write-Host "OK: $DevName IPv6 DadTransmits=0 RouterDiscovery=Disabled"
|
|
|
|
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"
|
|
|
|
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
|
|
}
|
|
}
|