#[cfg(target_os = "windows")]
pub fn apply_windows_scoped_host_route(
interface_index: u32,
target: IpAddr,
) -> Result<WindowsScopedHostRoute> {
let target = match target {
IpAddr::V4(target) => target,
IpAddr::V6(_) => {
return Err(anyhow!(
"Windows scoped WG upstream routes only support IPv4 targets"
));
}
};
let route_targets = vec![format!("{target}/32")];
crate::windows_tunnel::apply_windows_routes(interface_index, &route_targets)?;
Ok(WindowsScopedHostRoute {
interface_index,
route_targets,
reverted: false,
})
}
#[cfg(target_os = "windows")]
pub struct WindowsScopedHostRoute {
interface_index: u32,
route_targets: Vec<String>,
reverted: bool,
}
#[cfg(target_os = "windows")]
impl WindowsScopedHostRoute {
pub fn revert(&mut self) -> Result<()> {
if self.reverted {
return Ok(());
}
crate::windows_tunnel::remove_windows_routes(self.interface_index, &self.route_targets)?;
self.reverted = true;
Ok(())
}
}
#[cfg(target_os = "windows")]
impl Drop for WindowsScopedHostRoute {
fn drop(&mut self) {
if let Err(error) = self.revert() {
eprintln!(
"wg-upstream: WARNING — Windows scoped host route cleanup failed: {error}. \
You may need to run `netsh interface ipv4 delete route <target>/32 \
interface={}` manually.",
self.interface_index
);
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn run_checked(command: &mut ProcessCommand) -> Result<()> {
let status = command
.status()
.with_context(|| format!("spawn {:?}", command.get_program()))?;
if !status.success() {
return Err(anyhow!(
"{:?} {:?} failed: {status}",
command.get_program(),
command
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect::<Vec<_>>()
));
}
Ok(())
}
#[cfg(target_os = "macos")]
pub struct DaemonWgUpstream {
pub iface: String,
pub upstream: SocketAddr,
runtime: Option<WgUpstreamRuntime>,
full_route: Option<FullDefaultRoute>,
_tun: Arc<TunSocket>,
config_fingerprint: WireGuardExitFingerprint,
}
#[cfg(target_os = "windows")]
pub struct DaemonWgUpstream {
pub iface: String,
pub upstream: SocketAddr,
full_route: Option<WindowsFullDefaultRoute>,
backend: WindowsWgUpstreamBackend,
config_fingerprint: WireGuardExitFingerprint,
}
#[cfg(target_os = "windows")]
enum WindowsWgUpstreamBackend {
Native(WindowsNativeWireGuardTunnel),
Userspace {
runtime: Option<WgUpstreamRuntime>,
_session: Arc<WintunSession>,
_adapter: Arc<wintun::Adapter>,
},
}
#[cfg(target_os = "windows")]
struct WindowsNativeWireGuardTunnel {
name: String,
config_path: PathBuf,
wireguard_exe: PathBuf,
}
#[cfg(target_os = "macos")]
pub async fn apply_daemon_wg_upstream(
config: &WireGuardExitConfig,
handshake_timeout: Duration,
) -> Result<DaemonWgUpstream> {
let fingerprint = WireGuardExitFingerprint::from_config(config);
let interface_hint =
if config.interface.trim().is_empty() || !config.interface.starts_with("utun") {
"utun".to_string()
} else {
config.interface.clone()
};
let tun = TunSocket::new(&interface_hint)
.with_context(|| format!("create utun for WG upstream (hint='{interface_hint}')"))?
.set_non_blocking()
.context("set utun non-blocking")?;
let actual_iface = tun.name().context("read utun name (probably needs root)")?;
let tun = Arc::new(tun);
let runtime = start_wg_runtime_with_posix_tun(config, tun.clone())
.await
.context("start userspace WG runtime")?;
let upstream = runtime.upstream();
if !runtime.wait_for_handshake(handshake_timeout).await {
runtime.shutdown().await;
return Err(anyhow!(
"WG upstream handshake to {upstream} did not complete within {}s; \
routing table NOT modified",
handshake_timeout.as_secs()
));
}
let mtu = if config.mtu > 0 { config.mtu } else { 1420 };
let full_route = match apply_full_default_route(&actual_iface, &config.address, upstream, mtu) {
Ok(route) => route,
Err(error) => {
runtime.shutdown().await;
return Err(error.context("swap default route via WG upstream"));
}
};
Ok(DaemonWgUpstream {
iface: actual_iface,
upstream,
runtime: Some(runtime),
full_route: Some(full_route),
_tun: tun,
config_fingerprint: fingerprint,
})
}
#[cfg(target_os = "macos")]
impl DaemonWgUpstream {
pub fn matches(&self, new_config: &WireGuardExitConfig) -> bool {
self.config_fingerprint == WireGuardExitFingerprint::from_config(new_config)
}
pub async fn cleanup(mut self) {
if let Some(mut full_route) = self.full_route.take() {
if let Err(error) = full_route.revert() {
eprintln!(
"fips: WG upstream route revert failed: {error}. \
Routing table may need manual cleanup."
);
}
drop(full_route);
}
if let Some(runtime) = self.runtime.take() {
runtime.shutdown().await;
}
}
}
#[cfg(target_os = "windows")]
impl DaemonWgUpstream {
pub fn matches(&self, new_config: &WireGuardExitConfig) -> bool {
self.config_fingerprint == WireGuardExitFingerprint::from_config(new_config)
}
pub async fn cleanup(mut self) {
if let Some(mut full_route) = self.full_route.take() {
if let Err(error) = full_route.revert() {
eprintln!(
"fips: WG upstream route revert failed: {error}. \
Routing table may need manual cleanup."
);
}
drop(full_route);
}
match self.backend {
WindowsWgUpstreamBackend::Native(mut tunnel) => {
if let Err(error) = tunnel.cleanup() {
eprintln!(
"fips: native WireGuardNT tunnel cleanup failed: {error}. \
The WireGuardTunnel${} service may need manual removal.",
tunnel.name
);
}
}
WindowsWgUpstreamBackend::Userspace {
runtime: Some(runtime),
..
} => {
runtime.shutdown().await;
}
WindowsWgUpstreamBackend::Userspace { runtime: None, .. } => {}
}
}
}