use anyhow::{bail, Context, Result};
use std::io::IsTerminal;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use tokio::io::unix::AsyncFd;
const CAP_NET_ADMIN: u64 = 12;
pub fn have_net_admin() -> bool {
if unsafe { libc::geteuid() } == 0 {
return true;
}
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| s.lines().find(|l| l.starts_with("CapEff:")).map(|l| l.to_string()))
.and_then(|l| u64::from_str_radix(l["CapEff:".len()..].trim(), 16).ok())
.map(|caps| caps & (1 << CAP_NET_ADMIN) != 0)
.unwrap_or(false)
}
pub fn raise_net_admin_ambient() {
const CAP_NET_ADMIN: u32 = 12;
const VER3: u32 = 0x2008_0522; const PR_CAP_AMBIENT: libc::c_int = 47;
const PR_CAP_AMBIENT_RAISE: libc::c_ulong = 2;
#[repr(C)]
struct Hdr {
version: u32,
pid: libc::c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct Data {
effective: u32,
permitted: u32,
inheritable: u32,
}
unsafe {
let mut hdr = Hdr { version: VER3, pid: 0 };
let mut data = [Data { effective: 0, permitted: 0, inheritable: 0 }; 2];
if libc::syscall(libc::SYS_capget, &mut hdr as *mut Hdr, data.as_mut_ptr()) == 0
&& data[0].permitted & (1 << CAP_NET_ADMIN) != 0
{
data[0].inheritable |= 1 << CAP_NET_ADMIN;
let _ = libc::syscall(libc::SYS_capset, &hdr as *const Hdr, data.as_ptr());
}
let _ = libc::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_ADMIN as libc::c_ulong, 0, 0);
}
}
pub fn cap_grant_cmd() -> String {
let exe = std::env::current_exe()
.ok()
.and_then(|p| p.to_str().map(str::to_string))
.unwrap_or_else(|| "filament".into());
format!("sudo setcap cap_net_admin+eip {exe}")
}
fn hosts_writable() -> bool {
unsafe {
libc::geteuid() == 0
|| libc::access(b"/etc/hosts\0".as_ptr() as *const libc::c_char, libc::W_OK) == 0
}
}
pub fn hosts_grant_cmd() -> String {
let user = std::env::var("USER").unwrap_or_else(|_| "$(whoami)".into());
format!("sudo setfacl -m u:{user}:rw /etc/hosts")
}
pub fn ensure_hosts_writable() {
if hosts_writable() {
return;
}
let Ok(user) = std::env::var("USER") else { return };
let cmd = hosts_grant_cmd();
if std::io::stdin().is_terminal() && std::io::stderr().is_terminal() {
eprintln!(" MagicDNS names (<peer>.mesh) need write access to root-owned /etc/hosts.");
eprintln!(" granting a per-file ACL now: {cmd}");
let ok = std::process::Command::new("sudo")
.args(["setfacl", "-m", &format!("u:{user}:rw"), "/etc/hosts"])
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
eprintln!(" done. peer names will resolve after the next announce (overlay already works by IP).");
} else {
eprintln!(" could not grant automatically (install the `acl` package, or run it yourself):\n {cmd}");
eprintln!(" overlay still works by IP without this.");
}
} else {
eprintln!(" MagicDNS names need /etc/hosts write; run once:\n {cmd}\n overlay still works by IP without this.");
}
}
pub fn ensure_net_admin_for_l3() -> bool {
ensure_hosts_writable();
if have_net_admin() {
return true;
}
let cmd = cap_grant_cmd();
if std::io::stdin().is_terminal() && std::io::stderr().is_terminal() {
eprintln!(" L3 needs a one-time capability so a non-root daemon can open the tunnel device (like WireGuard).");
eprintln!(" granting now: {cmd}");
let granted = std::env::current_exe()
.ok()
.and_then(|exe| {
std::process::Command::new("sudo")
.args(["setcap", "cap_net_admin+eip"])
.arg(exe)
.status()
.ok()
})
.map(|s| s.success())
.unwrap_or(false);
if granted {
eprintln!(" done. restart the daemon (or run `filament up`) to bring up the overlay.");
} else {
eprintln!(" could not grant automatically; run it yourself, then restart the daemon:\n {cmd}");
}
granted
} else {
eprintln!(" L3 needs a one-time capability for a non-root daemon; run it, then restart:\n {cmd}");
false
}
}
const TUNSETIFF: libc::Ioctl = 0x4004_54ca;
const IFF_TUN: libc::c_short = 0x0001;
const IFF_NO_PI: libc::c_short = 0x1000;
#[repr(C)]
struct IfReq {
name: [libc::c_char; 16],
flags: libc::c_short,
_pad: [u8; 22],
}
pub struct KernelTun {
fd: AsyncFd<OwnedFd>,
name: String,
}
impl KernelTun {
pub fn name(&self) -> &str {
&self.name
}
pub fn open(name: &str, cidr: &str, mtu: u32) -> Result<KernelTun> {
if name.is_empty() || name.len() >= libc::IFNAMSIZ {
bail!("tun name '{name}' must be 1..15 bytes");
}
let raw = unsafe {
libc::open(
b"/dev/net/tun\0".as_ptr() as *const libc::c_char,
libc::O_RDWR | libc::O_NONBLOCK,
)
};
if raw < 0 {
let e = std::io::Error::last_os_error();
if matches!(e.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) {
bail!(
"open /dev/net/tun: permission denied. L3 needs CAP_NET_ADMIN; grant it once:\n {}\nthen restart the daemon (or run the daemon as root).",
cap_grant_cmd()
);
}
bail!("open /dev/net/tun: {e}");
}
let owned = unsafe { OwnedFd::from_raw_fd(raw) };
let mut req: IfReq = unsafe { std::mem::zeroed() };
for (i, &b) in name.as_bytes().iter().enumerate() {
req.name[i] = b as libc::c_char;
}
req.flags = IFF_TUN | IFF_NO_PI;
let rc = unsafe { libc::ioctl(owned.as_raw_fd(), TUNSETIFF, &mut req as *mut IfReq) };
if rc < 0 {
let e = std::io::Error::last_os_error();
if matches!(e.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) {
bail!(
"TUNSETIFF {name}: permission denied. L3 needs CAP_NET_ADMIN; grant it once:\n {}\nthen restart the daemon.",
cap_grant_cmd()
);
}
bail!("TUNSETIFF {name}: {e}");
}
raise_net_admin_ambient();
ip(&["addr", "add", cidr, "dev", name]).with_context(|| format!("ip addr add {cidr} dev {name}"))?;
ip(&["link", "set", "dev", name, "mtu", &mtu.to_string()]).context("ip link set mtu")?;
ip(&["link", "set", "dev", name, "up"]).context("ip link set up")?;
let fd = AsyncFd::new(owned).context("register tun fd with the reactor")?;
Ok(KernelTun { fd, name: name.to_string() })
}
pub async fn recv(&self, buf: &mut [u8]) -> Result<usize> {
loop {
let mut guard = self.fd.readable().await?;
match guard.try_io(|inner| {
let n = unsafe {
libc::read(inner.as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}) {
Ok(res) => return res.map_err(Into::into),
Err(_would_block) => continue,
}
}
}
pub async fn send(&self, packet: &[u8]) -> Result<usize> {
loop {
let mut guard = self.fd.writable().await?;
match guard.try_io(|inner| {
let n = unsafe {
libc::write(inner.as_raw_fd(), packet.as_ptr() as *const libc::c_void, packet.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}) {
Ok(res) => return res.map_err(Into::into),
Err(_would_block) => continue,
}
}
}
}
#[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
}
}
pub fn add_route(cidr: &str, dev: &str) -> Result<()> {
let fam = if cidr.contains(':') { "-6" } else { "-4" };
let out = std::process::Command::new("ip")
.args([fam, "route", "replace", cidr, "dev", dev])
.output()
.context("exec ip route")?;
if !out.status.success() {
bail!("ip {fam} route replace {cidr} dev {dev}: {}", String::from_utf8_lossy(&out.stderr).trim());
}
Ok(())
}
pub fn add_addr(cidr: &str, dev: &str) -> Result<()> {
let fam = if cidr.contains(':') { "-6" } else { "-4" };
let out = std::process::Command::new("ip")
.args([fam, "addr", "add", cidr, "dev", dev])
.output()
.context("exec ip addr")?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
if !err.contains("File exists") {
bail!("ip {fam} addr add {cidr} dev {dev}: {}", err.trim());
}
}
Ok(())
}
fn ip(args: &[&str]) -> Result<()> {
let out = std::process::Command::new("ip")
.args(args)
.output()
.context("exec ip (iproute2 not installed?)")?;
if !out.status.success() {
bail!("ip {}: {}", args.join(" "), String::from_utf8_lossy(&out.stderr).trim());
}
Ok(())
}