diff --git a/crates/network-scanner/examples/ipconfig.rs b/crates/network-scanner/examples/ipconfig.rs index aae3d3413..1a696f1e6 100644 --- a/crates/network-scanner/examples/ipconfig.rs +++ b/crates/network-scanner/examples/ipconfig.rs @@ -1,15 +1,15 @@ -#![allow(unused_crate_dependencies)] +use anyhow::Context; +use network_scanner::interfaces::{get_network_interfaces, Filter}; #[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::SubscriberBuilder::default() - .with_max_level(tracing::Level::DEBUG) - .with_line_number(true) - .init(); +pub async fn main() -> anyhow::Result<()> { + let interfaces = get_network_interfaces(Filter::default()) + .await + .context("Failed to get network interfaces")?; - let interfaces = network_scanner::interfaces::get_network_interfaces().await?; for interface in interfaces { - tracing::info!("{:?}", interface) + println!("{:#?}", interface); } + Ok(()) } diff --git a/crates/network-scanner/src/interfaces/filter.rs b/crates/network-scanner/src/interfaces/filter.rs new file mode 100644 index 000000000..55ebabc09 --- /dev/null +++ b/crates/network-scanner/src/interfaces/filter.rs @@ -0,0 +1,39 @@ +use std::net::IpAddr; + +use super::NetworkInterface; + +#[derive(Debug, Clone)] +pub struct Filter { + pub ignore_ipv6: bool, + pub include_loopback: bool, +} + +impl Default for Filter { + fn default() -> Self { + Self { + ignore_ipv6: true, + include_loopback: false, + } + } +} + +pub(crate) fn is_loop_back(interface: &NetworkInterface) -> bool { + for addr in &interface.routes { + if let IpAddr::V4(ipv4) = addr.ip { + if ipv4.octets()[0] == 127 { + return false; + } + } + } + + return true; +} + +pub(crate) fn filter_out_ipv6_if(ignore_ipv6: bool, mut interface: NetworkInterface) -> NetworkInterface { + if ignore_ipv6 { + interface.routes.retain(|addr| matches!(addr.ip, IpAddr::V4(_))); + interface.ip_adresses.retain(|addr| matches!(addr, IpAddr::V4(_))); + } + + interface +} diff --git a/crates/network-scanner/src/interfaces/linux.rs b/crates/network-scanner/src/interfaces/linux.rs index d28a8322c..92869638e 100644 --- a/crates/network-scanner/src/interfaces/linux.rs +++ b/crates/network-scanner/src/interfaces/linux.rs @@ -12,7 +12,7 @@ use rtnetlink::{new_connection, Handle}; use super::InterfaceAddress; -pub async fn get_network_interfaces() -> anyhow::Result> { +pub(crate) async fn get_network_interfaces() -> anyhow::Result> { let (connection, handle, _) = new_connection()?; tokio::spawn(connection); let mut links_info = vec![]; @@ -47,15 +47,10 @@ pub async fn get_network_interfaces() -> anyhow::Result> { let address = addresses .iter() - .map(|addr| AddressInfo::try_from(addr.clone())) - .collect::, _>>() - .inspect_err(|e| error!(error = format!("{e:#}"), "Failed to parse address info")); + .filter_map(|addr_msg| extract_ip(addr_msg.clone())) + .collect::>(); - let Ok(address) = address else { - continue; - }; - - link.addresses = address; + link.ip_addresses = address; links_info.push(link); } @@ -120,32 +115,35 @@ impl TryFrom<&LinkInfo> for NetworkInterface { } fn convert_link_info_to_network_interface(link_info: &LinkInfo) -> anyhow::Result { - let mut addresses = Vec::new(); - - for address_info in &link_info.addresses { - addresses.push((address_info.address, u32::from(address_info.prefix_len))); - } + let routes = link_info + .routes + .iter() + .filter_map(|route_info| { + route_info.destination.map(|dest| InterfaceAddress { + ip: dest, + prefixlen: u32::from(route_info.destination_prefix), + }) + }) + .collect(); let gateways = link_info .routes .iter() .filter_map(|route_info| route_info.gateway) - .collect(); + .collect::>(); + + let ip_adresses = link_info.ip_addresses.clone(); Ok(NetworkInterface { name: link_info.name.clone(), - description: None, mac_address: link_info.mac.as_slice().try_into().ok(), - addresses: addresses - .into_iter() - .map(|(addr, prefix)| InterfaceAddress { - ip: addr, - prefixlen: prefix, - }) - .collect(), + ip_adresses, + routes, operational_status: link_info.flags.contains(&LinkFlag::Up), gateways, dns_servers: link_info.dns_servers.clone(), + id: None, + description: None, }) } @@ -155,7 +153,7 @@ struct LinkInfo { flags: Vec, name: String, index: u32, - addresses: Vec, + ip_addresses: Vec, routes: Vec, dns_servers: Vec, } @@ -166,12 +164,6 @@ impl LinkInfo { } } -#[derive(Debug, Clone)] -struct AddressInfo { - address: IpAddr, - prefix_len: u8, -} - #[derive(Debug, Clone)] struct RouteInfo { gateway: Option, @@ -250,31 +242,14 @@ impl TryFrom for RouteInfo { } } -impl TryFrom for AddressInfo { - type Error = anyhow::Error; - - fn try_from(value: AddressMessage) -> Result { - let addr = value - .attributes - .iter() - .find_map(|attr| { - if let AddressAttribute::Address(addr) = attr { - Some(addr) - } else { - None - } - }) - .context("no address found")?; - - let prefix_len = value.header.prefix_len; - - let address_info = AddressInfo { - address: *addr, - prefix_len, - }; - - Ok(address_info) - } +fn extract_ip(addr_msg: AddressMessage) -> Option { + addr_msg.attributes.iter().find_map(|attr| { + if let AddressAttribute::Address(addr) = attr { + Some(*addr) + } else { + None + } + }) } async fn get_all_links(handle: Handle) -> anyhow::Result> { diff --git a/crates/network-scanner/src/interfaces/mod.rs b/crates/network-scanner/src/interfaces/mod.rs index 02c9eaa3e..8ad01c2f0 100644 --- a/crates/network-scanner/src/interfaces/mod.rs +++ b/crates/network-scanner/src/interfaces/mod.rs @@ -3,11 +3,9 @@ mod linux; #[cfg(target_os = "windows")] mod windows; -#[cfg(target_os = "windows")] -pub use windows::get_network_interfaces; +mod filter; -#[cfg(target_os = "linux")] -pub use linux::get_network_interfaces; +pub use filter::Filter; use std::net::IpAddr; @@ -17,6 +15,26 @@ pub enum MacAddr { Eui48([u8; 6]), } +pub async fn get_network_interfaces(filter: Filter) -> anyhow::Result> { + #[cfg(target_os = "windows")] + let result = { windows::get_network_interfaces().await }; + #[cfg(target_os = "linux")] + let result = { linux::get_network_interfaces().await }; + + result.map(|interfaces| { + interfaces + .into_iter() + .filter(|interface| { + if !filter.include_loopback { + return filter::is_loop_back(interface); + } + true + }) + .map(|interface| filter::filter_out_ipv6_if(filter.ignore_ipv6, interface)) + .collect() + }) +} + impl TryFrom<&[u8]> for MacAddr { type Error = anyhow::Error; @@ -72,10 +90,13 @@ pub struct InterfaceAddress { #[derive(Debug, Clone)] pub struct NetworkInterface { + // Linux interfaces does not come with an id + pub id: Option, pub name: String, pub description: Option, pub mac_address: Option, - pub addresses: Vec, + pub routes: Vec, + pub ip_adresses: Vec, pub operational_status: bool, pub gateways: Vec, pub dns_servers: Vec, diff --git a/crates/network-scanner/src/interfaces/windows.rs b/crates/network-scanner/src/interfaces/windows.rs index 807f1651b..77d9ba0ef 100644 --- a/crates/network-scanner/src/interfaces/windows.rs +++ b/crates/network-scanner/src/interfaces/windows.rs @@ -3,7 +3,7 @@ use anyhow::Context; use super::MacAddr; -pub async fn get_network_interfaces() -> anyhow::Result> { +pub(crate) async fn get_network_interfaces() -> anyhow::Result> { ipconfig::get_adapters() .context("failed to get network interfaces")? .into_iter() @@ -16,10 +16,12 @@ impl From for NetworkInterface { let mac_address: Option = adapter.physical_address().and_then(|addr| addr.try_into().ok()); NetworkInterface { - name: adapter.adapter_name().to_owned(), + id: Some(adapter.adapter_name().to_owned()), + name: adapter.friendly_name().to_owned(), description: Some(adapter.description().to_owned()), mac_address, - addresses: adapter + ip_adresses: adapter.ip_addresses().to_vec(), + routes: adapter .prefixes() .iter() .map(|(ip, prefix)| super::InterfaceAddress { diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index ea584a9a9..af6b775e7 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -2,7 +2,7 @@ use crate::http::HttpError; use crate::token::{ApplicationProtocol, Protocol}; use crate::DgwState; use axum::extract::ws::Message; -use axum::extract::WebSocketUpgrade; +use axum::extract::{Query, WebSocketUpgrade}; use axum::response::Response; use axum::{Json, Router}; use network_scanner::interfaces::{self, MacAddr}; @@ -26,7 +26,7 @@ pub fn make_router(state: DgwState) -> Router { pub async fn handle_network_scan( _token: crate::extract::NetScanToken, ws: WebSocketUpgrade, - query_params: axum::extract::Query, + query_params: Query, ) -> Result { let scanner_params: NetworkScannerParams = query_params.0.into(); @@ -188,12 +188,22 @@ impl NetworkScanResponse { } } +#[derive(Debug, Deserialize)] +pub struct NetworkConfigParams { + pub ignore_ipv6: Option, + pub include_loopback: Option, +} + /// Lists network interfaces #[cfg_attr(feature = "openapi", utoipa::path( get, operation_id = "GetNetConfig", tag = "Net", path = "/jet/net/config", + params( + (name = "ignore_ipv6", description = "Ignore IPv6 addresses", type = "boolean", example = false), + (name = "include_loopback", description = "Include loopback interfaces", type = "boolean", example = true), + ), responses( (status = 200, description = "Network interfaces", body = [Vec]), (status = 400, description = "Bad request"), @@ -203,8 +213,19 @@ impl NetworkScanResponse { ), security(("netscan_token" = [])), ))] -pub async fn get_net_config(_token: crate::extract::NetScanToken) -> Result>, HttpError> { - let interfaces = interfaces::get_network_interfaces() +pub async fn get_net_config( + Query(params): Query, + _token: crate::extract::NetScanToken, +) -> Result>, HttpError> { + let mut filter = interfaces::Filter::default(); + if let Some(ignore_ipv6) = params.ignore_ipv6 { + filter.ignore_ipv6 = ignore_ipv6; + } + if let Some(include_loopback) = params.include_loopback { + filter.include_loopback = include_loopback; + } + + let interfaces = interfaces::get_network_interfaces(filter) .await .map_err(HttpError::internal().with_msg("failed to get network interfaces").err())? .into_iter() @@ -225,30 +246,41 @@ pub struct InterfaceAddress { #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[derive(Debug, Clone, Serialize)] pub struct NetworkInterface { - pub name: String, + /// The id is a Windows specific concept, does not exist in linux + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// The description of the interface, also Windows specific #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[cfg_attr(feature = "openapi", schema(value_type = Option))] #[serde(skip_serializing_if = "Option::is_none")] pub mac_address: Option, + // routes is the list of IP addresses and their prefix lengths that this interface routes to. #[cfg_attr(feature = "openapi", schema(value_type = Vec))] - pub addresses: Vec, + pub routes: Vec, #[cfg_attr(feature = "openapi", schema(value_type = bool))] pub is_up: bool, #[cfg_attr(feature = "openapi", schema(value_type = Vec))] pub gateways: Vec, #[cfg_attr(feature = "openapi", schema(value_type = Vec))] pub nameservers: Vec, + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub name: String, + // Assigned IP addresses to this interface + #[cfg_attr(feature = "openapi", schema(value_type = Vec))] + pub ip_adresses: Vec, } impl From for NetworkInterface { fn from(iface: interfaces::NetworkInterface) -> Self { Self { + id: iface.id, name: iface.name, description: iface.description, mac_address: iface.mac_address, - addresses: iface - .addresses + ip_adresses: iface.ip_adresses, + routes: iface + .routes .into_iter() .map(|addr| InterfaceAddress { ip: addr.ip,