Skip to content

fix(dgw): improve network interface information extraction #1290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
16 changes: 8 additions & 8 deletions crates/network-scanner/examples/ipconfig.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
39 changes: 39 additions & 0 deletions crates/network-scanner/src/interfaces/filter.rs
Original file line number Diff line number Diff line change
@@ -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
}
85 changes: 30 additions & 55 deletions crates/network-scanner/src/interfaces/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use rtnetlink::{new_connection, Handle};

use super::InterfaceAddress;

pub async fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {
pub(crate) async fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {
let (connection, handle, _) = new_connection()?;
tokio::spawn(connection);
let mut links_info = vec![];
Expand Down Expand Up @@ -47,15 +47,10 @@ pub async fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {

let address = addresses
.iter()
.map(|addr| AddressInfo::try_from(addr.clone()))
.collect::<Result<Vec<_>, _>>()
.inspect_err(|e| error!(error = format!("{e:#}"), "Failed to parse address info"));
.filter_map(|addr_msg| extract_ip(addr_msg.clone()))
.collect::<Vec<_>>();

let Ok(address) = address else {
continue;
};

link.addresses = address;
link.ip_addresses = address;
links_info.push(link);
}

Expand Down Expand Up @@ -120,32 +115,35 @@ impl TryFrom<&LinkInfo> for NetworkInterface {
}

fn convert_link_info_to_network_interface(link_info: &LinkInfo) -> anyhow::Result<NetworkInterface> {
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::<Vec<_>>();

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,
})
}

Expand All @@ -155,7 +153,7 @@ struct LinkInfo {
flags: Vec<LinkFlag>,
name: String,
index: u32,
addresses: Vec<AddressInfo>,
ip_addresses: Vec<IpAddr>,
routes: Vec<RouteInfo>,
dns_servers: Vec<IpAddr>,
}
Expand All @@ -166,12 +164,6 @@ impl LinkInfo {
}
}

#[derive(Debug, Clone)]
struct AddressInfo {
address: IpAddr,
prefix_len: u8,
}

#[derive(Debug, Clone)]
struct RouteInfo {
gateway: Option<IpAddr>,
Expand Down Expand Up @@ -250,31 +242,14 @@ impl TryFrom<RouteMessage> for RouteInfo {
}
}

impl TryFrom<AddressMessage> for AddressInfo {
type Error = anyhow::Error;

fn try_from(value: AddressMessage) -> Result<Self, Self::Error> {
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<IpAddr> {
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<Receiver<LinkInfo>> {
Expand Down
31 changes: 26 additions & 5 deletions crates/network-scanner/src/interfaces/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,6 +15,26 @@ pub enum MacAddr {
Eui48([u8; 6]),
}

pub async fn get_network_interfaces(filter: Filter) -> anyhow::Result<Vec<NetworkInterface>> {
#[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;

Expand Down Expand Up @@ -72,10 +90,13 @@ pub struct InterfaceAddress {

#[derive(Debug, Clone)]
pub struct NetworkInterface {
// Linux interfaces does not come with an id
pub id: Option<String>,
Comment on lines +93 to +94
Copy link
Member

@CBenoit CBenoit Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should it be named adapter_id or adapter_name instead? Or is this naming used somewhere else? I think Paul linked a reference somewhere.

Copy link
Contributor Author

@irvingoujAtDevolution irvingoujAtDevolution Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think id is fine, the struct named as Adapter

pub name: String,
pub description: Option<String>,
pub mac_address: Option<MacAddr>,
pub addresses: Vec<InterfaceAddress>,
pub routes: Vec<InterfaceAddress>,
pub ip_adresses: Vec<IpAddr>,
pub operational_status: bool,
pub gateways: Vec<IpAddr>,
pub dns_servers: Vec<IpAddr>,
Expand Down
8 changes: 5 additions & 3 deletions crates/network-scanner/src/interfaces/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anyhow::Context;

use super::MacAddr;

pub async fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {
pub(crate) async fn get_network_interfaces() -> anyhow::Result<Vec<NetworkInterface>> {
ipconfig::get_adapters()
.context("failed to get network interfaces")?
.into_iter()
Expand All @@ -16,10 +16,12 @@ impl From<ipconfig::Adapter> for NetworkInterface {
let mac_address: Option<MacAddr> = 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 {
Expand Down
48 changes: 40 additions & 8 deletions devolutions-gateway/src/api/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -26,7 +26,7 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
pub async fn handle_network_scan(
_token: crate::extract::NetScanToken,
ws: WebSocketUpgrade,
query_params: axum::extract::Query<NetworkScanQueryParams>,
query_params: Query<NetworkScanQueryParams>,
) -> Result<Response, HttpError> {
let scanner_params: NetworkScannerParams = query_params.0.into();

Expand Down Expand Up @@ -188,12 +188,22 @@ impl NetworkScanResponse {
}
}

#[derive(Debug, Deserialize)]
pub struct NetworkConfigParams {
pub ignore_ipv6: Option<bool>,
pub include_loopback: Option<bool>,
}

/// 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<NetworkInterface>]),
(status = 400, description = "Bad request"),
Expand All @@ -203,8 +213,19 @@ impl NetworkScanResponse {
),
security(("netscan_token" = [])),
))]
pub async fn get_net_config(_token: crate::extract::NetScanToken) -> Result<Json<Vec<NetworkInterface>>, HttpError> {
let interfaces = interfaces::get_network_interfaces()
pub async fn get_net_config(
Query(params): Query<NetworkConfigParams>,
_token: crate::extract::NetScanToken,
) -> Result<Json<Vec<NetworkInterface>>, 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()
Expand All @@ -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<String>,
/// The description of the interface, also Windows specific
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_address: Option<MacAddr>,
// routes is the list of IP addresses and their prefix lengths that this interface routes to.
#[cfg_attr(feature = "openapi", schema(value_type = Vec<InterfaceAddress>))]
pub addresses: Vec<InterfaceAddress>,
pub routes: Vec<InterfaceAddress>,
#[cfg_attr(feature = "openapi", schema(value_type = bool))]
pub is_up: bool,
#[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
pub gateways: Vec<IpAddr>,
#[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
pub nameservers: Vec<IpAddr>,
#[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<IpAddr>))]
pub ip_adresses: Vec<IpAddr>,
}

impl From<interfaces::NetworkInterface> 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,
Expand Down
Loading