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;
fn inet_addr_from_ip_show(out: &str) -> Option<String> {
out.split_whitespace()
.skip_while(|t| *t != "inet")
.nth(1)
.map(|s| s.to_string())
}
#[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> {
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> {
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> {
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 {
use super::*;
#[test]
fn extracts_inet_address() {
let out = "5: zakuro0: <POINTOPOINT,NOARP,UP> mtu 1420 ...\n inet 10.13.13.6/24 scope global zakuro0\n";
assert_eq!(
inet_addr_from_ip_show(out).as_deref(),
Some("10.13.13.6/24")
);
}
}