use axum::{Json, response::IntoResponse, http::StatusCode, extract::Path}; use serde::{Deserialize, Serialize}; use std::process::Stdio; use tokio::process::Command; #[derive(Debug, Serialize, Deserialize)] pub struct IpAddrInfo { pub interfaces: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct InterfaceInfo { pub name: String, pub index: u32, pub state: String, pub mac_address: Option, pub ipv4_addresses: Vec, pub ipv6_addresses: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct Ipv4Address { pub address: String, pub prefix_len: u8, pub scope: String, } #[derive(Debug, Serialize, Deserialize)] pub struct Ipv6Address { pub address: String, pub prefix_len: u8, pub scope: String, } #[derive(Debug, Serialize, Deserialize)] pub struct IpRouteInfo { pub routes: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct RouteEntry { pub destination: Option, pub gateway: Option, pub device: String, pub metric: Option, pub route_type: String, } #[derive(Debug, Serialize, Deserialize)] pub struct IpLinkInfo { pub links: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct LinkInfo { pub index: u32, pub name: String, pub state: String, pub mac_address: Option, pub mtu: u32, pub qdisc: Option, pub link_type: String, } #[derive(Debug, Serialize)] struct ErrorResponse { error: String, } #[derive(Debug, Serialize)] struct OkResponse { status: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub(crate) enum AddrAction { Add, Del, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub(crate) enum RouteAction { Add, Del, Replace, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub(crate) enum IpFamily { Inet, Inet6, } #[derive(Debug, Deserialize)] pub(crate) struct LinkMtuRequest { pub mtu: u32, } #[derive(Debug, Deserialize)] pub(crate) struct AddrRequest { pub action: AddrAction, pub dev: String, pub address: String, pub prefix_len: u8, pub family: Option, pub scope: Option, pub label: Option, } #[derive(Debug, Deserialize)] pub(crate) struct RouteRequest { pub action: RouteAction, pub destination: String, pub gateway: Option, pub dev: Option, pub metric: Option, pub table: Option, pub proto: Option, pub scope: Option, } pub async fn get_ip_addr() -> impl IntoResponse { handle_ip("addr").await } pub async fn get_ip_route() -> impl IntoResponse { handle_ip("route").await } pub async fn get_ip_link() -> impl IntoResponse { handle_ip("link").await } pub async fn set_link_up(Path(ifname): Path) -> impl IntoResponse { handle_link_state(&ifname, "up").await } pub async fn set_link_down(Path(ifname): Path) -> impl IntoResponse { handle_link_state(&ifname, "down").await } pub async fn set_link_mtu( Path(ifname): Path, Json(payload): Json, ) -> impl IntoResponse { if let Err(e) = validate_ifname(&ifname) { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }), ) .into_response(); } if payload.mtu < 68 || payload.mtu > 65535 { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "mtu must be between 68 and 65535".to_string(), }), ) .into_response(); } let args = vec![ "link".to_string(), "set".to_string(), "dev".to_string(), ifname, "mtu".to_string(), payload.mtu.to_string(), ]; match run_ip_cmd(&args).await { Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), ) .into_response(), } } pub async fn addr_modify(Json(payload): Json) -> impl IntoResponse { if let Err(e) = validate_ifname(&payload.dev) { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }), ) .into_response(); } let family = payload.family.unwrap_or(IpFamily::Inet); let max_prefix = match family { IpFamily::Inet => 32, IpFamily::Inet6 => 128, }; if payload.prefix_len > max_prefix { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: format!("prefix_len must be <= {}", max_prefix), }), ) .into_response(); } let mut args: Vec = Vec::new(); match family { IpFamily::Inet => args.push("-4".to_string()), IpFamily::Inet6 => args.push("-6".to_string()), } args.push("addr".to_string()); args.push(match payload.action { AddrAction::Add => "add".to_string(), AddrAction::Del => "del".to_string(), }); args.push(format!("{}/{}", payload.address, payload.prefix_len)); args.push("dev".to_string()); args.push(payload.dev); if let Some(scope) = payload.scope { args.push("scope".to_string()); args.push(scope); } if let Some(label) = payload.label { args.push("label".to_string()); args.push(label); } match run_ip_cmd(&args).await { Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), ) .into_response(), } } pub async fn route_modify(Json(payload): Json) -> impl IntoResponse { if let Some(dev) = payload.dev.as_ref() { if let Err(e) = validate_ifname(dev) { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }), ) .into_response(); } } if payload.gateway.is_none() && payload.dev.is_none() { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "either gateway or dev must be provided".to_string(), }), ) .into_response(); } let mut args: Vec = Vec::new(); args.push("route".to_string()); args.push(match payload.action { RouteAction::Add => "add".to_string(), RouteAction::Del => "del".to_string(), RouteAction::Replace => "replace".to_string(), }); args.push(payload.destination); if let Some(gw) = payload.gateway { args.push("via".to_string()); args.push(gw); } if let Some(dev) = payload.dev { args.push("dev".to_string()); args.push(dev); } if let Some(metric) = payload.metric { args.push("metric".to_string()); args.push(metric.to_string()); } if let Some(table) = payload.table { args.push("table".to_string()); args.push(table); } if let Some(proto) = payload.proto { args.push("proto".to_string()); args.push(proto); } if let Some(scope) = payload.scope { args.push("scope".to_string()); args.push(scope); } match run_ip_cmd(&args).await { Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), ) .into_response(), } } async fn handle_ip(subcmd: &str) -> impl IntoResponse { match execute_ip_cmd(subcmd).await { Ok(data) => Json(data).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), ) .into_response(), } } async fn handle_link_state(ifname: &str, state: &str) -> impl IntoResponse { if let Err(e) = validate_ifname(ifname) { return ( StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }), ) .into_response(); } let args = vec![ "link".to_string(), "set".to_string(), "dev".to_string(), ifname.to_string(), state.to_string(), ]; match run_ip_cmd(&args).await { Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e }), ) .into_response(), } } async fn run_ip_cmd(args: &[String]) -> Result<(), String> { let output = Command::new("ip") .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .map_err(|e| format!("Failed to execute ip {}: {}", args.join(" "), e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("ip {} failed: {}", args.join(" "), stderr)); } Ok(()) } fn validate_ifname(name: &str) -> Result<(), String> { if name.is_empty() { return Err("ifname must not be empty".to_string()); } if name.len() > 64 { return Err("ifname is слишком длинное".to_string()); } if !name .chars() .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' )) { return Err("ifname содержит недопустимые символы".to_string()); } Ok(()) } async fn execute_ip_cmd(subcmd: &str) -> Result { // Prefer native JSON output from iproute2 for a complete and robust payload. let mut json_errors: Vec = Vec::new(); match execute_ip_cmd_json(subcmd, true).await { Ok(value) => return Ok(value), Err(e) => json_errors.push(e), } // Some ip versions support JSON but not -details. match execute_ip_cmd_json(subcmd, false).await { Ok(value) => return Ok(value), Err(e) => json_errors.push(e), } // Fallback to legacy text parsing for older ip versions without JSON support. match execute_ip_cmd_legacy(subcmd).await { Ok(value) => Ok(value), Err(legacy_err) => { let mut msg = String::new(); if !json_errors.is_empty() { msg.push_str(&format!("JSON attempts failed: {}; ", json_errors.join(" | "))); } msg.push_str(&format!("legacy parsing failed: {}", legacy_err)); Err(msg) } } } async fn execute_ip_cmd_json(subcmd: &str, details: bool) -> Result { let mut args: Vec<&str> = vec!["-j"]; if details { args.push("-details"); } args.push(subcmd); let output = Command::new("ip") .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .map_err(|e| format!("Failed to execute ip -j {}: {}", subcmd, e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("ip -j {} failed: {}", subcmd, stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); serde_json::from_str::(&stdout) .map_err(|e| format!("Failed to parse ip -j {} output: {}", subcmd, e)) } async fn execute_ip_cmd_legacy(subcmd: &str) -> Result { let output = Command::new("ip") .arg(subcmd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .map_err(|e| format!("Failed to execute ip {}: {}", subcmd, e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("ip {} failed: {}", subcmd, stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); let result = match subcmd { "addr" => parse_ip_addr(&stdout)?, "route" => parse_ip_route(&stdout)?, "link" => parse_ip_link(&stdout)?, _ => return Err(format!("Unknown subcommand: {}", subcmd)), }; Ok(result) } fn parse_ip_addr(output: &str) -> Result { let mut interfaces = Vec::new(); let mut current_interface: Option = None; for line in output.lines() { let line = line.trim(); // Начало описания интерфейса: "1: lo: ..." if let Some(idx) = line.split(':').next() { if let Ok(index) = idx.parse::() { if let Some(iface) = current_interface.take() { interfaces.push(iface); } let parts: Vec<&str> = line.split(':').collect(); if parts.len() >= 3 { let name = parts[1].trim().trim_start_matches('@').to_string(); let state = if line.contains("") || line.contains("UP") { "UP" } else if line.contains("DOWN") { "DOWN" } else { "UNKNOWN" }.to_string(); current_interface = Some(InterfaceInfo { name, index, state, mac_address: None, ipv4_addresses: Vec::new(), ipv6_addresses: Vec::new(), }); } continue; } } if let Some(ref mut iface) = current_interface { // MAC адрес: "link/ether aa:bb:cc:dd:ee:ff" if line.starts_with("link/") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 && parts[1] != "none" { iface.mac_address = Some(parts[1].to_string()); } continue; } // IPv4 адрес: "inet 192.168.1.1/24 brd ..." if line.starts_with("inet ") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { let addr_parts: Vec<&str> = parts[1].split('/').collect(); if addr_parts.len() == 2 { if let Ok(prefix) = addr_parts[1].parse::() { let scope = if let Some(idx) = line.find("scope") { line[idx..].split_whitespace().nth(1).unwrap_or("global").to_string() } else { "global".to_string() }; iface.ipv4_addresses.push(Ipv4Address { address: addr_parts[0].to_string(), prefix_len: prefix, scope, }); } } } continue; } // IPv6 адрес: "inet6 ::1/128 scope ..." if line.starts_with("inet6 ") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { let addr_parts: Vec<&str> = parts[1].split('/').collect(); if addr_parts.len() == 2 { if let Ok(prefix) = addr_parts[1].parse::() { let scope = if let Some(idx) = line.find("scope") { line[idx..].split_whitespace().nth(1).unwrap_or("global").to_string() } else { "global".to_string() }; iface.ipv6_addresses.push(Ipv6Address { address: addr_parts[0].to_string(), prefix_len: prefix, scope, }); } } } continue; } } } if let Some(iface) = current_interface { interfaces.push(iface); } Ok(serde_json::to_value(IpAddrInfo { interfaces }).unwrap()) } fn parse_ip_route(output: &str) -> Result { let mut routes = Vec::new(); for line in output.lines() { let line = line.trim(); if line.is_empty() { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); let mut route = RouteEntry { destination: None, gateway: None, device: String::new(), metric: None, route_type: "unicast".to_string(), }; let mut i = 0; while i < parts.len() { match parts[i] { "default" => { route.destination = Some("default".to_string()); } "via" => { if i + 1 < parts.len() { route.gateway = Some(parts[i + 1].to_string()); i += 1; } } "dev" => { if i + 1 < parts.len() { route.device = parts[i + 1].to_string(); i += 1; } } "metric" => { if i + 1 < parts.len() { route.metric = parts[i + 1].parse::().ok(); i += 1; } } "src" | "proto" | "scope" | "onlink" => { // Пропускаем эти параметры } _ => { if route.destination.is_none() && !["via", "dev", "metric", "src", "proto", "scope", "onlink"].contains(&parts[i]) { route.destination = Some(parts[i].to_string()); } } } i += 1; } if !route.device.is_empty() { routes.push(route); } } Ok(serde_json::to_value(IpRouteInfo { routes }).unwrap()) } fn parse_ip_link(output: &str) -> Result { let mut links = Vec::new(); let mut current_link: Option = None; for line in output.lines() { let line = line.trim(); if let Some(idx) = line.split(':').next() { if let Ok(index) = idx.parse::() { if let Some(link) = current_link.take() { links.push(link); } let parts: Vec<&str> = line.split(':').collect(); let name = if parts.len() >= 3 { parts[1].trim().trim_start_matches('@').to_string() } else { "unknown".to_string() }; let state = if line.contains("") || line.contains("UP") { "UP" } else if line.contains("DOWN") { "DOWN" } else { "UNKNOWN" }.to_string(); current_link = Some(LinkInfo { index, name, state, mac_address: None, mtu: 1500, qdisc: None, link_type: "unknown".to_string(), }); continue; } } if let Some(ref mut link) = current_link { // link/ether или link/type if line.starts_with("link/") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { link.link_type = parts[0].trim_start_matches("link/").to_string(); if parts[1] != "none" { link.mac_address = Some(parts[1].to_string()); } } continue; } // mtu if line.starts_with("mtu ") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { link.mtu = parts[1].parse::().unwrap_or(1500); } continue; } // qdisc if let Some(idx) = line.find("qdisc ") { let rest = &line[idx + 6..]; if let Some(qdisc) = rest.split_whitespace().next() { link.qdisc = Some(qdisc.to_string()); } continue; } } } if let Some(link) = current_link { links.push(link); } Ok(serde_json::to_value(IpLinkInfo { links }).unwrap()) }