use crate::vpn::connector::Connector;
use crate::vpn::profile::WgProfile;
use crate::vpn::{Backend, ConnectionInfo, NetError, PeerStatus};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
const IFACE: &str = "zakuro0";
#[derive(Default)]
pub struct NativeConnector;
pub fn mesh_ip() -> Option<String> {
crate::broker::discovery::get_mesh_ip()
}
#[cfg(unix)]
fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
fn is_root() -> bool {
false
}
fn augmented_path() -> String {
let base = std::env::var("PATH").unwrap_or_default();
format!("{base}:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin")
}
fn find(bin: &str) -> Option<String> {
let out = Command::new("sh")
.arg("-c")
.arg(format!("command -v {}", bin))
.env("PATH", augmented_path())
.output()
.ok()?;
if !out.status.success() {
return None;
}
let p = String::from_utf8_lossy(&out.stdout).trim().to_string();
if p.is_empty() {
None
} else {
Some(p)
}
}
fn have(bin: &str) -> bool {
find(bin).is_some()
}
fn conf_path() -> std::path::PathBuf {
std::path::PathBuf::from("/etc/wireguard").join(format!("{}.conf", IFACE))
}
fn wg_quick(action: &str) -> Result<String, NetError> {
crate::vpn::host_ops_allowed(&format!("wg-quick {action} {IFACE}"))?;
let wg_quick_bin = find("wg-quick").unwrap_or_else(|| "wg-quick".to_string());
let mut cmd = Command::new(&wg_quick_bin);
cmd.args([action, IFACE]);
cmd.env("PATH", augmented_path());
if cfg!(target_os = "macos") {
cmd.env("WG_QUICK_USERSPACE_IMPLEMENTATION", "wireguard-go");
}
crate::vpn::vlog(&format!("run: {} {} {}", wg_quick_bin, action, IFACE));
let out = cmd
.output()
.map_err(|e| NetError::Backend(format!("wg-quick: {}", e)))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if crate::vpn::verbose() {
for l in stdout.lines().chain(stderr.lines()) {
crate::vpn::vlog(&format!("wg-quick {}: {}", action, l));
}
}
if out.status.success() {
Ok(stdout.to_string())
} else {
Err(NetError::Backend(stderr.trim().to_string()))
}
}
impl Connector for NativeConnector {
fn available(&self) -> bool {
is_root() && (have("wg-quick") || have("wireguard-go"))
}
fn connect(&self, profile: &WgProfile) -> Result<ConnectionInfo, NetError> {
crate::vpn::host_ops_allowed("native connect")?;
std::fs::create_dir_all("/etc/wireguard")
.map_err(|e| NetError::Backend(format!("mkdir /etc/wireguard: {}", e)))?;
let conf_text = profile
.to_conf()
.map_err(|e| NetError::Backend(format!("invalid profile: {}", e)))?;
std::fs::write(conf_path(), conf_text)
.map_err(|e| NetError::Backend(format!("writing conf: {}", e)))?;
#[cfg(unix)]
std::fs::set_permissions(conf_path(), std::fs::Permissions::from_mode(0o600))
.map_err(|e| NetError::Backend(format!("chmod conf: {}", e)))?;
let _ = wg_quick("down"); wg_quick("up")?;
let wg_bin = find("wg").unwrap_or_else(|| "wg".to_string());
crate::vpn::vlog(&format!("verifying tunnel via {} show {}", wg_bin, IFACE));
let wg_out = Command::new(&wg_bin)
.args(["show", IFACE])
.env("PATH", augmented_path())
.output();
let up = match &wg_out {
Ok(o) => {
if crate::vpn::verbose() {
for l in String::from_utf8_lossy(&o.stdout).lines() {
crate::vpn::vlog(&format!("wg show: {}", l));
}
for l in String::from_utf8_lossy(&o.stderr).lines() {
crate::vpn::vlog(&format!("wg show (err): {}", l));
}
}
o.status.success()
}
Err(e) => {
crate::vpn::vlog(&format!(
"wg show failed to run: {} (is `wg` installed?)",
e
));
false
}
};
if !up {
return Err(NetError::Backend(
"tunnel did not come up (run `zc vpn connect --verbose` for details)".into(),
));
}
let address = profile.interface.address.clone();
crate::vpn::vlog(&format!("tunnel up: {} address {}", IFACE, address));
let info = ConnectionInfo {
backend: Backend::Native,
address,
link: IFACE.to_string(),
peers: read_peers(),
host_routable: true,
proxy: None,
};
crate::vpn::state::save_or_warn(&info);
Ok(info)
}
fn status(&self) -> Result<Option<ConnectionInfo>, NetError> {
if crate::vpn::host_ops_allowed("wg show").is_err() {
return Ok(None);
}
let up = Command::new("wg")
.args(["show", IFACE])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if up {
Ok(crate::vpn::state::load())
} else {
Ok(None)
}
}
fn disconnect(&self) -> Result<(), NetError> {
let _ = wg_quick("down");
let _ = crate::vpn::state::clear();
Ok(())
}
}
fn read_peers() -> Vec<PeerStatus> {
let allowed = Command::new(find("wg").unwrap_or_else(|| "wg".to_string()))
.env("PATH", augmented_path())
.args(["show", IFACE, "allowed-ips"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
let hs = Command::new(find("wg").unwrap_or_else(|| "wg".to_string()))
.env("PATH", augmented_path())
.args(["show", IFACE, "latest-handshakes"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.filter_map(|l| l.split_whitespace().nth(1))
.filter_map(|s| s.parse::<u64>().ok())
.collect::<Vec<_>>()
})
.unwrap_or_default();
allowed
.lines()
.enumerate()
.filter_map(|(i, line)| {
let cidr = line.split_whitespace().nth(1)?;
let ip = cidr.split('/').next()?.to_string();
let secs = hs.get(i).copied();
Some(PeerStatus {
ip,
last_handshake_secs: secs.filter(|s| *s > 0),
reachable: secs.map(|s| s > 0).unwrap_or(false),
})
})
.collect()
}
#[cfg(test)]
mod tests {
static MESH_IP_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct MeshIpEnvGuard {
_guard: std::sync::MutexGuard<'static, ()>,
prev_mesh: Option<String>,
prev_wg: Option<String>,
}
impl MeshIpEnvGuard {
fn set(value: Option<&str>) -> Self {
let guard = MESH_IP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev_mesh = std::env::var("ZAKURO_MESH_IP").ok();
let prev_wg = std::env::var("ZAKURO_WIREGUARD_IP").ok();
std::env::remove_var("ZAKURO_WIREGUARD_IP");
match value {
Some(v) => std::env::set_var("ZAKURO_MESH_IP", v),
None => std::env::remove_var("ZAKURO_MESH_IP"),
}
Self {
_guard: guard,
prev_mesh,
prev_wg,
}
}
}
impl Drop for MeshIpEnvGuard {
fn drop(&mut self) {
match &self.prev_mesh {
Some(v) => std::env::set_var("ZAKURO_MESH_IP", v),
None => std::env::remove_var("ZAKURO_MESH_IP"),
}
match &self.prev_wg {
Some(v) => std::env::set_var("ZAKURO_WIREGUARD_IP", v),
None => std::env::remove_var("ZAKURO_WIREGUARD_IP"),
}
}
}
#[test]
fn mesh_ip_agrees_with_discovery_get_mesh_ip_under_env_override() {
let _env = MeshIpEnvGuard::set(Some("10.13.13.42"));
assert_eq!(
super::mesh_ip(),
crate::broker::discovery::get_mesh_ip(),
"mesh_ip() must be the same detector as discovery::get_mesh_ip()"
);
assert_eq!(super::mesh_ip().as_deref(), Some("10.13.13.42"));
let ip = super::mesh_ip().expect("override always resolves");
assert!(!ip.contains('/'), "mesh_ip must strip any prefix length");
assert!(!ip.contains("://"), "mesh_ip must not be a URL");
}
#[test]
fn mesh_ip_agrees_with_discovery_get_mesh_ip_in_the_ambient_environment() {
let _env = MeshIpEnvGuard::set(None);
assert_eq!(super::mesh_ip(), crate::broker::discovery::get_mesh_ip());
}
#[test]
fn wg_quick_is_refused_in_unit_tests() {
assert!(matches!(
super::wg_quick("down"),
Err(crate::vpn::NetError::Refused(_))
));
}
#[test]
fn native_connect_is_refused_in_unit_tests() {
use crate::vpn::connector::Connector;
let profile: crate::vpn::profile::WgProfile = serde_json::from_str(
r#"{
"interface": { "private_key": "MKCl4DM7FbX1sFUiFd8VqKzdIGUMwJ28rTgFYezsclc=", "address": "10.13.13.6/24" },
"peer": { "public_key": "/Lzp+YIUBrNgdABzyR221uhfx2uOWi4m5ZAdgxXzhFs=",
"endpoint": "144.202.121.242:51822", "allowed_ips": "10.13.13.0/24",
"persistent_keepalive": 25 }
}"#,
)
.expect("sample profile parses");
assert!(matches!(
super::NativeConnector.connect(&profile),
Err(crate::vpn::NetError::Refused(_))
));
}
}