diff --git a/src/bin/collared.rs b/src/bin/collared.rs index 4507d09..ca76558 100644 --- a/src/bin/collared.rs +++ b/src/bin/collared.rs @@ -1,33 +1,53 @@ use std::env::args; use tokio::net::TcpListener; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use std::sync::Arc; + use axum::{ - routing::get + routing::get, + extract::{State, Request, Json, Path, Extension, Query}, + routing::post, }; +struct AppState { + leash: collar::NetworkLeash, +} + #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::filter::EnvFilter::try_from_default_env().unwrap_or_else(|_| { - "collar=debug,tower_http=debug,axum::rejection=trace" + "collar=trace,tower_http=debug,axum::rejection=trace" .into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); + let leash = collar::NetworkLeash::new(); + let state = Arc::new(AppState { leash }); // build our application with a single route let app = axum::Router::new() - .route("/", get(|| async { "hic!" })) + .route("/", get(|| async { "collared." })) + .route("/ng/eiface/{name}", post(post_ng_eiface)) + .with_state(state) .layer(tower_http::trace::TraceLayer::new_for_http()); // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } + +async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { + let fname = name.clone(); + let result = state.leash.gated(move || { + collar::ng::new_eiface(&name) + }).await; + Ok(format!("sup {} => {:?}", &fname, result)) +} diff --git a/src/error.rs b/src/error.rs index 0ba8d67..d8757f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,53 +1,56 @@ #[derive(thiserror::Error, Debug)] pub enum Error { #[error("sys err")] SystemError(errno::Errno), #[error("not a bridge")] PathIsNotABridge, #[error("invalid node type")] InvalidNodeType(String, String), #[error("bridge already exists")] BridgeLinkExists(String, String), #[error("utf8")] InvalidUtf8(std::str::Utf8Error), #[error("nul")] NulError(String), #[error("exists")] Exists, #[error("invalid file descriptor")] InvalidDescriptor, #[error("name too long")] NameTooLong, #[error("description too long")] DescriptionTooLong, #[error("interface not found")] InterfaceNotFound, #[error("out of memory")] OutOfMemory, #[error("operation not supported")] NotSupported, + #[error("join error")] + TaskJoin(#[from] tokio::task::JoinError), + #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Nul(#[from] std::ffi::NulError), #[error(transparent)] ParseNum(#[from] std::num::ParseIntError), } pub type Result = ::std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index a8b27b7..ed942eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,49 @@ pub mod error; pub mod netif; pub mod ng; + +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::error::{Error, Result}; + +pub struct NetworkLeash { + lock: Arc> +} + +impl NetworkLeash { + pub fn new() -> Self { + tracing::trace!("Creating new NetworkLeash"); + Self { + lock: Arc::new(Mutex::new(())), + } + } + + pub async fn with_interface(&self, name: &str, operation: F) -> Result + where + F: FnOnce(&mut crate::netif::Iface) -> Result + Send + 'static, + R: Send + 'static, + { + let _guard = self.lock.lock().await; + let name = name.to_string(); + + tokio::task::spawn_blocking(move || { + let mut iface = crate::netif::Iface::new(&name)?; + operation(&mut iface) + }).await? + } + + pub async fn gated(&self, operation: F) -> Result + where + F: FnOnce() -> Result + Send + 'static, + R: Send + 'static, + { + let _guard = self.lock.lock().await; + + tokio::task::spawn_blocking(move || { + operation() + }).await? + } +} diff --git a/src/netif/iface.rs b/src/netif/iface.rs index 4c41493..ee36087 100644 --- a/src/netif/iface.rs +++ b/src/netif/iface.rs @@ -1,155 +1,178 @@ use libc::{ - self, AF_INET, IFNAMSIZ, SOCK_DGRAM, c_char, c_void, ifreq, + self, AF_INET, IFNAMSIZ, SOCK_DGRAM, IFF_UP, IFF_RUNNING, c_char, c_void, ifreq, c_short }; use std::{ ffi::CString, mem, os::unix::io::AsRawFd, ptr, }; use crate::error::{Error, Result}; use crate::netif::{fd::Fd, sys, sys::*}; pub struct Iface { pub name: String, ctl: Fd } impl Iface { pub fn new(name: &str) -> Result { let ctl = unsafe { Fd::new(libc::socket(AF_INET, SOCK_DGRAM, 0), true)? }; Ok(Self { name: name.to_string(), ctl, }) } unsafe fn request(&self) -> Result { if self.name.len() >= IFNAMSIZ { return Err(Error::NameTooLong); } let mut req: ifreq = mem::zeroed(); let name_bytes = self.name.as_bytes(); ptr::copy_nonoverlapping( name_bytes.as_ptr() as *const c_char, req.ifr_name.as_mut_ptr(), name_bytes.len(), ); // Ensure null termination (mem::zeroed already did this) Ok(req) } pub fn set_name(&mut self, value: &str) -> Result<()> { if value.len() >= IFNAMSIZ { return Err(Error::NameTooLong); } unsafe { let mut req = self.request()?; let if_name = CString::new(value)?; req.ifr_ifru.ifru_data = if_name.as_ptr() as *mut c_char; // Perform syscall while CString is still alive let result = siocsifname(self.ctl.as_raw_fd(), &req); // CString drops here naturally, after syscall if let Err(err) = result { return Err(std::io::Error::from(err).into()); } self.name = value.to_string(); Ok(()) } } pub fn exists(&self) -> Result { unsafe { let mut req = self.request()?; // Try to get interface flags to check if interface exists match siocgifflags(self.ctl.as_raw_fd(), &mut req) { Ok(_) => Ok(true), Err(nix::errno::Errno::ENXIO) => Ok(false), // Device not configured Err(err) => Err(std::io::Error::from(err).into()), } } } pub fn set_description(&mut self, value: &str) -> Result<()> { unsafe { // Check if interface exists first if !self.exists()? { return Err(Error::InterfaceNotFound); } let mut req: sys::ifreq_ext = mem::zeroed(); // Copy interface name with proper bounds checking let name_bytes = self.name.as_bytes(); if name_bytes.len() >= IFNAMSIZ { return Err(Error::NameTooLong); } ptr::copy_nonoverlapping( name_bytes.as_ptr() as *const c_char, req.ifr_name.as_mut_ptr(), name_bytes.len(), ); if value.is_empty() { // Clear description req.ifr_ifru.ifru_buffer.length = 0; req.ifr_ifru.ifru_buffer.buffer = ptr::null_mut(); let result = siocsifdescr(self.ctl.as_raw_fd(), &req as *const _ as *const ifreq); if let Err(err) = result { return Err(std::io::Error::from(err).into()); } } else { if value.len() >= IFDESCRSIZ { return Err(Error::DescriptionTooLong); } // Create null-terminated string and keep it alive during syscall let desc_cstring = CString::new(value)?; req.ifr_ifru.ifru_buffer.length = desc_cstring.as_bytes_with_nul().len() as u32; req.ifr_ifru.ifru_buffer.buffer = desc_cstring.as_ptr() as *mut c_void; // Perform syscall while CString is still alive let result = siocsifdescr(self.ctl.as_raw_fd(), &req as *const _ as *const ifreq); // CString drops here naturally, after syscall if let Err(err) = result { return Err(std::io::Error::from(err).into()); } } Ok(()) } } pub fn set_mtu(&mut self, mtu: i32) -> Result<()> { unsafe { // Check if interface exists first if !self.exists()? { return Err(Error::InterfaceNotFound); } let mut req = self.request()?; - + // Set the MTU value req.ifr_ifru.ifru_mtu = mtu; // Perform the ioctl to set MTU let result = siocsifmtu(self.ctl.as_raw_fd(), &req); if let Err(err) = result { return Err(std::io::Error::from(err).into()); } Ok(()) } } + + pub fn enabled(&mut self, value: bool) -> Result<()> { + unsafe { + let mut req = self.request()?; + + if let Err(err) = siocgifflags(self.ctl.as_raw_fd(), &mut req) { + return Err(std::io::Error::from(err).into()); + } + + if value { + req.ifr_ifru.ifru_flags[0] |= (IFF_UP | IFF_RUNNING) as c_short; + } else { + req.ifr_ifru.ifru_flags[0] &= !(IFF_UP as c_short); + } + + if let Err(err) = siocsifflags(self.ctl.as_raw_fd(), &req) { + return Err(std::io::Error::from(err).into()); + } + + Ok(()) + } + } + }