mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-16 02:45:41 +00:00
feat: support allocating public IPv6 addresses from a provider (#2162)
* feat: support allocating public IPv6 addresses from a provider Add a provider/leaser architecture for public IPv6 address allocation between nodes in the same network: - A node with `--ipv6-public-addr-provider` advertises a delegable public IPv6 prefix (auto-detected from kernel routes or manually configured via `--ipv6-public-addr-prefix`). - Other nodes with `--ipv6-public-addr-auto` request a /128 lease from the selected provider via a new RPC service (PublicIpv6AddrRpc). - Leases have a 30s TTL, renewed every 10s by the client routine. - The provider allocates addresses deterministically from its prefix using instance-UUID-based hashing to prefer stable assignments. - Routes to peer leases are installed on the TUN device, and each client's own /128 is assigned as its IPv6 address. Also includes netlink IPv6 route table inspection, integration tests, and event-driven route/address reconciliation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ use std::{
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
|
||||
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr, Ipv6Inet};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
@@ -46,9 +46,10 @@ use crate::{
|
||||
peer_rpc::{
|
||||
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, OspfRouteRpc,
|
||||
OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion,
|
||||
PeerIdentityType, RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo,
|
||||
RoutePeerInfos, SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse,
|
||||
TrustedCredentialPubkey, TrustedCredentialPubkeyProof, route_foreign_network_infos,
|
||||
PeerIdentityType, PublicIpv6AddrRpcServer, RouteForeignNetworkInfos,
|
||||
RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError,
|
||||
SyncRouteInfoRequest, SyncRouteInfoResponse, TrustedCredentialPubkey,
|
||||
TrustedCredentialPubkeyProof, route_foreign_network_infos,
|
||||
route_foreign_network_summary, sync_route_info_request::ConnInfo,
|
||||
},
|
||||
rpc_types::{
|
||||
@@ -63,6 +64,9 @@ use super::{
|
||||
PeerPacketFilter,
|
||||
graph_algo::dijkstra_with_first_hop,
|
||||
peer_rpc::PeerRpcManager,
|
||||
public_ipv6::{
|
||||
PublicIpv6PeerRouteInfo, PublicIpv6RouteControl, PublicIpv6Service, PublicIpv6SyncTrigger,
|
||||
},
|
||||
route_trait::{
|
||||
DefaultRouteCostCalculator, ForeignNetworkRouteInfoMap, NextHopPolicy, RouteCostCalculator,
|
||||
RouteCostCalculatorInterface,
|
||||
@@ -137,6 +141,10 @@ fn raw_credential_bytes_from_route_info(
|
||||
.map(|credential| credential.encode_to_vec())
|
||||
}
|
||||
|
||||
fn route_peer_inst_id(info: &RoutePeerInfo) -> Option<uuid::Uuid> {
|
||||
info.inst_id.map(Into::into)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AtomicVersion(Arc<AtomicU32>);
|
||||
|
||||
@@ -205,6 +213,8 @@ impl RoutePeerInfo {
|
||||
quic_port: None,
|
||||
noise_static_pubkey: Vec::new(),
|
||||
trusted_credential_pubkeys: Vec::new(),
|
||||
ipv6_public_addr_prefix: None,
|
||||
ipv6_public_addr_lease: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +231,7 @@ impl RoutePeerInfo {
|
||||
my_peer_id: PeerId,
|
||||
peer_route_id: u64,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
public_ipv6_addr_lease: Option<Ipv6Inet>,
|
||||
) -> Self {
|
||||
let stun_info = global_ctx.get_stun_info_collector().get_stun_info();
|
||||
let noise_static_pubkey = global_ctx
|
||||
@@ -259,6 +270,14 @@ impl RoutePeerInfo {
|
||||
.unwrap_or(24),
|
||||
|
||||
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
|
||||
ipv6_public_addr_prefix: global_ctx.get_advertised_ipv6_public_addr_prefix().map(
|
||||
|prefix| {
|
||||
Ipv6Inet::new(prefix.first_address(), prefix.network_length())
|
||||
.unwrap()
|
||||
.into()
|
||||
},
|
||||
),
|
||||
ipv6_public_addr_lease: public_ipv6_addr_lease.map(Into::into),
|
||||
|
||||
groups: global_ctx.get_acl_groups(my_peer_id),
|
||||
|
||||
@@ -349,6 +368,8 @@ impl From<RoutePeerInfo> for crate::proto::api::instance::Route {
|
||||
path_latency_latency_first: None,
|
||||
|
||||
ipv6_addr: val.ipv6_addr,
|
||||
public_ipv6_addr: val.ipv6_public_addr_lease,
|
||||
ipv6_public_addr_prefix: val.ipv6_public_addr_prefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -964,8 +985,14 @@ impl SyncedRouteInfo {
|
||||
my_peer_id: PeerId,
|
||||
my_peer_route_id: u64,
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
public_ipv6_addr_lease: Option<Ipv6Inet>,
|
||||
) -> bool {
|
||||
let mut new = RoutePeerInfo::new_updated_self(my_peer_id, my_peer_route_id, global_ctx);
|
||||
let mut new = RoutePeerInfo::new_updated_self(
|
||||
my_peer_id,
|
||||
my_peer_route_id,
|
||||
global_ctx,
|
||||
public_ipv6_addr_lease,
|
||||
);
|
||||
let mut guard = self.peer_infos.upgradable_read();
|
||||
let old = guard.get(&my_peer_id);
|
||||
let new_version = old.map(|x| x.version).unwrap_or(0) + 1;
|
||||
@@ -1588,6 +1615,21 @@ impl RouteTable {
|
||||
.or_insert(peer_id_and_version);
|
||||
}
|
||||
|
||||
if let Some(ipv6_addr) = info
|
||||
.ipv6_public_addr_lease
|
||||
.as_ref()
|
||||
.and_then(|addr| addr.address)
|
||||
{
|
||||
self.ipv6_peer_id_map
|
||||
.entry(ipv6_addr.into())
|
||||
.and_modify(|v| {
|
||||
if is_new_peer_better(v) {
|
||||
*v = peer_id_and_version;
|
||||
}
|
||||
})
|
||||
.or_insert(peer_id_and_version);
|
||||
}
|
||||
|
||||
for cidr in info.proxy_cidrs.iter() {
|
||||
let Ok(cidr) = cidr.parse::<IpCidr>() else {
|
||||
tracing::warn!("invalid proxy cidr: {:?}, from peer: {:?}", cidr, peer_id);
|
||||
@@ -2019,6 +2061,8 @@ struct PeerRouteServiceImpl {
|
||||
foreign_network_owner_map: DashMap<NetworkIdentity, Vec<PeerId>>,
|
||||
foreign_network_my_peer_id_map: DashMap<(String, PeerId), PeerId>,
|
||||
synced_route_info: SyncedRouteInfo,
|
||||
public_ipv6_service: std::sync::Mutex<Weak<PublicIpv6Service>>,
|
||||
self_public_ipv6_addr_lease: std::sync::Mutex<Option<Ipv6Inet>>,
|
||||
cached_local_conn_map: std::sync::Mutex<RouteConnBitmap>,
|
||||
cached_local_conn_map_version: AtomicVersion,
|
||||
cached_interface_peer_snapshot: std::sync::Mutex<Arc<InterfacePeerSnapshot>>,
|
||||
@@ -2081,6 +2125,8 @@ impl PeerRouteServiceImpl {
|
||||
non_reusable_credential_owners: DashMap::new(),
|
||||
version: AtomicVersion::new(),
|
||||
},
|
||||
public_ipv6_service: std::sync::Mutex::new(Weak::new()),
|
||||
self_public_ipv6_addr_lease: std::sync::Mutex::new(None),
|
||||
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
|
||||
cached_local_conn_map_version: AtomicVersion::new(),
|
||||
cached_interface_peer_snapshot: std::sync::Mutex::new(Arc::new(
|
||||
@@ -2119,6 +2165,20 @@ impl PeerRouteServiceImpl {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn set_public_ipv6_service(&self, service: Weak<PublicIpv6Service>) {
|
||||
*self.public_ipv6_service.lock().unwrap() = service;
|
||||
}
|
||||
|
||||
fn public_ipv6_service(&self) -> Option<Arc<PublicIpv6Service>> {
|
||||
self.public_ipv6_service.lock().unwrap().upgrade()
|
||||
}
|
||||
|
||||
fn notify_public_ipv6_route_change(&self) -> bool {
|
||||
self.public_ipv6_service()
|
||||
.map(|service| service.handle_route_change())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_or_create_session(&self, dst_peer_id: PeerId) -> Arc<SyncRouteSession> {
|
||||
self.sessions
|
||||
.entry(dst_peer_id)
|
||||
@@ -2230,6 +2290,7 @@ impl PeerRouteServiceImpl {
|
||||
self.my_peer_id,
|
||||
self.my_peer_route_id,
|
||||
&self.global_ctx,
|
||||
*self.self_public_ipv6_addr_lease.lock().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2618,14 +2679,19 @@ impl PeerRouteServiceImpl {
|
||||
untrusted_changed = self.refresh_credential_trusts_and_disconnect().await;
|
||||
}
|
||||
|
||||
let mut public_ipv6_state_updated = false;
|
||||
if my_peer_info_updated || my_conn_info_updated || untrusted_changed {
|
||||
self.update_route_table_and_cached_local_conn_bitmap();
|
||||
self.update_foreign_network_owner_map();
|
||||
public_ipv6_state_updated = self.notify_public_ipv6_route_change();
|
||||
}
|
||||
if my_peer_info_updated {
|
||||
self.update_peer_info_last_update();
|
||||
}
|
||||
my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated
|
||||
my_peer_info_updated
|
||||
|| my_conn_info_updated
|
||||
|| my_foreign_network_updated
|
||||
|| public_ipv6_state_updated
|
||||
}
|
||||
|
||||
async fn refresh_acl_groups(&self) -> bool {
|
||||
@@ -2652,15 +2718,17 @@ impl PeerRouteServiceImpl {
|
||||
let untrusted = self.refresh_credential_trusts_with_current_topology();
|
||||
self.disconnect_untrusted_peers(&untrusted).await;
|
||||
|
||||
let mut public_ipv6_state_updated = false;
|
||||
if my_peer_info_updated || !untrusted.is_empty() {
|
||||
self.update_route_table_and_cached_local_conn_bitmap();
|
||||
self.update_foreign_network_owner_map();
|
||||
public_ipv6_state_updated = self.notify_public_ipv6_route_change();
|
||||
}
|
||||
if my_peer_info_updated {
|
||||
self.update_peer_info_last_update();
|
||||
}
|
||||
|
||||
my_peer_info_updated || !untrusted.is_empty()
|
||||
my_peer_info_updated || !untrusted.is_empty() || public_ipv6_state_updated
|
||||
}
|
||||
|
||||
fn refresh_credential_trusts(&self) -> Vec<PeerId> {
|
||||
@@ -2968,7 +3036,6 @@ impl PeerRouteServiceImpl {
|
||||
session
|
||||
.update_dst_saved_foreign_network_version(foreign_network, dst_peer_id);
|
||||
}
|
||||
|
||||
session.update_last_sync_succ_timestamp(next_last_sync_succ_timestamp);
|
||||
}
|
||||
}
|
||||
@@ -3493,7 +3560,13 @@ impl RouteSessionManager {
|
||||
}
|
||||
|
||||
if need_update_route_table || foreign_network_changed {
|
||||
service_impl.update_route_table_and_cached_local_conn_bitmap();
|
||||
service_impl.update_foreign_network_owner_map();
|
||||
if need_update_route_table
|
||||
&& let Some(public_ipv6_service) = service_impl.public_ipv6_service()
|
||||
{
|
||||
public_ipv6_service.handle_route_change();
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
@@ -3534,12 +3607,86 @@ impl RouteSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
struct OspfPublicIpv6RouteHandle {
|
||||
service_impl: Weak<PeerRouteServiceImpl>,
|
||||
}
|
||||
|
||||
impl PublicIpv6RouteControl for OspfPublicIpv6RouteHandle {
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
self.service_impl
|
||||
.upgrade()
|
||||
.map(|service_impl| service_impl.my_peer_id)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn peer_route_snapshot(&self) -> Vec<PublicIpv6PeerRouteInfo> {
|
||||
let Some(service_impl) = self.service_impl.upgrade() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.peer_infos
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(peer_id, info)| PublicIpv6PeerRouteInfo {
|
||||
peer_id: *peer_id,
|
||||
inst_id: route_peer_inst_id(info),
|
||||
is_provider: info
|
||||
.feature_flag
|
||||
.as_ref()
|
||||
.map(|flags| flags.ipv6_public_addr_provider)
|
||||
.unwrap_or(false),
|
||||
prefix: info
|
||||
.ipv6_public_addr_prefix
|
||||
.map(Into::into)
|
||||
.map(|prefix: Ipv6Inet| prefix.network()),
|
||||
lease: info.ipv6_public_addr_lease.map(Into::into),
|
||||
reachable: *peer_id == service_impl.my_peer_id
|
||||
|| service_impl.route_table.peer_reachable(*peer_id),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn publish_self_public_ipv6_lease(&self, lease: Option<Ipv6Inet>) -> bool {
|
||||
let Some(service_impl) = self.service_impl.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut current = service_impl.self_public_ipv6_addr_lease.lock().unwrap();
|
||||
if *current == lease {
|
||||
return false;
|
||||
}
|
||||
*current = lease;
|
||||
drop(current);
|
||||
|
||||
let changed = service_impl.update_my_peer_info();
|
||||
if changed {
|
||||
service_impl.update_route_table_and_cached_local_conn_bitmap();
|
||||
service_impl.update_foreign_network_owner_map();
|
||||
}
|
||||
changed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OspfPublicIpv6SyncTrigger {
|
||||
session_mgr: RouteSessionManager,
|
||||
}
|
||||
|
||||
impl PublicIpv6SyncTrigger for OspfPublicIpv6SyncTrigger {
|
||||
fn sync_now(&self, reason: &str) {
|
||||
self.session_mgr.sync_now(reason);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PeerRoute {
|
||||
my_peer_id: PeerId,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_rpc: Weak<PeerRpcManager>,
|
||||
|
||||
service_impl: Arc<PeerRouteServiceImpl>,
|
||||
public_ipv6_service: Arc<PublicIpv6Service>,
|
||||
session_mgr: RouteSessionManager,
|
||||
|
||||
tasks: std::sync::Mutex<JoinSet<()>>,
|
||||
@@ -3563,6 +3710,17 @@ impl PeerRoute {
|
||||
) -> Arc<Self> {
|
||||
let service_impl = Arc::new(PeerRouteServiceImpl::new(my_peer_id, global_ctx.clone()));
|
||||
let session_mgr = RouteSessionManager::new(service_impl.clone(), peer_rpc.clone());
|
||||
let public_ipv6_service = Arc::new(PublicIpv6Service::new(
|
||||
global_ctx.clone(),
|
||||
Arc::downgrade(&peer_rpc),
|
||||
Arc::new(OspfPublicIpv6RouteHandle {
|
||||
service_impl: Arc::downgrade(&service_impl),
|
||||
}),
|
||||
Arc::new(OspfPublicIpv6SyncTrigger {
|
||||
session_mgr: session_mgr.clone(),
|
||||
}),
|
||||
));
|
||||
service_impl.set_public_ipv6_service(Arc::downgrade(&public_ipv6_service));
|
||||
|
||||
Arc::new(PeerRoute {
|
||||
my_peer_id,
|
||||
@@ -3570,6 +3728,7 @@ impl PeerRoute {
|
||||
peer_rpc: Arc::downgrade(&peer_rpc),
|
||||
|
||||
service_impl,
|
||||
public_ipv6_service,
|
||||
session_mgr,
|
||||
|
||||
tasks: std::sync::Mutex::new(JoinSet::new()),
|
||||
@@ -3607,6 +3766,9 @@ impl PeerRoute {
|
||||
tracing::debug!("cost_calculator_need_update");
|
||||
service_impl.synced_route_info.version.inc();
|
||||
service_impl.update_route_table();
|
||||
if let Some(public_ipv6_service) = service_impl.public_ipv6_service() {
|
||||
public_ipv6_service.handle_route_change();
|
||||
}
|
||||
}
|
||||
|
||||
select! {
|
||||
@@ -3631,11 +3793,16 @@ impl PeerRoute {
|
||||
|
||||
// make sure my_peer_id is in the peer_infos.
|
||||
self.service_impl.update_my_infos().await;
|
||||
self.public_ipv6_service.handle_route_change();
|
||||
|
||||
peer_rpc.rpc_server().registry().register(
|
||||
OspfRouteRpcServer::new(self.session_mgr.clone()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
);
|
||||
peer_rpc.rpc_server().registry().register(
|
||||
PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
);
|
||||
|
||||
self.tasks
|
||||
.lock()
|
||||
@@ -3657,6 +3824,16 @@ impl PeerRoute {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.spawn(Self::clear_expired_peer(self.service_impl.clone()));
|
||||
|
||||
self.tasks
|
||||
.lock()
|
||||
.unwrap()
|
||||
.spawn(self.public_ipv6_service.clone().provider_gc_routine());
|
||||
|
||||
self.tasks
|
||||
.lock()
|
||||
.unwrap()
|
||||
.spawn(self.public_ipv6_service.clone().client_routine());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3677,6 +3854,10 @@ impl Drop for PeerRoute {
|
||||
OspfRouteRpcServer::new(self.session_mgr.clone()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
);
|
||||
peer_rpc.rpc_server().registry().unregister(
|
||||
PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()),
|
||||
&self.global_ctx.get_network_name(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3765,6 +3946,51 @@ impl Route for PeerRoute {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn list_public_ipv6_routes(&self) -> BTreeSet<Ipv6Inet> {
|
||||
self.public_ipv6_service.list_routes()
|
||||
}
|
||||
|
||||
async fn get_my_public_ipv6_addr(&self) -> Option<Ipv6Inet> {
|
||||
self.public_ipv6_service.my_addr()
|
||||
}
|
||||
|
||||
async fn get_public_ipv6_gateway_peer_id(&self) -> Option<PeerId> {
|
||||
self.public_ipv6_service.provider_peer_id_for_client()
|
||||
}
|
||||
|
||||
async fn get_local_public_ipv6_info(
|
||||
&self,
|
||||
) -> crate::proto::api::instance::ListPublicIpv6InfoResponse {
|
||||
let Some((provider, leases)) = self.public_ipv6_service.local_provider_state() else {
|
||||
return crate::proto::api::instance::ListPublicIpv6InfoResponse::default();
|
||||
};
|
||||
|
||||
crate::proto::api::instance::ListPublicIpv6InfoResponse {
|
||||
provider_prefix: Some(
|
||||
Ipv6Inet::new(
|
||||
provider.prefix.first_address(),
|
||||
provider.prefix.network_length(),
|
||||
)
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
provider_leases: leases
|
||||
.into_iter()
|
||||
.map(|lease| crate::proto::api::instance::PublicIpv6LeaseInfo {
|
||||
peer_id: lease.peer_id,
|
||||
inst_id: lease.inst_id.to_string(),
|
||||
leased_addr: Some(lease.addr.into()),
|
||||
valid_until_unix_seconds: lease
|
||||
.valid_until
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64,
|
||||
reused: lease.reused,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option<PeerId> {
|
||||
let route_table = &self.service_impl.route_table;
|
||||
if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
|
||||
@@ -5180,6 +5406,7 @@ mod tests {
|
||||
service_impl.my_peer_id,
|
||||
service_impl.my_peer_route_id,
|
||||
&service_impl.global_ctx,
|
||||
None,
|
||||
);
|
||||
let mut self_info = self_info;
|
||||
self_info.version = 1;
|
||||
|
||||
Reference in New Issue
Block a user