mirror of
https://github.com/mayanayza/netvisor.git
synced 2025-12-10 08:24:08 -06:00
attempted a fix for vpn subnet discovery
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::{
|
||||
server::{
|
||||
capabilities::types::base::Capability, daemons::types::api::{DaemonDiscoveryRequest, DaemonDiscoveryUpdate}, nodes::types::{
|
||||
api::NodeUpdateRequest, base::{DiscoveryStatus, Node, NodeBase}, status::NodeStatus, targets::{IpAddressTargetConfig, NodeTarget}, types::NodeType
|
||||
}, shared::types::api::ApiResponse, subnets::types::base::{NodeSubnetMembership, Subnet}
|
||||
}, shared::types::api::ApiResponse, subnets::types::base::{NodeSubnetMembership, Subnet, SubnetType}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -309,7 +309,10 @@ impl DaemonDiscoveryService {
|
||||
|
||||
// Gather host information
|
||||
let hostname = self.utils.get_hostname_for_ip(host_ip).await?;
|
||||
let mac_address = self.utils.get_mac_address_for_ip(host_ip).await?;
|
||||
let mac_address = match subnet.base.subnet_type {
|
||||
SubnetType::VpnTunnel => None, // ARP doesn't work through VPN tunnels
|
||||
_ => self.utils.get_mac_address_for_ip(host_ip).await?
|
||||
};
|
||||
|
||||
// Create node
|
||||
let mut node = Node::new(NodeBase {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::{Error, Result};
|
||||
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
|
||||
use anyhow::{Result};
|
||||
use anyhow::anyhow;
|
||||
use get_if_addrs::{get_if_addrs, Interface};
|
||||
use std::{sync::Arc};
|
||||
use crate::daemon::utils::base::{create_system_utils, PlatformSystemUtils};
|
||||
use crate::server::subnets::types::base::{Subnet, SubnetBase};
|
||||
use crate::server::subnets::types::base::{Subnet};
|
||||
use crate::{
|
||||
daemon::{shared::storage::ConfigStore}
|
||||
};
|
||||
@@ -32,79 +31,12 @@ impl DaemonSubnetService {
|
||||
|
||||
let subnets: Vec<Subnet> = interfaces.into_iter()
|
||||
.filter(|interface| !should_skip_interface(&interface))
|
||||
.filter_map(|interface| {
|
||||
if let Ok(cidr) = self.calculate_subnet_from_interface(&interface) {
|
||||
|
||||
if cidr.is_ipv6() {
|
||||
return None
|
||||
}
|
||||
|
||||
let subnet = Subnet::new(SubnetBase {
|
||||
cidr,
|
||||
name: self.generate_subnet_name(&interface),
|
||||
description: None,
|
||||
dns_resolvers: Vec::new(),
|
||||
gateways: Vec::new()
|
||||
});
|
||||
|
||||
tracing::debug!("Creating subnet {} for NIC {}", subnet.base.cidr, interface.name);
|
||||
|
||||
return Some(subnet);
|
||||
}
|
||||
tracing::debug!("Could not determine subnet for NIC {}", interface.name);
|
||||
None
|
||||
})
|
||||
.filter_map(|interface| Subnet::from_interface(&interface))
|
||||
.collect();
|
||||
|
||||
Ok(subnets)
|
||||
}
|
||||
|
||||
fn calculate_subnet_from_interface(&self, interface: &Interface) -> Result<IpCidr, Error> {
|
||||
match &interface.addr {
|
||||
get_if_addrs::IfAddr::V4(v4_addr) => {
|
||||
let netmask = v4_addr.netmask;
|
||||
let prefix_len = netmask.octets().iter()
|
||||
.map(|&octet| octet.count_ones())
|
||||
.sum::<u32>() as u8;
|
||||
|
||||
let network = std::net::Ipv4Addr::from(
|
||||
u32::from(v4_addr.ip) & u32::from(netmask)
|
||||
);
|
||||
|
||||
return Ok(IpCidr::V4(Ipv4Cidr::new(network, prefix_len)?));
|
||||
}
|
||||
get_if_addrs::IfAddr::V6(v6_addr) => {
|
||||
let netmask = v6_addr.netmask;
|
||||
let prefix_len = netmask.octets().iter()
|
||||
.map(|&octet| octet.count_ones())
|
||||
.sum::<u32>() as u8;
|
||||
|
||||
let ip_bytes = v6_addr.ip.octets();
|
||||
let mask_bytes = netmask.octets();
|
||||
let mut network_bytes = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
network_bytes[i] = ip_bytes[i] & mask_bytes[i];
|
||||
}
|
||||
let network = std::net::Ipv6Addr::from(network_bytes);
|
||||
|
||||
return Ok(IpCidr::V6(Ipv6Cidr::new(network, prefix_len)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_subnet_name(&self, interface: &Interface) -> String {
|
||||
if self.is_vpn_interface(&interface.name) {
|
||||
format!("VPN Network ({})", interface.name)
|
||||
} else {
|
||||
format!("LAN ({})", interface.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_vpn_interface(&self, name: &str) -> bool {
|
||||
["tun", "wg", "tap", "ppp", "vpn"]
|
||||
.iter()
|
||||
.any(|pattern| name.to_lowercase().starts_with(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_skip_interface(interface: &Interface) -> bool {
|
||||
|
||||
@@ -55,7 +55,7 @@ impl PartialEq for Node {
|
||||
let macs_a: Vec<Option<MacAddress>> = self.base.subnets.iter().map(|s| s.mac_address).collect();
|
||||
let macs_b: Vec<Option<MacAddress>> = other.base.subnets.iter().map(|s| s.mac_address).collect();
|
||||
|
||||
macs_a.iter().any(|mac_a| {
|
||||
let mac_match = macs_a.iter().any(|mac_a| {
|
||||
macs_b.iter().any(|mac_b| {
|
||||
match (mac_a, mac_b) {
|
||||
(Some(a), Some(b)) => !vec!(
|
||||
@@ -65,7 +65,15 @@ impl PartialEq for Node {
|
||||
(_, _) => false
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let subnet_ip_match = self.base.subnets.iter().any(|subnet_a| {
|
||||
other.base.subnets.iter().any(|subnet_b| {
|
||||
subnet_a.subnet_id == subnet_b.subnet_id && subnet_a.ip_address == subnet_b.ip_address
|
||||
})
|
||||
});
|
||||
|
||||
mac_match || subnet_ip_match
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ CREATE TABLE IF NOT EXISTS subnets (
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
dns_resolvers TEXT NOT NULL,
|
||||
gateways TEXT NOT NULL
|
||||
gateways TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_subnets_unique_cidr_gateways ON subnets(cidr, gateways);
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
||||
use cidr::IpCidr;
|
||||
use sqlx::{SqlitePool, Row};
|
||||
use uuid::Uuid;
|
||||
use crate::server::{subnets::types::base::{Subnet, SubnetBase}};
|
||||
use crate::server::subnets::types::base::{Subnet, SubnetBase, SubnetType};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SubnetStorage: Send + Sync {
|
||||
@@ -32,13 +32,14 @@ impl SubnetStorage for SqliteSubnetStorage {
|
||||
let cidr_str = serde_json::to_string(&subnet.base.cidr)?;
|
||||
let gateways_str = serde_json::to_string(&subnet.base.gateways)?;
|
||||
let dns_resolvers_str = serde_json::to_string(&subnet.base.dns_resolvers)?;
|
||||
let subnet_type_str = serde_json::to_string(&subnet.base.subnet_type)?;
|
||||
|
||||
// Try to insert, ignore if constraint sviolation
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR IGNORE INTO subnets (
|
||||
id, name, description, cidr, dns_resolvers, gateways, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
id, name, description, cidr, dns_resolvers, gateways, subnet_type, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#
|
||||
)
|
||||
.bind(&subnet.id)
|
||||
@@ -47,6 +48,7 @@ impl SubnetStorage for SqliteSubnetStorage {
|
||||
.bind(&cidr_str)
|
||||
.bind(dns_resolvers_str)
|
||||
.bind(&gateways_str)
|
||||
.bind(subnet_type_str)
|
||||
.bind(&subnet.created_at.to_rfc3339())
|
||||
.bind(&subnet.updated_at.to_rfc3339())
|
||||
.execute(&self.pool)
|
||||
@@ -111,11 +113,12 @@ impl SubnetStorage for SqliteSubnetStorage {
|
||||
let cidr_str = serde_json::to_string(&subnet.base.cidr)?;
|
||||
let dns_resolvers_str = serde_json::to_string(&subnet.base.dns_resolvers)?;
|
||||
let gateways_str = serde_json::to_string(&subnet.base.gateways)?;
|
||||
let subnet_type_str = serde_json::to_string(&subnet.base.subnet_type)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE subnets SET
|
||||
name = ?, description = ?, cidr = ?, dns_resolvers = ?, gateways = ?,
|
||||
name = ?, description = ?, cidr = ?, dns_resolvers = ?, gateways = ?, subnet_type = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
"#
|
||||
@@ -125,6 +128,7 @@ impl SubnetStorage for SqliteSubnetStorage {
|
||||
.bind(cidr_str)
|
||||
.bind(dns_resolvers_str)
|
||||
.bind(gateways_str)
|
||||
.bind(subnet_type_str)
|
||||
.bind(&subnet.updated_at.to_rfc3339())
|
||||
.bind(&subnet.id)
|
||||
.execute(&self.pool)
|
||||
@@ -148,6 +152,7 @@ fn row_to_subnet(row: sqlx::sqlite::SqliteRow) -> Result<Subnet> {
|
||||
let cidr: IpCidr = serde_json::from_str(&row.get::<String, _>("cidr"))?;
|
||||
let dns_resolvers: Vec<Uuid> = serde_json::from_str(&row.get::<String, _>("dns_resolvers"))?;
|
||||
let gateways: Vec<Uuid> = serde_json::from_str(&row.get::<String, _>("gateways"))?;
|
||||
let subnet_type: SubnetType = serde_json::from_str(&row.get::<String, _>("subnet_type"))?;
|
||||
|
||||
let created_at = chrono::DateTime::parse_from_rfc3339(&row.get::<String, _>("created_at"))?
|
||||
.with_timezone(&chrono::Utc);
|
||||
@@ -163,7 +168,8 @@ fn row_to_subnet(row: sqlx::sqlite::SqliteRow) -> Result<Subnet> {
|
||||
description: row.get("description"),
|
||||
cidr,
|
||||
dns_resolvers,
|
||||
gateways
|
||||
gateways,
|
||||
subnet_type
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::net::{IpAddr};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use anyhow::Error;
|
||||
use chrono::{DateTime, Utc};
|
||||
use cidr::IpCidr;
|
||||
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
|
||||
use get_if_addrs::Interface;
|
||||
use mac_address::MacAddress;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumDiscriminants, EnumIter};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::server::{capabilities::types::base::CapabilityDiscriminants, nodes::types::base::Node};
|
||||
@@ -13,11 +16,9 @@ pub struct SubnetBase {
|
||||
pub cidr: IpCidr,
|
||||
pub name: String, // "Home LAN", "VPN Network", etc.
|
||||
pub description: Option<String>,
|
||||
|
||||
// Network services (priority-ordered)
|
||||
pub dns_resolvers: Vec<Uuid>, // [primary_dns, secondary_dns, fallback_dns]
|
||||
pub gateways: Vec<Uuid>, // [default_gateway, backup_gateway]
|
||||
// Note: VPN servers are just another type of gateway
|
||||
pub subnet_type: SubnetType
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
|
||||
@@ -39,11 +40,74 @@ impl Subnet {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_interface(interface: &Interface) -> Option<Self> {
|
||||
if let Ok(calculated_cidr) = Subnet::calculate_cidr_from_interface(interface) {
|
||||
|
||||
let subnet_type = SubnetType::from_interface_name(&interface.name);
|
||||
|
||||
match calculated_cidr {
|
||||
IpCidr::V6(_) => return None,
|
||||
IpCidr::V4(ipv4_cidr) => {
|
||||
|
||||
let mut cidr = calculated_cidr;
|
||||
|
||||
if subnet_type == SubnetType::VpnTunnel && ipv4_cidr.network_length() == 32 {
|
||||
let octets = ipv4_cidr.first().address().octets();
|
||||
cidr = IpCidr::V4(Ipv4Cidr::new(Ipv4Addr::new(octets[0], octets[1], octets[2], 0), 24).ok()?);
|
||||
}
|
||||
|
||||
return Some(Subnet::new(SubnetBase {
|
||||
cidr,
|
||||
description: None,
|
||||
name: interface.name.clone(),
|
||||
subnet_type,
|
||||
dns_resolvers: Vec::new(),
|
||||
gateways: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
};
|
||||
return None
|
||||
}
|
||||
|
||||
pub fn update_node_relationships(&mut self, node: &Node) {
|
||||
if node.has_capability(CapabilityDiscriminants::Dns) { self.base.dns_resolvers.push(node.id) }
|
||||
if node.is_gateway_for_subnet(&self) { self.base.gateways.push(node.id) }
|
||||
}
|
||||
|
||||
fn calculate_cidr_from_interface(interface: &Interface) -> Result<IpCidr, Error> {
|
||||
match &interface.addr {
|
||||
get_if_addrs::IfAddr::V4(v4_addr) => {
|
||||
let netmask = v4_addr.netmask;
|
||||
let prefix_len = netmask.octets().iter()
|
||||
.map(|&octet| octet.count_ones())
|
||||
.sum::<u32>() as u8;
|
||||
|
||||
let network = std::net::Ipv4Addr::from(
|
||||
u32::from(v4_addr.ip) & u32::from(netmask)
|
||||
);
|
||||
|
||||
return Ok(IpCidr::V4(Ipv4Cidr::new(network, prefix_len)?));
|
||||
}
|
||||
get_if_addrs::IfAddr::V6(v6_addr) => {
|
||||
let netmask = v6_addr.netmask;
|
||||
let prefix_len = netmask.octets().iter()
|
||||
.map(|&octet| octet.count_ones())
|
||||
.sum::<u32>() as u8;
|
||||
|
||||
let ip_bytes = v6_addr.ip.octets();
|
||||
let mask_bytes = netmask.octets();
|
||||
let mut network_bytes = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
network_bytes[i] = ip_bytes[i] & mask_bytes[i];
|
||||
}
|
||||
let network = std::net::Ipv6Addr::from(network_bytes);
|
||||
|
||||
return Ok(IpCidr::V6(Ipv6Cidr::new(network, prefix_len)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Subnet {
|
||||
@@ -59,4 +123,43 @@ pub struct NodeSubnetMembership {
|
||||
pub subnet_id: Uuid,
|
||||
pub ip_address: IpAddr,
|
||||
pub mac_address: Option<MacAddress>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, EnumDiscriminants)]
|
||||
#[strum_discriminants(derive(Display, Hash, Serialize, Deserialize, EnumIter))]
|
||||
pub enum SubnetType {
|
||||
LocalLan,
|
||||
VpnTunnel,
|
||||
DockerBridge,
|
||||
Unknown
|
||||
}
|
||||
|
||||
impl SubnetType {
|
||||
pub fn from_interface_name(interface_name: &String) -> Self {
|
||||
|
||||
if Self::match_interface_names(&["docker", "br-"], interface_name) {
|
||||
return SubnetType::DockerBridge;
|
||||
}
|
||||
|
||||
if Self::match_interface_names(&["tun", "wg", "tap", "ppp", "vpn"], interface_name){
|
||||
return SubnetType::VpnTunnel;
|
||||
}
|
||||
|
||||
if Self::match_interface_names(&["eth", "en", "wlan", "wifi", "eno", "enp"], interface_name) {
|
||||
return SubnetType::LocalLan;
|
||||
}
|
||||
|
||||
SubnetType::Unknown
|
||||
}
|
||||
|
||||
fn match_interface_names(patterns: &[&str], interface_name: &String) -> bool {
|
||||
let name_lower = interface_name.to_lowercase();
|
||||
patterns.iter().any(|pattern| {
|
||||
name_lower.starts_with(pattern) &&
|
||||
// Ensure it's followed by a digit or end of string (not another letter)
|
||||
name_lower.get(pattern.len()..)
|
||||
.map(|rest| rest.is_empty() || rest.chars().next().unwrap().is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user