diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index 14357802..08f157c7 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -51,13 +51,14 @@ use easytier::{ ListCredentialsRequest, ListCredentialsResponse, ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest, ListPeerResponse, ListPortForwardRequest, ListPortForwardResponse, - ListRouteRequest, ListRouteResponse, MappedListener, MappedListenerManageRpc, + ListPublicIpv6InfoRequest, ListPublicIpv6InfoResponse, ListRouteRequest, + ListRouteResponse, MappedListener, MappedListenerManageRpc, MappedListenerManageRpcClientFactory, MetricSnapshot, NodeInfo, PeerManageRpc, PeerManageRpcClientFactory, PortForwardManageRpc, - PortForwardManageRpcClientFactory, RevokeCredentialRequest, ShowNodeInfoRequest, - StatsRpc, StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType, - TcpProxyRpc, TcpProxyRpcClientFactory, TrustedKeySourcePb, VpnPortalInfo, - VpnPortalRpc, VpnPortalRpcClientFactory, + PortForwardManageRpcClientFactory, RevokeCredentialRequest, Route as ApiRoute, + ShowNodeInfoRequest, StatsRpc, StatsRpcClientFactory, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, + TrustedKeySourcePb, VpnPortalInfo, VpnPortalRpc, VpnPortalRpcClientFactory, instance_identifier::{InstanceSelector, Selector}, list_global_foreign_network_response, list_peer_route_pair, }, @@ -193,6 +194,7 @@ struct PeerArgs { #[derive(Subcommand, Debug)] enum PeerSubCommand { List, + Ipv6, ListForeign { #[arg( long, @@ -536,6 +538,12 @@ struct RouteListData { peer_routes: Vec, } +struct PeerIpv6DataRaw { + node_info: NodeInfo, + routes: Vec, + provider_info: ListPublicIpv6InfoResponse, +} + #[derive(serde::Serialize)] struct PeerCenterRowData { node_id: String, @@ -963,6 +971,27 @@ impl<'a> CommandHandler<'a> { }) } + async fn fetch_local_public_ipv6_info(&self) -> Result { + Ok(self + .get_peer_manager_client() + .await? + .list_public_ipv6_info( + BaseController::default(), + ListPublicIpv6InfoRequest { + instance: Some(self.instance_selector.clone()), + }, + ) + .await?) + } + + async fn fetch_peer_ipv6_data(&self) -> Result { + Ok(PeerIpv6DataRaw { + node_info: self.fetch_node_info().await?, + routes: self.list_routes().await?.routes, + provider_info: self.fetch_local_public_ipv6_info().await?, + }) + } + async fn fetch_connector_list(&self) -> Result, Error> { Ok(self .get_connector_manager_client() @@ -1375,6 +1404,154 @@ impl<'a> CommandHandler<'a> { }) } + async fn handle_peer_ipv6(&self) -> Result<(), Error> { + #[derive(tabled::Tabled, serde::Serialize)] + struct PeerIpv6NodeRow { + peer_id: u32, + hostname: String, + inst_id: String, + ipv4: String, + public_ipv6_addr: String, + provider_prefix: String, + } + + #[derive(tabled::Tabled, serde::Serialize)] + struct ProviderLeaseRow { + peer_id: u32, + inst_id: String, + leased_addr: String, + valid_until: String, + reused: bool, + } + + #[derive(serde::Serialize)] + struct ProviderLeaseSection { + provider_prefix: String, + leases: Vec, + } + + #[derive(serde::Serialize)] + struct PeerIpv6View { + nodes: Vec, + local_provider: Option, + } + + fn fmt_ipv6_inet(value: Option) -> String { + value + .map(|value| value.to_string()) + .unwrap_or_else(|| "-".to_string()) + } + + fn fmt_valid_until(unix_seconds: i64) -> String { + chrono::DateTime::::from_timestamp(unix_seconds, 0) + .map(|ts| { + ts.with_timezone(&chrono::Local) + .format("%Y-%m-%d %H:%M:%S") + .to_string() + }) + .unwrap_or_else(|| unix_seconds.to_string()) + } + + let build_view = |data: &PeerIpv6DataRaw| { + let mut nodes = Vec::with_capacity(data.routes.len() + 1); + nodes.push(PeerIpv6NodeRow { + peer_id: data.node_info.peer_id, + hostname: data.node_info.hostname.clone(), + inst_id: data.node_info.inst_id.clone(), + ipv4: data.node_info.ipv4_addr.clone(), + public_ipv6_addr: fmt_ipv6_inet(data.node_info.public_ipv6_addr), + provider_prefix: fmt_ipv6_inet(data.node_info.ipv6_public_addr_prefix), + }); + nodes.extend(data.routes.iter().map(|route| { + PeerIpv6NodeRow { + peer_id: route.peer_id, + hostname: route.hostname.clone(), + inst_id: route.inst_id.clone(), + ipv4: route + .ipv4_addr + .map(|ipv4| ipv4.to_string()) + .unwrap_or_else(|| "-".to_string()), + public_ipv6_addr: fmt_ipv6_inet(route.public_ipv6_addr), + provider_prefix: fmt_ipv6_inet(route.ipv6_public_addr_prefix), + } + })); + nodes.sort_by_key(|row| { + ( + row.peer_id != data.node_info.peer_id, + row.peer_id, + row.inst_id.clone(), + ) + }); + + let local_provider = data.provider_info.provider_prefix.map(|provider_prefix| { + let mut leases = data + .provider_info + .provider_leases + .iter() + .map(|lease| ProviderLeaseRow { + peer_id: lease.peer_id, + inst_id: lease.inst_id.clone(), + leased_addr: fmt_ipv6_inet(lease.leased_addr), + valid_until: fmt_valid_until(lease.valid_until_unix_seconds), + reused: lease.reused, + }) + .collect::>(); + leases.sort_by_key(|lease| { + ( + lease.peer_id, + lease.inst_id.clone(), + lease.leased_addr.clone(), + ) + }); + ProviderLeaseSection { + provider_prefix: provider_prefix.to_string(), + leases, + } + }); + + PeerIpv6View { + nodes, + local_provider, + } + }; + + let results = self + .collect_instance_results(|handler| Box::pin(handler.fetch_peer_ipv6_data())) + .await?; + + if self.verbose || *self.output_format == OutputFormat::Json { + return self.print_json_results( + results + .into_iter() + .map(|result| result.map(|data| build_view(&data))) + .collect(), + ); + } + + self.print_results(&results, |data| { + let view = build_view(data); + print_output(&view.nodes, self.output_format, &[], &[], self.no_trunc)?; + + if let Some(local_provider) = view.local_provider { + println!(); + println!("Local provider prefix: {}", local_provider.provider_prefix); + if local_provider.leases.is_empty() { + println!("No active provider leases"); + } else { + print_output( + &local_provider.leases, + self.output_format, + &[], + &[], + self.no_trunc, + )?; + } + } + + Ok(()) + }) + } + async fn handle_route_dump(&self) -> Result<(), Error> { let results = self .collect_instance_results(|handler| Box::pin(handler.fetch_route_dump())) @@ -2652,6 +2829,9 @@ async fn main() -> Result<(), Error> { Some(PeerSubCommand::List) => { handler.handle_peer_list().await?; } + Some(PeerSubCommand::Ipv6) => { + handler.handle_peer_ipv6().await?; + } Some(PeerSubCommand::ListForeign { trusted_keys }) => { handler.handle_foreign_network_list(*trusted_keys).await?; } diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 98b28b48..d1267814 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -1299,6 +1299,10 @@ impl PeerManager { self.get_route().get_my_public_ipv6_addr().await } + pub async fn get_local_public_ipv6_info(&self) -> instance::ListPublicIpv6InfoResponse { + self.get_route().get_local_public_ipv6_info().await + } + pub async fn dump_route(&self) -> String { self.get_route().dump().await } diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 5d5b6685..5568aac6 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -369,6 +369,7 @@ impl From for crate::proto::api::instance::Route { ipv6_addr: val.ipv6_addr, public_ipv6_addr: val.ipv6_public_addr_lease, + ipv6_public_addr_prefix: val.ipv6_public_addr_prefix, } } } @@ -3953,6 +3954,39 @@ impl Route for PeerRoute { self.public_ipv6_service.my_addr() } + 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 { let route_table = &self.service_impl.route_table; if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) { diff --git a/easytier/src/peers/public_ipv6.rs b/easytier/src/peers/public_ipv6.rs index 1d1a99f9..711ae3c9 100644 --- a/easytier/src/peers/public_ipv6.rs +++ b/easytier/src/peers/public_ipv6.rs @@ -641,6 +641,20 @@ impl PublicIpv6Service { pub(crate) fn my_addr(&self) -> Option { *self.my_addr_cache.lock().unwrap() } + + pub(crate) fn local_provider_state( + &self, + ) -> Option<(PublicIpv6Provider, Vec)> { + let provider = self.selected_provider()?; + if provider.peer_id != self.my_peer_id() { + return None; + } + + let state = Self::prune_expired_leases(&provider, self.current_provider_state()); + let mut leases = state.leases.into_values().collect::>(); + leases.sort_by_key(|lease| (lease.peer_id, lease.inst_id, lease.addr)); + Some((provider, leases)) + } } #[derive(Clone)] diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index a43e0477..7577c9c8 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -9,9 +9,12 @@ use std::{ use crate::{ common::{PeerId, global_ctx::NetworkIdentity}, - proto::peer_rpc::{ - ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerIdentityType, - RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo, + proto::{ + api::instance::ListPublicIpv6InfoResponse, + peer_rpc::{ + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerIdentityType, + RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo, + }, }, }; @@ -102,6 +105,10 @@ pub trait Route { None } + async fn get_local_public_ipv6_info(&self) -> ListPublicIpv6InfoResponse { + ListPublicIpv6InfoResponse::default() + } + async fn get_peer_id_by_ipv4(&self, _ipv4: &Ipv4Addr) -> Option { None } diff --git a/easytier/src/peers/rpc_service.rs b/easytier/src/peers/rpc_service.rs index 44e3dd36..941d03f4 100644 --- a/easytier/src/peers/rpc_service.rs +++ b/easytier/src/peers/rpc_service.rs @@ -13,9 +13,9 @@ use crate::{ GetWhitelistRequest, GetWhitelistResponse, ListCredentialsRequest, ListCredentialsResponse, ListForeignNetworkRequest, ListForeignNetworkResponse, ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest, - ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, - RevokeCredentialRequest, RevokeCredentialResponse, ShowNodeInfoRequest, - ShowNodeInfoResponse, + ListPeerResponse, ListPublicIpv6InfoRequest, ListPublicIpv6InfoResponse, + ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, RevokeCredentialRequest, + RevokeCredentialResponse, ShowNodeInfoRequest, ShowNodeInfoResponse, }, rpc_types::{self, controller::BaseController}, }, @@ -99,6 +99,16 @@ impl PeerManageRpc for PeerManagerRpcService { Ok(reply) } + async fn list_public_ipv6_info( + &self, + _: BaseController, + _request: ListPublicIpv6InfoRequest, + ) -> Result { + Ok(weak_upgrade(&self.peer_manager)? + .get_local_public_ipv6_info() + .await) + } + async fn list_route( &self, _: BaseController, diff --git a/easytier/src/proto/api_instance.proto b/easytier/src/proto/api_instance.proto index 122f168b..12fe18f6 100644 --- a/easytier/src/proto/api_instance.proto +++ b/easytier/src/proto/api_instance.proto @@ -82,6 +82,7 @@ message Route { common.Ipv6Inet ipv6_addr = 15; common.Ipv6Inet public_ipv6_addr = 16; + common.Ipv6Inet ipv6_public_addr_prefix = 17; } message PeerRoutePair { @@ -109,6 +110,21 @@ message ShowNodeInfoRequest { InstanceIdentifier instance = 1; } message ShowNodeInfoResponse { NodeInfo node_info = 1; } +message PublicIpv6LeaseInfo { + uint32 peer_id = 1; + string inst_id = 2; + common.Ipv6Inet leased_addr = 3; + int64 valid_until_unix_seconds = 4; + bool reused = 5; +} + +message ListPublicIpv6InfoRequest { InstanceIdentifier instance = 1; } + +message ListPublicIpv6InfoResponse { + common.Ipv6Inet provider_prefix = 1; + repeated PublicIpv6LeaseInfo provider_leases = 2; +} + message ListRouteRequest { InstanceIdentifier instance = 1; } message ListRouteResponse { repeated Route routes = 1; } @@ -170,6 +186,8 @@ message GetForeignNetworkSummaryResponse { service PeerManageRpc { rpc ListPeer(ListPeerRequest) returns (ListPeerResponse); + rpc ListPublicIpv6Info(ListPublicIpv6InfoRequest) + returns (ListPublicIpv6InfoResponse); rpc ListRoute(ListRouteRequest) returns (ListRouteResponse); rpc DumpRoute(DumpRouteRequest) returns (DumpRouteResponse); rpc ListForeignNetwork(ListForeignNetworkRequest) diff --git a/easytier/src/rpc_service/peer_manage.rs b/easytier/src/rpc_service/peer_manage.rs index 274e3258..2e8369de 100644 --- a/easytier/src/rpc_service/peer_manage.rs +++ b/easytier/src/rpc_service/peer_manage.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use crate::{ instance_manager::NetworkInstanceManager, proto::{ - api::instance::{self, ListPeerRequest, ListPeerResponse, PeerManageRpc}, + api::instance::{ + self, ListPeerRequest, ListPeerResponse, ListPublicIpv6InfoRequest, + ListPublicIpv6InfoResponse, PeerManageRpc, + }, rpc_types::controller::BaseController, }, }; @@ -34,6 +37,17 @@ impl PeerManageRpc for PeerManageRpcService { .await } + async fn list_public_ipv6_info( + &self, + ctrl: Self::Controller, + req: ListPublicIpv6InfoRequest, + ) -> crate::proto::rpc_types::error::Result { + super::get_instance_service(&self.instance_manager, &req.instance)? + .get_peer_manage_service() + .list_public_ipv6_info(ctrl, req) + .await + } + async fn list_route( &self, ctrl: Self::Controller, diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 082c6f9e..a1f6d7ae 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -807,6 +807,32 @@ pub async fn public_ipv6_auto_addr_end_to_end() { .into() ) ); + let provider_info = provider + .get_peer_manager() + .get_local_public_ipv6_info() + .await; + let client_peer_id = client.get_peer_manager().get_my_info().await.peer_id; + assert_eq!( + provider_info.provider_prefix, + Some( + cidr::Ipv6Inet::new( + provider_prefix.first_address(), + provider_prefix.network_length() + ) + .unwrap() + .into() + ) + ); + assert_eq!(provider_info.provider_leases.len(), 1); + assert_eq!(provider_info.provider_leases[0].peer_id, client_peer_id); + assert_eq!( + provider_info.provider_leases[0].inst_id, + client_id.to_string() + ); + assert_eq!( + provider_info.provider_leases[0].leased_addr, + Some(leased.into()) + ); assert!( leased.address().segments()[0] & 0xfe00 != 0xfc00, "leased address should not be unique-local: {leased}"