use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::*;
use windows_sys::Win32::System::Rpc::RPC_C_AUTHN_WINNT;
use windows_sys::core::GUID;
const SUBLAYER_KEY: GUID = GUID::from_u128(0x6765_7068_0000_0000_0000_0000_0000_0002);
const IPPROTO_UDP: u8 = 17;
pub(super) trait Firewall {
fn preflight(&self) -> anyhow::Result<()>;
fn install(
&mut self,
geph_app_ids: &[std::path::PathBuf],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()>;
fn replace(
&mut self,
geph_app_ids: &[std::path::PathBuf],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()>;
fn remove(&mut self);
}
pub(super) struct WfpKillSwitch {
installed: bool,
}
impl WfpKillSwitch {
pub(super) fn new() -> Self {
WfpKillSwitch { installed: false }
}
}
impl Firewall for WfpKillSwitch {
fn preflight(&self) -> anyhow::Result<()> {
Ok(())
}
fn install(
&mut self,
geph_app_ids: &[std::path::PathBuf],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()> {
self.replace(geph_app_ids, wintun_luid, allow_lan)
}
fn replace(
&mut self,
geph_app_ids: &[std::path::PathBuf],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()> {
let engine = open_engine()?;
let owned_ids = match owned_filter_ids(engine) {
Ok(ids) => ids,
Err(error) => {
unsafe { FwpmEngineClose0(engine) };
return Err(error);
}
};
if let Err(error) = check(
unsafe { FwpmTransactionBegin0(engine, 0) },
"FwpmTransactionBegin0",
) {
unsafe { FwpmEngineClose0(engine) };
return Err(error);
}
for id in owned_ids {
if let Err(error) = check(
unsafe { FwpmFilterDeleteById0(engine, id) },
"FwpmFilterDeleteById0",
) {
unsafe {
let _ = FwpmTransactionAbort0(engine);
FwpmEngineClose0(engine);
}
return Err(error);
}
}
unsafe {
let _ = FwpmSubLayerDeleteByKey0(engine, &SUBLAYER_KEY);
}
let result = build_filters(engine, geph_app_ids, wintun_luid, allow_lan).and_then(|()| {
check(
unsafe { FwpmTransactionCommit0(engine) },
"FwpmTransactionCommit0",
)
});
if result.is_err() {
unsafe {
let _ = FwpmTransactionAbort0(engine);
}
}
unsafe { FwpmEngineClose0(engine) };
match result {
Ok(()) => {
self.installed = true;
tracing::info!(
wintun_luid,
allow_lan,
app_ids = ?geph_app_ids,
"WFP kill switch reconciled (fail-closed; DNS-leak guard active; survives manager crash)"
);
Ok(())
}
Err(e) => Err(e),
}
}
fn remove(&mut self) {
if self.installed {
purge_stale();
self.installed = false;
tracing::info!("WFP kill switch removed");
}
}
}
pub(super) fn purge_stale() {
let engine = match open_engine() {
Ok(e) => e,
Err(_) => return,
};
unsafe {
if let Ok(ids) = owned_filter_ids(engine) {
for id in ids {
let _ = FwpmFilterDeleteById0(engine, id);
}
}
let _ = FwpmSubLayerDeleteByKey0(engine, &SUBLAYER_KEY);
FwpmEngineClose0(engine);
}
}
fn owned_filter_ids(engine: HANDLE) -> anyhow::Result<Vec<u64>> {
let mut ids = Vec::new();
for layer in [
FWPM_LAYER_ALE_AUTH_CONNECT_V4,
FWPM_LAYER_ALE_AUTH_CONNECT_V6,
] {
let mut template: FWPM_FILTER_ENUM_TEMPLATE0 = unsafe { std::mem::zeroed() };
template.layerKey = layer;
template.enumType = FWP_FILTER_ENUM_FULLY_CONTAINED;
template.actionMask = 0xFFFF_FFFF;
let mut enum_handle: HANDLE = std::ptr::null_mut();
check(
unsafe { FwpmFilterCreateEnumHandle0(engine, &template, &mut enum_handle) },
"FwpmFilterCreateEnumHandle0",
)?;
loop {
let mut entries: *mut *mut FWPM_FILTER0 = std::ptr::null_mut();
let mut count = 0u32;
let code =
unsafe { FwpmFilterEnum0(engine, enum_handle, 64, &mut entries, &mut count) };
if let Err(error) = check(code, "FwpmFilterEnum0") {
unsafe {
let _ = FwpmFilterDestroyEnumHandle0(engine, enum_handle);
}
return Err(error);
}
if count == 0 {
break;
}
for index in 0..count as usize {
let filter = unsafe { *entries.add(index) };
if !filter.is_null() && unsafe { guid_eq(&(*filter).subLayerKey, &SUBLAYER_KEY) } {
ids.push(unsafe { (*filter).filterId });
}
}
unsafe {
FwpmFreeMemory0(&mut (entries as *mut core::ffi::c_void));
}
}
unsafe {
let _ = FwpmFilterDestroyEnumHandle0(engine, enum_handle);
}
}
Ok(ids)
}
fn guid_eq(left: &GUID, right: &GUID) -> bool {
left.data1 == right.data1
&& left.data2 == right.data2
&& left.data3 == right.data3
&& left.data4 == right.data4
}
fn open_engine() -> anyhow::Result<HANDLE> {
let mut name = wide("Geph kill switch");
let mut engine: HANDLE = std::ptr::null_mut();
let code = unsafe {
let mut session: FWPM_SESSION0 = std::mem::zeroed();
session.displayData = FWPM_DISPLAY_DATA0 {
name: name.as_mut_ptr(),
description: std::ptr::null_mut(),
};
FwpmEngineOpen0(
std::ptr::null(),
RPC_C_AUTHN_WINNT,
std::ptr::null(),
&session,
&mut engine,
)
};
check(code, "FwpmEngineOpen0")?;
Ok(engine)
}
fn build_filters(
engine: HANDLE,
geph_app_ids: &[std::path::PathBuf],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()> {
let mut sl_name = wide("Geph kill switch");
let code = unsafe {
let mut sublayer: FWPM_SUBLAYER0 = std::mem::zeroed();
sublayer.subLayerKey = SUBLAYER_KEY;
sublayer.displayData = FWPM_DISPLAY_DATA0 {
name: sl_name.as_mut_ptr(),
description: std::ptr::null_mut(),
};
sublayer.weight = 0xFFFF;
FwpmSubLayerAdd0(engine, &sublayer, std::ptr::null_mut())
};
check(code, "FwpmSubLayerAdd0")?;
let mut app_blobs: Vec<*mut FWP_BYTE_BLOB> = Vec::new();
for path in geph_app_ids {
match app_id_blob(path) {
Ok(blob) => app_blobs.push(blob),
Err(e) => {
for blob in app_blobs {
unsafe { FwpmFreeMemory0(&mut (blob as *mut core::ffi::c_void)) };
}
anyhow::bail!("could not resolve WFP app-id for {}: {e}", path.display());
}
}
}
let r = (|| {
install_family(engine, false, &app_blobs, wintun_luid, allow_lan)?;
install_family(engine, true, &app_blobs, wintun_luid, allow_lan)?;
Ok(())
})();
for blob in app_blobs {
unsafe { FwpmFreeMemory0(&mut (blob as *mut core::ffi::c_void)) };
}
r
}
const W_LOOPBACK: u8 = 15;
const W_APPID: u8 = 14;
const W_WINTUN: u8 = 13;
const W_DHCP: u8 = 12;
const W_DNS_BLOCK: u8 = 11;
const W_LAN: u8 = 10;
const W_DEFAULT_BLOCK: u8 = 0;
fn install_family(
engine: HANDLE,
v6: bool,
app_blobs: &[*mut FWP_BYTE_BLOB],
wintun_luid: u64,
allow_lan: bool,
) -> anyhow::Result<()> {
let layer = if v6 {
FWPM_LAYER_ALE_AUTH_CONNECT_V6
} else {
FWPM_LAYER_ALE_AUTH_CONNECT_V4
};
let mut back = Backing::default();
{
let c = if v6 {
vec![cond_v6(
&mut back,
&FWPM_CONDITION_IP_REMOTE_ADDRESS,
V6_LOOPBACK,
128,
)]
} else {
vec![cond_v4(
&mut back,
&FWPM_CONDITION_IP_REMOTE_ADDRESS,
0x7F00_0000,
0xFF00_0000,
)]
};
add_filter(engine, layer, FWP_ACTION_PERMIT, W_LOOPBACK, &c, "loopback")?;
}
for &blob in app_blobs {
let c = vec![cond_blob(&FWPM_CONDITION_ALE_APP_ID, blob)];
add_filter(engine, layer, FWP_ACTION_PERMIT, W_APPID, &c, "geph-app")?;
}
{
let c = vec![cond_u64(
&mut back,
&FWPM_CONDITION_IP_LOCAL_INTERFACE,
wintun_luid,
)];
add_filter(engine, layer, FWP_ACTION_PERMIT, W_WINTUN, &c, "on-wintun")?;
}
{
let dhcp_port: u16 = if v6 { 547 } else { 67 };
let c = vec![
cond_u8(&FWPM_CONDITION_IP_PROTOCOL, IPPROTO_UDP),
cond_u16(&FWPM_CONDITION_IP_REMOTE_PORT, dhcp_port),
];
add_filter(engine, layer, FWP_ACTION_PERMIT, W_DHCP, &c, "dhcp")?;
}
{
let c = vec![cond_u16(&FWPM_CONDITION_IP_REMOTE_PORT, 53)];
add_filter(
engine,
layer,
FWP_ACTION_BLOCK,
W_DNS_BLOCK,
&c,
"dns-leak-guard",
)?;
}
if allow_lan {
if v6 {
for (addr, prefix) in [(V6_ULA, 7u8), (V6_LINK_LOCAL, 10u8)] {
let c = vec![cond_v6(
&mut back,
&FWPM_CONDITION_IP_REMOTE_ADDRESS,
addr,
prefix,
)];
add_filter(engine, layer, FWP_ACTION_PERMIT, W_LAN, &c, "lan")?;
}
} else {
for (addr, mask) in [
(0x0A00_0000u32, 0xFF00_0000u32), (0xAC10_0000, 0xFFF0_0000), (0xC0A8_0000, 0xFFFF_0000), (0xA9FE_0000, 0xFFFF_0000), ] {
let c = vec![cond_v4(
&mut back,
&FWPM_CONDITION_IP_REMOTE_ADDRESS,
addr,
mask,
)];
add_filter(engine, layer, FWP_ACTION_PERMIT, W_LAN, &c, "lan")?;
}
}
}
add_filter(
engine,
layer,
FWP_ACTION_BLOCK,
W_DEFAULT_BLOCK,
&[],
"default-block",
)?;
drop(back);
Ok(())
}
const V6_LOOPBACK: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
const V6_ULA: [u8; 16] = [0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const V6_LINK_LOCAL: [u8; 16] = [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
#[derive(Default)]
struct Backing {
u64s: Vec<Box<u64>>,
v4s: Vec<Box<FWP_V4_ADDR_AND_MASK>>,
v6s: Vec<Box<FWP_V6_ADDR_AND_MASK>>,
}
impl Backing {
fn u64_ptr(&mut self, v: u64) -> *mut u64 {
let b = Box::new(v);
let p = b.as_ref() as *const u64 as *mut u64;
self.u64s.push(b);
p
}
fn v4_ptr(&mut self, addr: u32, mask: u32) -> *mut FWP_V4_ADDR_AND_MASK {
let b = Box::new(FWP_V4_ADDR_AND_MASK { addr, mask });
let p = b.as_ref() as *const FWP_V4_ADDR_AND_MASK as *mut FWP_V4_ADDR_AND_MASK;
self.v4s.push(b);
p
}
fn v6_ptr(&mut self, addr: [u8; 16], prefix: u8) -> *mut FWP_V6_ADDR_AND_MASK {
let b = Box::new(FWP_V6_ADDR_AND_MASK {
addr,
prefixLength: prefix,
});
let p = b.as_ref() as *const FWP_V6_ADDR_AND_MASK as *mut FWP_V6_ADDR_AND_MASK;
self.v6s.push(b);
p
}
}
fn cond(field: &GUID, ty: FWP_DATA_TYPE, val: FWP_CONDITION_VALUE0_0) -> FWPM_FILTER_CONDITION0 {
FWPM_FILTER_CONDITION0 {
fieldKey: *field,
matchType: FWP_MATCH_EQUAL,
conditionValue: FWP_CONDITION_VALUE0 {
r#type: ty,
Anonymous: val,
},
}
}
fn cond_u8(field: &GUID, v: u8) -> FWPM_FILTER_CONDITION0 {
cond(field, FWP_UINT8, FWP_CONDITION_VALUE0_0 { uint8: v })
}
fn cond_u16(field: &GUID, v: u16) -> FWPM_FILTER_CONDITION0 {
cond(field, FWP_UINT16, FWP_CONDITION_VALUE0_0 { uint16: v })
}
fn cond_u64(back: &mut Backing, field: &GUID, v: u64) -> FWPM_FILTER_CONDITION0 {
cond(
field,
FWP_UINT64,
FWP_CONDITION_VALUE0_0 {
uint64: back.u64_ptr(v),
},
)
}
fn cond_v4(back: &mut Backing, field: &GUID, addr: u32, mask: u32) -> FWPM_FILTER_CONDITION0 {
cond(
field,
FWP_V4_ADDR_MASK,
FWP_CONDITION_VALUE0_0 {
v4AddrMask: back.v4_ptr(addr, mask),
},
)
}
fn cond_v6(back: &mut Backing, field: &GUID, addr: [u8; 16], prefix: u8) -> FWPM_FILTER_CONDITION0 {
cond(
field,
FWP_V6_ADDR_MASK,
FWP_CONDITION_VALUE0_0 {
v6AddrMask: back.v6_ptr(addr, prefix),
},
)
}
fn cond_blob(field: &GUID, blob: *mut FWP_BYTE_BLOB) -> FWPM_FILTER_CONDITION0 {
cond(
field,
FWP_BYTE_BLOB_TYPE,
FWP_CONDITION_VALUE0_0 { byteBlob: blob },
)
}
fn add_filter(
engine: HANDLE,
layer: GUID,
action: FWP_ACTION_TYPE,
weight: u8,
conditions: &[FWPM_FILTER_CONDITION0],
label: &str,
) -> anyhow::Result<()> {
let mut name = wide(&format!("Geph: {label}"));
let mut id: u64 = 0;
let code = unsafe {
let mut filter: FWPM_FILTER0 = std::mem::zeroed();
filter.displayData = FWPM_DISPLAY_DATA0 {
name: name.as_mut_ptr(),
description: std::ptr::null_mut(),
};
filter.layerKey = layer;
filter.subLayerKey = SUBLAYER_KEY;
filter.flags = FWPM_FILTER_FLAG_NONE;
filter.weight = FWP_VALUE0 {
r#type: FWP_UINT8,
Anonymous: FWP_VALUE0_0 { uint8: weight },
};
filter.action.r#type = action;
filter.numFilterConditions = conditions.len() as u32;
filter.filterCondition = if conditions.is_empty() {
std::ptr::null_mut()
} else {
conditions.as_ptr() as *mut FWPM_FILTER_CONDITION0
};
FwpmFilterAdd0(engine, &filter, std::ptr::null_mut(), &mut id)
};
check(code, "FwpmFilterAdd0").map_err(|e| anyhow::anyhow!("{e} (filter: {label})"))
}
fn app_id_blob(path: &Path) -> anyhow::Result<*mut FWP_BYTE_BLOB> {
let wpath = wide(&path.to_string_lossy());
let mut blob: *mut FWP_BYTE_BLOB = std::ptr::null_mut();
let code = unsafe { FwpmGetAppIdFromFileName0(wpath.as_ptr(), &mut blob) };
check(code, "FwpmGetAppIdFromFileName0")?;
Ok(blob)
}
fn check(code: u32, what: &str) -> anyhow::Result<()> {
if code == 0 {
Ok(())
} else {
anyhow::bail!("{what} failed: 0x{code:08X}")
}
}
fn wide(s: &str) -> Vec<u16> {
std::ffi::OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}