diff --git a/.github/workflows/smoke/smoke-windows.ps1 b/.github/workflows/smoke/smoke-windows.ps1 index e9eedb9c..0436598d 100644 --- a/.github/workflows/smoke/smoke-windows.ps1 +++ b/.github/workflows/smoke/smoke-windows.ps1 @@ -247,6 +247,14 @@ catch { 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 { diff --git a/overlay/tun_bypass_windows_386.go b/overlay/tun_bypass_windows_386.go index 515b32ce..366430b0 100644 --- a/overlay/tun_bypass_windows_386.go +++ b/overlay/tun_bypass_windows_386.go @@ -5,9 +5,7 @@ package overlay import "log/slog" -// installInterfaceBypass is a no-op on windows-386. WFP support relies on -// 64-bit pointer-sized struct layouts that don't translate cleanly to 32-bit, -// and windows-386 isn't a release target. +// installInterfaceBypass is a no-op on windows-386 because we don't currently build for it. func installInterfaceBypass(_ *slog.Logger, _ uint64) closer { return nil } diff --git a/wfp/wfp_windows.go b/wfp/wfp_windows.go index 9666938e..22aa0565 100644 --- a/wfp/wfp_windows.go +++ b/wfp/wfp_windows.go @@ -73,8 +73,15 @@ 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. -const rpcCAuthnDefault uint32 = 0xFFFFFFFF +// 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 { @@ -245,7 +252,7 @@ func openDynamicEngine() (uintptr, error) { var engine uintptr r1, _, _ := procFwpmEngineOpen0.Call( 0, // serverName == NULL (local) - uintptr(rpcCAuthnDefault), + uintptr(rpcCAuthnWinNT), 0, // authIdentity == NULL uintptr(unsafe.Pointer(&session)), uintptr(unsafe.Pointer(&engine)), @@ -300,6 +307,7 @@ func addInterfaceFilter(engine uintptr, sublayerKey, layer windows.GUID, luid ui 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)}, @@ -347,6 +355,7 @@ func addUDPPortFilter(engine uintptr, sublayerKey, layer windows.GUID, port uint filter := fwpmFilter0{ displayData: fwpmDisplayData0{name: name, description: desc}, + flags: fwpmFilterFlagClearActionRight, layerKey: layer, subLayerKey: sublayerKey, weight: fwpValue0{type_: fwpUint8, value: uintptr(15)},