use async_trait::async_trait; use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig}; use regex::Regex; use std::collections::HashMap; use sha2::Digest; 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, }; use std::sync::Arc; use wtfnet_core::ErrorCode; use wtfnet_platform::{ CertProvider, ConnSocket, DnsConfigSnapshot, ListenSocket, NeighborEntry, NeighProvider, NetInterface, Platform, PlatformError, PortsProvider, RootCert, RouteEntry, SysProvider, }; pub fn platform() -> Platform { Platform { sys: Arc::new(WindowsSysProvider), ports: Arc::new(WindowsPortsProvider), cert: Arc::new(WindowsCertProvider), neigh: Arc::new(WindowsNeighProvider), } } struct WindowsSysProvider; struct WindowsPortsProvider; struct WindowsCertProvider; struct WindowsNeighProvider; #[async_trait] impl SysProvider for WindowsSysProvider { 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 interfaces = NetworkInterface::show() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; parse_windows_routes(&interfaces) } async fn dns_config(&self) -> Result { let output = std::process::Command::new("ipconfig") .arg("/all") .output() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; if !output.status.success() { return Err(PlatformError::new( ErrorCode::IoError, "ipconfig /all failed", )); } let text = String::from_utf8_lossy(&output.stdout); Ok(parse_ipconfig_dns(&text)) } } 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_windows_routes( interfaces: &[NetworkInterface], ) -> Result, PlatformError> { let output = std::process::Command::new("route") .arg("print") .output() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; if !output.status.success() { return Err(PlatformError::new( ErrorCode::IoError, "route print failed", )); } let text = String::from_utf8_lossy(&output.stdout); let mut routes = Vec::new(); routes.extend(parse_windows_ipv4_routes(&text, interfaces)); routes.extend(parse_windows_ipv6_routes(&text, interfaces)); Ok(routes) } fn parse_windows_ipv4_routes( text: &str, interfaces: &[NetworkInterface], ) -> Vec { let mut routes = Vec::new(); let mut in_ipv4 = false; let mut in_active = false; for line in text.lines() { let trimmed = line.trim(); if trimmed.starts_with("IPv4 Route Table") { in_ipv4 = true; in_active = false; continue; } if trimmed.starts_with("IPv6 Route Table") { in_ipv4 = false; in_active = false; continue; } if !in_ipv4 { continue; } if trimmed.starts_with("Active Routes:") { in_active = true; continue; } if !in_active { continue; } if trimmed.starts_with("====") || trimmed.is_empty() || trimmed.starts_with("Network") { continue; } let parts: Vec<&str> = trimmed.split_whitespace().collect(); if parts.len() < 5 { continue; } let destination = parts[0]; let netmask = parts[1]; let gateway = parts[2]; let interface_addr = parts[3]; let metric = parts[4].parse::().ok(); let prefix = parse_ipv4_prefix(netmask); let destination = if let Some(prefix) = prefix { format!("{}/{}", destination, prefix) } else { destination.to_string() }; let iface = interface_name_from_ip(interfaces, interface_addr); routes.push(RouteEntry { destination, gateway: Some(gateway.to_string()).filter(|g| g != "0.0.0.0"), interface: iface, metric, }); } routes } fn parse_windows_ipv6_routes( text: &str, interfaces: &[NetworkInterface], ) -> Vec { let mut routes = Vec::new(); let mut in_ipv6 = false; let mut in_active = false; for line in text.lines() { let trimmed = line.trim(); if trimmed.starts_with("IPv6 Route Table") { in_ipv6 = true; in_active = false; continue; } if trimmed.starts_with("====") && in_ipv6 && in_active { break; } if !in_ipv6 { continue; } if trimmed.starts_with("Active Routes:") { in_active = true; continue; } if !in_active { continue; } if trimmed.is_empty() || trimmed.starts_with("If") { continue; } let parts: Vec<&str> = trimmed.split_whitespace().collect(); if parts.len() < 4 { continue; } let iface_index = parts[0]; let metric = parts[1].parse::().ok(); let destination = parts[2].to_string(); let gateway = parts[3].to_string(); let iface = interface_name_from_index(interfaces, iface_index); routes.push(RouteEntry { destination, gateway: Some(gateway).filter(|g| g != "On-link"), interface: iface, metric, }); } routes } fn parse_ipv4_prefix(netmask: &str) -> Option { let mask: std::net::Ipv4Addr = netmask.parse().ok()?; Some(u32::from_be_bytes(mask.octets()).count_ones()) } fn interface_name_from_ip( interfaces: &[NetworkInterface], addr: &str, ) -> Option { let parsed: std::net::IpAddr = addr.parse().ok()?; for iface in interfaces { if iface.addr.iter().any(|entry| entry.ip() == parsed) { return Some(iface.name.clone()); } } None } fn interface_name_from_index( interfaces: &[NetworkInterface], index: &str, ) -> Option { let index = index.parse::().ok()?; interfaces .iter() .find(|iface| iface.index == index) .map(|iface| iface.name.clone()) } fn parse_ipconfig_dns(text: &str) -> DnsConfigSnapshot { let mut servers = Vec::new(); let mut search_domains = Vec::new(); let dns_server_re = Regex::new(r"^DNS Servers?\s*[:.]\s*(.+)$").unwrap(); let dns_suffix_re = Regex::new(r"^DNS Suffix Search List\.?\s*[:.]\s*(.+)$").unwrap(); let mut in_dns_servers = false; for line in text.lines() { let trimmed = line.trim(); if trimmed.is_empty() { in_dns_servers = false; continue; } if let Some(caps) = dns_server_re.captures(trimmed) { if let Some(value) = caps.get(1) { servers.push(value.as_str().to_string()); } in_dns_servers = true; continue; } if in_dns_servers && !trimmed.contains(':') { servers.push(trimmed.to_string()); continue; } if let Some(caps) = dns_suffix_re.captures(trimmed) { if let Some(value) = caps.get(1) { let list = value.as_str(); for entry in list.split_whitespace() { search_domains.push(entry.to_string()); } } continue; } } DnsConfigSnapshot { servers, search_domains, } } fn parse_windows_listeners() -> Result, PlatformError> { let proc_map = load_windows_process_map(); let output = std::process::Command::new("netstat") .arg("-ano") .output() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; if !output.status.success() { return Err(PlatformError::new(ErrorCode::IoError, "netstat -ano failed")); } let text = String::from_utf8_lossy(&output.stdout); let mut sockets = Vec::new(); for line in text.lines() { let trimmed = line.trim(); if trimmed.starts_with("TCP") { if let Some(mut socket) = parse_netstat_tcp_line(trimmed) { enrich_socket(&mut socket, &proc_map); sockets.push(socket); } } else if trimmed.starts_with("UDP") { if let Some(mut socket) = parse_netstat_udp_line(trimmed) { enrich_socket(&mut socket, &proc_map); sockets.push(socket); } } } Ok(sockets) } fn parse_windows_connections() -> Result, PlatformError> { let proc_map = load_windows_process_map(); let output = std::process::Command::new("netstat") .arg("-ano") .output() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; if !output.status.success() { return Err(PlatformError::new(ErrorCode::IoError, "netstat -ano failed")); } let text = String::from_utf8_lossy(&output.stdout); let mut sockets = Vec::new(); for line in text.lines() { let trimmed = line.trim(); if !trimmed.starts_with("TCP") { continue; } if let Some(mut socket) = parse_netstat_tcp_conn_line(trimmed) { enrich_conn_socket(&mut socket, &proc_map); sockets.push(socket); } } Ok(sockets) } fn parse_netstat_tcp_line(line: &str) -> Option { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 5 { return None; } let local = parts[1]; let state = parts[3]; let pid = parts[4].parse::().ok(); if state != "LISTENING" { return None; } Some(ListenSocket { proto: "tcp".to_string(), local_addr: local.to_string(), state: Some(state.to_string()), pid, ppid: None, process_name: None, process_path: None, owner: None, }) } fn parse_netstat_tcp_conn_line(line: &str) -> Option { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 5 { return None; } let local = parts[1]; let remote = parts[2]; let state = parts[3]; let pid = parts[4].parse::().ok(); if state == "LISTENING" { return None; } Some(ConnSocket { proto: "tcp".to_string(), local_addr: local.to_string(), remote_addr: remote.to_string(), state: Some(state.to_string()), pid, ppid: None, process_name: None, process_path: None, }) } fn parse_netstat_udp_line(line: &str) -> Option { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { return None; } let local = parts[1]; let pid = parts[3].parse::().ok(); Some(ListenSocket { proto: "udp".to_string(), local_addr: local.to_string(), state: None, pid, ppid: None, process_name: None, process_path: None, owner: None, }) } fn parse_arp_output(text: &str) -> Vec { let mut neighbors = Vec::new(); let mut current_iface = None; for line in text.lines() { let trimmed = line.trim(); if trimmed.starts_with("Interface:") { current_iface = trimmed .split_whitespace() .nth(1) .map(|value| value.to_string()); continue; } if trimmed.starts_with("Internet Address") || trimmed.is_empty() { continue; } let parts: Vec<&str> = trimmed.split_whitespace().collect(); if parts.len() < 3 { continue; } neighbors.push(NeighborEntry { ip: parts[0].to_string(), mac: Some(parts[1].to_string()), interface: current_iface.clone(), state: Some(parts[2].to_string()), }); } neighbors } fn extract_port(value: &str) -> Option { if let Some(pos) = value.rfind(':') { return value[pos + 1..].parse::().ok(); } None } fn enrich_socket(socket: &mut ListenSocket, map: &HashMap) { let pid = match socket.pid { Some(pid) => pid, None => return, }; if let Some(info) = map.get(&pid) { socket.process_name = info.name.clone(); socket.process_path = info.path.clone(); } } fn enrich_conn_socket(socket: &mut ConnSocket, map: &HashMap) { let pid = match socket.pid { Some(pid) => pid, None => return, }; if let Some(info) = map.get(&pid) { socket.process_name = info.name.clone(); socket.process_path = info.path.clone(); } } #[derive(Clone)] struct ProcInfo { name: Option, path: Option, } fn load_windows_process_map() -> HashMap { let mut map = HashMap::new(); let mut name_map = HashMap::new(); let tasklist = std::process::Command::new("tasklist") .args(["/fo", "csv", "/nh"]) .output(); if let Ok(output) = tasklist { if output.status.success() { let text = String::from_utf8_lossy(&output.stdout); for line in text.lines() { let parts = parse_csv_line(line); if parts.len() < 2 { continue; } if let Ok(pid) = parts[1].parse::() { name_map.insert(pid, parts[0].to_string()); } } } } let wmic = std::process::Command::new("wmic") .args(["process", "get", "ProcessId,ExecutablePath", "/FORMAT:CSV"]) .output(); if let Ok(output) = wmic { if output.status.success() { let text = String::from_utf8_lossy(&output.stdout); for line in text.lines() { let parts = parse_csv_line(line); if parts.len() < 3 { continue; } let path = parts[1].trim(); let pid = parts[2].trim().parse::().ok(); if let Some(pid) = pid { let name = name_map.get(&pid).cloned(); let path = if path.is_empty() { None } else { Some(path.to_string()) }; map.insert(pid, ProcInfo { name, path }); } } } } for (pid, name) in name_map { map.entry(pid) .or_insert_with(|| ProcInfo { name: Some(name), path: None, }); } map } fn parse_csv_line(line: &str) -> Vec { let mut out = Vec::new(); let mut current = String::new(); let mut in_quotes = false; for ch in line.chars() { match ch { '"' => { in_quotes = !in_quotes; } ',' if !in_quotes => { out.push(current.trim_matches('"').to_string()); current.clear(); } _ => current.push(ch), } } if !current.is_empty() { out.push(current.trim_matches('"').to_string()); } out } 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 WindowsPortsProvider { async fn listening(&self) -> Result, PlatformError> { let sockets = parse_windows_listeners()?; Ok(sockets) } async fn who_owns(&self, port: u16) -> Result, PlatformError> { let sockets = parse_windows_listeners()?; Ok(sockets .into_iter() .filter(|socket| extract_port(&socket.local_addr) == Some(port)) .collect()) } async fn connections(&self) -> Result, PlatformError> { parse_windows_connections() } } #[async_trait] impl CertProvider for WindowsCertProvider { async fn trusted_roots(&self) -> Result, PlatformError> { load_native_roots("windows") } } #[async_trait] impl NeighProvider for WindowsNeighProvider { async fn neighbors(&self) -> Result, PlatformError> { let output = std::process::Command::new("arp") .arg("-a") .output() .map_err(|err| PlatformError::new(ErrorCode::IoError, err.to_string()))?; if !output.status.success() { return Err(PlatformError::new(ErrorCode::IoError, "arp -a failed")); } let text = String::from_utf8_lossy(&output.stdout); Ok(parse_arp_output(&text)) } }