use super::netns::NetworkNamespace;
use crate::util::sudo_command;
use anyhow::Context;
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub struct VethPair {
pub source: String,
pub dest: String,
pub nm_unmanaged: Option<NetworkManagerUnmanaged>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct NetworkManagerUnmanaged {
pub backup_file: Option<PathBuf>,
}
impl VethPair {
pub fn new(source: String, dest: String, netns: &NetworkNamespace) -> anyhow::Result<Self> {
assert!(source.len() <= 15, "ifname must be <= 15 chars: {source}");
assert!(dest.len() <= 15, "ifname must be <= 15 chars: {dest}");
let nm_path = PathBuf::from_str("/etc/NetworkManager")?;
let nm_running = if which::which("nmcli").is_ok() {
std::process::Command::new("nmcli")
.arg("general")
.arg("status")
.status()
.map(|x| x.success())
.unwrap_or(false)
} else {
false
};
if nm_running {
debug!("Detected NetworkManager running");
} else {
debug!("NetworkManager not detected running");
}
let nm_unmanaged = if nm_path.exists() && nm_running {
debug!(
"NetworkManager detected, adding {} to unmanaged devices",
dest
);
let mut nm_config_path = nm_path.clone();
nm_config_path.push("conf.d");
std::fs::create_dir_all(&nm_config_path)?;
nm_config_path.push("unmanaged.conf");
let backup_file = if nm_config_path.exists() {
let mut backup_path = nm_path;
backup_path.push("conf.d/unmanaged.conf.vopono.bak");
std::fs::copy(&nm_config_path, &backup_path)?;
Some(backup_path)
} else {
None
};
{
let mut file = if nm_config_path.exists() {
debug!(
"Appending to existing NetworkManager config file: {}",
nm_config_path.as_path().to_string_lossy()
);
OpenOptions::new().append(true).open(nm_config_path)?
} else {
debug!(
"Creating new NetworkManager config file: {}",
nm_config_path.as_path().to_string_lossy()
);
std::fs::File::create(nm_config_path)?
};
write!(file, "[keyfile]\nunmanaged-devices=interface-name:{dest}\n")?;
}
if let Err(e) = sudo_command(&["nmcli", "connection", "reload"])
.context("Failed to reload NetworkManager configuration")
{
warn!(
"Tried but failed to reload NetworkManager configuration - is NetworkManager running? : {e}"
);
}
Some(NetworkManagerUnmanaged { backup_file })
} else {
None
};
let firewalld_running = if which::which("firewall-cmd").is_ok() {
std::process::Command::new("firewall-cmd")
.arg("--state")
.status()
.map(|x| x.success())
.unwrap_or(false)
} else {
false
};
if firewalld_running {
debug!("Detected firewalld running");
} else {
debug!("firewalld not detected running");
}
if firewalld_running {
debug!("Detected firewalld running, adding {dest} veth device to trusted zone");
match std::process::Command::new("firewall-cmd")
.arg("--zone=trusted")
.arg(format!("--add-interface={dest}").as_str())
.status()
.map(|x| x.success())
{
Err(e) => {
warn!("Failed to add veth device {dest} to firewalld trusted zone, error: {e}")
}
Ok(false) => warn!(
"Possibly failed to add veth device {dest} to firewalld trusted zone (non-zero exit code)"
),
_ => {}
}
}
sudo_command(&[
"ip",
"link",
"add",
dest.as_str(),
"type",
"veth",
"peer",
"name",
source.as_str(),
])
.with_context(|| format!("Failed to create veth pair {}, {}", source, dest))?;
sudo_command(&["ip", "link", "set", dest.as_str(), "up"])
.with_context(|| format!("Failed to bring up destination veth: {}", dest))?;
sudo_command(&[
"ip",
"link",
"set",
source.as_str(),
"netns",
&netns.name,
"up",
])
.with_context(|| format!("Failed to bring up source veth: {}", dest))?;
Ok(Self {
source,
dest,
nm_unmanaged,
})
}
}
fn link_is_missing(name: &str) -> anyhow::Result<bool> {
let output = std::process::Command::new("ip")
.args(["link", "show", name])
.output()
.with_context(|| format!("Failed to inspect veth pair: {name}"))?;
if output.status.success() {
return Ok(false);
}
let stderr = String::from_utf8_lossy(&output.stderr);
let normalized = stderr.to_ascii_lowercase();
if normalized.contains("does not exist") || normalized.contains("cannot find device") {
return Ok(true);
}
Err(anyhow::anyhow!(
"ip link show {name} failed with status {}: {}",
output.status,
stderr.trim()
))
}
impl Drop for VethPair {
fn drop(&mut self) {
if let Err(error) = sudo_command(&["ip", "link", "delete", &self.dest]) {
match link_is_missing(&self.dest) {
Ok(true) => {
debug!("Veth pair {} was already removed", self.dest);
}
Ok(false) => {
log::error!("Failed to delete veth pair {}: {error}", self.dest);
}
Err(inspect_error) => {
log::error!(
"Failed to delete veth pair {}: {error}; could not verify whether it was removed: {inspect_error}",
self.dest
);
}
}
}
}
}
impl Drop for NetworkManagerUnmanaged {
fn drop(&mut self) {
if let Ok(namespaces) = crate::util::get_lock_namespaces() {
if !namespaces.is_empty() {
return;
}
let nm_path = PathBuf::from_str("/etc/NetworkManager/conf.d/unmanaged.conf")
.expect("Failed to build path");
if let Some(backup_file) = self.backup_file.as_ref() {
std::fs::copy(backup_file, &nm_path)
.expect("Failed to restore backup of NetworkManager unmanaged.conf");
std::fs::remove_file(backup_file)
.expect("Failed to delete backup of NetworkManager unmanaged.conf");
} else {
std::fs::remove_file(&nm_path)
.expect("Failed to delete NetworkManager unmanaged.conf");
}
sudo_command(&["nmcli", "connection", "reload"])
.expect("Failed to reload NetworkManager configuration");
}
}
}