use std::collections::{HashMap, HashSet};
use std::io::{Read, Write as IoWrite};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream};
use std::process::Command;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use chrono::Local;
use crate::types::{LanDevice, NetworkCategory, RemoteNetwork};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[link(name = "iphlpapi")]
extern "system" {
fn IcmpCreateFile() -> isize;
fn IcmpCloseHandle(IcmpHandle: isize) -> i32;
fn IcmpSendEcho(
IcmpHandle: isize,
DestinationAddress: u32,
RequestData: *const u8,
RequestSize: u16,
RequestOptions: *const u8,
ReplyBuffer: *mut u8,
ReplySize: u32,
Timeout: u32,
) -> u32;
}
const INVALID_HANDLE_VALUE: isize = -1;
#[derive(Clone, Debug)]
struct VpnSubnet {
network: Ipv4Addr,
prefix: u32,
mask: Ipv4Addr,
interface_hint: String,
source: String,
}
#[derive(Clone, Debug)]
struct VpnPeer {
ip: Ipv4Addr,
hostname: Option<String>,
info: String,
method: &'static str,
}
const TCP_PROBE_PORTS: &[u16] = &[80, 443, 22, 3389, 445, 8080];
const OPENVPN_MGMT_PORTS: &[u16] = &[7505, 7506, 7507, 1195, 7500];
fn openvpn_log_paths() -> Vec<String> {
let mut paths = vec![
r"C:\Program Files\OpenVPN\log\openvpn.log".to_string(),
r"C:\Program Files\OpenVPN\log\client.log".to_string(),
r"C:\Program Files (x86)\OpenVPN\log\openvpn.log".to_string(),
r"C:\Program Files\OpenVPN\config\openvpn.log".to_string(),
];
if let Ok(profile) = std::env::var("USERPROFILE") {
paths.push(format!(r"{}\OpenVPN\log\openvpn.log", profile));
paths.push(format!(r"{}\OpenVPN\log\client.log", profile));
}
if let Ok(appdata) = std::env::var("APPDATA") {
paths.push(format!(r"{}\OpenVPN\log\openvpn.log", appdata));
}
if let Ok(programdata) = std::env::var("PROGRAMDATA") {
paths.push(format!(r"{}\OpenVPN\log\openvpn.log", programdata));
}
paths
}
pub fn discover(primary_ip: Option<Ipv4Addr>) -> Vec<RemoteNetwork> {
let subnets: Mutex<Vec<VpnSubnet>> = Mutex::new(Vec::new());
let peers: Mutex<Vec<VpnPeer>> = Mutex::new(Vec::new());
let ras_connections: Mutex<Vec<String>> = Mutex::new(Vec::new());
thread::scope(|s| {
let sub_ref = &subnets;
s.spawn(move || {
if let Some(found) = route_table_vpn_subnets(primary_ip) {
sub_ref.lock().unwrap().extend(found);
}
});
let peer_ref = &peers;
s.spawn(move || {
let found = openvpn_management_query();
peer_ref.lock().unwrap().extend(found);
});
let peer_ref = &peers;
let sub_ref = &subnets;
s.spawn(move || {
let (wg_peers, wg_subnets) = wireguard_show();
peer_ref.lock().unwrap().extend(wg_peers);
sub_ref.lock().unwrap().extend(wg_subnets);
});
let peer_ref = &peers;
s.spawn(move || {
let found = openvpn_log_parse();
peer_ref.lock().unwrap().extend(found);
});
let ras_ref = &ras_connections;
s.spawn(move || {
if let Some(conns) = rasdial_enumerate() {
ras_ref.lock().unwrap().extend(conns);
}
});
});
let mut all_subnets = subnets.into_inner().unwrap();
let mut all_peers = peers.into_inner().unwrap();
let ras_conns = ras_connections.into_inner().unwrap();
dedup_subnets(&mut all_subnets);
if all_subnets.is_empty() && all_peers.is_empty() {
return Vec::new();
}
let scan_peers: Mutex<Vec<VpnPeer>> = Mutex::new(Vec::new());
if !all_subnets.is_empty() {
thread::scope(|s| {
for subnet in &all_subnets {
let targets = subnet_hosts(subnet.network, subnet.mask, primary_ip);
if targets.is_empty() {
continue;
}
let sp = &scan_peers;
let tgts = targets.clone();
s.spawn(move || {
let found = icmp_sweep_vpn(&tgts);
sp.lock().unwrap().extend(found);
});
let sp = &scan_peers;
s.spawn(move || {
let found = tcp_probe_vpn(&targets);
sp.lock().unwrap().extend(found);
});
}
});
}
all_peers.extend(scan_peers.into_inner().unwrap());
build_networks(all_subnets, all_peers, ras_conns, primary_ip)
}
fn route_table_vpn_subnets(primary_ip: Option<Ipv4Addr>) -> Option<Vec<VpnSubnet>> {
let mut cmd = Command::new("route");
cmd.args(["print"]);
#[cfg(target_os = "windows")]
cmd.creation_flags(CREATE_NO_WINDOW);
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let mut subnets = Vec::new();
let primary = primary_ip.unwrap_or(Ipv4Addr::UNSPECIFIED);
let mut in_ipv4_table = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.contains("IPv4 Route Table") || trimmed.contains("IPv4") && trimmed.contains("Route") {
in_ipv4_table = true;
continue;
}
if trimmed.contains("IPv6 Route Table") {
in_ipv4_table = false;
continue;
}
if !in_ipv4_table {
continue;
}
if trimmed.starts_with("Network") || trimmed.starts_with("=") || trimmed.is_empty() {
continue;
}
if trimmed.starts_with("Active") || trimmed.starts_with("Persistent") {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 5 {
continue;
}
let dest: Ipv4Addr = match parts[0].parse() {
Ok(ip) => ip,
Err(_) => continue,
};
let mask: Ipv4Addr = match parts[1].parse() {
Ok(m) => m,
Err(_) => continue,
};
let _gateway: Ipv4Addr = match parts[2].parse() {
Ok(g) => g,
Err(_) => continue,
};
let iface: Ipv4Addr = match parts[3].parse() {
Ok(i) => i,
Err(_) => continue,
};
if dest == Ipv4Addr::UNSPECIFIED && mask == Ipv4Addr::UNSPECIFIED {
continue;
}
if dest.is_loopback() || dest.octets()[0] >= 224 {
continue;
}
if iface == primary {
continue;
}
let prefix = u32::from(mask).count_ones();
if prefix == 32 {
continue;
}
if prefix < 8 {
continue;
}
let octets = dest.octets();
if octets[0] == 169 && octets[1] == 254 {
continue;
}
let is_private = octets[0] == 10
|| (octets[0] == 172 && (16..=31).contains(&octets[1]))
|| (octets[0] == 192 && octets[1] == 168);
if !is_private {
continue;
}
subnets.push(VpnSubnet {
network: dest,
prefix,
mask,
interface_hint: iface.to_string(),
source: format!("route-table via {}", iface),
});
}
if subnets.is_empty() {
None
} else {
Some(subnets)
}
}
fn openvpn_management_query() -> Vec<VpnPeer> {
let mut all_peers = Vec::new();
for &port in OPENVPN_MGMT_PORTS {
if let Some(peers) = try_openvpn_mgmt(port) {
all_peers.extend(peers);
break; }
}
all_peers
}
fn try_openvpn_mgmt(port: u16) -> Option<Vec<VpnPeer>> {
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port));
let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(500)).ok()?;
stream.set_read_timeout(Some(Duration::from_millis(2000))).ok()?;
stream.set_write_timeout(Some(Duration::from_millis(500))).ok()?;
let mut banner = [0u8; 512];
let _ = stream.read(&mut banner);
stream.write_all(b"status\r\n").ok()?;
let mut response = String::new();
let mut buf = [0u8; 4096];
loop {
match stream.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
response.push_str(&String::from_utf8_lossy(&buf[..n]));
if response.contains("END") {
break;
}
}
Err(_) => break,
}
}
let _ = stream.write_all(b"quit\r\n");
parse_openvpn_status(&response)
}
fn parse_openvpn_status(status: &str) -> Option<Vec<VpnPeer>> {
let mut peers = Vec::new();
let mut in_client_list = false;
let mut in_routing_table = false;
for line in status.lines() {
let trimmed = line.trim();
if trimmed.starts_with("HEADER,CLIENT_LIST") || trimmed == "OpenVPN CLIENT LIST" {
in_client_list = true;
in_routing_table = false;
continue;
}
if trimmed.starts_with("HEADER,ROUTING_TABLE") || trimmed == "ROUTING TABLE" {
in_client_list = false;
in_routing_table = true;
continue;
}
if trimmed == "END" || trimmed.starts_with("GLOBAL STATS") {
in_client_list = false;
in_routing_table = false;
continue;
}
if in_client_list {
let parts: Vec<&str> = trimmed.split(',').collect();
if parts.len() >= 4 {
let offset = if parts[0] == "CLIENT_LIST" { 1 } else { 0 };
let cn = parts.get(offset).unwrap_or(&"").trim();
let virtual_addr = if parts.len() > offset + 2 {
parts[offset + 2].trim()
} else {
""
};
if let Ok(ip) = virtual_addr.parse::<Ipv4Addr>() {
peers.push(VpnPeer {
ip,
hostname: if cn.is_empty() { None } else { Some(cn.to_string()) },
info: format!("OpenVPN CN={}", cn),
method: "OVPN-Mgmt",
});
}
}
}
if in_routing_table {
let parts: Vec<&str> = trimmed.split(',').collect();
if parts.len() >= 3 {
let offset = if parts[0] == "ROUTING_TABLE" { 1 } else { 0 };
let vaddr = parts.get(offset).unwrap_or(&"").trim();
let cn = parts.get(offset + 1).unwrap_or(&"").trim();
if let Ok(ip) = vaddr.parse::<Ipv4Addr>() {
if !peers.iter().any(|p| p.ip == ip) {
peers.push(VpnPeer {
ip,
hostname: if cn.is_empty() { None } else { Some(cn.to_string()) },
info: format!("OpenVPN route CN={}", cn),
method: "OVPN-Mgmt",
});
}
}
}
}
}
if peers.is_empty() { None } else { Some(peers) }
}
fn wireguard_show() -> (Vec<VpnPeer>, Vec<VpnSubnet>) {
let mut peers = Vec::new();
let mut subnets = Vec::new();
let mut cmd = Command::new("wg");
cmd.args(["show"]);
#[cfg(target_os = "windows")]
cmd.creation_flags(CREATE_NO_WINDOW);
let output = match cmd.output() {
Ok(o) if o.status.success() => o,
_ => return (peers, subnets),
};
let text = String::from_utf8_lossy(&output.stdout);
let mut current_iface = String::new();
let mut current_peer_key = String::new();
let mut current_endpoint = String::new();
let mut current_handshake = String::new();
let mut current_allowed_ips: Vec<String> = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("interface:") {
flush_wg_peer(
¤t_peer_key,
¤t_endpoint,
¤t_handshake,
¤t_allowed_ips,
¤t_iface,
&mut peers,
&mut subnets,
);
current_peer_key.clear();
current_endpoint.clear();
current_handshake.clear();
current_allowed_ips.clear();
current_iface = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim().to_string();
continue;
}
if trimmed.starts_with("peer:") {
flush_wg_peer(
¤t_peer_key,
¤t_endpoint,
¤t_handshake,
¤t_allowed_ips,
¤t_iface,
&mut peers,
&mut subnets,
);
current_peer_key = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim().to_string();
current_endpoint.clear();
current_handshake.clear();
current_allowed_ips.clear();
continue;
}
if trimmed.starts_with("endpoint:") {
current_endpoint = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim().to_string();
} else if trimmed.starts_with("latest handshake:") {
current_handshake = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim().to_string();
} else if trimmed.starts_with("allowed ips:") {
let ips_str = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim();
current_allowed_ips = ips_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
}
flush_wg_peer(
¤t_peer_key,
¤t_endpoint,
¤t_handshake,
¤t_allowed_ips,
¤t_iface,
&mut peers,
&mut subnets,
);
(peers, subnets)
}
fn flush_wg_peer(
peer_key: &str,
_endpoint: &str,
handshake: &str,
allowed_ips: &[String],
iface: &str,
peers: &mut Vec<VpnPeer>,
subnets: &mut Vec<VpnSubnet>,
) {
if peer_key.is_empty() {
return;
}
let short_key = if peer_key.len() > 8 {
&peer_key[..8]
} else {
peer_key
};
for cidr in allowed_ips {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
continue;
}
let ip: Ipv4Addr = match parts[0].parse() {
Ok(ip) => ip,
Err(_) => continue,
};
let prefix: u32 = match parts[1].parse() {
Ok(p) => p,
Err(_) => continue,
};
if prefix == 32 {
let info = if handshake.is_empty() {
format!("WireGuard peer:{}.. iface:{}", short_key, iface)
} else {
format!(
"WireGuard peer:{}.. iface:{} handshake:{}",
short_key, iface, handshake
)
};
peers.push(VpnPeer {
ip,
hostname: None,
info,
method: "WireGuard",
});
} else if prefix >= 8 && prefix < 32 {
let mask_u32 = if prefix == 0 { 0 } else { !0u32 << (32 - prefix) };
let net_u32 = u32::from(ip) & mask_u32;
subnets.push(VpnSubnet {
network: Ipv4Addr::from(net_u32),
prefix,
mask: Ipv4Addr::from(mask_u32),
interface_hint: iface.to_string(),
source: format!("WireGuard allowed-ips (peer:{}..)", short_key),
});
}
}
}
fn openvpn_log_parse() -> Vec<VpnPeer> {
let mut all_peers = Vec::new();
for path in openvpn_log_paths() {
if let Ok(content) = std::fs::read_to_string(&path) {
let mut found = parse_openvpn_log_content(&content);
all_peers.append(&mut found);
}
}
let mut seen = HashSet::new();
all_peers.retain(|p| seen.insert(p.ip));
all_peers
}
fn parse_openvpn_log_content(content: &str) -> Vec<VpnPeer> {
let mut peers = Vec::new();
let mut seen_ips = HashSet::new();
for line in content.lines() {
if line.contains("MULTI: Learn:") || line.contains("MULTI_sva: Learn:") {
if let Some(ip) = extract_first_private_ip(line) {
if seen_ips.insert(ip) {
let cn = extract_cn_from_line(line);
peers.push(VpnPeer {
ip,
hostname: cn.clone(),
info: format!("OpenVPN log ({})", cn.unwrap_or_default()),
method: "OVPN-Log",
});
}
}
}
if line.contains("ifconfig_pool_set") {
if let Some(ip) = extract_first_private_ip(line) {
if seen_ips.insert(ip) {
peers.push(VpnPeer {
ip,
hostname: None,
info: "OpenVPN log (pool assignment)".to_string(),
method: "OVPN-Log",
});
}
}
}
if line.contains("CONNECTED,SUCCESS") {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 3 {
if let Ok(ip) = parts[2].trim().parse::<Ipv4Addr>() {
if is_private_ip(ip) && seen_ips.insert(ip) {
peers.push(VpnPeer {
ip,
hostname: None,
info: "OpenVPN log (connected)".to_string(),
method: "OVPN-Log",
});
}
}
}
}
}
peers
}
fn extract_first_private_ip(line: &str) -> Option<Ipv4Addr> {
for word in line.split_whitespace() {
let clean = word.trim_matches(|c: char| !c.is_ascii_digit() && c != '.');
if let Ok(ip) = clean.parse::<Ipv4Addr>() {
if is_private_ip(ip) && !ip.is_loopback() {
return Some(ip);
}
}
}
None
}
fn extract_cn_from_line(line: &str) -> Option<String> {
if let Some(pos) = line.find("CN=") {
let rest = &line[pos + 3..];
let end = rest.find(|c: char| c == ',' || c == '/' || c == ' ').unwrap_or(rest.len());
let cn = rest[..end].trim();
if !cn.is_empty() {
return Some(cn.to_string());
}
}
if let Some(pos) = line.find("-> ") {
let rest = &line[pos + 3..];
let end = rest.find('/').unwrap_or(rest.len());
let name = rest[..end].trim();
if !name.is_empty() && !name.contains(' ') {
return Some(name.to_string());
}
}
None
}
fn is_private_ip(ip: Ipv4Addr) -> bool {
let o = ip.octets();
o[0] == 10
|| (o[0] == 172 && (16..=31).contains(&o[1]))
|| (o[0] == 192 && o[1] == 168)
}
fn rasdial_enumerate() -> Option<Vec<String>> {
let mut cmd = Command::new("rasdial");
#[cfg(target_os = "windows")]
cmd.creation_flags(CREATE_NO_WINDOW);
let output = cmd.output().ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let mut connections = Vec::new();
if text.to_lowercase().contains("no connections") {
return None;
}
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.contains("Command completed")
|| trimmed.starts_with("Connected")
|| trimmed.starts_with("The following")
{
continue;
}
connections.push(trimmed.to_string());
}
if connections.is_empty() {
None
} else {
Some(connections)
}
}
fn icmp_sweep_vpn(targets: &[Ipv4Addr]) -> Vec<VpnPeer> {
let hits: Mutex<Vec<VpnPeer>> = Mutex::new(Vec::new());
for chunk in targets.chunks(64) {
thread::scope(|s| {
for &target in chunk {
let h = &hits;
s.spawn(move || {
if icmp_ping_one(target) {
h.lock().unwrap().push(VpnPeer {
ip: target,
hostname: None,
info: "ICMP echo reply".to_string(),
method: "ICMP",
});
}
});
}
});
}
hits.into_inner().unwrap()
}
fn icmp_ping_one(target: Ipv4Addr) -> bool {
unsafe {
let handle = IcmpCreateFile();
if handle == INVALID_HANDLE_VALUE || handle == 0 {
return false;
}
let data = b"vpn\0";
let mut reply_buf = [0u8; 64];
let dest = u32::from(target).to_be();
let ret = IcmpSendEcho(
handle,
dest,
data.as_ptr(),
data.len() as u16,
std::ptr::null(),
reply_buf.as_mut_ptr(),
reply_buf.len() as u32,
300, );
IcmpCloseHandle(handle);
ret > 0
}
}
fn tcp_probe_vpn(targets: &[Ipv4Addr]) -> Vec<VpnPeer> {
let hits: Mutex<Vec<VpnPeer>> = Mutex::new(Vec::new());
let found: Mutex<HashSet<Ipv4Addr>> = Mutex::new(HashSet::new());
for chunk in targets.chunks(32) {
thread::scope(|s| {
for &target in chunk {
let h = &hits;
let f = &found;
s.spawn(move || {
let mut open = Vec::new();
for &port in TCP_PROBE_PORTS {
let addr = SocketAddr::V4(SocketAddrV4::new(target, port));
if TcpStream::connect_timeout(&addr, Duration::from_millis(250)).is_ok() {
open.push(port);
}
}
if !open.is_empty() && f.lock().unwrap().insert(target) {
let port_str = open
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",");
h.lock().unwrap().push(VpnPeer {
ip: target,
hostname: None,
info: format!("TCP open ports: {}", port_str),
method: "TCP",
});
}
});
}
});
}
hits.into_inner().unwrap()
}
fn subnet_hosts(
network: Ipv4Addr,
mask: Ipv4Addr,
exclude: Option<Ipv4Addr>,
) -> Vec<Ipv4Addr> {
let net_u32 = u32::from(network);
let mask_u32 = u32::from(mask);
let net_start = net_u32 & mask_u32;
let broadcast = net_start | !mask_u32;
let host_count = broadcast - net_start;
let (start, end) = if host_count > 254 {
let base = net_u32 & 0xFFFFFF00;
(base + 1, base + 255)
} else {
(net_start + 1, broadcast)
};
(start..end)
.map(Ipv4Addr::from)
.filter(|&a| {
if let Some(exc) = exclude {
a != exc
} else {
true
}
})
.filter(|a| !a.is_loopback() && !a.is_broadcast())
.collect()
}
fn dedup_subnets(subnets: &mut Vec<VpnSubnet>) {
let mut seen: HashSet<(Ipv4Addr, u32)> = HashSet::new();
subnets.retain(|s| seen.insert((s.network, s.prefix)));
}
fn build_networks(
subnets: Vec<VpnSubnet>,
peers: Vec<VpnPeer>,
ras_connections: Vec<String>,
primary_ip: Option<Ipv4Addr>,
) -> Vec<RemoteNetwork> {
let now = Local::now().time();
let primary = primary_ip.unwrap_or(Ipv4Addr::UNSPECIFIED);
let mut subnet_peers: HashMap<usize, Vec<&VpnPeer>> = HashMap::new();
let mut unmatched_peers: Vec<&VpnPeer> = Vec::new();
for peer in &peers {
if peer.ip == primary {
continue;
}
let mut matched = false;
for (i, subnet) in subnets.iter().enumerate() {
let peer_u32 = u32::from(peer.ip);
let net_u32 = u32::from(subnet.network);
let mask_u32 = u32::from(subnet.mask);
if (peer_u32 & mask_u32) == (net_u32 & mask_u32) {
subnet_peers.entry(i).or_default().push(peer);
matched = true;
break;
}
}
if !matched {
unmatched_peers.push(peer);
}
}
let mut networks = Vec::new();
for (i, subnet) in subnets.iter().enumerate() {
let mut devices = Vec::new();
let mut device_ips: HashSet<Ipv4Addr> = HashSet::new();
if let Some(peers_in_subnet) = subnet_peers.get(&i) {
for peer in peers_in_subnet {
if !device_ips.insert(peer.ip) {
if let Some(dev) = devices.iter_mut().find(|d: &&mut LanDevice| {
d.ip == IpAddr::V4(peer.ip)
}) {
if !dev.discovery_info.contains(peer.method) {
dev.discovery_info
.push_str(&format!(" | {}:{}", peer.method, peer.info));
}
}
continue;
}
devices.push(LanDevice {
ip: IpAddr::V4(peer.ip),
mac: String::new(),
hostname: peer.hostname.clone(),
vendor: Some("VPN Peer".to_string()),
first_seen: now,
last_seen: now,
is_online: true,
custom_name: None,
discovery_info: format!("{}:{}", peer.method, peer.info),
open_ports: String::new(),
bytes_sent: 0,
bytes_received: 0,
tick_sent: 0,
tick_received: 0,
speed_sent: 0.0,
speed_received: 0.0,
});
}
}
let name = if subnet.source.contains("WireGuard") {
format!("WireGuard: {}", subnet.interface_hint)
} else if subnet.source.contains("OpenVPN") || subnet.source.contains("OVPN") {
format!("OpenVPN: {}/{}", subnet.network, subnet.prefix)
} else {
let ras_name = ras_connections.first().cloned();
if let Some(rn) = ras_name {
format!("VPN: {} ({}/{})", rn, subnet.network, subnet.prefix)
} else {
format!("VPN: {}/{}", subnet.network, subnet.prefix)
}
};
let cidr_str = format!("{}/{}", subnet.network, subnet.prefix);
let local_ip = subnet
.interface_hint
.parse::<Ipv4Addr>()
.unwrap_or(subnet.network);
networks.push(RemoteNetwork {
name,
category: NetworkCategory::Vpn,
adapter_name: subnet.interface_hint.clone(),
local_ip,
subnet_mask: subnet.mask,
subnet_cidr: cidr_str,
gateway: None,
devices,
});
}
if !unmatched_peers.is_empty() {
let mut devices = Vec::new();
let mut device_ips: HashSet<Ipv4Addr> = HashSet::new();
for peer in &unmatched_peers {
if !device_ips.insert(peer.ip) {
if let Some(dev) = devices.iter_mut().find(|d: &&mut LanDevice| {
d.ip == IpAddr::V4(peer.ip)
}) {
if !dev.discovery_info.contains(peer.method) {
dev.discovery_info
.push_str(&format!(" | {}:{}", peer.method, peer.info));
}
}
continue;
}
devices.push(LanDevice {
ip: IpAddr::V4(peer.ip),
mac: String::new(),
hostname: peer.hostname.clone(),
vendor: Some("VPN Peer".to_string()),
first_seen: now,
last_seen: now,
is_online: true,
custom_name: None,
discovery_info: format!("{}:{}", peer.method, peer.info),
open_ports: String::new(),
bytes_sent: 0,
bytes_received: 0,
tick_sent: 0,
tick_received: 0,
speed_sent: 0.0,
speed_received: 0.0,
});
}
if !devices.is_empty() {
let first_ip = unmatched_peers[0].ip;
let octets = first_ip.octets();
let net = Ipv4Addr::new(octets[0], octets[1], octets[2], 0);
let name = if let Some(rn) = ras_connections.first() {
format!("VPN: {} (peers)", rn)
} else {
"VPN Peers (discovered)".to_string()
};
networks.push(RemoteNetwork {
name,
category: NetworkCategory::Vpn,
adapter_name: String::new(),
local_ip: net,
subnet_mask: Ipv4Addr::new(255, 255, 255, 0),
subnet_cidr: format!("{}/24", net),
gateway: None,
devices,
});
}
}
networks
}