538 lines
16 KiB
Rust
538 lines
16 KiB
Rust
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<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 mut routes = Vec::new();
|
|
routes.extend(parse_ipv4_routes()?);
|
|
routes.extend(parse_ipv6_routes()?);
|
|
Ok(routes)
|
|
}
|
|
|
|
async fn dns_config(&self) -> Result<DnsConfigSnapshot, PlatformError> {
|
|
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<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_ipv4_routes() -> Result<Vec<RouteEntry>, 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::<u32>().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<Vec<RouteEntry>, 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<std::net::Ipv4Addr> {
|
|
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<std::net::Ipv6Addr> {
|
|
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<String, ProcInfo>,
|
|
) -> Result<Vec<ListenSocket>, 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<String, ProcInfo>,
|
|
) -> Result<Vec<ListenSocket>, 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<String> {
|
|
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<NeighborEntry> {
|
|
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<u16> {
|
|
if let Some(pos) = value.rfind(':') {
|
|
return value[pos + 1..].parse::<u16>().ok();
|
|
}
|
|
None
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct ProcInfo {
|
|
pid: u32,
|
|
ppid: Option<u32>,
|
|
name: Option<String>,
|
|
path: Option<String>,
|
|
}
|
|
|
|
fn build_inode_map() -> HashMap<String, ProcInfo> {
|
|
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::<u32>() {
|
|
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<String> {
|
|
let value = value.strip_prefix("socket:[")?;
|
|
let value = value.strip_suffix(']')?;
|
|
Some(value.to_string())
|
|
}
|
|
|
|
fn read_ppid(pid: u32) -> Option<u32> {
|
|
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::<u32>().ok()?;
|
|
Some(ppid)
|
|
}
|
|
|
|
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 LinuxPortsProvider {
|
|
async fn listening(&self) -> Result<Vec<ListenSocket>, 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<Vec<ListenSocket>, 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<Vec<RootCert>, PlatformError> {
|
|
load_native_roots("linux")
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl NeighProvider for LinuxNeighProvider {
|
|
async fn neighbors(&self) -> Result<Vec<NeighborEntry>, 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))
|
|
}
|
|
}
|