From c0bd050742a8e64f2abc6646406f73669752f1e1 Mon Sep 17 00:00:00 2001 From: "sijie.sun" Date: Sat, 13 Jun 2026 00:32:07 +0800 Subject: [PATCH] add shared virtual nic --- easytier/src/instance/mod.rs | 3 + easytier/src/instance/shared_virtual_nic.rs | 503 ++++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 easytier/src/instance/shared_virtual_nic.rs diff --git a/easytier/src/instance/mod.rs b/easytier/src/instance/mod.rs index 2535fd1b..3140e275 100644 --- a/easytier/src/instance/mod.rs +++ b/easytier/src/instance/mod.rs @@ -8,6 +8,9 @@ mod public_ipv6_provider; pub mod proxy_cidrs_monitor; +#[cfg(feature = "tun")] +pub mod shared_virtual_nic; + #[cfg(feature = "tun")] pub mod virtual_nic; diff --git a/easytier/src/instance/shared_virtual_nic.rs b/easytier/src/instance/shared_virtual_nic.rs new file mode 100644 index 00000000..b24b536e --- /dev/null +++ b/easytier/src/instance/shared_virtual_nic.rs @@ -0,0 +1,503 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + net::{Ipv4Addr, Ipv6Addr}, + sync::Arc, +}; + +use cidr::{Ipv4Inet, Ipv6Inet}; +use tokio::sync::Mutex; + +use crate::common::global_ctx::ArcGlobalCtx; + +use super::virtual_nic::VirtualNic; + +pub type SharedVirtualNicMemberId = uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SharedIpv4Route { + pub address: Ipv4Addr, + pub prefix: u8, + pub cost: Option, +} + +impl SharedIpv4Route { + pub fn new(address: Ipv4Addr, prefix: u8, cost: Option) -> Self { + Self { + address, + prefix, + cost, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SharedIpv6Route { + pub address: Ipv6Addr, + pub prefix: u8, + pub cost: Option, +} + +impl SharedIpv6Route { + pub fn new(address: Ipv6Addr, prefix: u8, cost: Option) -> Self { + Self { + address, + prefix, + cost, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SharedIfConfigClaims { + pub ipv4_addresses: BTreeSet, + pub ipv6_addresses: BTreeSet, + pub ipv4_routes: BTreeSet, + pub ipv6_routes: BTreeSet, + pub mtu: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OwnedItemDelta { + pub added: BTreeSet, + pub removed: BTreeSet, +} + +impl Default for OwnedItemDelta { + fn default() -> Self { + Self { + added: BTreeSet::new(), + removed: BTreeSet::new(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SharedMtuChange { + pub old: Option, + pub new: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SharedIfConfigDelta { + pub ipv4_addresses: OwnedItemDelta, + pub ipv6_addresses: OwnedItemDelta, + pub ipv4_routes: OwnedItemDelta, + pub ipv6_routes: OwnedItemDelta, + pub mtu: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SharedIfConfigSnapshot { + pub ipv4_addresses: BTreeMap>, + pub ipv6_addresses: BTreeMap>, + pub ipv4_routes: BTreeMap>, + pub ipv6_routes: BTreeMap>, + pub effective_mtu: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct SharedIfConfig { + member_claims: BTreeMap, + ipv4_address_owners: BTreeMap>, + ipv6_address_owners: BTreeMap>, + ipv4_route_owners: BTreeMap>, + ipv6_route_owners: BTreeMap>, + member_mtu: BTreeMap, +} + +impl SharedIfConfig { + pub fn apply_member_claims( + &mut self, + member_id: SharedVirtualNicMemberId, + claims: SharedIfConfigClaims, + ) -> SharedIfConfigDelta { + let old_claims = self + .member_claims + .get(&member_id) + .cloned() + .unwrap_or_default(); + let old_mtu = self.effective_mtu(); + + let ipv4_addresses = update_owned_items( + &mut self.ipv4_address_owners, + member_id, + &old_claims.ipv4_addresses, + &claims.ipv4_addresses, + ); + let ipv6_addresses = update_owned_items( + &mut self.ipv6_address_owners, + member_id, + &old_claims.ipv6_addresses, + &claims.ipv6_addresses, + ); + let ipv4_routes = update_owned_items( + &mut self.ipv4_route_owners, + member_id, + &old_claims.ipv4_routes, + &claims.ipv4_routes, + ); + let ipv6_routes = update_owned_items( + &mut self.ipv6_route_owners, + member_id, + &old_claims.ipv6_routes, + &claims.ipv6_routes, + ); + + update_member_mtu(&mut self.member_mtu, member_id, claims.mtu); + self.member_claims.insert(member_id, claims); + + SharedIfConfigDelta { + ipv4_addresses, + ipv6_addresses, + ipv4_routes, + ipv6_routes, + mtu: mtu_delta(old_mtu, self.effective_mtu()), + } + } + + pub fn remove_member( + &mut self, + member_id: SharedVirtualNicMemberId, + ) -> Option { + let old_claims = self.member_claims.remove(&member_id)?; + let old_mtu = self.effective_mtu(); + + let ipv4_addresses = remove_owned_items( + &mut self.ipv4_address_owners, + member_id, + &old_claims.ipv4_addresses, + ); + let ipv6_addresses = remove_owned_items( + &mut self.ipv6_address_owners, + member_id, + &old_claims.ipv6_addresses, + ); + let ipv4_routes = remove_owned_items( + &mut self.ipv4_route_owners, + member_id, + &old_claims.ipv4_routes, + ); + let ipv6_routes = remove_owned_items( + &mut self.ipv6_route_owners, + member_id, + &old_claims.ipv6_routes, + ); + + self.member_mtu.remove(&member_id); + + Some(SharedIfConfigDelta { + ipv4_addresses, + ipv6_addresses, + ipv4_routes, + ipv6_routes, + mtu: mtu_delta(old_mtu, self.effective_mtu()), + }) + } + + pub fn effective_mtu(&self) -> Option { + self.member_mtu.values().copied().min() + } + + pub fn owners_of_ipv4_route( + &self, + route: &SharedIpv4Route, + ) -> BTreeSet { + owners_of(&self.ipv4_route_owners, route) + } + + pub fn owners_of_ipv6_route( + &self, + route: &SharedIpv6Route, + ) -> BTreeSet { + owners_of(&self.ipv6_route_owners, route) + } + + pub fn owners_of_ipv4_address(&self, address: &Ipv4Inet) -> BTreeSet { + owners_of(&self.ipv4_address_owners, address) + } + + pub fn owners_of_ipv6_address(&self, address: &Ipv6Inet) -> BTreeSet { + owners_of(&self.ipv6_address_owners, address) + } + + pub fn snapshot(&self) -> SharedIfConfigSnapshot { + SharedIfConfigSnapshot { + ipv4_addresses: self.ipv4_address_owners.clone(), + ipv6_addresses: self.ipv6_address_owners.clone(), + ipv4_routes: self.ipv4_route_owners.clone(), + ipv6_routes: self.ipv6_route_owners.clone(), + effective_mtu: self.effective_mtu(), + } + } +} + +pub struct SharedVirtualNic { + nic: Arc>, + ifcfg: SharedIfConfig, +} + +impl SharedVirtualNic { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + Self { + nic: Arc::new(Mutex::new(VirtualNic::new(global_ctx))), + ifcfg: SharedIfConfig::default(), + } + } + + pub fn attach_member( + &mut self, + member_id: SharedVirtualNicMemberId, + claims: SharedIfConfigClaims, + ) -> SharedIfConfigDelta { + self.ifcfg.apply_member_claims(member_id, claims) + } + + pub fn update_member_claims( + &mut self, + member_id: SharedVirtualNicMemberId, + claims: SharedIfConfigClaims, + ) -> SharedIfConfigDelta { + self.ifcfg.apply_member_claims(member_id, claims) + } + + pub fn detach_member( + &mut self, + member_id: SharedVirtualNicMemberId, + ) -> Option { + self.ifcfg.remove_member(member_id) + } + + pub fn ifcfg(&self) -> &SharedIfConfig { + &self.ifcfg + } + + pub fn nic(&self) -> Arc> { + self.nic.clone() + } +} + +fn update_owned_items( + owners: &mut BTreeMap>, + member_id: SharedVirtualNicMemberId, + old_items: &BTreeSet, + new_items: &BTreeSet, +) -> OwnedItemDelta +where + T: Ord + Clone, +{ + let mut delta = OwnedItemDelta::default(); + + for item in old_items.difference(new_items) { + if remove_item_owner(owners, item, member_id) { + delta.removed.insert(item.clone()); + } + } + + for item in new_items.difference(old_items) { + if add_item_owner(owners, item.clone(), member_id) { + delta.added.insert(item.clone()); + } + } + + delta +} + +fn remove_owned_items( + owners: &mut BTreeMap>, + member_id: SharedVirtualNicMemberId, + items: &BTreeSet, +) -> OwnedItemDelta +where + T: Ord + Clone, +{ + let mut delta = OwnedItemDelta::default(); + + for item in items { + if remove_item_owner(owners, item, member_id) { + delta.removed.insert(item.clone()); + } + } + + delta +} + +fn add_item_owner( + owners: &mut BTreeMap>, + item: T, + member_id: SharedVirtualNicMemberId, +) -> bool +where + T: Ord, +{ + let entry = owners.entry(item).or_default(); + let is_new_item = entry.is_empty(); + entry.insert(member_id); + is_new_item +} + +fn remove_item_owner( + owners: &mut BTreeMap>, + item: &T, + member_id: SharedVirtualNicMemberId, +) -> bool +where + T: Ord, +{ + let Some(entry) = owners.get_mut(item) else { + return false; + }; + + entry.remove(&member_id); + if !entry.is_empty() { + return false; + } + + owners.remove(item); + true +} + +fn update_member_mtu( + member_mtu: &mut BTreeMap, + member_id: SharedVirtualNicMemberId, + mtu: Option, +) { + match mtu { + Some(mtu) => { + member_mtu.insert(member_id, mtu); + } + None => { + member_mtu.remove(&member_id); + } + } +} + +fn mtu_delta(old: Option, new: Option) -> Option { + (old != new).then_some(SharedMtuChange { old, new }) +} + +fn owners_of( + owners: &BTreeMap>, + item: &T, +) -> BTreeSet +where + T: Ord, +{ + owners.get(item).cloned().unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use std::str::FromStr as _; + + use crate::common::global_ctx::tests::get_mock_global_ctx; + + use super::*; + + fn member_id(n: u128) -> SharedVirtualNicMemberId { + uuid::Uuid::from_u128(n) + } + + fn claims_with_ipv4_route(route: SharedIpv4Route, mtu: Option) -> SharedIfConfigClaims { + SharedIfConfigClaims { + ipv4_routes: BTreeSet::from([route]), + mtu, + ..Default::default() + } + } + + #[test] + fn duplicate_routes_keep_owner_sets_and_single_os_delta() { + let route = SharedIpv4Route::new(Ipv4Addr::new(10, 10, 0, 0), 24, None); + let first = member_id(1); + let second = member_id(2); + let mut ifcfg = SharedIfConfig::default(); + + let first_delta = + ifcfg.apply_member_claims(first, claims_with_ipv4_route(route.clone(), Some(1400))); + let second_delta = + ifcfg.apply_member_claims(second, claims_with_ipv4_route(route.clone(), Some(1300))); + + assert_eq!( + first_delta.ipv4_routes.added, + BTreeSet::from([route.clone()]) + ); + assert!(second_delta.ipv4_routes.added.is_empty()); + assert_eq!( + ifcfg.owners_of_ipv4_route(&route), + BTreeSet::from([first, second]) + ); + assert_eq!(ifcfg.effective_mtu(), Some(1300)); + } + + #[test] + fn removing_one_owner_keeps_shared_route_until_last_owner_leaves() { + let route = SharedIpv4Route::new(Ipv4Addr::new(10, 20, 0, 0), 24, None); + let first = member_id(1); + let second = member_id(2); + let mut ifcfg = SharedIfConfig::default(); + ifcfg.apply_member_claims(first, claims_with_ipv4_route(route.clone(), None)); + ifcfg.apply_member_claims(second, claims_with_ipv4_route(route.clone(), None)); + + let first_delta = ifcfg.remove_member(first).unwrap(); + let second_delta = ifcfg.remove_member(second).unwrap(); + + assert!(first_delta.ipv4_routes.removed.is_empty()); + assert_eq!( + second_delta.ipv4_routes.removed, + BTreeSet::from([route.clone()]) + ); + assert!(ifcfg.owners_of_ipv4_route(&route).is_empty()); + } + + #[test] + fn member_claim_update_tracks_ip_ownership() { + let first_ip = Ipv4Inet::from_str("10.30.0.2/24").unwrap(); + let second_ip = Ipv4Inet::from_str("10.30.0.3/24").unwrap(); + let member = member_id(1); + let mut ifcfg = SharedIfConfig::default(); + + let first_delta = ifcfg.apply_member_claims( + member, + SharedIfConfigClaims { + ipv4_addresses: BTreeSet::from([first_ip]), + ..Default::default() + }, + ); + let second_delta = ifcfg.apply_member_claims( + member, + SharedIfConfigClaims { + ipv4_addresses: BTreeSet::from([second_ip]), + ..Default::default() + }, + ); + + assert_eq!(first_delta.ipv4_addresses.added, BTreeSet::from([first_ip])); + assert_eq!( + second_delta.ipv4_addresses.removed, + BTreeSet::from([first_ip]) + ); + assert_eq!( + second_delta.ipv4_addresses.added, + BTreeSet::from([second_ip]) + ); + assert_eq!( + ifcfg.owners_of_ipv4_address(&second_ip), + BTreeSet::from([member]) + ); + } + + #[tokio::test] + async fn shared_virtual_nic_wraps_virtual_nic_and_tracks_ifcfg() { + let mut shared_nic = SharedVirtualNic::new(get_mock_global_ctx()); + let member = member_id(1); + let route = SharedIpv4Route::new(Ipv4Addr::new(10, 40, 0, 0), 24, None); + + shared_nic.attach_member(member, claims_with_ipv4_route(route.clone(), None)); + + assert_eq!( + shared_nic.ifcfg().owners_of_ipv4_route(&route), + BTreeSet::from([member]) + ); + drop(shared_nic.nic()); + } +}