@@ -2,7 +2,10 @@ use std::{
22 collections:: { HashMap , HashSet } ,
33 fs,
44 path:: PathBuf ,
5- sync:: Arc ,
5+ sync:: {
6+ Arc ,
7+ atomic:: { AtomicUsize , Ordering } ,
8+ } ,
69 time:: Duration ,
710} ;
811
@@ -11,7 +14,7 @@ use iroh::{
1114 Endpoint , EndpointAddr , EndpointId ,
1215 endpoint:: {
1316 AfterHandshakeOutcome , Connection , EndpointHooks , Incoming , RecvStream , SendStream , Side ,
14- VarInt , presets,
17+ TransportAddrUsage , VarInt , presets,
1518 } ,
1619} ;
1720use tokio:: {
@@ -23,11 +26,12 @@ use tokio::{
2326use tokio_util:: sync:: CancellationToken ;
2427
2528use crate :: {
26- config:: { FabricHome , PeerBook , load_or_create_identity, validate_protocol} ,
27- control:: { ControlRequest , ControlResponse } ,
29+ config:: { FabricHome , Peer , PeerBook , load_or_create_identity, validate_protocol} ,
30+ control:: { ControlRequest , ControlResponse , PeerReachability } ,
2831} ;
2932
3033const BUILTIN_ECHO_ALPN : & [ u8 ] = b"fabric/echo/0" ;
34+ const REACHABILITY_TIMEOUT : Duration = Duration :: from_secs ( 3 ) ;
3135
3236#[ derive( Debug ) ]
3337struct AllowListHook {
@@ -59,6 +63,7 @@ pub struct DaemonState {
5963 allowed : Arc < RwLock < HashSet < EndpointId > > > ,
6064 exposures : RwLock < HashMap < Vec < u8 > , PathBuf > > ,
6165 dial_sockets : Mutex < HashMap < ( String , String ) , PathBuf > > ,
66+ builtin_echo_hits : AtomicUsize ,
6267 cancel : CancellationToken ,
6368}
6469
@@ -86,6 +91,7 @@ impl DaemonState {
8691 allowed,
8792 exposures : RwLock :: new ( HashMap :: new ( ) ) ,
8893 dial_sockets : Mutex :: new ( HashMap :: new ( ) ) ,
94+ builtin_echo_hits : AtomicUsize :: new ( 0 ) ,
8995 cancel,
9096 } ) )
9197 }
@@ -122,6 +128,10 @@ impl DaemonState {
122128
123129 pub async fn ping ( & self , peer : & str ) -> Result < PingOutcome > {
124130 let peer_addr = self . peer_book . read ( ) . await . resolve ( peer) ?;
131+ self . ping_addr ( peer, peer_addr) . await
132+ }
133+
134+ async fn ping_addr ( & self , peer : & str , peer_addr : EndpointAddr ) -> Result < PingOutcome > {
125135 let nonce = rand:: random :: < [ u8 ; 32 ] > ( ) ;
126136 let started = std:: time:: Instant :: now ( ) ;
127137 let connection = self
@@ -136,6 +146,12 @@ impl DaemonState {
136146
137147 let response = recv. read_to_end ( nonce. len ( ) + 1 ) . await ?;
138148 let round_trip = started. elapsed ( ) ;
149+ let mut transport = classify_connection_transport ( & connection) ;
150+ if transport. is_none ( )
151+ && let Some ( info) = self . endpoint . remote_info ( peer_addr. id ) . await
152+ {
153+ transport = classify_remote_transport ( & info) ;
154+ }
139155 if response != nonce {
140156 bail ! (
141157 "ping nonce mismatch from {peer:?}: sent {} bytes, got {} bytes" ,
@@ -148,6 +164,7 @@ impl DaemonState {
148164 peer : peer_addr. id . to_string ( ) ,
149165 bytes : response. len ( ) ,
150166 round_trip,
167+ transport,
151168 } )
152169 }
153170
@@ -184,7 +201,9 @@ impl DaemonState {
184201 Ok ( socket_path)
185202 }
186203
187- async fn status_response ( & self ) -> Result < ControlResponse > {
204+ async fn local_status_fields (
205+ & self ,
206+ ) -> Result < ( String , serde_json:: Value , Vec < String > , Vec < PathBuf > ) > {
188207 let exposed_protocols = self
189208 . exposures
190209 . read ( )
@@ -193,20 +212,99 @@ impl DaemonState {
193212 . map ( |alpn| String :: from_utf8_lossy ( alpn) . to_string ( ) )
194213 . collect ( ) ;
195214 let dial_sockets = self . dial_sockets . lock ( ) . await . values ( ) . cloned ( ) . collect ( ) ;
215+ Ok ( (
216+ self . id ( ) . to_string ( ) ,
217+ serde_json:: to_value ( self . addr ( ) ) ?,
218+ exposed_protocols,
219+ dial_sockets,
220+ ) )
221+ }
222+
223+ async fn status_response ( & self ) -> Result < ControlResponse > {
224+ let ( node_id, endpoint_addr, exposed_protocols, dial_sockets) =
225+ self . local_status_fields ( ) . await ?;
196226 Ok ( ControlResponse :: Status {
197- node_id : self . id ( ) . to_string ( ) ,
198- endpoint_addr : serde_json :: to_value ( self . addr ( ) ) ? ,
227+ node_id,
228+ endpoint_addr,
199229 exposed_protocols,
200230 dial_sockets,
201231 } )
202232 }
233+
234+ async fn reachability_status_response ( & self ) -> Result < ControlResponse > {
235+ let ( node_id, endpoint_addr, exposed_protocols, dial_sockets) =
236+ self . local_status_fields ( ) . await ?;
237+ let peers = self . peer_reachability ( ) . await ;
238+ Ok ( ControlResponse :: ReachabilityStatus {
239+ node_id,
240+ endpoint_addr,
241+ exposed_protocols,
242+ dial_sockets,
243+ peers,
244+ } )
245+ }
246+
247+ pub async fn peer_reachability ( & self ) -> Vec < PeerReachability > {
248+ let peers = self . peer_book . read ( ) . await . peers ( ) . to_vec ( ) ;
249+ let mut statuses = Vec :: with_capacity ( peers. len ( ) ) ;
250+ for peer in peers {
251+ statuses. push ( self . check_peer_reachability ( peer) . await ) ;
252+ }
253+ statuses
254+ }
255+
256+ async fn check_peer_reachability ( & self , peer : Peer ) -> PeerReachability {
257+ let addr = peer
258+ . addr
259+ . clone ( )
260+ . unwrap_or_else ( || EndpointAddr :: new ( peer. id ) ) ;
261+ let label = peer. name . clone ( ) . unwrap_or_else ( || peer. id . to_string ( ) ) ;
262+
263+ match tokio:: time:: timeout ( REACHABILITY_TIMEOUT , self . ping_addr ( & label, addr) ) . await {
264+ Ok ( Ok ( pong) ) => PeerReachability {
265+ id : peer. id . to_string ( ) ,
266+ name : peer. name ,
267+ reachable : true ,
268+ bytes : Some ( pong. bytes ) ,
269+ round_trip_micros : Some ( pong. round_trip . as_micros ( ) . try_into ( ) . unwrap_or ( u64:: MAX ) ) ,
270+ transport : pong. transport ,
271+ error : None ,
272+ } ,
273+ Ok ( Err ( error) ) => PeerReachability {
274+ id : peer. id . to_string ( ) ,
275+ name : peer. name ,
276+ reachable : false ,
277+ bytes : None ,
278+ round_trip_micros : None ,
279+ transport : None ,
280+ error : Some ( format ! ( "{error:#}" ) ) ,
281+ } ,
282+ Err ( _) => PeerReachability {
283+ id : peer. id . to_string ( ) ,
284+ name : peer. name ,
285+ reachable : false ,
286+ bytes : None ,
287+ round_trip_micros : None ,
288+ transport : None ,
289+ error : Some ( format ! (
290+ "timed out after {:.1}s" ,
291+ REACHABILITY_TIMEOUT . as_secs_f32( )
292+ ) ) ,
293+ } ,
294+ }
295+ }
296+
297+ pub fn builtin_echo_hits ( & self ) -> usize {
298+ self . builtin_echo_hits . load ( Ordering :: SeqCst )
299+ }
203300}
204301
205302#[ derive( Debug , Clone ) ]
206303pub struct PingOutcome {
207304 pub peer : String ,
208305 pub bytes : usize ,
209306 pub round_trip : Duration ,
307+ pub transport : Option < String > ,
210308}
211309
212310pub struct FabricNode {
@@ -344,6 +442,7 @@ async fn process_control_request(
344442) -> Result < ControlResponse > {
345443 let response = match request {
346444 ControlRequest :: Status => state. status_response ( ) . await ?,
445+ ControlRequest :: ReachabilityStatus => state. reachability_status_response ( ) . await ?,
347446 ControlRequest :: ReloadPeers => {
348447 state. reload_peers ( ) . await ?;
349448 ControlResponse :: Ok
@@ -362,6 +461,7 @@ async fn process_control_request(
362461 peer : pong. peer ,
363462 bytes : pong. bytes ,
364463 round_trip_micros : pong. round_trip . as_micros ( ) . try_into ( ) . unwrap_or ( u64:: MAX ) ,
464+ transport : pong. transport ,
365465 }
366466 }
367467 ControlRequest :: Shutdown => {
@@ -398,7 +498,7 @@ async fn process_incoming_iroh(incoming: Incoming, state: Arc<DaemonState>) -> R
398498 let alpn = accepting. alpn ( ) . await ?;
399499 if alpn == BUILTIN_ECHO_ALPN {
400500 let connection = accepting. await ?;
401- handle_builtin_echo ( connection) . await ?;
501+ handle_builtin_echo ( connection, state ) . await ?;
402502 return Ok ( ( ) ) ;
403503 }
404504
@@ -419,7 +519,8 @@ async fn process_incoming_iroh(incoming: Incoming, state: Arc<DaemonState>) -> R
419519 Ok ( ( ) )
420520}
421521
422- async fn handle_builtin_echo ( connection : Connection ) -> Result < ( ) > {
522+ async fn handle_builtin_echo ( connection : Connection , state : Arc < DaemonState > ) -> Result < ( ) > {
523+ state. builtin_echo_hits . fetch_add ( 1 , Ordering :: SeqCst ) ;
423524 let ( mut send, mut recv) = connection. accept_bi ( ) . await ?;
424525 tokio:: io:: copy ( & mut recv, & mut send) . await ?;
425526 send. finish ( ) ?;
@@ -434,6 +535,52 @@ fn accepted_alpns(exposures: &HashMap<Vec<u8>, PathBuf>) -> Vec<Vec<u8>> {
434535 alpns
435536}
436537
538+ fn classify_connection_transport ( connection : & Connection ) -> Option < String > {
539+ let paths = connection. paths ( ) ;
540+ let mut selected_ip = false ;
541+ let mut selected_relay = false ;
542+ let mut any_ip = false ;
543+ let mut any_relay = false ;
544+
545+ for path in paths. iter ( ) {
546+ let is_ip = path. is_ip ( ) ;
547+ let is_relay = path. is_relay ( ) ;
548+ any_ip |= is_ip;
549+ any_relay |= is_relay;
550+ if path. is_selected ( ) {
551+ selected_ip |= is_ip;
552+ selected_relay |= is_relay;
553+ }
554+ }
555+
556+ classify_transport ( selected_ip, selected_relay)
557+ . or_else ( || classify_transport ( any_ip, any_relay) )
558+ }
559+
560+ fn classify_remote_transport ( info : & iroh:: endpoint:: RemoteInfo ) -> Option < String > {
561+ let mut active_ip = false ;
562+ let mut active_relay = false ;
563+
564+ for addr in info. addrs ( ) {
565+ if !matches ! ( addr. usage( ) , TransportAddrUsage :: Active ) {
566+ continue ;
567+ }
568+ active_ip |= addr. addr ( ) . is_ip ( ) ;
569+ active_relay |= addr. addr ( ) . is_relay ( ) ;
570+ }
571+
572+ classify_transport ( active_ip, active_relay)
573+ }
574+
575+ fn classify_transport ( has_ip : bool , has_relay : bool ) -> Option < String > {
576+ match ( has_ip, has_relay) {
577+ ( true , true ) => Some ( "mixed" . to_string ( ) ) ,
578+ ( true , false ) => Some ( "direct" . to_string ( ) ) ,
579+ ( false , true ) => Some ( "relay" . to_string ( ) ) ,
580+ ( false , false ) => None ,
581+ }
582+ }
583+
437584async fn run_dial_socket (
438585 listener : UnixListener ,
439586 endpoint : Endpoint ,
0 commit comments