attempted a fix for vpn subnet discovery

This commit is contained in:
Maya
2025-09-04 15:19:51 -04:00
parent ea07f3af52
commit 6d4fae2a4a
6 changed files with 139 additions and 86 deletions
+5 -2
View File
@@ -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 {
+3 -71
View File
@@ -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 {
+10 -2
View File
@@ -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
}
}
+2 -1
View File
@@ -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);
+11 -5
View File
@@ -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
}
})
}
+108 -5
View File
@@ -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)
})
}
}