use async_trait::async_trait; use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig}; use sha2::Digest; use std::collections::HashMap; use std::sync::Arc; use wtfnet_core::ErrorCode; use wtfnet_platform::{ CertProvider, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, }; use x509_parser::oid_registry::{ OID_KEY_TYPE_DSA, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_KEY_TYPE_GOST_R3410_2012_256, OID_KEY_TYPE_GOST_R3410_2012_512, OID_PKCS1_RSAENCRYPTION, }; pub fn platform() -> Platform { Platform { sys: Arc::new(LinuxSysProvider), ports: Arc::new(LinuxPortsProvider), cert: Arc::new(LinuxCertProvider), neigh: Arc::new(LinuxNeighProvider), } } struct LinuxSysProvider; struct LinuxPortsProvider; struct LinuxCertProvider; struct LinuxNeighProvider; #[async_trait] impl SysProvider for LinuxSysProvider { async fn interfaces(&self) -> Result, PlatformError> { let interfaces = NetworkInterface::show() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; Ok(interfaces.into_iter().map(map_interface).collect()) } async fn routes(&self) -> Result, PlatformError> { let mut routes = Vec::new(); routes.extend(parse_ipv4_routes()?); routes.extend(parse_ipv6_routes()?); Ok(routes) } async fn dns_config(&self) -> Result { let contents = std::fs::read_to_string("/etc/resolv.conf") .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let cfg = resolv_conf::Config::parse(&contents) .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let servers = cfg .nameservers .iter() .map(|ns| ns.to_string()) .collect(); let search_domains = cfg .get_last_search_or_domain() .map(|domain| domain.to_string()) .collect(); Ok(DnsConfigSnapshot { servers, search_domains, }) } } fn map_interface(iface: NetworkInterface) -> NetInterface { let addresses = iface .addr .into_iter() .map(|addr| match addr { Addr::V4(v4) => wtfnet_platform::NetAddress { ip: v4.ip.to_string(), prefix_len: prefix_from_v4_netmask(v4.netmask), scope: None, }, Addr::V6(v6) => wtfnet_platform::NetAddress { ip: v6.ip.to_string(), prefix_len: prefix_from_v6_netmask(v6.netmask), scope: None, }, }) .collect(); NetInterface { name: iface.name, index: Some(iface.index), is_up: None, mtu: None, mac: iface.mac_addr, addresses, } } fn prefix_from_v4_netmask(netmask: Option) -> Option { netmask.map(|mask| u32::from_be_bytes(mask.octets()).count_ones() as u8) } fn prefix_from_v6_netmask(netmask: Option) -> Option { netmask.map(|mask| u128::from_be_bytes(mask.octets()).count_ones() as u8) } fn parse_ipv4_routes() -> Result, PlatformError> { let contents = std::fs::read_to_string("/proc/net/route") .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let mut routes = Vec::new(); for (idx, line) in contents.lines().enumerate() { if idx == 0 { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 8 { continue; } let iface = parts[0].to_string(); let dest = parse_ipv4_hex(parts[1]); let gateway = parse_ipv4_hex(parts[2]); let mask = parse_ipv4_hex(parts[7]); let metric = parts[6].parse::().ok(); let destination = match (dest, mask) { (Some(dest), Some(mask)) => { let prefix = u32::from(mask).count_ones(); format!("{}/{}", dest, prefix) } (Some(dest), None) => dest.to_string(), _ => continue, }; routes.push(RouteEntry { destination, gateway: gateway.map(|ip| ip.to_string()).filter(|ip| ip != "0.0.0.0"), interface: Some(iface), metric, }); } Ok(routes) } fn parse_ipv6_routes() -> Result, PlatformError> { let contents = std::fs::read_to_string("/proc/net/ipv6_route") .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let mut routes = Vec::new(); for line in contents.lines() { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 10 { continue; } let dest = parse_ipv6_hex(parts[0]); let dest_prefix = u32::from_str_radix(parts[1], 16).ok(); let gateway = parse_ipv6_hex(parts[4]); let metric = u32::from_str_radix(parts[5], 16).ok(); let iface = parts[9].to_string(); let destination = match (dest, dest_prefix) { (Some(dest), Some(prefix)) => format!("{}/{}", dest, prefix), (Some(dest), None) => dest.to_string(), _ => continue, }; routes.push(RouteEntry { destination, gateway: gateway.map(|ip| ip.to_string()).filter(|ip| ip != "::"), interface: Some(iface), metric, }); } Ok(routes) } fn parse_ipv4_hex(value: &str) -> Option { if value.len() != 8 { return None; } let raw = u32::from_str_radix(value, 16).ok()?; let bytes = raw.to_le_bytes(); Some(std::net::Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3])) } fn parse_ipv6_hex(value: &str) -> Option { if value.len() != 32 { return None; } let mut bytes = [0u8; 16]; for i in 0..16 { let start = i * 2; let chunk = &value[start..start + 2]; bytes[i] = u8::from_str_radix(chunk, 16).ok()?; } Some(std::net::Ipv6Addr::from(bytes)) } fn parse_linux_tcp_with_inode_map( path: &str, is_v6: bool, inode_map: &HashMap, ) -> Result, PlatformError> { let contents = std::fs::read_to_string(path) .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let mut sockets = Vec::new(); for (idx, line) in contents.lines().enumerate() { if idx == 0 { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { continue; } let local = parts[1]; let state = parts[3]; if state != "0A" { continue; } let inode = parts.get(9).copied(); if let Some(local_addr) = parse_proc_socket_addr(local, is_v6) { let (pid, ppid, process_name, process_path) = inode.and_then(|value| inode_map.get(value)).map_or( (None, None, None, None), |info| { ( Some(info.pid), info.ppid, info.name.clone(), info.path.clone(), ) }, ); sockets.push(ListenSocket { proto: "tcp".to_string(), local_addr, state: Some("LISTEN".to_string()), pid, ppid, process_name, process_path, owner: None, }); } } Ok(sockets) } fn parse_linux_udp_with_inode_map( path: &str, is_v6: bool, inode_map: &HashMap, ) -> Result, PlatformError> { let contents = std::fs::read_to_string(path) .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let mut sockets = Vec::new(); for (idx, line) in contents.lines().enumerate() { if idx == 0 { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 2 { continue; } let local = parts[1]; let inode = parts.get(9).copied(); if let Some(local_addr) = parse_proc_socket_addr(local, is_v6) { let (pid, ppid, process_name, process_path) = inode.and_then(|value| inode_map.get(value)).map_or( (None, None, None, None), |info| { ( Some(info.pid), info.ppid, info.name.clone(), info.path.clone(), ) }, ); sockets.push(ListenSocket { proto: "udp".to_string(), local_addr, state: None, pid, ppid, process_name, process_path, owner: None, }); } } Ok(sockets) } fn parse_proc_socket_addr(value: &str, is_v6: bool) -> Option { let mut parts = value.split(':'); let addr_hex = parts.next()?; let port_hex = parts.next()?; let port = u16::from_str_radix(port_hex, 16).ok()?; if is_v6 { let addr = parse_ipv6_hex(addr_hex)?; Some(format!("[{}]:{}", addr, port)) } else { let addr = parse_ipv4_hex(addr_hex)?; Some(format!("{}:{}", addr, port)) } } fn parse_linux_arp(contents: &str) -> Vec { let mut neighbors = Vec::new(); for (idx, line) in contents.lines().enumerate() { if idx == 0 { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 6 { continue; } let flags = parts[2]; let state = match flags { "0x2" => Some("reachable".to_string()), _ => Some("stale".to_string()), }; neighbors.push(NeighborEntry { ip: parts[0].to_string(), mac: Some(parts[3].to_string()).filter(|mac| mac != "00:00:00:00:00:00"), interface: Some(parts[5].to_string()), state, }); } neighbors } fn extract_port(value: &str) -> Option { if let Some(pos) = value.rfind(':') { return value[pos + 1..].parse::().ok(); } None } #[derive(Clone)] struct ProcInfo { pid: u32, ppid: Option, name: Option, path: Option, } fn build_inode_map() -> HashMap { let mut map = HashMap::new(); let entries = match std::fs::read_dir("/proc") { Ok(entries) => entries, Err(_) => return map, }; for entry in entries.flatten() { let file_name = entry.file_name(); let name = match file_name.to_str() { Some(name) => name, None => continue, }; let pid = match name.parse::() { Ok(pid) => pid, Err(_) => continue, }; let comm = std::fs::read_to_string(format!("/proc/{}/comm", pid)) .ok() .map(|value| value.trim().to_string()); let path = std::fs::read_link(format!("/proc/{}/exe", pid)) .ok() .and_then(|value| value.to_str().map(|s| s.to_string())); let ppid = read_ppid(pid); let info = ProcInfo { pid, ppid, name: comm, path, }; let fd_dir = match std::fs::read_dir(format!("/proc/{}/fd", pid)) { Ok(dir) => dir, Err(_) => continue, }; for fd in fd_dir.flatten() { if let Ok(target) = std::fs::read_link(fd.path()) { if let Some(target) = target.to_str() { if let Some(inode) = parse_socket_inode(target) { map.entry(inode).or_insert_with(|| info.clone()); } } } } } map } fn parse_socket_inode(value: &str) -> Option { let value = value.strip_prefix("socket:[")?; let value = value.strip_suffix(']')?; Some(value.to_string()) } fn read_ppid(pid: u32) -> Option { let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?; let end = stat.rfind(')')?; let rest = stat.get(end + 2..)?; let mut parts = rest.split_whitespace(); let _state = parts.next()?; let ppid = parts.next()?.parse::().ok()?; Some(ppid) } fn load_native_roots(store: &str) -> Result, PlatformError> { let certs = rustls_native_certs::load_native_certs() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; let mut roots = Vec::new(); for cert in certs { let der = cert.as_ref(); let parsed = match x509_parser::parse_x509_certificate(der) { Ok((_, cert)) => cert, Err(_) => continue, }; let subject = parsed.subject().to_string(); let issuer = parsed.issuer().to_string(); let not_before = parsed.validity().not_before.to_string(); let not_after = parsed.validity().not_after.to_string(); let serial = parsed.tbs_certificate.raw_serial_as_string(); let sha1 = format_fingerprint(sha1::Sha1::digest(der).as_slice()); let sha256 = format_fingerprint(sha2::Sha256::digest(der).as_slice()); let (key_algorithm, key_size) = key_info(&parsed); roots.push(RootCert { subject, issuer, not_before, not_after, serial_number: serial, sha1, sha256, key_algorithm, key_size, store: Some(store.to_string()), }); } Ok(roots) } fn key_info(cert: &x509_parser::certificate::X509Certificate<'_>) -> (String, Option) { let algorithm = &cert.subject_pki.algorithm.algorithm; let name = if algorithm == &OID_PKCS1_RSAENCRYPTION { "RSA" } else if algorithm == &OID_KEY_TYPE_EC_PUBLIC_KEY { "EC" } else if algorithm == &OID_KEY_TYPE_DSA { "DSA" } else if algorithm == &OID_KEY_TYPE_GOST_R3410_2012_256 { "GOST2012-256" } else if algorithm == &OID_KEY_TYPE_GOST_R3410_2012_512 { "GOST2012-512" } else { "Unknown" }; let key_size = cert .subject_pki .parsed() .ok() .map(|key| key.key_size() as u32) .filter(|size| *size > 0); (name.to_string(), key_size) } fn format_fingerprint(bytes: &[u8]) -> String { let mut out = String::new(); for (idx, byte) in bytes.iter().enumerate() { if idx > 0 { out.push(':'); } use std::fmt::Write; let _ = write!(out, "{:02x}", byte); } out } #[async_trait] impl PortsProvider for LinuxPortsProvider { async fn listening(&self) -> Result, PlatformError> { let inode_map = build_inode_map(); let mut sockets = Vec::new(); sockets.extend(parse_linux_tcp_with_inode_map( "/proc/net/tcp", false, &inode_map, )?); sockets.extend(parse_linux_tcp_with_inode_map( "/proc/net/tcp6", true, &inode_map, )?); sockets.extend(parse_linux_udp_with_inode_map( "/proc/net/udp", false, &inode_map, )?); sockets.extend(parse_linux_udp_with_inode_map( "/proc/net/udp6", true, &inode_map, )?); Ok(sockets) } async fn who_owns(&self, port: u16) -> Result, PlatformError> { let sockets = self.listening().await?; Ok(sockets .into_iter() .filter(|socket| extract_port(&socket.local_addr) == Some(port)) .collect()) } } #[async_trait] impl CertProvider for LinuxCertProvider { async fn trusted_roots(&self) -> Result, PlatformError> { load_native_roots("linux") } } #[async_trait] impl NeighProvider for LinuxNeighProvider { async fn neighbors(&self) -> Result, PlatformError> { let contents = std::fs::read_to_string("/proc/net/arp") .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; Ok(parse_linux_arp(&contents)) } }