diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 15e8006c3f..58695d4579 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -26,7 +26,7 @@ use url::Url; use self::hooks::EndpointHooksList; pub use super::magicsock::{ DirectAddr, DirectAddrType, PathInfo, - remote_map::{PathInfoList, Source}, + remote_map::{PathInfoList, RemoteInfo, Source, TransportAddrInfo, TransportAddrUsage}, }; #[cfg(wasm_browser)] use crate::discovery::pkarr::PkarrResolver; @@ -1060,6 +1060,18 @@ impl Endpoint { &self.msock.metrics } + /// Returns addressing information about a recently used remote endpoint. + /// + /// The returned [`RemoteInfo`] contains a list of all transport addresses for the remote + /// that we know about. This is a snapshot in time and not a watcher. + /// + /// Returns `None` if the endpoint doesn't have information about the remote. + /// When remote endpoints are no longer used, our endpoint will keep information around + /// for a little while, and then drop it. Afterwards, this will return `None`. + pub async fn remote_info(&self, endpoint_id: EndpointId) -> Option { + self.msock.remote_info(endpoint_id).await + } + // # Methods for less common state updates. /// Notifies the system of potential network changes. diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e4d3ad11a0..0511aaba08 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -58,7 +58,7 @@ use crate::{ defaults::timeouts::NET_REPORT_TIMEOUT, discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, EndpointData, UserData}, endpoint::hooks::EndpointHooksList, - magicsock::remote_map::PathsWatcher, + magicsock::remote_map::{PathsWatcher, RemoteInfo}, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; @@ -313,6 +313,16 @@ impl MagicSock { } } + /// Fetches the [`RemoteInfo`] about a remote from the `RemoteStateActor`. + /// + /// Returns `None` if no actor is running for the remote. + pub(crate) async fn remote_info(&self, id: EndpointId) -> Option { + let actor = self.remote_map.remote_state_actor_if_exists(id)?; + let (tx, rx) = oneshot::channel(); + actor.send(RemoteStateMessage::RemoteInfo(tx)).await.ok()?; + rx.await.ok() + } + pub(crate) async fn insert_relay( &self, relay: RelayUrl, diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index ec1ef280b2..3ae839bdfa 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -13,7 +13,9 @@ use tokio::sync::mpsc; pub(crate) use self::remote_state::PathsWatcher; pub(super) use self::remote_state::RemoteStateMessage; -pub use self::remote_state::{PathInfo, PathInfoList}; +pub use self::remote_state::{ + PathInfo, PathInfoList, RemoteInfo, TransportAddrInfo, TransportAddrUsage, +}; use self::remote_state::{RemoteStateActor, RemoteStateHandle}; use super::{ DirectAddr, MagicsockMetrics, @@ -118,6 +120,17 @@ impl RemoteMap { } } + pub(super) fn remote_state_actor_if_exists( + &self, + eid: EndpointId, + ) -> Option> { + self.actor_handles + .lock() + .expect("poisoned") + .get(&eid) + .and_then(|handle| handle.sender.get()) + } + /// Starts a new remote state actor and returns a handle and a sender. /// /// The handle is not inserted into the endpoint map, this must be done by the caller of this function. diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index d34c3455c8..d7c819ab1c 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -24,6 +24,7 @@ use tokio::sync::oneshot; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; +pub use self::remote_info::{RemoteInfo, TransportAddrInfo, TransportAddrUsage}; use self::{ guarded_channel::{GuardedReceiver, GuardedSender, guarded_channel}, path_state::RemotePathState, @@ -49,6 +50,7 @@ const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); mod guarded_channel; mod path_state; +mod remote_info; // TODO: use this // /// The latency at or under which we don't try to upgrade to a better path. @@ -336,6 +338,14 @@ impl RemoteStateActor { RemoteStateMessage::ResolveRemote(addrs, tx) => { self.handle_msg_resolve_remote(addrs, tx); } + RemoteStateMessage::RemoteInfo(tx) => { + let addrs = self.paths.to_remote_addrs(); + let info = RemoteInfo { + endpoint_id: self.endpoint_id, + addrs, + }; + tx.send(info).ok(); + } } } @@ -1090,6 +1100,10 @@ pub(crate) enum RemoteStateMessage { BTreeSet, oneshot::Sender>, ), + /// Returns information about the remote. + /// + /// This currently only includes a list of all known transport addresses for the remote. + RemoteInfo(oneshot::Sender), } /// A handle to a [`RemoteStateActor`]. diff --git a/iroh/src/magicsock/remote_map/remote_state/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs index dd9d6af513..a3d961e8d9 100644 --- a/iroh/src/magicsock/remote_map/remote_state/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -11,7 +11,7 @@ use rustc_hash::FxHashMap; use tokio::sync::oneshot; use tracing::trace; -use super::Source; +use super::{Source, TransportAddrInfo, TransportAddrUsage}; use crate::{discovery::DiscoveryError, magicsock::transports, metrics::MagicsockMetrics}; /// Maximum number of IP paths we keep around per endpoint. @@ -62,6 +62,25 @@ impl RemotePathState { metrics, } } + + pub(super) fn to_remote_addrs(&self) -> Vec { + self.paths + .iter() + .flat_map(|(addr, state)| { + let usage = match state.status { + PathStatus::Open => TransportAddrUsage::Active, + PathStatus::Inactive(_) | PathStatus::Unusable | PathStatus::Unknown => { + TransportAddrUsage::Inactive + } + }; + Some(TransportAddrInfo { + addr: addr.clone().into(), + usage, + }) + }) + .collect() + } + /// Insert a new address of an open path into our list of paths. /// /// This will emit pending resolve requests and trigger pruning paths. diff --git a/iroh/src/magicsock/remote_map/remote_state/remote_info.rs b/iroh/src/magicsock/remote_map/remote_state/remote_info.rs new file mode 100644 index 0000000000..f4f6e46985 --- /dev/null +++ b/iroh/src/magicsock/remote_map/remote_state/remote_info.rs @@ -0,0 +1,91 @@ +use iroh_base::{EndpointId, TransportAddr}; + +/// Information about a remote endpoint. +/// +/// This information is a snapshot in time, i.e. it is not updating and may +/// already be outdated by the time you are reading this. Updated information +/// can only be retrieved by calling [`Endpoint::remote_info`] again. +/// +/// [`Endpoint::remote_info`]: crate::Endpoint::remote_info +#[derive(Debug, Clone)] +pub struct RemoteInfo { + pub(super) endpoint_id: EndpointId, + pub(super) addrs: Vec, +} + +impl RemoteInfo { + /// Returns the remote's endpoint id. + pub fn id(&self) -> EndpointId { + self.endpoint_id + } + + /// Returns an iterator over known all addresses for this remote. + /// + /// Note that this may include outdated or unusable addresses. + pub fn addrs(&self) -> impl Iterator { + self.addrs.iter() + } + + /// Converts into an iterator over known all addresses for this remote. + /// + /// Note that this may include outdated or unusable addresses. You can use [`TransportAddrInfo::usage`] + /// to filter for addresses that are actively used. + /// + /// You can use this to construct an [`EndpointAddr`] for this remote: + /// + /// ```no_run + /// # use iroh::{Endpoint, EndpointId, EndpointAddr}; + /// # #[tokio::main] + /// # async fn main() { + /// # let endpoint = Endpoint::bind().await.unwrap(); + /// # let remote_id = EndpointId::from_bytes(&[0u8; 32]).unwrap(); + /// let info = endpoint.remote_info(remote_id).unwrap(); + /// let addr = EndpointAddr::from_parts(info.id(), info.into_addrs().map(|addr| addr.into_addr())); + /// # } + /// ``` + /// + /// [`EndpointAddr`]: crate::EndpointAddr + pub fn into_addrs(self) -> impl Iterator { + self.addrs.into_iter() + } +} + +/// Address of a remote with some metadata +#[derive(Debug, Clone)] +pub struct TransportAddrInfo { + pub(super) addr: TransportAddr, + pub(super) usage: TransportAddrUsage, +} + +impl TransportAddrInfo { + /// Returns the [`TransportAddr`]. + pub fn addr(&self) -> &TransportAddr { + &self.addr + } + + /// Converts into [`TransportAddr`]. + pub fn into_addr(self) -> TransportAddr { + self.addr + } + + /// Returns information how this address is used. + pub fn usage(&self) -> TransportAddrUsage { + self.usage + } +} + +impl From for TransportAddr { + fn from(value: TransportAddrInfo) -> Self { + value.addr + } +} + +/// Information how a transport address is used. +#[derive(Debug, Copy, Clone)] +#[non_exhaustive] +pub enum TransportAddrUsage { + /// The address is in active use. + Active, + /// The address is not currently used. + Inactive, +}