use anyhow::{bail, Context, Result};
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
pub struct KernelTun {
session: Arc<wintun::Session>,
name: String,
rx: Mutex<mpsc::UnboundedReceiver<Vec<u8>>>,
_reader: std::thread::JoinHandle<()>,
}
impl KernelTun {
pub fn name(&self) -> &str {
&self.name
}
pub fn open(name: &str, cidr: &str, mtu: u32) -> Result<KernelTun> {
let wintun = load_wintun().context("load wintun.dll (bundle it beside filament.exe)")?;
let adapter = wintun::Adapter::open(&wintun, name)
.or_else(|_| wintun::Adapter::create(&wintun, name, "Filament", None))
.map_err(|e| {
anyhow::anyhow!(
"create Wintun adapter '{name}': {e}. L3 on Windows needs Administrator and wintun.dll."
)
})?;
let session =
Arc::new(adapter.start_session(wintun::MAX_RING_CAPACITY).context("start Wintun session")?);
let (tx, rx) = mpsc::unbounded_channel::<Vec<u8>>();
let rsess = session.clone();
let reader = std::thread::spawn(move || loop {
match rsess.receive_blocking() {
Ok(pkt) => {
if tx.send(pkt.bytes().to_vec()).is_err() {
break; }
}
Err(_) => break, }
});
let (addr, prefixlen) = split_cidr(cidr);
let proto = if addr.contains(':') { "ipv6" } else { "ipv4" };
let addr_spec = format!("address={addr}/{prefixlen}");
let iface = format!("interface={name}");
netsh(&["interface", proto, "set", "address", &iface, &addr_spec])
.or_else(|_| netsh(&["interface", proto, "add", "address", &iface, &addr_spec]))
.with_context(|| format!("netsh set address {addr}/{prefixlen} on {name}"))?;
let _ = netsh(&["interface", proto, "set", "subinterface", name, &format!("mtu={mtu}"), "store=active"]);
Ok(KernelTun { session, name: name.to_string(), rx: Mutex::new(rx), _reader: reader })
}
pub async fn recv(&self, buf: &mut [u8]) -> Result<usize> {
let mut rx = self.rx.lock().await;
match rx.recv().await {
Some(pkt) => {
let n = pkt.len().min(buf.len());
buf[..n].copy_from_slice(&pkt[..n]);
Ok(n)
}
None => bail!("wintun reader stopped"),
}
}
pub async fn send(&self, packet: &[u8]) -> Result<usize> {
let len = packet.len();
let mut p = self
.session
.allocate_send_packet(len as u16)
.map_err(|e| anyhow::anyhow!("wintun allocate_send_packet: {e}"))?;
p.bytes_mut().copy_from_slice(packet);
self.session.send_packet(p);
Ok(len)
}
}
#[async_trait::async_trait]
impl crate::tun::TunDevice for KernelTun {
fn name(&self) -> &str {
KernelTun::name(self)
}
async fn recv(&self, buf: &mut [u8]) -> Result<usize> {
KernelTun::recv(self, buf).await
}
async fn send(&self, packet: &[u8]) -> Result<usize> {
KernelTun::send(self, packet).await
}
}
impl Drop for KernelTun {
fn drop(&mut self) {
let _ = self.session.shutdown();
}
}
fn load_wintun() -> Result<wintun::Wintun> {
let exe = std::env::current_exe().context("resolve current exe")?;
let beside = exe.parent().context("exe directory")?.join("wintun.dll");
let system32 = {
let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
std::path::PathBuf::from(root).join("System32\\wintun.dll")
};
let dll = if beside.exists() {
beside
} else if system32.exists() {
system32
} else {
bail!(
"wintun.dll not found next to filament.exe or in System32; refusing to search CWD/PATH \
(DLL-hijack guard). Reinstall filament, or place WireGuard's signed wintun.dll beside filament.exe."
);
};
unsafe { wintun::load_from_path(&dll) }.map_err(|e| anyhow::anyhow!("load {}: {e}", dll.display()))
}
fn split_cidr(cidr: &str) -> (String, String) {
match cidr.split_once('/') {
Some((a, p)) => (a.to_string(), p.to_string()),
None => (cidr.to_string(), if cidr.contains(':') { "128".into() } else { "32".into() }),
}
}
pub fn add_route(cidr: &str, dev: &str) -> Result<()> {
let proto = if cidr.contains(':') { "ipv6" } else { "ipv4" };
let out = std::process::Command::new("netsh")
.args(["interface", proto, "add", "route", cidr, &format!("interface={dev}")])
.output()
.context("exec netsh")?;
if !out.status.success() {
let msg = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
if !msg.to_lowercase().contains("already exists") {
bail!("netsh add route {cidr} interface={dev}: {}", msg.trim());
}
}
Ok(())
}
pub fn add_addr(cidr: &str, dev: &str) -> Result<()> {
let (addr, prefixlen) = split_cidr(cidr);
let proto = if addr.contains(':') { "ipv6" } else { "ipv4" };
let addr_spec = format!("address={addr}/{prefixlen}");
let iface = format!("interface={dev}");
netsh(&["interface", proto, "set", "address", &iface, &addr_spec])
.or_else(|_| netsh(&["interface", proto, "add", "address", &iface, &addr_spec]))
.with_context(|| format!("netsh add address {addr}/{prefixlen} on {dev}"))
}
pub fn ensure_net_admin_for_l3() -> bool {
eprintln!(" L3 on Windows needs Administrator and wintun.dll beside filament.exe.");
true
}
pub fn ensure_hosts_writable() {}
fn netsh(args: &[&str]) -> Result<()> {
let out = std::process::Command::new("netsh").args(args).output().context("exec netsh")?;
if !out.status.success() {
let msg = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
bail!("netsh {}: {}", args.join(" "), msg.trim());
}
Ok(())
}