mod firewall;
use std::{
io::{Read, Write},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
process::{ChildStdin, ChildStdout, Command},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread::JoinHandle,
};
use anyhow::{Context, bail};
use wintun::{Adapter, Session, Wintun};
use firewall::{Firewall, WfpKillSwitch};
const TUN_NAME: &str = "Geph";
const TUN_GUID: u128 = 0x6765_7068_0000_0000_0000_0000_0000_0001;
const TUN_V4_ADDR: &str = "100.64.0.1";
const TUN_V4_MASK: &str = "255.192.0.0"; const TUN_V6_CIDR: &str = "fd00:6765::1/64";
const TUN_MTU: usize = 16384;
const RING_CAPACITY: u32 = 0x40_0000;
const V4_SPLIT: [&str; 2] = ["0.0.0.0/1", "128.0.0.0/1"];
const V6_SPLIT: [&str; 2] = ["::/1", "8000::/1"];
fn sentinel_dns() -> [IpAddr; 2] {
[
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111)),
]
}
#[derive(Clone, Debug)]
pub(super) struct PhysIface {
index4: u32,
index6: u32,
}
impl PhysIface {
pub(super) fn bind_indices(&self) -> (u32, u32) {
(self.index4, self.index6)
}
}
pub(super) struct VpnHandle {
#[allow(dead_code)]
wintun: Wintun,
adapter: Arc<Adapter>,
phys: PhysIface,
firewall: WfpKillSwitch,
}
impl VpnHandle {
pub(super) fn bind_indices(&self) -> (u32, u32) {
(self.phys.index4, self.phys.index6)
}
pub(super) fn start_session(&self) -> anyhow::Result<Arc<Session>> {
Ok(Arc::new(
self.adapter
.start_session(RING_CAPACITY)
.context("start wintun session")?,
))
}
fn cleanup(&mut self) {
self.firewall.remove();
let _ = self.adapter.set_dns_servers(&[]);
for prefix in V4_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv4",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
}
for prefix in V6_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv6",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
}
}
}
pub(super) fn cleanup(mut handle: VpnHandle) {
handle.cleanup();
}
#[derive(Clone)]
pub(super) struct NetworkSnapshot {
bind_indices: (u32, u32),
}
pub(super) fn network_snapshot(handle: &VpnHandle) -> NetworkSnapshot {
NetworkSnapshot {
bind_indices: handle.bind_indices(),
}
}
pub(super) fn network_check(snapshot: &NetworkSnapshot) -> super::NetworkAction {
match physical_iface() {
Ok(current) if current.bind_indices() != snapshot.bind_indices => {
super::NetworkAction::Reconcile
}
_ => super::NetworkAction::Healthy,
}
}
pub(super) fn physical_iface() -> anyhow::Result<PhysIface> {
let index4 = ps_u32(
"(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | \
Sort-Object RouteMetric | Select-Object -First 1).ifIndex",
)
.context("could not find the IPv4 default-route interface")?;
let index6 = ps_u32(
"(Get-NetRoute -DestinationPrefix '::/0' -ErrorAction SilentlyContinue | \
Sort-Object RouteMetric | Select-Object -First 1).ifIndex",
)
.unwrap_or(index4);
Ok(PhysIface { index4, index6 })
}
pub(super) fn setup(phys: PhysIface, allow_lan: bool) -> anyhow::Result<VpnHandle> {
let wintun = unsafe { wintun::load() }.map_err(|e| anyhow::anyhow!("load wintun.dll: {e}"))?;
cleanup_stale_with_wintun(&wintun);
let rollback = scopeguard::guard(&wintun, |wintun| cleanup_stale_with_wintun(wintun));
let adapter = match Adapter::open(&wintun, TUN_NAME) {
Ok(existing) => existing,
Err(_) => Adapter::create(&wintun, TUN_NAME, TUN_NAME, Some(TUN_GUID))
.map_err(|e| anyhow::anyhow!("create wintun adapter: {e}"))?,
};
run(
"netsh",
&[
"interface",
"ipv4",
"set",
"address",
&format!("name={TUN_NAME}"),
"source=static",
&format!("address={TUN_V4_ADDR}"),
&format!("mask={TUN_V4_MASK}"),
],
)
.context("assign tun IPv4 address")?;
let _ = run(
"netsh",
&[
"interface",
"ipv6",
"add",
"address",
&format!("interface={TUN_NAME}"),
&format!("address={TUN_V6_CIDR}"),
"store=active",
],
);
let _ = adapter.set_mtu(TUN_MTU);
let _ = adapter.set_dns_servers(&sentinel_dns());
let mut firewall = WfpKillSwitch::new();
firewall.preflight().context("kill switch preflight")?;
let luid = unsafe { adapter.get_luid().Value };
firewall
.install(&geph_app_ids(), luid, allow_lan)
.context("install kill switch")?;
ensure_split_routes()?;
scopeguard::ScopeGuard::into_inner(rollback);
Ok(VpnHandle {
wintun,
adapter,
phys,
firewall,
})
}
pub(super) fn reconcile(
handle: &mut VpnHandle,
phys: PhysIface,
allow_lan: bool,
) -> anyhow::Result<()> {
run(
"netsh",
&[
"interface",
"ipv4",
"set",
"address",
&format!("name={TUN_NAME}"),
"source=static",
&format!("address={TUN_V4_ADDR}"),
&format!("mask={TUN_V4_MASK}"),
],
)
.context("reassert tun IPv4 address")?;
let _ = handle.adapter.set_mtu(TUN_MTU);
let _ = handle.adapter.set_dns_servers(&sentinel_dns());
let luid = unsafe { handle.adapter.get_luid().Value };
handle
.firewall
.replace(&geph_app_ids(), luid, allow_lan)
.context("reconcile kill switch")?;
ensure_split_routes()?;
handle.phys = phys;
Ok(())
}
fn ensure_split_routes() -> anyhow::Result<()> {
for prefix in V4_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv4",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
run(
"netsh",
&[
"interface",
"ipv4",
"add",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
"store=active",
],
)
.with_context(|| format!("add tun route {prefix}"))?;
}
for prefix in V6_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv6",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
let _ = run(
"netsh",
&[
"interface",
"ipv6",
"add",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
"store=active",
],
);
}
Ok(())
}
pub(super) fn cleanup_stale() {
match unsafe { wintun::load() } {
Ok(wintun) => cleanup_stale_with_wintun(&wintun),
Err(_) => firewall::purge_stale(),
}
}
fn cleanup_stale_with_wintun(wintun: &Wintun) {
if let Ok(adapter) = Adapter::open(wintun, TUN_NAME) {
let _ = adapter.set_dns_servers(&[]);
for prefix in V4_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv4",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
}
for prefix in V6_SPLIT {
let _ = run(
"netsh",
&[
"interface",
"ipv6",
"delete",
"route",
&format!("prefix={prefix}"),
&format!("interface={TUN_NAME}"),
],
);
}
}
firewall::purge_stale();
}
fn geph_app_ids() -> Vec<std::path::PathBuf> {
let mut ids = Vec::new();
if let Ok(exe) = std::env::current_exe() {
ids.push(exe);
}
ids.push(crate::platform::engine_bin_path());
ids
}
pub(super) struct Pump {
session: Arc<Session>,
stop: Arc<AtomicBool>,
threads: Vec<JoinHandle<()>>,
}
impl Pump {
pub(super) fn start(
session: Arc<Session>,
child_stdin: ChildStdin,
child_stdout: ChildStdout,
) -> Pump {
let stop = Arc::new(AtomicBool::new(false));
let up = {
let session = session.clone();
let stop = stop.clone();
std::thread::spawn(move || pump_up(session, child_stdin, stop))
};
let dn = {
let session = session.clone();
let stop = stop.clone();
std::thread::spawn(move || pump_down(session, child_stdout, stop))
};
Pump {
session,
stop,
threads: vec![up, dn],
}
}
}
impl Drop for Pump {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
let _ = self.session.shutdown();
for handle in self.threads.drain(..) {
let _ = handle.join();
}
}
}
fn pump_up(session: Arc<Session>, mut child_stdin: ChildStdin, stop: Arc<AtomicBool>) {
while !stop.load(Ordering::SeqCst) {
let packet = match session.receive_blocking() {
Ok(p) => p,
Err(_) => break,
};
let bytes = packet.bytes();
let len = std::cmp::min(bytes.len(), u16::MAX as usize);
if child_stdin.write_all(&(len as u16).to_be_bytes()).is_err()
|| child_stdin.write_all(&bytes[..len]).is_err()
|| child_stdin.flush().is_err()
{
break;
}
}
}
fn pump_down(session: Arc<Session>, mut child_stdout: ChildStdout, stop: Arc<AtomicBool>) {
let mut len_buf = [0u8; 2];
while !stop.load(Ordering::SeqCst) {
if child_stdout.read_exact(&mut len_buf).is_err() {
break;
}
let len = u16::from_be_bytes(len_buf) as usize;
if len == 0 {
continue;
}
let mut buf = vec![0u8; len];
if child_stdout.read_exact(&mut buf).is_err() {
break;
}
match session.allocate_send_packet(len as u16) {
Ok(mut packet) => {
packet.bytes_mut().copy_from_slice(&buf);
session.send_packet(packet);
}
Err(_) => break,
}
}
}
fn powershell(expr: &str) -> Option<String> {
let out = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", expr])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}
fn ps_u32(expr: &str) -> Option<u32> {
powershell(expr)?.parse().ok()
}
fn run(cmd: &str, args: &[&str]) -> anyhow::Result<()> {
let out = Command::new(cmd)
.args(args)
.output()
.with_context(|| format!("spawning {cmd}"))?;
if !out.status.success() {
bail!(
"`{cmd} {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}