mirror of
https://github.com/slackhq/nebula.git
synced 2026-05-15 20:37:36 +02:00
Add a way to set the network type on windows + tests (#1710)
Some checks failed
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / freebsd-amd64 (push) Failing after 2s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 3s
smoke-extra / netbsd-amd64 (push) Failing after 3s
smoke-extra / openbsd-amd64 (push) Failing after 3s
smoke-extra / linux-386 (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 3s
Build and test / Build and test on linux with boringcrypto (push) Failing after 2s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
smoke-extra / Run windows smoke test (push) Has been cancelled
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
Some checks failed
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / freebsd-amd64 (push) Failing after 2s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 3s
smoke-extra / netbsd-amd64 (push) Failing after 3s
smoke-extra / openbsd-amd64 (push) Failing after 3s
smoke-extra / linux-386 (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 3s
Build and test / Build and test on linux with boringcrypto (push) Failing after 2s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
smoke-extra / Run windows smoke test (push) Has been cancelled
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
This commit is contained in:
49
.github/workflows/smoke-extra.yml
vendored
49
.github/workflows/smoke-extra.yml
vendored
@@ -81,3 +81,52 @@ jobs:
|
|||||||
run: make smoke-vagrant/linux-386
|
run: make smoke-vagrant/linux-386
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
smoke-windows:
|
||||||
|
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-extra')
|
||||||
|
name: Run windows smoke test
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
# WSL2 + Ubuntu so the smoke can run a real linux peer with its own
|
||||||
|
# netns. iputils-ping is needed for the in-WSL ping check. WSL1 has no
|
||||||
|
# real kernel and would lack /dev/net/tun, so we have to force WSL2.
|
||||||
|
- uses: Vampire/setup-wsl@v3
|
||||||
|
with:
|
||||||
|
distribution: Ubuntu-24.04
|
||||||
|
additional-packages: iputils-ping iproute2
|
||||||
|
|
||||||
|
# Vampire/setup-wsl provisions WSL1 even when the WSL2 platform is present.
|
||||||
|
# Convert the distro to WSL2 explicitly before we try to use /dev/net/tun.
|
||||||
|
- name: convert distro to WSL2
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
wsl --set-version Ubuntu-24.04 2
|
||||||
|
wsl --shutdown
|
||||||
|
wsl --list --verbose
|
||||||
|
|
||||||
|
- name: build windows nebula
|
||||||
|
run: make bin-windows
|
||||||
|
|
||||||
|
- name: build linux nebula for WSL
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
mkdir -p build/linux-amd64
|
||||||
|
go build -o build/linux-amd64/nebula ./cmd/nebula
|
||||||
|
|
||||||
|
- name: run smoke-windows
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: ./smoke-windows.ps1
|
||||||
|
|
||||||
|
timeout-minutes: 15
|
||||||
|
|||||||
272
.github/workflows/smoke/smoke-windows.ps1
vendored
Normal file
272
.github/workflows/smoke/smoke-windows.ps1
vendored
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
#!/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'
|
||||||
|
$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" -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" -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: 1300
|
||||||
|
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: 1300
|
||||||
|
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; 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"
|
||||||
|
|
||||||
|
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 "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"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,14 @@ listen:
|
|||||||
# max, net.core.rmem_max and net.core.wmem_max
|
# max, net.core.rmem_max and net.core.wmem_max
|
||||||
#read_buffer: 10485760
|
#read_buffer: 10485760
|
||||||
#write_buffer: 10485760
|
#write_buffer: 10485760
|
||||||
|
|
||||||
|
# On Windows only
|
||||||
|
# When true, Nebula installs a WFP (Windows Filtering Platform) PERMIT filter scoped to UDP at the listener port.
|
||||||
|
# WFP sits below Windows Defender Firewall, so this lets peer handshakes reach Nebula's outside socket regardless
|
||||||
|
# of WDF's inbound rules.
|
||||||
|
# Default true; set to false to leave WDF in charge of inbound decisions on the listener port. Not reloadable.
|
||||||
|
#windows_bypass_wdf: true
|
||||||
|
|
||||||
# By default, Nebula replies to packets it has no tunnel for with a "recv_error" packet. This packet helps speed up reconnection
|
# By default, Nebula replies to packets it has no tunnel for with a "recv_error" packet. This packet helps speed up reconnection
|
||||||
# in the case that Nebula on either side did not shut down cleanly. This response can be abused as a way to discover if Nebula is running
|
# in the case that Nebula on either side did not shut down cleanly. This response can be abused as a way to discover if Nebula is running
|
||||||
# on a host though. This option lets you configure if you want to send "recv_error" packets always, never, or only to private network remotes.
|
# on a host though. This option lets you configure if you want to send "recv_error" packets always, never, or only to private network remotes.
|
||||||
@@ -286,6 +294,24 @@ tun:
|
|||||||
# metric: 100
|
# metric: 100
|
||||||
# install: true
|
# install: true
|
||||||
|
|
||||||
|
# On Windows only, sets the network category of the nebula interface. Without this, Windows often
|
||||||
|
# leaves the network as "Unidentified" and treats it as Public, which makes the host firewall more
|
||||||
|
# restrictive than you usually want for an overlay between trusted peers. Valid values:
|
||||||
|
# private - treat the nebula network as a private/trusted network (default)
|
||||||
|
# public - treat it as a public/untrusted network
|
||||||
|
# domain - treat it as a domain-authenticated network
|
||||||
|
# unset - leave whatever Windows decided alone
|
||||||
|
# Not reloadable.
|
||||||
|
#network_category: private
|
||||||
|
|
||||||
|
# On Windows only
|
||||||
|
# When true, Nebula installs a WFP (Windows Filtering Platform) PERMIT filter scoped to the nebula adapter LUID.
|
||||||
|
# WFP sits below Windows Defender Firewall, so this lets inbound traffic through regardless of WDF rules.
|
||||||
|
# Filters are auto-removed when the adapter goes away.
|
||||||
|
# See listen.windows_bypass_wdf for the matching control over inbound to nebula's outside UDP listener.
|
||||||
|
# Default true; set to false to leave WDF in charge of inbound decisions on the nebula interface. Not reloadable.
|
||||||
|
#windows_bypass_wdf: true
|
||||||
|
|
||||||
# On linux only, set to true to manage unsafe routes directly on the system route table with gateway routes instead of
|
# On linux only, set to true to manage unsafe routes directly on the system route table with gateway routes instead of
|
||||||
# in nebula configuration files. Default false, not reloadable.
|
# in nebula configuration files. Default false, not reloadable.
|
||||||
#use_system_route_table: false
|
#use_system_route_table: false
|
||||||
|
|||||||
358
overlay/network_category_windows.go
Normal file
358
overlay/network_category_windows.go
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
//go:build !e2e_testing
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package overlay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// networkCategory mirrors NLM_NETWORK_CATEGORY from netlistmgr.h.
|
||||||
|
type networkCategory int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
networkCategoryPublic networkCategory = 0
|
||||||
|
networkCategoryPrivate networkCategory = 1
|
||||||
|
networkCategoryDomainAuthenticated networkCategory = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c networkCategory) String() string {
|
||||||
|
switch c {
|
||||||
|
case networkCategoryPublic:
|
||||||
|
return "public"
|
||||||
|
case networkCategoryPrivate:
|
||||||
|
return "private"
|
||||||
|
case networkCategoryDomainAuthenticated:
|
||||||
|
return "domain"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("unknown(%d)", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNetworkCategory accepts the user-supplied tun.network_category. A
|
||||||
|
// second return of false means "leave the category alone".
|
||||||
|
func parseNetworkCategory(s string) (networkCategory, bool, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "", "unset":
|
||||||
|
return 0, false, nil
|
||||||
|
case "public":
|
||||||
|
return networkCategoryPublic, true, nil
|
||||||
|
case "private":
|
||||||
|
return networkCategoryPrivate, true, nil
|
||||||
|
case "domain", "domainauthenticated":
|
||||||
|
return networkCategoryDomainAuthenticated, true, nil
|
||||||
|
}
|
||||||
|
return 0, false, fmt.Errorf("unknown tun.network_category %q (expected public, private, domain, or unset)", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLSID_NetworkListManager {DCB00C01-570F-4A9B-8D69-199FDBA5723B}
|
||||||
|
var clsidNetworkListManager = windows.GUID{
|
||||||
|
Data1: 0xDCB00C01, Data2: 0x570F, Data3: 0x4A9B,
|
||||||
|
Data4: [8]byte{0x8D, 0x69, 0x19, 0x9F, 0xDB, 0xA5, 0x72, 0x3B},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IID_INetworkListManager {DCB00000-570F-4A9B-8D69-199FDBA5723B}
|
||||||
|
var iidINetworkListManager = windows.GUID{
|
||||||
|
Data1: 0xDCB00000, Data2: 0x570F, Data3: 0x4A9B,
|
||||||
|
Data4: [8]byte{0x8D, 0x69, 0x19, 0x9F, 0xDB, 0xA5, 0x72, 0x3B},
|
||||||
|
}
|
||||||
|
|
||||||
|
// x/sys/windows doesn't expose CoCreateInstance, so we bind it ourselves.
|
||||||
|
var procCoCreateInstance = windows.NewLazySystemDLL("ole32.dll").NewProc("CoCreateInstance")
|
||||||
|
|
||||||
|
const clsCtxAll = windows.CLSCTX_INPROC_SERVER | windows.CLSCTX_INPROC_HANDLER |
|
||||||
|
windows.CLSCTX_LOCAL_SERVER | windows.CLSCTX_REMOTE_SERVER
|
||||||
|
|
||||||
|
const (
|
||||||
|
hrSFALSE = 0x00000001
|
||||||
|
hrRPCEChangedMode = 0x80010106
|
||||||
|
)
|
||||||
|
|
||||||
|
type hresult uint32
|
||||||
|
|
||||||
|
func (h hresult) failed() bool { return int32(h) < 0 }
|
||||||
|
func (h hresult) String() string {
|
||||||
|
return fmt.Sprintf("HRESULT 0x%08x", uint32(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
var errAdapterNotFound = errors.New("adapter not present in network connections enumeration")
|
||||||
|
|
||||||
|
// Vtable layouts. Slot order must match the declaration order in netlistmgr.h.
|
||||||
|
// All NLM interfaces here derive from IDispatch, which derives from IUnknown.
|
||||||
|
|
||||||
|
type iUnknownVtbl struct {
|
||||||
|
QueryInterface uintptr
|
||||||
|
AddRef uintptr
|
||||||
|
Release uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iDispatchVtbl struct {
|
||||||
|
iUnknownVtbl
|
||||||
|
GetTypeInfoCount uintptr
|
||||||
|
GetTypeInfo uintptr
|
||||||
|
GetIDsOfNames uintptr
|
||||||
|
Invoke uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetworkListManagerVtbl struct {
|
||||||
|
iDispatchVtbl
|
||||||
|
GetNetworks uintptr
|
||||||
|
GetNetwork uintptr
|
||||||
|
GetNetworkConnections uintptr
|
||||||
|
GetNetworkConnection uintptr
|
||||||
|
IsConnectedToInternet uintptr
|
||||||
|
IsConnected uintptr
|
||||||
|
GetConnectivity uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetworkListManager struct{ Vtbl *iNetworkListManagerVtbl }
|
||||||
|
|
||||||
|
func (n *iNetworkListManager) Release() {
|
||||||
|
syscall.SyscallN(n.Vtbl.Release, uintptr(unsafe.Pointer(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *iNetworkListManager) GetNetworkConnections() (*iEnumNetworkConnections, error) {
|
||||||
|
var enum *iEnumNetworkConnections
|
||||||
|
r1, _, _ := syscall.SyscallN(n.Vtbl.GetNetworkConnections,
|
||||||
|
uintptr(unsafe.Pointer(n)), uintptr(unsafe.Pointer(&enum)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return nil, fmt.Errorf("INetworkListManager.GetNetworkConnections: %s", hr)
|
||||||
|
}
|
||||||
|
return enum, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type iEnumNetworkConnectionsVtbl struct {
|
||||||
|
iDispatchVtbl
|
||||||
|
NewEnum uintptr
|
||||||
|
Next uintptr
|
||||||
|
Skip uintptr
|
||||||
|
Reset uintptr
|
||||||
|
Clone uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iEnumNetworkConnections struct{ Vtbl *iEnumNetworkConnectionsVtbl }
|
||||||
|
|
||||||
|
func (e *iEnumNetworkConnections) Release() {
|
||||||
|
syscall.SyscallN(e.Vtbl.Release, uintptr(unsafe.Pointer(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next connection, or (nil, nil) at the end of the enumeration.
|
||||||
|
func (e *iEnumNetworkConnections) Next() (*iNetworkConnection, error) {
|
||||||
|
var conn *iNetworkConnection
|
||||||
|
var fetched uint32
|
||||||
|
r1, _, _ := syscall.SyscallN(e.Vtbl.Next,
|
||||||
|
uintptr(unsafe.Pointer(e)), 1,
|
||||||
|
uintptr(unsafe.Pointer(&conn)), uintptr(unsafe.Pointer(&fetched)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return nil, fmt.Errorf("IEnumNetworkConnections.Next: %s", hr)
|
||||||
|
}
|
||||||
|
if fetched == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetworkConnectionVtbl struct {
|
||||||
|
iDispatchVtbl
|
||||||
|
GetNetwork uintptr
|
||||||
|
IsConnectedToInternet uintptr
|
||||||
|
IsConnected uintptr
|
||||||
|
GetConnectivity uintptr
|
||||||
|
GetConnectionId uintptr
|
||||||
|
GetAdapterId uintptr
|
||||||
|
GetDomainType uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetworkConnection struct{ Vtbl *iNetworkConnectionVtbl }
|
||||||
|
|
||||||
|
func (c *iNetworkConnection) Release() {
|
||||||
|
syscall.SyscallN(c.Vtbl.Release, uintptr(unsafe.Pointer(c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *iNetworkConnection) GetAdapterId() (windows.GUID, error) {
|
||||||
|
var g windows.GUID
|
||||||
|
r1, _, _ := syscall.SyscallN(c.Vtbl.GetAdapterId,
|
||||||
|
uintptr(unsafe.Pointer(c)), uintptr(unsafe.Pointer(&g)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return windows.GUID{}, fmt.Errorf("INetworkConnection.GetAdapterId: %s", hr)
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *iNetworkConnection) GetNetwork() (*iNetwork, error) {
|
||||||
|
var net *iNetwork
|
||||||
|
r1, _, _ := syscall.SyscallN(c.Vtbl.GetNetwork,
|
||||||
|
uintptr(unsafe.Pointer(c)), uintptr(unsafe.Pointer(&net)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return nil, fmt.Errorf("INetworkConnection.GetNetwork: %s", hr)
|
||||||
|
}
|
||||||
|
return net, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetworkVtbl struct {
|
||||||
|
iDispatchVtbl
|
||||||
|
GetName uintptr
|
||||||
|
SetName uintptr
|
||||||
|
GetDescription uintptr
|
||||||
|
SetDescription uintptr
|
||||||
|
GetNetworkId uintptr
|
||||||
|
GetDomainType uintptr
|
||||||
|
GetNetworkConnections uintptr
|
||||||
|
GetTimeCreatedAndConnected uintptr
|
||||||
|
IsConnectedToInternet uintptr
|
||||||
|
IsConnected uintptr
|
||||||
|
GetConnectivity uintptr
|
||||||
|
GetCategory uintptr
|
||||||
|
SetCategory uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type iNetwork struct{ Vtbl *iNetworkVtbl }
|
||||||
|
|
||||||
|
func (n *iNetwork) Release() {
|
||||||
|
syscall.SyscallN(n.Vtbl.Release, uintptr(unsafe.Pointer(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *iNetwork) GetCategory() (networkCategory, error) {
|
||||||
|
var c networkCategory
|
||||||
|
r1, _, _ := syscall.SyscallN(n.Vtbl.GetCategory,
|
||||||
|
uintptr(unsafe.Pointer(n)), uintptr(unsafe.Pointer(&c)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return 0, fmt.Errorf("INetwork.GetCategory: %s", hr)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *iNetwork) SetCategory(c networkCategory) error {
|
||||||
|
r1, _, _ := syscall.SyscallN(n.Vtbl.SetCategory,
|
||||||
|
uintptr(unsafe.Pointer(n)), uintptr(int32(c)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return fmt.Errorf("INetwork.SetCategory: %s", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// coInit initializes COM for the current OS thread. The returned function must
|
||||||
|
// be deferred to balance a successful init. RPC_E_CHANGED_MODE means COM is
|
||||||
|
// already initialized in a different mode on this thread, which is still fine
|
||||||
|
// for our calls but we must not Uninitialize in that case.
|
||||||
|
func coInit() (func(), error) {
|
||||||
|
err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED)
|
||||||
|
if err == nil {
|
||||||
|
return windows.CoUninitialize, nil
|
||||||
|
}
|
||||||
|
if e, ok := err.(syscall.Errno); ok {
|
||||||
|
switch uint32(e) {
|
||||||
|
case hrSFALSE:
|
||||||
|
return windows.CoUninitialize, nil
|
||||||
|
case hrRPCEChangedMode:
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CoInitializeEx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNetworkListManager() (*iNetworkListManager, error) {
|
||||||
|
var nlm *iNetworkListManager
|
||||||
|
r1, _, _ := procCoCreateInstance.Call(
|
||||||
|
uintptr(unsafe.Pointer(&clsidNetworkListManager)),
|
||||||
|
0,
|
||||||
|
uintptr(clsCtxAll),
|
||||||
|
uintptr(unsafe.Pointer(&iidINetworkListManager)),
|
||||||
|
uintptr(unsafe.Pointer(&nlm)),
|
||||||
|
)
|
||||||
|
if hr := hresult(r1); hr.failed() {
|
||||||
|
return nil, fmt.Errorf("CoCreateInstance(NetworkListManager): %s", hr)
|
||||||
|
}
|
||||||
|
return nlm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setNetworkCategory locates the network connection bound to adapterGUID and
|
||||||
|
// sets the category of its parent network. Returns errAdapterNotFound if the
|
||||||
|
// adapter is not yet visible in the NLM enumeration.
|
||||||
|
func setNetworkCategory(adapterGUID windows.GUID, cat networkCategory) error {
|
||||||
|
deinit, err := coInit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer deinit()
|
||||||
|
|
||||||
|
nlm, err := createNetworkListManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer nlm.Release()
|
||||||
|
|
||||||
|
enum, err := nlm.GetNetworkConnections()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer enum.Release()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := enum.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
return errAdapterNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
guid, err := conn.GetAdapterId()
|
||||||
|
if err != nil || guid != adapterGUID {
|
||||||
|
conn.Release()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
net, err := conn.GetNetwork()
|
||||||
|
conn.Release()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = net.SetCategory(cat)
|
||||||
|
net.Release()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyNetworkCategory polls until the wintun adapter shows up in the NLM
|
||||||
|
// enumeration, then sets the category. Intended to run in its own goroutine.
|
||||||
|
func applyNetworkCategory(l *slog.Logger, adapterGUID windows.GUID, cat networkCategory) {
|
||||||
|
// COM Init/Uninit must be paired on the same OS thread.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
const (
|
||||||
|
attempts = 30
|
||||||
|
interval = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
for i := 0; i < attempts; i++ {
|
||||||
|
err := setNetworkCategory(adapterGUID, cat)
|
||||||
|
if err == nil {
|
||||||
|
l.Info("Set Windows network category", "category", cat.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errAdapterNotFound) {
|
||||||
|
l.Warn("Failed to set Windows network category", "error", err, "category", cat.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(interval)
|
||||||
|
}
|
||||||
|
l.Warn("Gave up waiting for adapter to appear in NLM enumeration; network category not set",
|
||||||
|
"category", cat.String(),
|
||||||
|
"waited", time.Duration(attempts)*interval,
|
||||||
|
)
|
||||||
|
}
|
||||||
109
overlay/network_category_windows_test.go
Normal file
109
overlay/network_category_windows_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
//go:build !e2e_testing
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package overlay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseNetworkCategory(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
wantCat networkCategory
|
||||||
|
wantApply bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"", 0, false, false},
|
||||||
|
{"unset", 0, false, false},
|
||||||
|
{" UNSET ", 0, false, false},
|
||||||
|
{"private", networkCategoryPrivate, true, false},
|
||||||
|
{"Private", networkCategoryPrivate, true, false},
|
||||||
|
{" PRIVATE ", networkCategoryPrivate, true, false},
|
||||||
|
{"public", networkCategoryPublic, true, false},
|
||||||
|
{"PUBLIC", networkCategoryPublic, true, false},
|
||||||
|
{"domain", networkCategoryDomainAuthenticated, true, false},
|
||||||
|
{"DomainAuthenticated", networkCategoryDomainAuthenticated, true, false},
|
||||||
|
{"garbage", 0, false, true},
|
||||||
|
{"privates", 0, false, true},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
cat, apply, err := parseNetworkCategory(tc.in)
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("parseNetworkCategory(%q) err=%v, wantErr=%v", tc.in, err, tc.wantErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cat != tc.wantCat || apply != tc.wantApply {
|
||||||
|
t.Errorf("parseNetworkCategory(%q) = (%v, %v), want (%v, %v)", tc.in, cat, apply, tc.wantCat, tc.wantApply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_NLM_round_trip exercises every COM call path used by setNetworkCategory
|
||||||
|
// without mutating the host's network state. It validates the CLSID/IID
|
||||||
|
// constants and every vtable index by enumerating connections, fetching the
|
||||||
|
// adapter id and parent network, reading the current category, and writing it
|
||||||
|
// back unchanged.
|
||||||
|
//
|
||||||
|
// Requires Windows but does not require admin or the wintun driver. Skips if
|
||||||
|
// no network connections are available (unlikely outside of an isolated
|
||||||
|
// container).
|
||||||
|
func Test_NLM_round_trip(t *testing.T) {
|
||||||
|
deinit, err := coInit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("coInit: %v", err)
|
||||||
|
}
|
||||||
|
defer deinit()
|
||||||
|
|
||||||
|
nlm, err := createNetworkListManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createNetworkListManager: %v", err)
|
||||||
|
}
|
||||||
|
defer nlm.Release()
|
||||||
|
|
||||||
|
enum, err := nlm.GetNetworkConnections()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetNetworkConnections: %v", err)
|
||||||
|
}
|
||||||
|
defer enum.Release()
|
||||||
|
|
||||||
|
saw := 0
|
||||||
|
for {
|
||||||
|
conn, err := enum.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnumNetworkConnections.Next: %v", err)
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
saw++
|
||||||
|
|
||||||
|
if _, err := conn.GetAdapterId(); err != nil {
|
||||||
|
conn.Release()
|
||||||
|
t.Fatalf("INetworkConnection.GetAdapterId: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
net, err := conn.GetNetwork()
|
||||||
|
conn.Release()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("INetworkConnection.GetNetwork: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cat, err := net.GetCategory()
|
||||||
|
if err != nil {
|
||||||
|
net.Release()
|
||||||
|
t.Fatalf("INetwork.GetCategory: %v", err)
|
||||||
|
}
|
||||||
|
// Set to the current value so the host's NLM state is unchanged but
|
||||||
|
// SetCategory's vtable slot is still validated end-to-end.
|
||||||
|
if err := net.SetCategory(cat); err != nil {
|
||||||
|
net.Release()
|
||||||
|
t.Fatalf("INetwork.SetCategory(%v): %v", cat, err)
|
||||||
|
}
|
||||||
|
net.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if saw == 0 {
|
||||||
|
t.Skip("no NLM network connections available; skipping round-trip")
|
||||||
|
}
|
||||||
|
}
|
||||||
23
overlay/tun_bypass_windows.go
Normal file
23
overlay/tun_bypass_windows.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build (amd64 || arm64) && !e2e_testing
|
||||||
|
// +build amd64 arm64
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package overlay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/wfp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// installInterfaceBypass installs a WFP PERMIT filter scoped to the wintun interface LUID so inbound traffic on the
|
||||||
|
// nebula adapter bypasses Windows Defender Firewall.
|
||||||
|
func installInterfaceBypass(l *slog.Logger, luid uint64) closer {
|
||||||
|
s, err := wfp.PermitInterface(luid)
|
||||||
|
if err != nil {
|
||||||
|
l.Warn("Failed to install WFP bypass filters on nebula interface", "error", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l.Info("Installed WFP filters bypassing Windows Defender Firewall on nebula interface")
|
||||||
|
return s
|
||||||
|
}
|
||||||
11
overlay/tun_bypass_windows_386.go
Normal file
11
overlay/tun_bypass_windows_386.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !e2e_testing
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package overlay
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
// installInterfaceBypass is a no-op on windows-386 because we don't currently build for it.
|
||||||
|
func installInterfaceBypass(_ *slog.Logger, _ uint64) closer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -25,15 +25,24 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type closer interface {
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
const tunGUIDLabel = "Fixed Nebula Windows GUID v1"
|
const tunGUIDLabel = "Fixed Nebula Windows GUID v1"
|
||||||
|
|
||||||
type winTun struct {
|
type winTun struct {
|
||||||
Device string
|
Device string
|
||||||
vpnNetworks []netip.Prefix
|
vpnNetworks []netip.Prefix
|
||||||
MTU int
|
MTU int
|
||||||
Routes atomic.Pointer[[]Route]
|
Routes atomic.Pointer[[]Route]
|
||||||
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
||||||
l *slog.Logger
|
guid windows.GUID
|
||||||
|
networkCategory networkCategory
|
||||||
|
setCategory bool
|
||||||
|
bypassWDF bool
|
||||||
|
wdfBypass closer
|
||||||
|
l *slog.Logger
|
||||||
|
|
||||||
tun *wintun.NativeTun
|
tun *wintun.NativeTun
|
||||||
}
|
}
|
||||||
@@ -54,11 +63,20 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*w
|
|||||||
return nil, fmt.Errorf("generate GUID failed: %w", err)
|
return nil, fmt.Errorf("generate GUID failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cat, setCat, err := parseNetworkCategory(c.GetString("tun.network_category", "private"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
t := &winTun{
|
t := &winTun{
|
||||||
Device: deviceName,
|
Device: deviceName,
|
||||||
vpnNetworks: vpnNetworks,
|
vpnNetworks: vpnNetworks,
|
||||||
MTU: c.GetInt("tun.mtu", DefaultMTU),
|
MTU: c.GetInt("tun.mtu", DefaultMTU),
|
||||||
l: l,
|
guid: *guid,
|
||||||
|
networkCategory: cat,
|
||||||
|
setCategory: setCat,
|
||||||
|
bypassWDF: c.GetBool("tun.windows_bypass_wdf", true),
|
||||||
|
l: l,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.reload(c, true)
|
err = t.reload(c, true)
|
||||||
@@ -142,6 +160,17 @@ func (t *winTun) Activate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.setCategory {
|
||||||
|
// The wintun adapter takes a moment to register with the Network List
|
||||||
|
// Manager, so we apply the category in the background and retry until
|
||||||
|
// it shows up.
|
||||||
|
go applyNetworkCategory(t.l, t.guid, t.networkCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.bypassWDF {
|
||||||
|
t.wdfBypass = installInterfaceBypass(t.l, uint64(t.tun.LUID()))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +284,11 @@ func (t *winTun) Close() error {
|
|||||||
_ = luid.FlushDNS(windows.AF_INET)
|
_ = luid.FlushDNS(windows.AF_INET)
|
||||||
_ = luid.FlushDNS(windows.AF_INET6)
|
_ = luid.FlushDNS(windows.AF_INET6)
|
||||||
|
|
||||||
|
if t.wdfBypass != nil {
|
||||||
|
t.wdfBypass.Close()
|
||||||
|
t.wdfBypass = nil
|
||||||
|
}
|
||||||
|
|
||||||
return t.tun.Close()
|
return t.tun.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ package udp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ package udp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
57
udp/udp_bypass_windows.go
Normal file
57
udp/udp_bypass_windows.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//go:build (amd64 || arm64) && !e2e_testing
|
||||||
|
// +build amd64 arm64
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/wfp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapWithWDFBypass wraps a Conn so that the first ReloadConfig consults listen.windows_bypass_wdf
|
||||||
|
// and installs a WFP PERMIT filter for the listener's bound UDP port. The session is released when Close runs.
|
||||||
|
func wrapWithWDFBypass(l *slog.Logger, conn Conn) Conn {
|
||||||
|
return &bypassConn{Conn: conn, l: l}
|
||||||
|
}
|
||||||
|
|
||||||
|
type bypassConn struct {
|
||||||
|
Conn
|
||||||
|
|
||||||
|
l *slog.Logger
|
||||||
|
installOnce sync.Once
|
||||||
|
session *wfp.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bypassConn) ReloadConfig(c *config.C) {
|
||||||
|
b.installOnce.Do(func() {
|
||||||
|
if !c.GetBool("listen.windows_bypass_wdf", true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr, err := b.Conn.LocalAddr()
|
||||||
|
if err != nil {
|
||||||
|
b.l.Warn("Failed to query listener port for WFP bypass", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s, err := wfp.PermitUDPPort(addr.Port())
|
||||||
|
if err != nil {
|
||||||
|
b.l.Warn("Failed to install WFP bypass filters for listener", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.l.Info("Installed WFP filters bypassing Windows Defender Firewall on UDP listener port",
|
||||||
|
"port", addr.Port())
|
||||||
|
b.session = s
|
||||||
|
})
|
||||||
|
b.Conn.ReloadConfig(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bypassConn) Close() error {
|
||||||
|
if b.session != nil {
|
||||||
|
b.session.Close()
|
||||||
|
b.session = nil
|
||||||
|
}
|
||||||
|
return b.Conn.Close()
|
||||||
|
}
|
||||||
11
udp/udp_bypass_windows_386.go
Normal file
11
udp/udp_bypass_windows_386.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !e2e_testing
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
package udp
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
// wrapWithWDFBypass is a no-op on windows-386 since we don't currently build for it.
|
||||||
|
func wrapWithWDFBypass(_ *slog.Logger, conn Conn) Conn {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
@@ -7,12 +7,11 @@ package udp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,18 @@ func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int)
|
|||||||
return nil, fmt.Errorf("multiple udp listeners not supported on windows")
|
return nil, fmt.Errorf("multiple udp listeners not supported on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var conn Conn
|
||||||
rc, err := NewRIOListener(l, ip, port)
|
rc, err := NewRIOListener(l, ip, port)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return rc, nil
|
conn = rc
|
||||||
|
} else {
|
||||||
|
l.Error("Falling back to standard udp sockets", "error", err)
|
||||||
|
conn, err = NewGenericListener(l, ip, port, multi, batch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return wrapWithWDFBypass(l, conn), nil
|
||||||
l.Error("Falling back to standard udp sockets", "error", err)
|
|
||||||
return NewGenericListener(l, ip, port, multi, batch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListenConfig(multi bool) net.ListenConfig {
|
func NewListenConfig(multi bool) net.ListenConfig {
|
||||||
|
|||||||
377
wfp/wfp_windows.go
Normal file
377
wfp/wfp_windows.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
//go:build (amd64 || arm64) && !e2e_testing
|
||||||
|
// +build amd64 arm64
|
||||||
|
// +build !e2e_testing
|
||||||
|
|
||||||
|
// Package wfp installs Windows Filtering Platform (WFP) PERMIT filters in a dynamic, session-scoped sublayer.
|
||||||
|
// Because WFP sits below Windows Defender Firewall, a high-weight permit at FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4/V6 lets
|
||||||
|
// the matching inbound traffic through regardless of WDF rules.
|
||||||
|
//
|
||||||
|
// Each Session owns its own engine handle. When the handle closes, every dynamic object added during the session
|
||||||
|
// is auto-deleted by Windows, so there are no orphaned filters.
|
||||||
|
//
|
||||||
|
// Type definitions and constants are derived from the wireguard-windows firewall package (MIT).
|
||||||
|
// Only the subset we exercise is reproduced.
|
||||||
|
package wfp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM layer GUIDs (fwpmu.h).
|
||||||
|
//
|
||||||
|
// FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = e1cd9fe7-f4b5-4273-96c0-592e487b8650
|
||||||
|
// FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = a3b42c97-9f04-4672-b87e-cee9c483257f
|
||||||
|
var (
|
||||||
|
fwpmLayerAleAuthRecvAcceptV4 = windows.GUID{
|
||||||
|
Data1: 0xe1cd9fe7, Data2: 0xf4b5, Data3: 0x4273,
|
||||||
|
Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50},
|
||||||
|
}
|
||||||
|
fwpmLayerAleAuthRecvAcceptV6 = windows.GUID{
|
||||||
|
Data1: 0xa3b42c97, Data2: 0x9f04, Data3: 0x4672,
|
||||||
|
Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWPM_CONDITION_IP_LOCAL_INTERFACE = 4cd62a49-59c3-4969-b7f3-bda5d32890a4
|
||||||
|
var fwpmConditionIPLocalInterface = windows.GUID{
|
||||||
|
Data1: 0x4cd62a49, Data2: 0x59c3, Data3: 0x4969,
|
||||||
|
Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_CONDITION_IP_PROTOCOL = 3971ef2b-623e-4f9a-8cb1-6e79b806b9a7
|
||||||
|
var fwpmConditionIPProtocol = windows.GUID{
|
||||||
|
Data1: 0x3971ef2b, Data2: 0x623e, Data3: 0x4f9a,
|
||||||
|
Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7},
|
||||||
|
}
|
||||||
|
|
||||||
|
// FWPM_CONDITION_IP_LOCAL_PORT = 0c1ba1af-5765-453f-af22-a8f791ac775b
|
||||||
|
var fwpmConditionIPLocalPort = windows.GUID{
|
||||||
|
Data1: 0x0c1ba1af, Data2: 0x5765, Data3: 0x453f,
|
||||||
|
Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPPROTO_UDP from in.h.
|
||||||
|
const ipprotoUDP uint8 = 17
|
||||||
|
|
||||||
|
// FWP_ACTION_TYPE values (fwptypes.h). PERMIT is terminating.
|
||||||
|
const fwpActionPermit uint32 = 0x00001002 // 0x2 | FWP_ACTION_FLAG_TERMINATING(0x1000)
|
||||||
|
|
||||||
|
// FWP_DATA_TYPE values we use.
|
||||||
|
const (
|
||||||
|
fwpEmpty uint32 = 0
|
||||||
|
fwpUint8 uint32 = 1
|
||||||
|
fwpUint16 uint32 = 2
|
||||||
|
fwpUint64 uint32 = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// FWP_MATCH_TYPE values.
|
||||||
|
const fwpMatchEqual uint32 = 0
|
||||||
|
|
||||||
|
// FWPM_SESSION flags.
|
||||||
|
const fwpmSessionFlagDynamic uint32 = 0x1
|
||||||
|
|
||||||
|
// FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT prevents lower-priority filters in other sublayers,
|
||||||
|
// notably Windows Defender Firewall's MPSSVC_WF sublayer, which shares our 0xFFFF weight from overriding this PERMIT.
|
||||||
|
// Without it, a default WDF block at the same sublayer weight can still win arbitration.
|
||||||
|
const fwpmFilterFlagClearActionRight uint32 = 0x8
|
||||||
|
|
||||||
|
// RPC authentication.
|
||||||
|
// RPC_C_AUTHN_WINNT works on workgroup machines with no domain context
|
||||||
|
// RPC_C_AUTHN_DEFAULT falls back through a chain that can land on something WFP doesn't accept on a fresh box.
|
||||||
|
const rpcCAuthnWinNT uint32 = 10
|
||||||
|
|
||||||
|
// fwpByteBlob (FWP_BYTE_BLOB). 16 bytes on 64-bit.
|
||||||
|
type fwpByteBlob struct {
|
||||||
|
size uint32
|
||||||
|
_ uint32 // padding
|
||||||
|
data *uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpValue0 / FWP_CONDITION_VALUE0 layout. 16 bytes on 64-bit.
|
||||||
|
// The union is pointer-sized; types <= 32 bits (UINT8/16/32, INT8/16/32, float) live inline in the low bytes
|
||||||
|
// of `value`, while UINT64/INT64/double and aggregate types are stored *by pointer*, even on 64-bit, where the
|
||||||
|
// union member is declared as UINT64*. So when populating an FWP_UINT64 condition, pass
|
||||||
|
// uintptr(unsafe.Pointer(&luidVar)) instead of the LUID inline.
|
||||||
|
type fwpValue0 struct {
|
||||||
|
type_ uint32
|
||||||
|
_ uint32 // padding before union to 8-byte alignment
|
||||||
|
value uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmDisplayData0 / FWPM_DISPLAY_DATA0. 16 bytes on 64-bit.
|
||||||
|
type fwpmDisplayData0 struct {
|
||||||
|
name *uint16
|
||||||
|
description *uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmAction0 / FWPM_ACTION0. 20 bytes; no leading padding because actionType
|
||||||
|
// is uint32 and GUID's first field is uint32.
|
||||||
|
type fwpmAction0 struct {
|
||||||
|
actionType uint32
|
||||||
|
filterType windows.GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmFilterCondition0. 40 bytes on 64-bit.
|
||||||
|
type fwpmFilterCondition0 struct {
|
||||||
|
fieldKey windows.GUID // 16
|
||||||
|
matchType uint32 // 4
|
||||||
|
_ uint32 // 4 padding
|
||||||
|
conditionValue fwpValue0 // 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmFilter0. 200 bytes on 64-bit.
|
||||||
|
type fwpmFilter0 struct {
|
||||||
|
filterKey windows.GUID
|
||||||
|
displayData fwpmDisplayData0
|
||||||
|
flags uint32
|
||||||
|
_ uint32 // padding before *GUID
|
||||||
|
providerKey *windows.GUID
|
||||||
|
providerData fwpByteBlob
|
||||||
|
layerKey windows.GUID
|
||||||
|
subLayerKey windows.GUID
|
||||||
|
weight fwpValue0
|
||||||
|
numFilterConditions uint32
|
||||||
|
_ uint32 // padding before pointer
|
||||||
|
filterCondition *fwpmFilterCondition0
|
||||||
|
action fwpmAction0
|
||||||
|
_ [4]byte // layout correction
|
||||||
|
providerContextKey windows.GUID
|
||||||
|
reserved *windows.GUID
|
||||||
|
filterID uint64
|
||||||
|
effectiveWeight fwpValue0
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmSublayer0. 72 bytes on 64-bit.
|
||||||
|
type fwpmSublayer0 struct {
|
||||||
|
subLayerKey windows.GUID
|
||||||
|
displayData fwpmDisplayData0
|
||||||
|
flags uint32
|
||||||
|
_ uint32 // padding before *GUID
|
||||||
|
providerKey *windows.GUID
|
||||||
|
providerData fwpByteBlob
|
||||||
|
weight uint16
|
||||||
|
_ [6]byte // padding to 72 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpmSession0. 72 bytes on 64-bit.
|
||||||
|
type fwpmSession0 struct {
|
||||||
|
sessionKey windows.GUID
|
||||||
|
displayData fwpmDisplayData0
|
||||||
|
flags uint32
|
||||||
|
txnWaitTimeoutInMSec uint32
|
||||||
|
processId uint32
|
||||||
|
_ uint32 // padding before *SID
|
||||||
|
sid *windows.SID
|
||||||
|
username *uint16
|
||||||
|
kernelMode uint8
|
||||||
|
_ [7]byte // tail padding
|
||||||
|
}
|
||||||
|
|
||||||
|
// fwpuclnt.dll bindings. Only the calls we use.
|
||||||
|
var (
|
||||||
|
modFwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll")
|
||||||
|
procFwpmEngineOpen0 = modFwpuclnt.NewProc("FwpmEngineOpen0")
|
||||||
|
procFwpmEngineClose0 = modFwpuclnt.NewProc("FwpmEngineClose0")
|
||||||
|
procFwpmSubLayerAdd0 = modFwpuclnt.NewProc("FwpmSubLayerAdd0")
|
||||||
|
procFwpmFilterAdd0 = modFwpuclnt.NewProc("FwpmFilterAdd0")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session holds the WFP engine handle for a single bypass operation. The handle owns a dynamic session:
|
||||||
|
// when it is closed, every WFP object added during the session (sublayer + filters) is automatically deleted by
|
||||||
|
// Windows. That gives us correct cleanup even if the host process is killed hard between Permit* and Close.
|
||||||
|
type Session struct {
|
||||||
|
engine uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the engine handle. Windows deletes every dynamic object (sublayer + filters) the session installed.
|
||||||
|
// Safe to call on a nil receiver.
|
||||||
|
func (s *Session) Close() {
|
||||||
|
if s == nil || s.engine == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
procFwpmEngineClose0.Call(s.engine)
|
||||||
|
s.engine = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermitInterface installs PERMIT filters at FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 and _V6 scoped to the given network
|
||||||
|
// interface LUID. Inbound traffic on that interface bypasses Windows Defender Firewall.
|
||||||
|
func PermitInterface(luid uint64) (*Session, error) {
|
||||||
|
s, sublayerKey, err := newSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addInterfaceFilter(s.engine, sublayerKey, fwpmLayerAleAuthRecvAcceptV4, luid); err != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, fmt.Errorf("add v4 filter: %w", err)
|
||||||
|
}
|
||||||
|
if err := addInterfaceFilter(s.engine, sublayerKey, fwpmLayerAleAuthRecvAcceptV6, luid); err != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, fmt.Errorf("add v6 filter: %w", err)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermitUDPPort installs PERMIT filters at FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 and _V6 scoped to UDP traffic with the
|
||||||
|
// given local port. Inbound UDP to that port on any interface bypasses Windows Defender Firewall.
|
||||||
|
func PermitUDPPort(port uint16) (*Session, error) {
|
||||||
|
s, sublayerKey, err := newSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addUDPPortFilter(s.engine, sublayerKey, fwpmLayerAleAuthRecvAcceptV4, port); err != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, fmt.Errorf("add v4 filter: %w", err)
|
||||||
|
}
|
||||||
|
if err := addUDPPortFilter(s.engine, sublayerKey, fwpmLayerAleAuthRecvAcceptV6, port); err != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, fmt.Errorf("add v6 filter: %w", err)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession() (*Session, windows.GUID, error) {
|
||||||
|
engine, err := openDynamicEngine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, windows.GUID{}, err
|
||||||
|
}
|
||||||
|
sublayerKey, err := registerSublayer(engine)
|
||||||
|
if err != nil {
|
||||||
|
procFwpmEngineClose0.Call(engine)
|
||||||
|
return nil, windows.GUID{}, err
|
||||||
|
}
|
||||||
|
return &Session{engine: engine}, sublayerKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDynamicEngine() (uintptr, error) {
|
||||||
|
session := fwpmSession0{flags: fwpmSessionFlagDynamic}
|
||||||
|
var engine uintptr
|
||||||
|
r1, _, _ := procFwpmEngineOpen0.Call(
|
||||||
|
0, // serverName == NULL (local)
|
||||||
|
uintptr(rpcCAuthnWinNT),
|
||||||
|
0, // authIdentity == NULL
|
||||||
|
uintptr(unsafe.Pointer(&session)),
|
||||||
|
uintptr(unsafe.Pointer(&engine)),
|
||||||
|
)
|
||||||
|
if r1 != 0 {
|
||||||
|
return 0, fmt.Errorf("FwpmEngineOpen0: 0x%x", r1)
|
||||||
|
}
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerSublayer adds a session-scoped sublayer with a freshly generated GUID, weight 0xFFFF so its filters arbitrate
|
||||||
|
// above WDF's default sublayer. The sublayer is dynamic (no PERSISTENT flag) and goes away when the engine handle closes.
|
||||||
|
func registerSublayer(engine uintptr) (windows.GUID, error) {
|
||||||
|
key, err := windows.GenerateGUID()
|
||||||
|
if err != nil {
|
||||||
|
return windows.GUID{}, fmt.Errorf("GenerateGUID for sublayer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, _ := windows.UTF16PtrFromString("Nebula WDF bypass sublayer")
|
||||||
|
desc, _ := windows.UTF16PtrFromString("Permit filters bypassing Windows Defender Firewall")
|
||||||
|
sl := fwpmSublayer0{
|
||||||
|
subLayerKey: key,
|
||||||
|
displayData: fwpmDisplayData0{name: name, description: desc},
|
||||||
|
weight: 0xFFFF,
|
||||||
|
}
|
||||||
|
r1, _, _ := procFwpmSubLayerAdd0.Call(
|
||||||
|
engine,
|
||||||
|
uintptr(unsafe.Pointer(&sl)),
|
||||||
|
0, // sd == NULL
|
||||||
|
)
|
||||||
|
if r1 != 0 {
|
||||||
|
return windows.GUID{}, fmt.Errorf("FwpmSubLayerAdd0: 0x%x", r1)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addInterfaceFilter(engine uintptr, sublayerKey, layer windows.GUID, luid uint64) error {
|
||||||
|
name, _ := windows.UTF16PtrFromString("Nebula allow interface inbound")
|
||||||
|
desc, _ := windows.UTF16PtrFromString("Permits inbound traffic on a nebula interface")
|
||||||
|
|
||||||
|
// luid must remain addressable through the syscall -- FWP_UINT64 is stored
|
||||||
|
// by pointer in the FWP_VALUE0 union.
|
||||||
|
cond := fwpmFilterCondition0{
|
||||||
|
fieldKey: fwpmConditionIPLocalInterface,
|
||||||
|
matchType: fwpMatchEqual,
|
||||||
|
conditionValue: fwpValue0{
|
||||||
|
type_: fwpUint64,
|
||||||
|
value: uintptr(unsafe.Pointer(&luid)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := fwpmFilter0{
|
||||||
|
// filterKey left zero: WFP assigns one when the filter is added.
|
||||||
|
displayData: fwpmDisplayData0{name: name, description: desc},
|
||||||
|
flags: fwpmFilterFlagClearActionRight,
|
||||||
|
layerKey: layer,
|
||||||
|
subLayerKey: sublayerKey,
|
||||||
|
weight: fwpValue0{type_: fwpUint8, value: uintptr(15)},
|
||||||
|
numFilterConditions: 1,
|
||||||
|
filterCondition: &cond,
|
||||||
|
action: fwpmAction0{actionType: fwpActionPermit},
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, _ := procFwpmFilterAdd0.Call(
|
||||||
|
engine,
|
||||||
|
uintptr(unsafe.Pointer(&filter)),
|
||||||
|
0, // sd == NULL
|
||||||
|
0, // id == NULL
|
||||||
|
)
|
||||||
|
if r1 != 0 {
|
||||||
|
return fmt.Errorf("FwpmFilterAdd0: 0x%x", r1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addUDPPortFilter installs a PERMIT filter that matches (IP_PROTOCOL == UDP) AND (IP_LOCAL_PORT == port).
|
||||||
|
// FWP_UINT8 and FWP_UINT16 are <= 32 bits so they live inline in the FWP_VALUE0 union.
|
||||||
|
func addUDPPortFilter(engine uintptr, sublayerKey, layer windows.GUID, port uint16) error {
|
||||||
|
name, _ := windows.UTF16PtrFromString("Nebula allow UDP port inbound")
|
||||||
|
desc, _ := windows.UTF16PtrFromString("Permits inbound UDP to a nebula listener port")
|
||||||
|
|
||||||
|
conds := [2]fwpmFilterCondition0{
|
||||||
|
{
|
||||||
|
fieldKey: fwpmConditionIPProtocol,
|
||||||
|
matchType: fwpMatchEqual,
|
||||||
|
conditionValue: fwpValue0{
|
||||||
|
type_: fwpUint8,
|
||||||
|
value: uintptr(ipprotoUDP),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldKey: fwpmConditionIPLocalPort,
|
||||||
|
matchType: fwpMatchEqual,
|
||||||
|
conditionValue: fwpValue0{
|
||||||
|
type_: fwpUint16,
|
||||||
|
value: uintptr(port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := fwpmFilter0{
|
||||||
|
displayData: fwpmDisplayData0{name: name, description: desc},
|
||||||
|
flags: fwpmFilterFlagClearActionRight,
|
||||||
|
layerKey: layer,
|
||||||
|
subLayerKey: sublayerKey,
|
||||||
|
weight: fwpValue0{type_: fwpUint8, value: uintptr(15)},
|
||||||
|
numFilterConditions: 2,
|
||||||
|
filterCondition: &conds[0],
|
||||||
|
action: fwpmAction0{actionType: fwpActionPermit},
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, _ := procFwpmFilterAdd0.Call(
|
||||||
|
engine,
|
||||||
|
uintptr(unsafe.Pointer(&filter)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if r1 != 0 {
|
||||||
|
return fmt.Errorf("FwpmFilterAdd0: 0x%x", r1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user