Add base subcrates
This commit is contained in:
529
crates/wtfnet-platform-windows/src/lib.rs
Normal file
529
crates/wtfnet-platform-windows/src/lib.rs
Normal file
@@ -0,0 +1,529 @@
|
||||
use async_trait::async_trait;
|
||||
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
|
||||
use regex::Regex;
|
||||
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, 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<Vec<NetInterface>, 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<Vec<RouteEntry>, 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<DnsConfigSnapshot, PlatformError> {
|
||||
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<std::net::Ipv4Addr>) -> Option<u8> {
|
||||
netmask.map(|mask| u32::from_be_bytes(mask.octets()).count_ones() as u8)
|
||||
}
|
||||
|
||||
fn prefix_from_v6_netmask(netmask: Option<std::net::Ipv6Addr>) -> Option<u8> {
|
||||
netmask.map(|mask| u128::from_be_bytes(mask.octets()).count_ones() as u8)
|
||||
}
|
||||
|
||||
fn parse_windows_routes(
|
||||
interfaces: &[NetworkInterface],
|
||||
) -> Result<Vec<RouteEntry>, 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<RouteEntry> {
|
||||
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::<u32>().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<RouteEntry> {
|
||||
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::<u32>().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<u32> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
let index = index.parse::<u32>().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<Vec<ListenSocket>, PlatformError> {
|
||||
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(socket) = parse_netstat_tcp_line(trimmed) {
|
||||
sockets.push(socket);
|
||||
}
|
||||
} else if trimmed.starts_with("UDP") {
|
||||
if let Some(socket) = parse_netstat_udp_line(trimmed) {
|
||||
sockets.push(socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sockets)
|
||||
}
|
||||
|
||||
fn parse_netstat_tcp_line(line: &str) -> Option<ListenSocket> {
|
||||
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::<u32>().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_udp_line(line: &str) -> Option<ListenSocket> {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let local = parts[1];
|
||||
let pid = parts[3].parse::<u32>().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<NeighborEntry> {
|
||||
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<u16> {
|
||||
if let Some(pos) = value.rfind(':') {
|
||||
return value[pos + 1..].parse::<u16>().ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn load_native_roots(store: &str) -> Result<Vec<RootCert>, 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<u32>) {
|
||||
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<Vec<ListenSocket>, PlatformError> {
|
||||
let sockets = parse_windows_listeners()?;
|
||||
Ok(sockets)
|
||||
}
|
||||
|
||||
async fn who_owns(&self, port: u16) -> Result<Vec<ListenSocket>, PlatformError> {
|
||||
let sockets = parse_windows_listeners()?;
|
||||
Ok(sockets
|
||||
.into_iter()
|
||||
.filter(|socket| extract_port(&socket.local_addr) == Some(port))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CertProvider for WindowsCertProvider {
|
||||
async fn trusted_roots(&self) -> Result<Vec<RootCert>, PlatformError> {
|
||||
load_native_roots("windows")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NeighProvider for WindowsNeighProvider {
|
||||
async fn neighbors(&self) -> Result<Vec<NeighborEntry>, 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user