2866 lines
106 KiB
Rust
2866 lines
106 KiB
Rust
//! Implements a socket that can change its communication path while in use, actively searching for the best way to communicate.
|
|||
|
|
//!
|
||
|
|
//!
|
||
|
|
//! ### `RelayOnly` path selection:
|
||
|
|
//! When set this will force all packets to be sent over
|
||
|
|
//! the relay connection, regardless of whether or
|
||
|
|
//! not we have a direct UDP address for the given endpoint.
|
||
|
|
//!
|
||
|
|
//! The intended use is for testing the relay protocol inside the Socket
|
||
|
|
//! to ensure that we can rely on the relay to send packets when two endpoints
|
||
|
|
//! are unable to find direct UDP connections to each other.
|
||
|
|
//!
|
||
|
|
//! This also prevent this endpoint from attempting to hole punch and prevents it
|
||
|
|
//! from responding to any hole punching attempts. This endpoint will still,
|
||
|
|
//! however, read any packets that come off the UDP sockets.
|
||
|
|
|
||
|
|
use std::{
|
||
|
|
collections::{BTreeMap, BTreeSet},
|
||
|
|
fmt::Display,
|
||
|
|
io,
|
||
|
|
net::{IpAddr, SocketAddr},
|
||
|
|
sync::{
|
||
|
|
Arc, Mutex, RwLock,
|
||
|
|
atomic::{AtomicBool, Ordering},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr};
|
||
|
|
use iroh_relay::{RelayConfig, RelayMap};
|
||
|
|
use mapped_addrs::MultipathMappedAddr;
|
||
|
|
use n0_error::{AnyError, anyerr, bail, e, stack_error};
|
||
|
|
use n0_future::{
|
||
|
|
MaybeFuture,
|
||
|
|
task::{self, AbortOnDropHandle},
|
||
|
|
time::{self, Duration, Instant},
|
||
|
|
};
|
||
|
|
use n0_watcher::{self, Watchable, Watcher};
|
||
|
|
use netwatch::netmon;
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
use netwatch::{
|
||
|
|
interfaces::{IpNet, Ipv6AddrFlags},
|
||
|
|
ip::LocalAddresses,
|
||
|
|
};
|
||
|
|
use noq::{
|
||
|
|
NetworkChangeHint, TokenStore,
|
||
|
|
crypto::rustls::{QuicClientConfig, QuicServerConfig},
|
||
|
|
};
|
||
|
|
use rand::RngExt;
|
||
|
|
use rustc_hash::FxHashSet;
|
||
|
|
use tokio::sync::{
|
||
|
|
Mutex as AsyncMutex,
|
||
|
|
mpsc::{self},
|
||
|
|
oneshot,
|
||
|
|
};
|
||
|
|
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
|
||
|
|
use tracing::{Instrument, Level, Span, debug, error, event, info_span, instrument, trace, warn};
|
||
|
|
use transports::{LocalAddrsWatch, Transport, TransportConfig};
|
||
|
|
use url::Url;
|
||
|
|
|
||
|
|
use self::{
|
||
|
|
remote_map::{RemoteMap, RemoteStateMessage},
|
||
|
|
transports::{RelayActorConfig, Transports},
|
||
|
|
};
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
use crate::dns::DnsResolver;
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
use crate::net_report::QuicConfig;
|
||
|
|
use crate::{
|
||
|
|
address_lookup::{self, AddressLookupFailed, EndpointData, UserData},
|
||
|
|
defaults::timeouts::NET_REPORT_TIMEOUT,
|
||
|
|
endpoint::{
|
||
|
|
LocalTransportAddr, RelayStatus, hooks::EndpointHooksList, quic::QuicTransportConfig,
|
||
|
|
},
|
||
|
|
metrics::EndpointMetrics,
|
||
|
|
net_report::{self, IfStateDetails, Report},
|
||
|
|
portmapper,
|
||
|
|
runtime::Runtime,
|
||
|
|
socket::{
|
||
|
|
concurrent_read_map::ReadOnlyMap,
|
||
|
|
remote_map::{MappedAddrs, PathSelector, PathStateReceiver, RemoteInfo},
|
||
|
|
transports::{HomeRelayWatch, HomeRelayWatcher},
|
||
|
|
},
|
||
|
|
tls::{
|
||
|
|
self,
|
||
|
|
misc::{Blake3HmacKey, RustlsTokenKey},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
mod metrics;
|
||
|
|
|
||
|
|
pub(crate) mod biased_rtt_path_selector;
|
||
|
|
pub(crate) mod concurrent_read_map;
|
||
|
|
pub(crate) mod mapped_addrs;
|
||
|
|
pub(crate) mod remote_map;
|
||
|
|
pub(crate) mod transports;
|
||
|
|
|
||
|
|
use self::mapped_addrs::{EndpointIdMappedAddr, MappedAddr};
|
||
|
|
pub use self::metrics::Metrics;
|
||
|
|
|
||
|
|
// TODO: Use this
|
||
|
|
// /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically
|
||
|
|
// /// expire at 30 seconds, so this is a few seconds shy of that.
|
||
|
|
// const ENDPOINTS_FRESH_ENOUGH_DURATION: Duration = Duration::from_secs(27);
|
||
|
|
|
||
|
|
/// The duration in which we send keep-alives.
|
||
|
|
///
|
||
|
|
/// If a path is idle for this long, a PING frame will be sent to keep the connection
|
||
|
|
/// alive.
|
||
|
|
pub(crate) const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||
|
|
|
||
|
|
/// The maximum time a path can stay idle before being closed.
|
||
|
|
///
|
||
|
|
/// 15s gives 3x [`HEARTBEAT_INTERVAL`] (5s) for multiple retry chances, and enough
|
||
|
|
/// margin for real-world outages (WiFi reconnect 2-5s, cellular handoff 2-10s).
|
||
|
|
/// iroh 0.35 used 10s at the QUIC level; tailscale uses 45s at the WireGuard session
|
||
|
|
/// level with 3s heartbeats.
|
||
|
|
pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
|
||
|
|
|
||
|
|
/// The maximum time a relay path can stay idle before being closed.
|
||
|
|
///
|
||
|
|
/// Relay paths need a longer idle timeout than direct paths because the relay actor
|
||
|
|
/// manages the WebSocket connection and transparently reconnects after network changes
|
||
|
|
/// or relay server restarts. During network outages the interface may be down for
|
||
|
|
/// 5-15s, during which no relay traffic flows. Once the interface recovers, the relay
|
||
|
|
/// actor reconnects (DNS + TCP + TLS + WebSocket upgrade), which adds another 1-2s.
|
||
|
|
///
|
||
|
|
/// Set to match the connection-level idle timeout (30s) so the relay path survives
|
||
|
|
/// as long as the connection itself.
|
||
|
|
pub(crate) const RELAY_PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
||
|
|
|
||
|
|
/// Maximum number of concurrent QUIC multipath paths per connection.
|
||
|
|
///
|
||
|
|
/// We expect 1 relay path, and then leave space for ~3 IP and custom transport paths.
|
||
|
|
/// On top of that, when we expect a network change, we might be closing these paths
|
||
|
|
/// (except for the relay path) and open new ones, and give us 3 more paths to spare.
|
||
|
|
/// And finally we round that up to 8 for good measure.
|
||
|
|
pub(crate) const MAX_MULTIPATH_PATHS: u32 = 8;
|
||
|
|
|
||
|
|
/// Maximum number of n0 QUIC NAT Traversal addresses that the QUIC stack should allow.
|
||
|
|
///
|
||
|
|
/// This needs to be big enough to accommodate for machines which have lots of network
|
||
|
|
/// interfaces enabled. We've seen MacOS machines with >25 interfaces in the wild
|
||
|
|
/// (mostly due to VPN TUN and docket interfaces), so this seems like a reasonable
|
||
|
|
/// value.
|
||
|
|
pub(crate) const MAX_QNT_ADDRESSES: u8 = 32;
|
||
|
|
|
||
|
|
/// Error returned when the endpoint state actor stopped while waiting for a reply.
|
||
|
|
#[stack_error(add_meta, derive)]
|
||
|
|
#[error("endpoint state actor stopped")]
|
||
|
|
#[derive(Clone)]
|
||
|
|
pub(crate) struct RemoteStateActorStoppedError;
|
||
|
|
|
||
|
|
impl From<mpsc::error::SendError<RemoteStateMessage>> for RemoteStateActorStoppedError {
|
||
|
|
#[track_caller]
|
||
|
|
fn from(_value: mpsc::error::SendError<RemoteStateMessage>) -> Self {
|
||
|
|
Self::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Contains options for `Socket::listen`.
|
||
|
|
#[derive(derive_more::Debug)]
|
||
|
|
pub(crate) struct Options {
|
||
|
|
/// The configuration for the different transports.
|
||
|
|
pub(crate) transports: Vec<TransportConfig>,
|
||
|
|
|
||
|
|
/// Secret key for this endpoint.
|
||
|
|
pub(crate) secret_key: SecretKey,
|
||
|
|
|
||
|
|
/// Optional user-defined Address Lookup data.
|
||
|
|
pub(crate) address_lookup_user_data: Option<UserData>,
|
||
|
|
|
||
|
|
/// A DNS resolver to use for resolving relay URLs.
|
||
|
|
///
|
||
|
|
/// You can use [`crate::dns::DnsResolver::new`] for a resolver
|
||
|
|
/// that uses the system's DNS configuration.
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
pub(crate) dns_resolver: DnsResolver,
|
||
|
|
|
||
|
|
/// Proxy configuration.
|
||
|
|
pub(crate) proxy_url: Option<Url>,
|
||
|
|
|
||
|
|
/// TLS configuration for HTTPS and non-iroh-QUIC connections.
|
||
|
|
pub(crate) tls_config: rustls::ClientConfig,
|
||
|
|
|
||
|
|
/// ServerConfig for the internal QUIC endpoint
|
||
|
|
pub(crate) server_config: noq_proto::ServerConfig,
|
||
|
|
|
||
|
|
pub(crate) metrics: EndpointMetrics,
|
||
|
|
pub(crate) hooks: EndpointHooksList,
|
||
|
|
pub(crate) path_selector: Arc<dyn PathSelector>,
|
||
|
|
pub(crate) portmapper_config: portmapper::PortmapperConfig,
|
||
|
|
pub(crate) net_report_config: crate::net_report::NetReportConfig,
|
||
|
|
|
||
|
|
/// Static configuration for the endpoint.
|
||
|
|
pub(crate) static_config: StaticConfig,
|
||
|
|
|
||
|
|
/// Explicitly configured external addresses to advertise.
|
||
|
|
pub(crate) configured_addrs: BTreeSet<SocketAddr>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Inner state for an iroh [`crate::Endpoint`].
|
||
|
|
///
|
||
|
|
/// Dereferences to [`Socket`], and handles closing.
|
||
|
|
#[derive(Debug, derive_more::Deref)]
|
||
|
|
pub(crate) struct EndpointInner {
|
||
|
|
#[deref(forward)]
|
||
|
|
sock: Arc<Socket>,
|
||
|
|
// empty when shutdown
|
||
|
|
actor_task: Mutex<Option<AbortOnDropHandle<()>>>,
|
||
|
|
/// Channel to send to the internal actor.
|
||
|
|
actor_sender: mpsc::Sender<ActorMessage>,
|
||
|
|
// noq endpoint
|
||
|
|
endpoint: noq::Endpoint,
|
||
|
|
// Runtime used by noq
|
||
|
|
runtime: Arc<Runtime>,
|
||
|
|
/// Static configuration for the endpoint.
|
||
|
|
pub(crate) static_config: StaticConfig,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Drop for EndpointInner {
|
||
|
|
fn drop(&mut self) {
|
||
|
|
if self.sock.is_closed() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
tracing::error!(
|
||
|
|
"Endpoint dropped without calling `Endpoint::close`. Aborting ungracefully."
|
||
|
|
);
|
||
|
|
self.abort();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Configuration for a [`noq::Endpoint`] that cannot be changed at runtime.
|
||
|
|
#[derive(derive_more::Debug)]
|
||
|
|
pub(crate) struct StaticConfig {
|
||
|
|
pub(crate) tls_config: tls::TlsConfig,
|
||
|
|
#[debug("QuicServerConifg")]
|
||
|
|
pub(crate) server_config: QuicServerConfig,
|
||
|
|
#[debug("QuicClientConfig")]
|
||
|
|
pub(crate) client_config: QuicClientConfig,
|
||
|
|
#[debug("Arc<RustlsTokenKey>")]
|
||
|
|
pub(crate) token_key: Arc<RustlsTokenKey>,
|
||
|
|
#[debug("Arc<dyn TokenStore>")]
|
||
|
|
pub(crate) token_store: Arc<dyn TokenStore>,
|
||
|
|
pub(crate) transport_config: QuicTransportConfig,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl StaticConfig {
|
||
|
|
/// Create a [`noq_proto::ServerConfig`] with the specified ALPN protocols.
|
||
|
|
pub(crate) fn create_server_config(
|
||
|
|
&self,
|
||
|
|
alpn_protocols: Vec<Vec<u8>>,
|
||
|
|
) -> noq_proto::ServerConfig {
|
||
|
|
let mut quic_server_config = self.server_config.clone();
|
||
|
|
quic_server_config.set_alpn_protocols(alpn_protocols);
|
||
|
|
let mut inner =
|
||
|
|
noq::ServerConfig::new(Arc::new(quic_server_config), self.token_key.clone());
|
||
|
|
inner.transport_config(self.transport_config.to_inner_arc());
|
||
|
|
inner
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a [`noq_proto::ClientConfig`] with the specified ALPN protocols.
|
||
|
|
pub(crate) fn create_client_config(
|
||
|
|
&self,
|
||
|
|
alpn_protocols: Vec<Vec<u8>>,
|
||
|
|
transport_config: Arc<noq::TransportConfig>,
|
||
|
|
) -> noq_proto::ClientConfig {
|
||
|
|
let mut quic_client_config = self.client_config.clone();
|
||
|
|
quic_client_config.set_alpn_protocols(alpn_protocols);
|
||
|
|
let mut inner = noq::ClientConfig::new(Arc::new(quic_client_config));
|
||
|
|
inner.transport_config(transport_config);
|
||
|
|
inner.token_store(self.token_store.clone());
|
||
|
|
inner
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// This coordinates the shutdown of the [`Socket`] and all its tasks.
|
||
|
|
///
|
||
|
|
/// It also tightly binds to the [`EndpointInner`] and [`Actor`] closing as that is where
|
||
|
|
/// most of the logic lives.
|
||
|
|
#[derive(Debug)]
|
||
|
|
struct ShutdownState {
|
||
|
|
/// Token that is cancelled at the moment [`crate::Endpoint::close`] is called.
|
||
|
|
///
|
||
|
|
/// Currently cancelled from [`EndpointInner::close`].
|
||
|
|
at_close_start: CancellationToken,
|
||
|
|
/// Token that is cancelled once the [`noq::Endpoint`] is drained.
|
||
|
|
///
|
||
|
|
/// Only 100ms after this is cancelled will the [`Actor`] task be cancelled, it should
|
||
|
|
/// have exited already by then as it is considered an error if it was still running.
|
||
|
|
at_endpoint_closed: CancellationToken,
|
||
|
|
/// Set if the endpoint is closed and all tasks are stopped.
|
||
|
|
///
|
||
|
|
/// This is only set once both [`Self::at_close_start`] and [`Self::at_endpoint_closed`]
|
||
|
|
/// are cancelled **and** the [`Actor`] task is no longer running.
|
||
|
|
closed: AtomicBool,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for ShutdownState {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
at_close_start: CancellationToken::new(),
|
||
|
|
at_endpoint_closed: CancellationToken::new(),
|
||
|
|
closed: AtomicBool::new(false),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ShutdownState {
|
||
|
|
/// Whether the endpoint has started closing, or is already closed.
|
||
|
|
///
|
||
|
|
/// This is true once [`crate::Endpoint::close`] is called, and remains true forever
|
||
|
|
/// after. Tasks might still be shutting down.
|
||
|
|
fn is_closing(&self) -> bool {
|
||
|
|
self.at_close_start.is_cancelled()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the endpoint is fully closed and all tasks stopped.
|
||
|
|
///
|
||
|
|
/// The endpoint will be drained, all transports and sockets will be closed.
|
||
|
|
fn is_closed(&self) -> bool {
|
||
|
|
self.closed.load(Ordering::Relaxed)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Iroh connectivity layer.
|
||
|
|
///
|
||
|
|
/// This is responsible for routing packets to endpoints based on endpoint IDs, it will initially
|
||
|
|
/// route packets via a relay and transparently try and establish an endpoint-to-endpoint
|
||
|
|
/// connection and upgrade to it. It will also keep looking for better connections as the
|
||
|
|
/// network details of both endpoints change.
|
||
|
|
///
|
||
|
|
/// It is usually only necessary to use a single [`Socket`] instance in an application, it
|
||
|
|
/// means any QUIC endpoints on top will be sharing as much information about endpoints as
|
||
|
|
/// possible.
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub(crate) struct Socket {
|
||
|
|
/// Read-only view of the per-remote `RemoteStateActor` inboxes.
|
||
|
|
///
|
||
|
|
/// Lets callers send to an existing `RemoteStateActor` without going through
|
||
|
|
/// the socket actor.
|
||
|
|
///
|
||
|
|
/// A missing entry means no actor is running for that remote. Spawning new
|
||
|
|
/// `RemoteStateActor`s must go through the socket actor channel.
|
||
|
|
remote_actors: ReadOnlyMap<EndpointId, mpsc::Sender<RemoteStateMessage>>,
|
||
|
|
|
||
|
|
// - Shutdown Management
|
||
|
|
shutdown: ShutdownState,
|
||
|
|
|
||
|
|
// - Networking Info
|
||
|
|
/// Our discovered direct addresses.
|
||
|
|
direct_addrs: DiscoveredDirectAddrs,
|
||
|
|
/// Our latest net-report
|
||
|
|
net_report: Watchable<(Option<Report>, UpdateReason)>,
|
||
|
|
/// If the last net_report report, reports IPv6 to be available.
|
||
|
|
ipv6_reported: Arc<AtomicBool>,
|
||
|
|
/// Maps for resolving mapped addrs to/from IP and relay addresses.
|
||
|
|
mapped_addrs: MappedAddrs,
|
||
|
|
|
||
|
|
/// Local addresses
|
||
|
|
local_addrs_watch: LocalAddrsWatch,
|
||
|
|
home_relay_watch: HomeRelayWatcher,
|
||
|
|
/// Currently bound IP addresses of all sockets
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
ip_bind_addrs: Vec<SocketAddr>,
|
||
|
|
/// The DNS resolver to be used in this socket.
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
dns_resolver: DnsResolver,
|
||
|
|
relay_map: RelayMap,
|
||
|
|
|
||
|
|
/// Optional Address Lookup
|
||
|
|
address_lookup: address_lookup::AddressLookupServices,
|
||
|
|
/// Optional user-defined discover data.
|
||
|
|
address_lookup_user_data: RwLock<Option<UserData>>,
|
||
|
|
/// Explicitly configured external addresses to advertise.
|
||
|
|
configured_addrs: RwLock<BTreeSet<SocketAddr>>,
|
||
|
|
|
||
|
|
pub(crate) tls_config: rustls::ClientConfig,
|
||
|
|
|
||
|
|
/// Metrics
|
||
|
|
pub(crate) metrics: EndpointMetrics,
|
||
|
|
pub(crate) hooks: EndpointHooksList,
|
||
|
|
/// Tracing span for this endpoint.
|
||
|
|
pub(crate) span: Span,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Socket {
|
||
|
|
/// Returns the relay endpoint we are connected to, that has the best latency.
|
||
|
|
///
|
||
|
|
/// If `None`, then we are not connected to any relay endpoints.
|
||
|
|
pub(crate) fn my_relay(&self) -> Option<RelayUrl> {
|
||
|
|
self.local_addr().into_iter().find_map(|a| {
|
||
|
|
if let transports::Addr::Relay(url, _) = a {
|
||
|
|
Some(url)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the iroh endpoint is closed and all its actors stopped.
|
||
|
|
pub(crate) fn is_closed(&self) -> bool {
|
||
|
|
self.shutdown.is_closed()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether [`crate::Endpoint::close`] has been called.
|
||
|
|
fn is_closing(&self) -> bool {
|
||
|
|
self.shutdown.is_closing()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a future that resolves once endpoint shutdown has started.
|
||
|
|
pub(crate) fn closed(&self) -> WaitForCancellationFutureOwned {
|
||
|
|
self.shutdown.at_close_start.clone().cancelled_owned()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get the cached version of addresses.
|
||
|
|
pub(crate) fn local_addr(&self) -> Vec<transports::Addr> {
|
||
|
|
self.local_addrs_watch.clone().get()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
fn ip_bind_addrs(&self) -> &[SocketAddr] {
|
||
|
|
&self.ip_bind_addrs
|
||
|
|
}
|
||
|
|
|
||
|
|
fn ip_local_addrs(&self) -> impl Iterator<Item = SocketAddr> + use<> {
|
||
|
|
self.local_addr()
|
||
|
|
.into_iter()
|
||
|
|
.filter_map(|addr| addr.into_socket_addr())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Tries to send a [`RemoteStateMessage`] to the `RemoteStateActor` for given [`EndpointId`].
|
||
|
|
///
|
||
|
|
/// Returns an error if there currently is no remote state actor running for this, or when it
|
||
|
|
/// is currently shutting down.
|
||
|
|
pub(crate) fn try_send_remote_state_msg(
|
||
|
|
&self,
|
||
|
|
endpoint_id: EndpointId,
|
||
|
|
message: RemoteStateMessage,
|
||
|
|
) -> Result<(), RemoteStateMessage> {
|
||
|
|
let Some(sender) = self.remote_actors.get(&endpoint_id) else {
|
||
|
|
return Err(message);
|
||
|
|
};
|
||
|
|
sender.try_send(message).map_err(|err| err.into_inner())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a [`Watcher`] for this socket's direct addresses.
|
||
|
|
///
|
||
|
|
/// The [`Socket`] continuously monitors the direct addresses, the network addresses
|
||
|
|
/// it might be able to be contacted on, for changes. Whenever changes are detected
|
||
|
|
/// this [`Watcher`] will yield a new list of addresses.
|
||
|
|
///
|
||
|
|
/// Upon the first creation on the [`Socket`] it may not yet have completed a first
|
||
|
|
/// net report to discover IP addresses, in this case the current item in this [`Watcher`] will be
|
||
|
|
/// [`None`]. Once the first set of ip addresses are discovered the [`Watcher`] will
|
||
|
|
/// store [`Some`] set of addresses.
|
||
|
|
///
|
||
|
|
/// To get the current direct addresses, use [`Watcher::initialized`].
|
||
|
|
///
|
||
|
|
/// [`Watcher`]: n0_watcher::Watcher
|
||
|
|
/// [`Watcher::initialized`]: n0_watcher::Watcher::initialized
|
||
|
|
pub(crate) fn ip_addrs(&self) -> n0_watcher::Direct<BTreeSet<DirectAddr>> {
|
||
|
|
self.direct_addrs.addrs.watch()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a [`Watcher`] for this socket's net-report.
|
||
|
|
///
|
||
|
|
/// The [`Socket`] continuously monitors the network conditions for changes.
|
||
|
|
/// Whenever changes are detected this [`Watcher`] will yield a new report.
|
||
|
|
///
|
||
|
|
/// Upon the first creation on the [`Socket`] it may not yet have completed
|
||
|
|
/// a first net-report. In this case, the current item in this [`Watcher`] will
|
||
|
|
/// be [`None`]. Once the first report has been run, the [`Watcher`] will
|
||
|
|
/// store [`Some`] report.
|
||
|
|
///
|
||
|
|
/// To get the current `net-report`, use [`Watcher::initialized`].
|
||
|
|
///
|
||
|
|
/// [`Watcher`]: n0_watcher::Watcher
|
||
|
|
/// [`Watcher::initialized`]: n0_watcher::Watcher::initialized
|
||
|
|
#[cfg(feature = "unstable-net-report")]
|
||
|
|
pub(crate) fn net_report(&self) -> impl Watcher<Value = Option<Report>> + use<> {
|
||
|
|
self.net_report.watch().map(|(r, _)| r)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Watch for changes to the home relay.
|
||
|
|
///
|
||
|
|
/// Note that this can be used to wait for the initial home relay to be known using
|
||
|
|
/// [`Watcher::initialized`].
|
||
|
|
pub(crate) fn home_relay(&self) -> impl Watcher<Value = Vec<RelayUrl>> + use<> {
|
||
|
|
self.local_addrs_watch.clone().map(|addrs| {
|
||
|
|
addrs
|
||
|
|
.into_iter()
|
||
|
|
.filter_map(|addr| {
|
||
|
|
if let transports::Addr::Relay(url, _) = addr {
|
||
|
|
Some(url)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) fn home_relay_status(&self) -> impl Watcher<Value = Vec<RelayStatus>> + use<> {
|
||
|
|
self.home_relay_watch.clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Stores a new set of direct addresses.
|
||
|
|
///
|
||
|
|
/// If the direct addresses have changed from the previous set, they are published to
|
||
|
|
/// the address lookup system.
|
||
|
|
fn store_direct_addresses(&self, addrs: BTreeSet<DirectAddr>) {
|
||
|
|
let updated = self.direct_addrs.update(addrs);
|
||
|
|
if updated {
|
||
|
|
self.publish_my_addr();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get a reference to the DNS resolver used in this [`Socket`].
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
pub(crate) fn dns_resolver(&self) -> &DnsResolver {
|
||
|
|
&self.dns_resolver
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Translates a possible IP-mapped [`SocketAddr`] into a [`transports::Addr`].
|
||
|
|
///
|
||
|
|
/// For regular IP addresses this returns `Addr::Ip`. For mapped addresses this performs
|
||
|
|
/// a reverse lookup.
|
||
|
|
pub(crate) fn to_transport_addr(&self, addr: SocketAddr) -> Option<transports::Addr> {
|
||
|
|
self.mapped_addrs.to_transport_addr(addr)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) fn to_local_transport_addr(
|
||
|
|
&self,
|
||
|
|
local_ip: Option<IpAddr>,
|
||
|
|
remote_addr: SocketAddr,
|
||
|
|
) -> LocalTransportAddr {
|
||
|
|
let remote_addr = self.to_transport_addr(remote_addr).unwrap_or_else(|| {
|
||
|
|
error!(
|
||
|
|
mapped_addr = ?remote_addr,
|
||
|
|
"Socket::to_local_transport_addr: invalid mapped address",
|
||
|
|
);
|
||
|
|
transports::Addr::Ip(remote_addr)
|
||
|
|
});
|
||
|
|
LocalTransportAddr::from_noq_local_ip(
|
||
|
|
local_ip,
|
||
|
|
&remote_addr,
|
||
|
|
&self.mapped_addrs.custom_addrs,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Reference to the internal Address Lookup
|
||
|
|
pub(crate) fn address_lookup(&self) -> &address_lookup::AddressLookupServices {
|
||
|
|
&self.address_lookup
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Updates the user-defined Address Lookup data for this endpoint.
|
||
|
|
pub(crate) fn set_user_data_for_address_lookup(&self, user_data: Option<UserData>) {
|
||
|
|
let mut guard = self
|
||
|
|
.address_lookup_user_data
|
||
|
|
.write()
|
||
|
|
.expect("lock poisened");
|
||
|
|
if *guard != user_data {
|
||
|
|
*guard = user_data;
|
||
|
|
drop(guard);
|
||
|
|
self.publish_my_addr();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Process datagrams received from all the transports.
|
||
|
|
///
|
||
|
|
/// All the `bufs` and `metas` should have initialized packets in them.
|
||
|
|
///
|
||
|
|
/// This fixes up the datagrams to use the correct [`MultipathMappedAddr`] and extracts
|
||
|
|
/// DISCO packets, processing them inside the socket.
|
||
|
|
///
|
||
|
|
/// [`MultipathMappedAddr`]: mapped_addrs::MultipathMappedAddr
|
||
|
|
fn process_datagrams(
|
||
|
|
&self,
|
||
|
|
bufs: &mut [io::IoSliceMut<'_>],
|
||
|
|
metas: &mut [noq_udp::RecvMeta],
|
||
|
|
recv_infos: &[transports::RecvInfo],
|
||
|
|
) {
|
||
|
|
assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas");
|
||
|
|
assert_eq!(
|
||
|
|
bufs.len(),
|
||
|
|
recv_infos.len(),
|
||
|
|
"non matching bufs & recv_infos"
|
||
|
|
);
|
||
|
|
|
||
|
|
// zip is slow :(
|
||
|
|
for i in 0..metas.len() {
|
||
|
|
let noq_meta = &mut metas[i];
|
||
|
|
let recv_info = &recv_infos[i];
|
||
|
|
let remote_addr = recv_info.remote();
|
||
|
|
let datagram_count = if noq_meta.stride == 0 {
|
||
|
|
if noq_meta.len > 0 {
|
||
|
|
warn!(
|
||
|
|
src = ?remote_addr,
|
||
|
|
len = noq_meta.len,
|
||
|
|
"received datagram with stride=0 but len>0",
|
||
|
|
);
|
||
|
|
// fix the weird len
|
||
|
|
noq_meta.len = 0;
|
||
|
|
}
|
||
|
|
// one empty datagram
|
||
|
|
1
|
||
|
|
} else {
|
||
|
|
noq_meta.len.div_ceil(noq_meta.stride)
|
||
|
|
};
|
||
|
|
self.metrics
|
||
|
|
.socket
|
||
|
|
.recv_datagrams
|
||
|
|
.inc_by(datagram_count as _);
|
||
|
|
if noq_meta.len > noq_meta.stride {
|
||
|
|
trace!(
|
||
|
|
src = ?remote_addr,
|
||
|
|
len = noq_meta.len,
|
||
|
|
stride = %noq_meta.stride,
|
||
|
|
datagram_count,
|
||
|
|
"GRO datagram received",
|
||
|
|
);
|
||
|
|
self.metrics.socket.recv_gro_datagrams.inc();
|
||
|
|
} else {
|
||
|
|
trace!(src = ?remote_addr, len = noq_meta.len, "datagram received");
|
||
|
|
}
|
||
|
|
match remote_addr {
|
||
|
|
transports::Addr::Ip(SocketAddr::V4(..)) => {
|
||
|
|
self.metrics.socket.recv_data_ipv4.inc_by(noq_meta.len as _);
|
||
|
|
}
|
||
|
|
transports::Addr::Ip(SocketAddr::V6(..)) => {
|
||
|
|
self.metrics.socket.recv_data_ipv6.inc_by(noq_meta.len as _);
|
||
|
|
}
|
||
|
|
transports::Addr::Relay(src_url, src_node) => {
|
||
|
|
self.metrics
|
||
|
|
.socket
|
||
|
|
.recv_data_relay
|
||
|
|
.inc_by(noq_meta.len as _);
|
||
|
|
|
||
|
|
// Fill in the correct mapped address
|
||
|
|
let mapped_addr = self
|
||
|
|
.mapped_addrs
|
||
|
|
.relay_addrs
|
||
|
|
.get(&(src_url.clone(), *src_node));
|
||
|
|
noq_meta.addr = mapped_addr.private_socket_addr();
|
||
|
|
}
|
||
|
|
transports::Addr::Custom(remote) => {
|
||
|
|
self.metrics
|
||
|
|
.socket
|
||
|
|
.recv_data_custom
|
||
|
|
.inc_by(noq_meta.len as _);
|
||
|
|
// Fill in the correct mapped address
|
||
|
|
let mapped_addr = self.mapped_addrs.custom_addrs.get(remote);
|
||
|
|
noq_meta.addr = mapped_addr.private_socket_addr();
|
||
|
|
if let Some(local) = recv_info.local() {
|
||
|
|
let local_mapped = self.mapped_addrs.custom_addrs.get(local);
|
||
|
|
noq_meta.dst_ip = Some(local_mapped.private_socket_addr().ip());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Publishes our address to an address lookup service, if configured.
|
||
|
|
///
|
||
|
|
/// Called whenever our addresses or home relay endpoint changes.
|
||
|
|
fn publish_my_addr(&self) {
|
||
|
|
let relay_url = self.my_relay();
|
||
|
|
let mut addrs: Vec<_> = self
|
||
|
|
.direct_addrs
|
||
|
|
.sockaddrs()
|
||
|
|
.map(TransportAddr::Ip)
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
let user_data = self
|
||
|
|
.address_lookup_user_data
|
||
|
|
.read()
|
||
|
|
.expect("lock poisened")
|
||
|
|
.clone();
|
||
|
|
if relay_url.is_none() && addrs.is_empty() && user_data.is_none() {
|
||
|
|
// do not bother publishing if we don't have any information
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if let Some(url) = relay_url {
|
||
|
|
addrs.push(TransportAddr::Relay(url));
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut data = EndpointData::new(addrs);
|
||
|
|
data.set_user_data(user_data);
|
||
|
|
self.address_lookup.publish(&data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Manages currently running net reports to learn this endpoint's IP addresses.
|
||
|
|
///
|
||
|
|
/// Invariants:
|
||
|
|
/// - only one direct addr update must be running at a time
|
||
|
|
/// - if an update is scheduled while another one is running, remember that
|
||
|
|
/// and start a new one when the current one has finished
|
||
|
|
#[derive(Debug)]
|
||
|
|
struct DirectAddrUpdateState {
|
||
|
|
/// If set, start a new update as soon as the current one is finished.
|
||
|
|
want_update: Option<UpdateReason>,
|
||
|
|
sock: Arc<Socket>,
|
||
|
|
port_mapper: portmapper::Client,
|
||
|
|
/// The prober that discovers local network conditions, including the closest relay relay and NAT mappings.
|
||
|
|
net_reporter: Arc<AsyncMutex<net_report::Client>>,
|
||
|
|
relay_map: RelayMap,
|
||
|
|
run_done: mpsc::Sender<()>,
|
||
|
|
shutdown_token: CancellationToken,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
|
||
|
|
enum UpdateReason {
|
||
|
|
/// Initial state
|
||
|
|
#[default]
|
||
|
|
None,
|
||
|
|
Periodic,
|
||
|
|
PortmapUpdated,
|
||
|
|
LinkChangeMajor,
|
||
|
|
LinkChangeMinor,
|
||
|
|
RelayMapChange,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpdateReason {
|
||
|
|
fn is_major(self) -> bool {
|
||
|
|
matches!(self, Self::LinkChangeMajor | Self::RelayMapChange)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl DirectAddrUpdateState {
|
||
|
|
fn new(
|
||
|
|
sock: Arc<Socket>,
|
||
|
|
port_mapper: portmapper::Client,
|
||
|
|
net_reporter: Arc<AsyncMutex<net_report::Client>>,
|
||
|
|
relay_map: RelayMap,
|
||
|
|
run_done: mpsc::Sender<()>,
|
||
|
|
shutdown_token: CancellationToken,
|
||
|
|
) -> Self {
|
||
|
|
DirectAddrUpdateState {
|
||
|
|
want_update: Default::default(),
|
||
|
|
port_mapper,
|
||
|
|
net_reporter,
|
||
|
|
sock,
|
||
|
|
relay_map,
|
||
|
|
run_done,
|
||
|
|
shutdown_token,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Schedules a new run, either starting it immediately if none is running or
|
||
|
|
/// scheduling it for later.
|
||
|
|
fn schedule_run(&mut self, why: UpdateReason, if_state: IfStateDetails) {
|
||
|
|
match self.net_reporter.clone().try_lock_owned() {
|
||
|
|
Ok(net_reporter) => {
|
||
|
|
self.run(why, if_state, net_reporter);
|
||
|
|
}
|
||
|
|
Err(_) => {
|
||
|
|
let _ = self.want_update.insert(why);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// If another run is needed, triggers this run, otherwise does nothing.
|
||
|
|
fn try_run(&mut self, if_state: IfStateDetails) {
|
||
|
|
match self.net_reporter.clone().try_lock_owned() {
|
||
|
|
Ok(net_reporter) => {
|
||
|
|
if let Some(why) = self.want_update.take() {
|
||
|
|
self.run(why, if_state, net_reporter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(_) => {
|
||
|
|
// do nothing
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trigger a new run.
|
||
|
|
fn run(
|
||
|
|
&mut self,
|
||
|
|
why: UpdateReason,
|
||
|
|
if_state: IfStateDetails,
|
||
|
|
mut net_reporter: tokio::sync::OwnedMutexGuard<net_report::Client>,
|
||
|
|
) {
|
||
|
|
debug!("starting direct addr update ({:?})", why);
|
||
|
|
// Don't start a net report probe if we know
|
||
|
|
// we are shutting down
|
||
|
|
if self.shutdown_token.is_cancelled() {
|
||
|
|
debug!("skipping net_report, socket is shutting down");
|
||
|
|
// deactivate portmapper
|
||
|
|
self.port_mapper.deactivate();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if self.relay_map.is_empty() {
|
||
|
|
debug!("skipping net_report, empty RelayMap");
|
||
|
|
self.sock.net_report.set((None, why)).ok();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
self.sock.metrics.net_report.portmap_attempts.inc();
|
||
|
|
self.port_mapper.procure_mapping();
|
||
|
|
|
||
|
|
trace!("requesting net_report report");
|
||
|
|
let sock = self.sock.clone();
|
||
|
|
|
||
|
|
let run_done = self.run_done.clone();
|
||
|
|
|
||
|
|
// Ensure that reports are cancelled when we shutdown
|
||
|
|
let token = self.shutdown_token.child_token();
|
||
|
|
let inner_token = token.child_token();
|
||
|
|
task::spawn(
|
||
|
|
async move {
|
||
|
|
let fut = token.run_until_cancelled(time::timeout(
|
||
|
|
NET_REPORT_TIMEOUT,
|
||
|
|
net_reporter.get_report(if_state, why.is_major(), inner_token),
|
||
|
|
));
|
||
|
|
|
||
|
|
match fut.await {
|
||
|
|
Some(Ok(report)) => {
|
||
|
|
sock.net_report.set((Some(report), why)).ok();
|
||
|
|
}
|
||
|
|
Some(Err(time::Elapsed { .. })) => {
|
||
|
|
warn!("net_report report timed out");
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
trace!("net_report cancelled");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// mark run as finished
|
||
|
|
debug!("direct addr update done ({:?})", why);
|
||
|
|
run_done.send(()).await.ok();
|
||
|
|
}
|
||
|
|
.instrument(tracing::Span::current()),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[allow(missing_docs)]
|
||
|
|
#[stack_error(derive, add_meta)]
|
||
|
|
#[non_exhaustive]
|
||
|
|
pub enum BindError {
|
||
|
|
#[error("Failed to bind sockets")]
|
||
|
|
Sockets { source: io::Error },
|
||
|
|
#[error("Failed to create internal QUIC endpoint")]
|
||
|
|
CreateQuicEndpoint { source: io::Error },
|
||
|
|
#[error("Failed to create netmon monitor")]
|
||
|
|
CreateNetmonMonitor { source: AnyError },
|
||
|
|
#[error("Invalid transport configuration")]
|
||
|
|
InvalidTransportConfig,
|
||
|
|
#[error("Invalid CA root configuration")]
|
||
|
|
InvalidCaRootConfig { source: io::Error },
|
||
|
|
#[error("Failed to create an address lookup service")]
|
||
|
|
AddressLookup {
|
||
|
|
#[error(from)]
|
||
|
|
source: crate::address_lookup::AddressLookupBuilderError,
|
||
|
|
},
|
||
|
|
#[error("Missing or incompatible rustls crypto provider configured")]
|
||
|
|
InvalidCryptoProvider,
|
||
|
|
#[error("Error constructing TLS configuration")]
|
||
|
|
TlsConfigError {
|
||
|
|
#[error(from)]
|
||
|
|
source: tls::TlsConfigError,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
impl EndpointInner {
|
||
|
|
/// Creates a [`EndpointInner`].
|
||
|
|
pub(crate) async fn bind(opts: Options) -> Result<Self, BindError> {
|
||
|
|
// Use the current span as the main span for all tasks spawned in this endpoint.
|
||
|
|
// `EndpointInner::bind` is not public and only called from `crate::endpoint::Builder::bind`,
|
||
|
|
// which instruments the call with a span created for this purpose.
|
||
|
|
let span = tracing::Span::current();
|
||
|
|
|
||
|
|
let Options {
|
||
|
|
secret_key,
|
||
|
|
transports: transport_configs,
|
||
|
|
address_lookup_user_data,
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
dns_resolver,
|
||
|
|
proxy_url,
|
||
|
|
server_config,
|
||
|
|
tls_config,
|
||
|
|
metrics,
|
||
|
|
hooks,
|
||
|
|
path_selector,
|
||
|
|
portmapper_config,
|
||
|
|
net_report_config,
|
||
|
|
static_config,
|
||
|
|
configured_addrs,
|
||
|
|
} = opts;
|
||
|
|
|
||
|
|
let address_lookup = address_lookup::AddressLookupServices::default();
|
||
|
|
let port_mapper = portmapper::create_client(&portmapper_config);
|
||
|
|
|
||
|
|
let relay_transport_configs: Vec<_> = transport_configs
|
||
|
|
.iter()
|
||
|
|
.filter(|t| matches!(t, TransportConfig::Relay { .. }))
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
// Currently we only support a single relay transport
|
||
|
|
if relay_transport_configs.len() > 1 {
|
||
|
|
bail!(BindError::InvalidTransportConfig);
|
||
|
|
}
|
||
|
|
let relay_map = relay_transport_configs
|
||
|
|
.iter()
|
||
|
|
.filter_map(|t| {
|
||
|
|
#[allow(irrefutable_let_patterns)]
|
||
|
|
if let TransportConfig::Relay { relay_map, .. } = t {
|
||
|
|
Some(relay_map.clone())
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.next()
|
||
|
|
.unwrap_or_else(RelayMap::empty);
|
||
|
|
|
||
|
|
let ipv6_reported = Arc::new(AtomicBool::new(false));
|
||
|
|
|
||
|
|
let relay_actor_config = RelayActorConfig {
|
||
|
|
my_relay: HomeRelayWatch::default(),
|
||
|
|
secret_key: secret_key.clone(),
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
dns_resolver: dns_resolver.clone(),
|
||
|
|
proxy_url: proxy_url.clone(),
|
||
|
|
ipv6_reported: ipv6_reported.clone(),
|
||
|
|
tls_config: tls_config.clone(),
|
||
|
|
metrics: metrics.socket.clone(),
|
||
|
|
relay_map: relay_map.clone(),
|
||
|
|
};
|
||
|
|
|
||
|
|
let shutdown_state = ShutdownState::default();
|
||
|
|
let shutdown_token = shutdown_state.at_endpoint_closed.child_token();
|
||
|
|
|
||
|
|
let transports = Transports::bind(
|
||
|
|
&transport_configs,
|
||
|
|
relay_actor_config,
|
||
|
|
&metrics,
|
||
|
|
shutdown_token.child_token(),
|
||
|
|
)
|
||
|
|
.map_err(|err| e!(BindError::Sockets, err))?;
|
||
|
|
|
||
|
|
if let Some(v4_port) = transports.local_addrs().into_iter().find_map(|t| {
|
||
|
|
if let transports::Addr::Ip(SocketAddr::V4(addr)) = t {
|
||
|
|
Some(addr.port())
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}) {
|
||
|
|
// NOTE: we can end up with a zero port if `netwatch::UdpSocket::socket_addr` fails
|
||
|
|
match v4_port.try_into() {
|
||
|
|
Ok(non_zero_port) => {
|
||
|
|
port_mapper.update_local_port(non_zero_port);
|
||
|
|
}
|
||
|
|
Err(_zero_port) => debug!("Skipping port mapping with zero local port"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let (actor_sender, actor_receiver) = mpsc::channel(256);
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
let has_ipv6_transport = transports
|
||
|
|
.ip_bind_addrs()
|
||
|
|
.into_iter()
|
||
|
|
.any(|addr| addr.is_ipv6());
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
let has_ip_transports = !transports.ip_bind_addrs().is_empty();
|
||
|
|
|
||
|
|
let direct_addrs = DiscoveredDirectAddrs::default();
|
||
|
|
|
||
|
|
let remote_map = {
|
||
|
|
RemoteMap::new(
|
||
|
|
metrics.socket.clone(),
|
||
|
|
direct_addrs.addrs.watch(),
|
||
|
|
address_lookup.clone(),
|
||
|
|
shutdown_token.child_token(),
|
||
|
|
path_selector,
|
||
|
|
span.clone(),
|
||
|
|
)
|
||
|
|
};
|
||
|
|
|
||
|
|
let home_relay_watch = transports.home_relay_watch();
|
||
|
|
|
||
|
|
let sock = Arc::new(Socket {
|
||
|
|
remote_actors: remote_map.senders(),
|
||
|
|
shutdown: shutdown_state,
|
||
|
|
ipv6_reported,
|
||
|
|
mapped_addrs: remote_map.mapped_addrs.clone(),
|
||
|
|
address_lookup,
|
||
|
|
relay_map: relay_map.clone(),
|
||
|
|
address_lookup_user_data: RwLock::new(address_lookup_user_data),
|
||
|
|
configured_addrs: RwLock::new(configured_addrs),
|
||
|
|
direct_addrs,
|
||
|
|
net_report: Watchable::new((None, UpdateReason::None)),
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
dns_resolver: dns_resolver.clone(),
|
||
|
|
metrics: metrics.clone(),
|
||
|
|
local_addrs_watch: transports.local_addrs_watch(),
|
||
|
|
home_relay_watch,
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
ip_bind_addrs: transports.ip_bind_addrs(),
|
||
|
|
tls_config: tls_config.clone(),
|
||
|
|
hooks,
|
||
|
|
span: span.clone(),
|
||
|
|
});
|
||
|
|
|
||
|
|
let mut endpoint_config =
|
||
|
|
noq::EndpointConfig::new(Arc::new(Blake3HmacKey::new(&mut rand::rng())));
|
||
|
|
// Setting this to false means that noq will ignore packets that have the QUIC fixed bit
|
||
|
|
// set to 0. The fixed bit is the 3rd bit of the first byte of a packet.
|
||
|
|
// For performance reasons and to not rewrite buffers we pass non-QUIC UDP packets straight
|
||
|
|
// through to noq. We set the first byte of the packet to zero, which makes noq ignore
|
||
|
|
// the packet if grease_quic_bit is set to false.
|
||
|
|
endpoint_config.grease_quic_bit(false);
|
||
|
|
|
||
|
|
let local_addrs_watch = transports.local_addrs_watch();
|
||
|
|
let transports_network_change = transports.create_network_change_sender();
|
||
|
|
|
||
|
|
let runtime = Arc::new(Runtime::new(secret_key.public()));
|
||
|
|
|
||
|
|
let endpoint = noq::Endpoint::new_with_abstract_socket(
|
||
|
|
endpoint_config,
|
||
|
|
Some(server_config),
|
||
|
|
Box::new(Transport::new(sock.clone(), transports)),
|
||
|
|
runtime.clone(),
|
||
|
|
)
|
||
|
|
.map_err(|err| e!(BindError::CreateQuicEndpoint, err))?;
|
||
|
|
|
||
|
|
let network_monitor = netmon::Monitor::new()
|
||
|
|
.await
|
||
|
|
.map_err(|err| e!(BindError::CreateNetmonMonitor, anyerr!(err)))?;
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
let net_report_config = {
|
||
|
|
// Set a `QuicConfig` for address discovery (QAD), but only if we have IP transports.
|
||
|
|
//
|
||
|
|
// If there are no IP transports configured, then we don't set a QuicConfig.
|
||
|
|
// If we would, the `noq::Endpoint` passed along will not have IP connectivity,
|
||
|
|
// and the QAD probes that connect to the relay's QUIC endpoints would time out
|
||
|
|
// because all outgoing packets to IP destinations would be dropped.
|
||
|
|
let qad_config = has_ip_transports.then(|| QuicConfig {
|
||
|
|
ep: endpoint.clone(),
|
||
|
|
client_config: tls_config.clone(),
|
||
|
|
ipv4: true,
|
||
|
|
ipv6: has_ipv6_transport,
|
||
|
|
});
|
||
|
|
net_report::Options::new(tls_config.clone())
|
||
|
|
.quic_config(qad_config)
|
||
|
|
.proxy_url(proxy_url.clone())
|
||
|
|
.net_report_config(net_report_config)
|
||
|
|
};
|
||
|
|
|
||
|
|
#[cfg(wasm_browser)]
|
||
|
|
let net_report_config = net_report::Options::default().net_report_config(net_report_config);
|
||
|
|
|
||
|
|
let net_reporter = net_report::Client::new(
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
dns_resolver,
|
||
|
|
relay_map.clone(),
|
||
|
|
net_report_config,
|
||
|
|
metrics.net_report.clone(),
|
||
|
|
);
|
||
|
|
|
||
|
|
let (direct_addr_done_tx, direct_addr_done_rx) = mpsc::channel(8);
|
||
|
|
let direct_addr_update_state = DirectAddrUpdateState::new(
|
||
|
|
sock.clone(),
|
||
|
|
port_mapper,
|
||
|
|
Arc::new(AsyncMutex::new(net_reporter)),
|
||
|
|
relay_map,
|
||
|
|
direct_addr_done_tx,
|
||
|
|
sock.shutdown.at_close_start.child_token(),
|
||
|
|
);
|
||
|
|
|
||
|
|
let local_interfaces_watcher = network_monitor.interface_state();
|
||
|
|
|
||
|
|
#[cfg_attr(not(wasm_browser), allow(unused_mut))]
|
||
|
|
let mut actor = Actor {
|
||
|
|
endpoint: endpoint.clone(),
|
||
|
|
sock: sock.clone(),
|
||
|
|
remote_map,
|
||
|
|
periodic_re_stun_timer: new_re_stun_timer(false),
|
||
|
|
network_monitor,
|
||
|
|
local_interfaces_watcher,
|
||
|
|
direct_addr_update_state,
|
||
|
|
transports_network_change,
|
||
|
|
direct_addr_done_rx,
|
||
|
|
call_notify_quic_network_change: None,
|
||
|
|
};
|
||
|
|
// Initialize addresses
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
actor.update_direct_addresses(None);
|
||
|
|
|
||
|
|
let actor_task = task::spawn(
|
||
|
|
actor
|
||
|
|
.run(
|
||
|
|
actor_receiver,
|
||
|
|
shutdown_token.child_token(),
|
||
|
|
local_addrs_watch,
|
||
|
|
)
|
||
|
|
.instrument(info_span!(parent: span, "actor")),
|
||
|
|
);
|
||
|
|
|
||
|
|
let actor_task = Mutex::new(Some(AbortOnDropHandle::new(actor_task)));
|
||
|
|
|
||
|
|
Ok(EndpointInner {
|
||
|
|
sock,
|
||
|
|
actor_sender,
|
||
|
|
actor_task,
|
||
|
|
endpoint,
|
||
|
|
runtime,
|
||
|
|
static_config,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a reference to the underlying [`noq::Endpoint`].
|
||
|
|
pub(crate) fn noq_endpoint(&self) -> &noq::Endpoint {
|
||
|
|
&self.endpoint
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Closes the iroh endpoint.
|
||
|
|
///
|
||
|
|
/// Only the first close does anything. Any later closes return nil. Polling the socket
|
||
|
|
/// ([`noq::AsyncUdpSocket::poll_recv`]) will return [`Poll::Pending`] indefinitely
|
||
|
|
/// after this call.
|
||
|
|
///
|
||
|
|
/// [`Poll::Pending`]: std::task::Poll::Pending
|
||
|
|
#[instrument(skip_all, parent = self.sock.span.clone())]
|
||
|
|
pub(crate) async fn close(&self) {
|
||
|
|
if self.sock.is_closed() || self.sock.is_closing() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
trace!("socket closing...");
|
||
|
|
|
||
|
|
// Cancel at_close_start token, which cancels running netreports.
|
||
|
|
self.sock.shutdown.at_close_start.cancel();
|
||
|
|
|
||
|
|
// Remove address lookup services
|
||
|
|
self.sock.address_lookup().clear();
|
||
|
|
|
||
|
|
// Initiate closing all connections, and refuse future connections.
|
||
|
|
self.noq_endpoint().close(0u16.into(), b"");
|
||
|
|
|
||
|
|
// In the history of this code, this call had been
|
||
|
|
// - removed: https://github.com/n0-computer/iroh/pull/1753
|
||
|
|
// - then added back in: https://github.com/n0-computer/iroh/pull/2227/files#diff-ba27e40e2986a3919b20f6b412ad4fe63154af648610ea5d9ed0b5d5b0e2d780R573
|
||
|
|
// - then removed again: https://github.com/n0-computer/iroh/pull/3165
|
||
|
|
// and finally added back in together with this comment.
|
||
|
|
// So before removing this call, please consider carefully.
|
||
|
|
// Among other things, this call tries its best to make sure that any queued close frames
|
||
|
|
// (e.g. via the call to `endpoint.close(...)` above), are flushed out to the sockets
|
||
|
|
// *and acknowledged* (or time out with the "probe timeout" of usually 3 seconds).
|
||
|
|
// This allows the other endpoints for these connections to be notified to release
|
||
|
|
// their resources, or - depending on the protocol - that all data was received.
|
||
|
|
// With the current noq API, this is the only way to ensure protocol code can use
|
||
|
|
// connection close codes, and close the endpoint properly.
|
||
|
|
// If this call is skipped, then connections that protocols close just shortly before the
|
||
|
|
// call to `Endpoint::close` will in most cases cause connection time-outs on remote ends.
|
||
|
|
trace!("wait_all_draining start");
|
||
|
|
self.noq_endpoint().wait_all_draining().await;
|
||
|
|
trace!("wait_all_draining done");
|
||
|
|
|
||
|
|
// Start cancellation of all actors.
|
||
|
|
self.sock.shutdown.at_endpoint_closed.cancel();
|
||
|
|
|
||
|
|
// MutexGuard is not held across await points
|
||
|
|
let task = self.actor_task.lock().expect("poisoned").take();
|
||
|
|
if let Some(task) = task {
|
||
|
|
// give the tasks a moment to shutdown cleanly
|
||
|
|
let shutdown_done = time::timeout(Duration::from_millis(100), async move {
|
||
|
|
if let Err(err) = task.await {
|
||
|
|
warn!("unexpected error in task shutdown: {:?}", err);
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
match shutdown_done {
|
||
|
|
Ok(_) => trace!("tasks finished in time, shutdown complete"),
|
||
|
|
Err(time::Elapsed { .. }) => {
|
||
|
|
// Dropping the task will abort it
|
||
|
|
warn!("tasks didn't finish in time, aborting");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Waits for the EndpointDriver and all ConnectionDrivers to shut down
|
||
|
|
// Expects that the `noq::Endpoint` has been closed before this call,
|
||
|
|
// otherwise, the runtime will never shutdown.
|
||
|
|
self.runtime.shutdown().await;
|
||
|
|
|
||
|
|
self.sock.shutdown.closed.store(true, Ordering::SeqCst);
|
||
|
|
|
||
|
|
trace!("socket closed");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Aborts the endpoint ungracefully:
|
||
|
|
///
|
||
|
|
/// - Calls cancellation token that stops running net reports
|
||
|
|
/// - Removes all address lookup services
|
||
|
|
/// - Calls cancellation token that stops all the Socket actors
|
||
|
|
/// - Aborts the runtime
|
||
|
|
/// - Drops the actor task
|
||
|
|
/// - Sets the `Socket::is_closed` state to true
|
||
|
|
///
|
||
|
|
/// This does not wait for any current connections or tasks to close gracefully.
|
||
|
|
///
|
||
|
|
/// This should only be called in the `iroh::Endpoint` `Drop` impl when the
|
||
|
|
/// `iroh::Endpoint` is dropped without first calling `Endpoint::close`.
|
||
|
|
#[instrument(skip_all, parent = self.sock.span.clone())]
|
||
|
|
pub(crate) fn abort(&self) {
|
||
|
|
if self.sock.is_closed() || self.sock.is_closing() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
trace!("socket aborting...");
|
||
|
|
|
||
|
|
// Cancel at_close_start token, which cancels running netreports.
|
||
|
|
self.sock.shutdown.at_close_start.cancel();
|
||
|
|
|
||
|
|
self.sock.address_lookup().clear();
|
||
|
|
|
||
|
|
// Cancel all actors.
|
||
|
|
self.sock.shutdown.at_endpoint_closed.cancel();
|
||
|
|
|
||
|
|
// Aborts all tasks, not waiting for any to close gracefully.
|
||
|
|
self.runtime.abort();
|
||
|
|
|
||
|
|
self.actor_task.lock().expect("poisoned").take();
|
||
|
|
|
||
|
|
self.sock.shutdown.closed.store(true, Ordering::SeqCst);
|
||
|
|
trace!("socket closed");
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) async fn insert_relay(
|
||
|
|
&self,
|
||
|
|
relay: RelayUrl,
|
||
|
|
endpoint: Arc<RelayConfig>,
|
||
|
|
) -> Option<Arc<RelayConfig>> {
|
||
|
|
let res = self.relay_map.insert(relay, endpoint);
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::RelayMapChange)
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
res
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) async fn remove_relay(&self, relay: &RelayUrl) -> Option<Arc<RelayConfig>> {
|
||
|
|
let res = self.relay_map.remove(relay);
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::RelayMapChange)
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
res
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Adds an external address to advertise to peers.
|
||
|
|
pub(crate) async fn add_external_addr(&self, addr: SocketAddr) {
|
||
|
|
self.sock
|
||
|
|
.configured_addrs
|
||
|
|
.write()
|
||
|
|
.expect("poisoned")
|
||
|
|
.insert(addr);
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::DirectAddrRefresh)
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Removes a configured external address. Returns `true` if it was present.
|
||
|
|
pub(crate) async fn remove_external_addr(&self, addr: &SocketAddr) -> bool {
|
||
|
|
let removed = self
|
||
|
|
.sock
|
||
|
|
.configured_addrs
|
||
|
|
.write()
|
||
|
|
.expect("poisoned")
|
||
|
|
.remove(addr);
|
||
|
|
if removed {
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::DirectAddrRefresh)
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
}
|
||
|
|
removed
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Call to notify the system of potential network changes.
|
||
|
|
pub(crate) async fn network_change(&self) {
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::NetworkChange)
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(all(test, with_crypto_provider))]
|
||
|
|
async fn force_network_change(&self, is_major: bool) {
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::ForceNetworkChange(is_major))
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Resolves an [`EndpointAddr`] to an [`EndpointIdMappedAddr`] to connect to via [`EndpointInner::endpoint`].
|
||
|
|
///
|
||
|
|
/// This starts a `RemoteStateActor` for the remote if not running already, and then checks
|
||
|
|
/// if the actor has any known paths to the remote. If not, it starts address lookup and waits for
|
||
|
|
/// at least one result to arrive.
|
||
|
|
///
|
||
|
|
/// Returns `Ok(Ok(EndpointIdMappedAddr))` if there is a known path or Address Lookup produced
|
||
|
|
/// at least one result. This does not mean there is a working path, only that we have at least
|
||
|
|
/// one transport address we can try to connect to.
|
||
|
|
///
|
||
|
|
/// Returns `Ok(Err(address_lookup_error))` if there are no known paths to the remote and Address Lookup
|
||
|
|
/// failed or produced no results. This means that we don't have any transport address for
|
||
|
|
/// the remote, thus there is no point in trying to connect over the noq endpoint.
|
||
|
|
///
|
||
|
|
/// Returns `Err(RemoteStateActorStoppedError)` if the `RemoteStateActor` for the remote has stopped,
|
||
|
|
/// which may never happen and thus is a bug if it does.
|
||
|
|
pub(crate) async fn resolve_remote(
|
||
|
|
&self,
|
||
|
|
addr: EndpointAddr,
|
||
|
|
) -> Result<Result<EndpointIdMappedAddr, AddressLookupFailed>, RemoteStateActorStoppedError>
|
||
|
|
{
|
||
|
|
let (tx, rx) = oneshot::channel();
|
||
|
|
let remote_id = addr.id;
|
||
|
|
self.actor_sender
|
||
|
|
.send(ActorMessage::ResolveRemote(addr, tx))
|
||
|
|
.await
|
||
|
|
.ok();
|
||
|
|
let reply = rx.await.map_err(|_| RemoteStateActorStoppedError::new())?;
|
||
|
|
match reply {
|
||
|
|
Ok(()) => Ok(Ok(self.mapped_addrs.endpoint_addrs.get(&remote_id))),
|
||
|
|
Err(err) => Ok(Err(err)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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<RemoteInfo> {
|
||
|
|
let (tx, rx) = oneshot::channel();
|
||
|
|
self.remote_actors
|
||
|
|
.get(&id)?
|
||
|
|
.send(RemoteStateMessage::RemoteInfo(tx))
|
||
|
|
.await
|
||
|
|
.ok()?;
|
||
|
|
rx.await.ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Registers the connection in the `RemoteStateActor`.
|
||
|
|
///
|
||
|
|
/// The actor is responsible for holepunching and opening additional paths to this
|
||
|
|
/// connection.
|
||
|
|
///
|
||
|
|
/// Returns a future that resolves to a [`PathStateReceiver`] for the new connection.
|
||
|
|
///
|
||
|
|
/// The returned future is `'static`, so it can be stored without being lifetime-bound to `&self`.
|
||
|
|
pub(crate) fn register_connection(
|
||
|
|
&self,
|
||
|
|
remote: EndpointId,
|
||
|
|
conn: noq::Connection,
|
||
|
|
) -> impl Future<Output = Result<PathStateReceiver, RemoteStateActorStoppedError>> + Send + 'static
|
||
|
|
{
|
||
|
|
let (tx, rx) = oneshot::channel();
|
||
|
|
let sender = self.actor_sender.clone();
|
||
|
|
async move {
|
||
|
|
sender
|
||
|
|
.send(ActorMessage::AddConnection(remote, conn, tx))
|
||
|
|
.await
|
||
|
|
.map_err(|_| RemoteStateActorStoppedError::new())?;
|
||
|
|
rx.await.map_err(|_| RemoteStateActorStoppedError::new())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(derive_more::Debug)]
|
||
|
|
#[allow(clippy::enum_variant_names)]
|
||
|
|
enum ActorMessage {
|
||
|
|
NetworkChange,
|
||
|
|
RelayMapChange,
|
||
|
|
#[debug("ResolveRemote(..)")]
|
||
|
|
ResolveRemote(
|
||
|
|
EndpointAddr,
|
||
|
|
oneshot::Sender<Result<(), AddressLookupFailed>>,
|
||
|
|
),
|
||
|
|
#[debug("AddConnection(..)")]
|
||
|
|
AddConnection(
|
||
|
|
EndpointId,
|
||
|
|
noq::Connection,
|
||
|
|
oneshot::Sender<PathStateReceiver>,
|
||
|
|
),
|
||
|
|
/// Re-evaluate direct addresses, e.g. after configured external addresses changed.
|
||
|
|
DirectAddrRefresh,
|
||
|
|
#[cfg(all(test, with_crypto_provider))]
|
||
|
|
ForceNetworkChange(bool),
|
||
|
|
}
|
||
|
|
|
||
|
|
/// State for polling until a default route is available after a network change.
|
||
|
|
///
|
||
|
|
/// When a network change is detected but no default route exists yet (e.g.,
|
||
|
|
/// interface just came up but gateway not assigned), we poll with exponential
|
||
|
|
/// backoff until the gateway appears. This avoids the fixed 2s delay that was
|
||
|
|
/// too slow for interface recovery scenarios.
|
||
|
|
struct PendingNetworkChangeNotify {
|
||
|
|
/// Next time to check for default route.
|
||
|
|
next_check: Instant,
|
||
|
|
/// Current backoff interval.
|
||
|
|
interval: Duration,
|
||
|
|
/// Whether this was a major change.
|
||
|
|
is_major: bool,
|
||
|
|
/// When we started polling (to enforce a max wait).
|
||
|
|
started: Instant,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PendingNetworkChangeNotify {
|
||
|
|
const INITIAL_INTERVAL: Duration = Duration::from_millis(100);
|
||
|
|
const MAX_INTERVAL: Duration = Duration::from_secs(1);
|
||
|
|
const MAX_WAIT: Duration = Duration::from_secs(5);
|
||
|
|
|
||
|
|
fn new(is_major: bool) -> Self {
|
||
|
|
Self {
|
||
|
|
next_check: Instant::now() + Self::INITIAL_INTERVAL,
|
||
|
|
interval: Self::INITIAL_INTERVAL,
|
||
|
|
is_major,
|
||
|
|
started: Instant::now(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Advance to the next check interval (exponential backoff, capped).
|
||
|
|
fn advance(&mut self) {
|
||
|
|
self.interval = (self.interval * 2).min(Self::MAX_INTERVAL);
|
||
|
|
self.next_check = Instant::now() + self.interval;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether we've exceeded the maximum wait time.
|
||
|
|
fn expired(&self) -> bool {
|
||
|
|
self.started.elapsed() >= Self::MAX_WAIT
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct Actor {
|
||
|
|
/// A clone of the quinn Endpoint.
|
||
|
|
///
|
||
|
|
/// The task of this actor is currently owned by the [`crate::Endpoint`] and wrapped in
|
||
|
|
/// an [`AbortOnDropHandle`]. When [`crate::Endpoint::close`] is called various
|
||
|
|
/// subsystems are being stopped. Then, when [`ShutdownState::at_endpoint_closed`] is
|
||
|
|
/// called by [`crate::Endpoint::close`], this actor itself is stopped via it's
|
||
|
|
/// [`CancellationToken`] and we will drop this clone of the endpoint. The endpoint is
|
||
|
|
/// then finally dropped when the [`crate::Endpoint`] itself is dropped.
|
||
|
|
///
|
||
|
|
/// All of this to say: keeping the quinn endpoint alive here does not impact the
|
||
|
|
/// lifetime of it since it's lifetime is shorter than that one that's stored in the
|
||
|
|
/// [`crate::Endpoint`].
|
||
|
|
endpoint: noq::Endpoint,
|
||
|
|
/// Shared state between an awful lot of iroh subsystems.
|
||
|
|
///
|
||
|
|
/// In particular both the [`EndpointInner`] as well as this actor itself have a
|
||
|
|
/// copy. But also other subsystems that consequently have access to way to much state.
|
||
|
|
sock: Arc<Socket>,
|
||
|
|
/// Tracks the networkmap endpoint entity for each endpoint discovery key.
|
||
|
|
remote_map: RemoteMap,
|
||
|
|
/// When set, is an AfterFunc timer that will call Socket::do_periodic_stun.
|
||
|
|
periodic_re_stun_timer: time::Interval,
|
||
|
|
/// An actor watching the local network interfaces.
|
||
|
|
///
|
||
|
|
/// The monitored changes are emitted via [`Self::local_interfaces_watcher`].
|
||
|
|
network_monitor: netmon::Monitor,
|
||
|
|
/// Watcher for changes to the local network interfaces, IP addresses and routes.
|
||
|
|
local_interfaces_watcher: n0_watcher::Direct<netmon::State>,
|
||
|
|
transports_network_change: transports::NetworkChangeSender,
|
||
|
|
/// Indicates the direct addr update state.
|
||
|
|
direct_addr_update_state: DirectAddrUpdateState,
|
||
|
|
direct_addr_done_rx: mpsc::Receiver<()>,
|
||
|
|
/// Polling state for [`Actor::notify_quic_network_change`].
|
||
|
|
///
|
||
|
|
/// When a network change is detected but no default route is available yet,
|
||
|
|
/// we poll with exponential backoff (100ms, 200ms, 400ms, 800ms, 1s, 1s, ...)
|
||
|
|
/// until the gateway appears. Once it does, we notify immediately.
|
||
|
|
/// After 5s total we notify anyway even without a gateway.
|
||
|
|
call_notify_quic_network_change: Option<PendingNetworkChangeNotify>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Actor {
|
||
|
|
async fn run(
|
||
|
|
mut self,
|
||
|
|
mut msg_receiver: mpsc::Receiver<ActorMessage>,
|
||
|
|
shutdown_token: CancellationToken,
|
||
|
|
mut local_addrs_watcher: impl Watcher<Value = Vec<transports::Addr>> + Send + Sync,
|
||
|
|
) {
|
||
|
|
// Setup network monitoring
|
||
|
|
let mut current_netmon_state = self.local_interfaces_watcher.get();
|
||
|
|
|
||
|
|
let mut portmap_watcher = self
|
||
|
|
.direct_addr_update_state
|
||
|
|
.port_mapper
|
||
|
|
.watch_external_address();
|
||
|
|
|
||
|
|
let mut receiver_closed = false;
|
||
|
|
let mut portmap_watcher_closed = false;
|
||
|
|
|
||
|
|
let mut net_report_watcher = self.sock.net_report.watch();
|
||
|
|
|
||
|
|
// ensure we are doing an initial publish of our addresses
|
||
|
|
self.sock.publish_my_addr();
|
||
|
|
|
||
|
|
while !shutdown_token.is_cancelled() {
|
||
|
|
self.sock.metrics.socket.actor_tick_main.inc();
|
||
|
|
let portmap_watcher_changed = portmap_watcher.changed();
|
||
|
|
|
||
|
|
let notify_quic_network_change = match &self.call_notify_quic_network_change {
|
||
|
|
Some(pending) => {
|
||
|
|
MaybeFuture::Some(n0_future::time::sleep_until(pending.next_check))
|
||
|
|
}
|
||
|
|
None => MaybeFuture::None,
|
||
|
|
};
|
||
|
|
n0_future::pin!(notify_quic_network_change);
|
||
|
|
|
||
|
|
tokio::select! {
|
||
|
|
_ = shutdown_token.cancelled() => {
|
||
|
|
debug!("tick: shutting down");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
msg = msg_receiver.recv(), if !receiver_closed => {
|
||
|
|
let Some(msg) = msg else {
|
||
|
|
trace!("tick: socket receiver closed");
|
||
|
|
self.sock.metrics.socket.actor_tick_other.inc();
|
||
|
|
receiver_closed = true;
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
|
||
|
|
trace!(?msg, "tick: msg");
|
||
|
|
self.sock.metrics.socket.actor_tick_msg.inc();
|
||
|
|
self.handle_actor_message(msg).await;
|
||
|
|
}
|
||
|
|
tick = self.periodic_re_stun_timer.tick() => {
|
||
|
|
trace!("tick: re_stun {:?}", tick);
|
||
|
|
self.sock.metrics.socket.actor_tick_re_stun.inc();
|
||
|
|
self.re_stun(UpdateReason::Periodic);
|
||
|
|
}
|
||
|
|
new_addr = local_addrs_watcher.updated() => {
|
||
|
|
match new_addr {
|
||
|
|
Ok(addrs) => {
|
||
|
|
if !addrs.is_empty() {
|
||
|
|
trace!(?addrs, "local addrs");
|
||
|
|
self.sock.publish_my_addr();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(_) => {
|
||
|
|
warn!("local addr watcher stopped");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
report = net_report_watcher.updated() => {
|
||
|
|
match report {
|
||
|
|
Ok((report, _)) => {
|
||
|
|
self.handle_net_report_report(report);
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
{
|
||
|
|
self.periodic_re_stun_timer = new_re_stun_timer(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(_) => {
|
||
|
|
warn!("net report watcher stopped");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
reason = self.direct_addr_done_rx.recv() => {
|
||
|
|
match reason {
|
||
|
|
Some(()) => {
|
||
|
|
// check if a new run needs to be scheduled
|
||
|
|
let state = self.local_interfaces_watcher.get();
|
||
|
|
self.direct_addr_update_state.try_run(state.into());
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
warn!("direct addr watcher died");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
change = portmap_watcher_changed, if !portmap_watcher_closed => {
|
||
|
|
if change.is_err() {
|
||
|
|
trace!("tick: portmap watcher closed");
|
||
|
|
self.sock.metrics.socket.actor_tick_other.inc();
|
||
|
|
|
||
|
|
portmap_watcher_closed = true;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
trace!("tick: portmap changed");
|
||
|
|
self.sock.metrics.socket.actor_tick_portmap_changed.inc();
|
||
|
|
let new_external_address = *portmap_watcher.borrow();
|
||
|
|
if new_external_address.is_some() {
|
||
|
|
self.sock.metrics.net_report.portmap_external_address_updated.inc();
|
||
|
|
}
|
||
|
|
debug!("external address updated: {new_external_address:?}");
|
||
|
|
self.re_stun(UpdateReason::PortmapUpdated);
|
||
|
|
},
|
||
|
|
state = self.local_interfaces_watcher.updated() => {
|
||
|
|
let Ok(state) = state else {
|
||
|
|
trace!("tick: link change receiver closed");
|
||
|
|
self.sock.metrics.socket.actor_tick_other.inc();
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let is_major = state.is_major_change(¤t_netmon_state);
|
||
|
|
event!(
|
||
|
|
target: "iroh::_events::link_change",
|
||
|
|
Level::DEBUG,
|
||
|
|
?state,
|
||
|
|
is_major
|
||
|
|
);
|
||
|
|
current_netmon_state = state;
|
||
|
|
self.sock.metrics.socket.actor_link_change.inc();
|
||
|
|
self.handle_network_change(is_major);
|
||
|
|
}
|
||
|
|
_remote_id = self.remote_map.cleanup() => {},
|
||
|
|
_ = &mut notify_quic_network_change => {
|
||
|
|
let has_network = self.has_usable_network();
|
||
|
|
let Some(pending) = self.call_notify_quic_network_change.as_mut() else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
if has_network || pending.expired() {
|
||
|
|
// Gateway appeared or we've waited long enough, notify now.
|
||
|
|
let is_major = pending.is_major;
|
||
|
|
self.call_notify_quic_network_change = None;
|
||
|
|
self.notify_quic_network_change(is_major);
|
||
|
|
} else {
|
||
|
|
// No gateway yet, back off and try again.
|
||
|
|
trace!(
|
||
|
|
interval = ?pending.interval,
|
||
|
|
elapsed = ?pending.started.elapsed(),
|
||
|
|
"no default route yet, retrying"
|
||
|
|
);
|
||
|
|
pending.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else => {
|
||
|
|
trace!("tick: else");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the local network has a default route and at least one IP address.
|
||
|
|
fn has_usable_network(&mut self) -> bool {
|
||
|
|
#[cfg(target_family = "wasm")]
|
||
|
|
{
|
||
|
|
true
|
||
|
|
}
|
||
|
|
#[cfg(not(target_family = "wasm"))]
|
||
|
|
{
|
||
|
|
let interfaces = self.local_interfaces_watcher.get();
|
||
|
|
interfaces.default_route_interface.is_some()
|
||
|
|
&& (interfaces.have_v4 || interfaces.have_v6)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Handles a change detected in the local network conditions.
|
||
|
|
///
|
||
|
|
/// This is triggered when the netmon actor detects a change in the local network
|
||
|
|
/// interfaces, assigned IP addresses and routes.
|
||
|
|
fn handle_network_change(&mut self, is_major: bool) {
|
||
|
|
debug!(is_major, "link change detected");
|
||
|
|
|
||
|
|
if is_major {
|
||
|
|
if let Err(err) = self.transports_network_change.rebind() {
|
||
|
|
warn!("failed to rebind transports: {err:?}");
|
||
|
|
}
|
||
|
|
self.transports_network_change.check_relay_connection();
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
self.sock.dns_resolver.reset();
|
||
|
|
self.re_stun(UpdateReason::LinkChangeMajor);
|
||
|
|
} else {
|
||
|
|
self.re_stun(UpdateReason::LinkChangeMinor);
|
||
|
|
}
|
||
|
|
|
||
|
|
if self.has_usable_network() {
|
||
|
|
// This is considered a usable network change, propagate it to the QUIC stack
|
||
|
|
// right away.
|
||
|
|
self.call_notify_quic_network_change = None;
|
||
|
|
self.notify_quic_network_change(is_major);
|
||
|
|
} else {
|
||
|
|
// No default route yet (e.g., interface just came up but gateway not
|
||
|
|
// assigned). Poll with exponential backoff until the gateway appears.
|
||
|
|
match &mut self.call_notify_quic_network_change {
|
||
|
|
Some(pending) => {
|
||
|
|
// Update is_major if this change is more severe.
|
||
|
|
pending.is_major |= is_major;
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
self.call_notify_quic_network_change =
|
||
|
|
Some(PendingNetworkChangeNotify::new(is_major));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Notifies the QUIC stack of the network change we observed.
|
||
|
|
///
|
||
|
|
/// This is decoupled from receiving the network change, because we try to debounce
|
||
|
|
/// network changes as they often arrive in groups.
|
||
|
|
fn notify_quic_network_change(&mut self, is_major: bool) {
|
||
|
|
#[derive(Debug)]
|
||
|
|
struct Hint {
|
||
|
|
local_addrs: FxHashSet<IpAddr>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl NetworkChangeHint for Hint {
|
||
|
|
fn is_path_recoverable(
|
||
|
|
&self,
|
||
|
|
_path_id: noq::PathId,
|
||
|
|
network_path: noq_proto::FourTuple,
|
||
|
|
) -> bool {
|
||
|
|
match MultipathMappedAddr::from(network_path.remote()) {
|
||
|
|
MultipathMappedAddr::Mixed(_) => {
|
||
|
|
// This address is only ever used to send an Initial packet to, it
|
||
|
|
// should never appear as an established path.
|
||
|
|
error!("A mixed address can not be used for network changes");
|
||
|
|
false
|
||
|
|
}
|
||
|
|
MultipathMappedAddr::Relay(_) => {
|
||
|
|
// We pretend the relay path is never affected by link changes. The
|
||
|
|
// relay actor transparently reconnects and the addresses never
|
||
|
|
// change.
|
||
|
|
true
|
||
|
|
}
|
||
|
|
MultipathMappedAddr::Ip(_) => {
|
||
|
|
// If we no longer have a valid interface to send from for a local
|
||
|
|
// IP then it can not be recovered.
|
||
|
|
match network_path.local_ip() {
|
||
|
|
Some(local_ip) => self.local_addrs.contains(&local_ip),
|
||
|
|
None => true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
MultipathMappedAddr::Custom(_) => {
|
||
|
|
// Assume it is unrecoverable for now
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let hint = Hint {
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
local_addrs: {
|
||
|
|
let interfaces = self.local_interfaces_watcher.get();
|
||
|
|
interfaces
|
||
|
|
.local_addresses
|
||
|
|
.regular
|
||
|
|
.iter()
|
||
|
|
.chain(interfaces.local_addresses.loopback.iter())
|
||
|
|
.copied()
|
||
|
|
.collect()
|
||
|
|
},
|
||
|
|
#[cfg(wasm_browser)]
|
||
|
|
local_addrs: Default::default(),
|
||
|
|
};
|
||
|
|
|
||
|
|
self.endpoint.handle_network_change(Some(Arc::new(hint)));
|
||
|
|
self.remote_map.on_network_change(is_major);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn handle_relay_map_change(&mut self) {
|
||
|
|
self.re_stun(UpdateReason::RelayMapChange);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn re_stun(&mut self, why: UpdateReason) {
|
||
|
|
let state = self.local_interfaces_watcher.get();
|
||
|
|
self.direct_addr_update_state
|
||
|
|
.schedule_run(why, state.into());
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Processes an incoming actor message.
|
||
|
|
///
|
||
|
|
/// Returns `true` if it was a shutdown.
|
||
|
|
async fn handle_actor_message(&mut self, msg: ActorMessage) {
|
||
|
|
match msg {
|
||
|
|
ActorMessage::NetworkChange => {
|
||
|
|
self.network_monitor.network_change().await.ok();
|
||
|
|
}
|
||
|
|
ActorMessage::RelayMapChange => {
|
||
|
|
self.handle_relay_map_change();
|
||
|
|
}
|
||
|
|
ActorMessage::ResolveRemote(addr, tx) => {
|
||
|
|
self.remote_map.resolve_remote(addr, tx).await;
|
||
|
|
}
|
||
|
|
ActorMessage::AddConnection(remote, conn, tx) => {
|
||
|
|
self.remote_map.add_connection(remote, conn, tx).await;
|
||
|
|
}
|
||
|
|
ActorMessage::DirectAddrRefresh => {
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
{
|
||
|
|
let (report, _reason) = self.sock.net_report.get();
|
||
|
|
self.update_direct_addresses(report.as_ref());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#[cfg(all(test, with_crypto_provider))]
|
||
|
|
ActorMessage::ForceNetworkChange(is_major) => {
|
||
|
|
self.handle_network_change(is_major);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Updates the direct addresses of this socket.
|
||
|
|
///
|
||
|
|
/// Updates the [`DiscoveredDirectAddrs`] of this [`Socket`] with the current set of
|
||
|
|
/// direct addresses from:
|
||
|
|
///
|
||
|
|
/// - The portmapper.
|
||
|
|
/// - A net_report report.
|
||
|
|
/// - The local interfaces IP addresses.
|
||
|
|
/// - User configured addresses.
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
fn update_direct_addresses(&mut self, net_report_report: Option<&net_report::Report>) {
|
||
|
|
// We only want to have one DirectAddr for each SocketAddr we have. So we store
|
||
|
|
// this as a map of SocketAddr -> DirectAddrType. At the end we will construct a
|
||
|
|
// DirectAddr from each entry.
|
||
|
|
let mut addrs: BTreeMap<SocketAddr, (DirectAddrType, Option<Ipv6AddrFlags>)> =
|
||
|
|
BTreeMap::new();
|
||
|
|
|
||
|
|
// First add PortMapper provided addresses.
|
||
|
|
let portmap_watcher = self
|
||
|
|
.direct_addr_update_state
|
||
|
|
.port_mapper
|
||
|
|
.watch_external_address();
|
||
|
|
let maybe_port_mapped = *portmap_watcher.borrow();
|
||
|
|
if let Some(portmap_ext) = maybe_port_mapped.map(SocketAddr::V4) {
|
||
|
|
addrs
|
||
|
|
.entry(portmap_ext)
|
||
|
|
.or_insert((DirectAddrType::Portmapped, None));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Next add STUN addresses from the net_report report.
|
||
|
|
if let Some(net_report_report) = net_report_report {
|
||
|
|
if let Some(global_v4) = net_report_report.global_v4 {
|
||
|
|
addrs
|
||
|
|
.entry(global_v4.into())
|
||
|
|
.or_insert((DirectAddrType::Qad, None));
|
||
|
|
|
||
|
|
// If they're behind a hard NAT and are using a fixed
|
||
|
|
// port locally, assume they might've added a static
|
||
|
|
// port mapping on their router to the same explicit
|
||
|
|
// port that we are running with. Worst case it's an invalid candidate mapping.
|
||
|
|
let port = self.sock.ip_bind_addrs().iter().find_map(|addr| {
|
||
|
|
if addr.port() != 0 {
|
||
|
|
Some(addr.port())
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if let Some(port) = port
|
||
|
|
&& net_report_report
|
||
|
|
.mapping_varies_by_dest()
|
||
|
|
.unwrap_or_default()
|
||
|
|
{
|
||
|
|
let mut addr = global_v4;
|
||
|
|
addr.set_port(port);
|
||
|
|
addrs
|
||
|
|
.entry(addr.into())
|
||
|
|
.or_insert((DirectAddrType::Qad4LocalPort, None));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if let Some(global_v6) = net_report_report.global_v6 {
|
||
|
|
addrs
|
||
|
|
.entry(global_v6.into())
|
||
|
|
.or_insert((DirectAddrType::Qad, None));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
self.collect_local_addresses(&mut addrs);
|
||
|
|
|
||
|
|
// Add configured external addresses.
|
||
|
|
for addr in self.sock.configured_addrs.read().expect("poisoned").iter() {
|
||
|
|
addrs.entry(*addr).or_insert((DirectAddrType::Config, None));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Finally create and store store all these direct addresses
|
||
|
|
let stored_addrs = addrs
|
||
|
|
.into_iter()
|
||
|
|
.filter_map(|(addr, (typ, flags))| {
|
||
|
|
// Filter out deprecated IPs
|
||
|
|
let is_deprecated = flags.map(|f| f.deprecated).unwrap_or(false);
|
||
|
|
if is_deprecated {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(DirectAddr { addr, typ })
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
self.sock.store_direct_addresses(stored_addrs);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
fn collect_local_addresses(
|
||
|
|
&mut self,
|
||
|
|
addrs: &mut BTreeMap<SocketAddr, (DirectAddrType, Option<Ipv6AddrFlags>)>,
|
||
|
|
) {
|
||
|
|
let netmon_state = self.local_interfaces_watcher.get();
|
||
|
|
|
||
|
|
// Matches the addresses that have been bound vs the requested ones.
|
||
|
|
let local_addrs: Vec<(SocketAddr, SocketAddr)> = self
|
||
|
|
.sock
|
||
|
|
.ip_bind_addrs()
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.zip(self.sock.ip_local_addrs())
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
// Do we listen on any IPv4 unspecified address?
|
||
|
|
let has_ipv4_unspecified = local_addrs.iter().find_map(|(_, a)| {
|
||
|
|
if a.is_ipv4() && a.ip().is_unspecified() {
|
||
|
|
Some(a.port())
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
});
|
||
|
|
// Do we listen on any IPv6 unspecified address?
|
||
|
|
let has_ipv6_unspecified = local_addrs.iter().find_map(|(_, a)| {
|
||
|
|
if a.is_ipv6() && a.ip().is_unspecified() {
|
||
|
|
Some(a.port())
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// If a socket is bound to the unspecified address, create SocketAddrs for
|
||
|
|
// each local IP address by pairing it with the port the socket is bound on.
|
||
|
|
if local_addrs
|
||
|
|
.iter()
|
||
|
|
.any(|(_, local)| local.ip().is_unspecified())
|
||
|
|
{
|
||
|
|
let LocalAddresses {
|
||
|
|
regular: mut ips,
|
||
|
|
loopback,
|
||
|
|
} = self.local_interfaces_watcher.get().local_addresses;
|
||
|
|
if ips.is_empty() && addrs.is_empty() {
|
||
|
|
// Include loopback addresses only if there are no other interfaces
|
||
|
|
// or public addresses, this allows testing offline.
|
||
|
|
ips = loopback;
|
||
|
|
}
|
||
|
|
|
||
|
|
for ip in ips {
|
||
|
|
let port_if_unspecified = match ip {
|
||
|
|
IpAddr::V4(_) => has_ipv4_unspecified,
|
||
|
|
IpAddr::V6(_) => has_ipv6_unspecified,
|
||
|
|
};
|
||
|
|
if let Some(port) = port_if_unspecified {
|
||
|
|
let addr = SocketAddr::new(ip, port);
|
||
|
|
let flags = find_flags(&netmon_state, ip);
|
||
|
|
addrs.entry(addr).or_insert((DirectAddrType::Local, flags));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If a socket is bound to a specific address, add it.
|
||
|
|
for (bound, local) in local_addrs {
|
||
|
|
if !bound.ip().is_unspecified() {
|
||
|
|
let flags = find_flags(&netmon_state, local.ip());
|
||
|
|
addrs.entry(local).or_insert((DirectAddrType::Local, flags));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn handle_net_report_report(&mut self, mut report: Option<net_report::Report>) {
|
||
|
|
if let Some(ref mut r) = report {
|
||
|
|
self.sock.ipv6_reported.store(r.udp_v6, Ordering::Relaxed);
|
||
|
|
if r.preferred_relay.is_none()
|
||
|
|
&& let Some(my_relay) = self.sock.my_relay()
|
||
|
|
{
|
||
|
|
r.preferred_relay.replace(my_relay);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Notify all transports
|
||
|
|
self.transports_network_change.on_network_change(r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
self.update_direct_addresses(report.as_ref());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(not(wasm_browser))]
|
||
|
|
fn find_flags(state: &netmon::State, ip: IpAddr) -> Option<Ipv6AddrFlags> {
|
||
|
|
if ip.is_ipv6() {
|
||
|
|
state
|
||
|
|
.interfaces
|
||
|
|
.values()
|
||
|
|
.flat_map(|i| i.addrs())
|
||
|
|
.find_map(|addr| match addr {
|
||
|
|
IpNet::V4(_) => None,
|
||
|
|
IpNet::V6 { net, flags, .. } => {
|
||
|
|
if net.addr() == ip {
|
||
|
|
Some(flags)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn new_re_stun_timer(initial_delay: bool) -> time::Interval {
|
||
|
|
// Pick a random duration between 20 and 26 seconds (just under 30s,
|
||
|
|
// a common UDP NAT timeout on Linux,etc)
|
||
|
|
let mut rng = rand::rng();
|
||
|
|
let d: Duration = rng.random_range(Duration::from_secs(20)..=Duration::from_secs(26));
|
||
|
|
if initial_delay {
|
||
|
|
debug!("scheduling periodic_stun to run in {}s", d.as_secs());
|
||
|
|
time::interval_at(time::Instant::now() + d, d)
|
||
|
|
} else {
|
||
|
|
debug!(
|
||
|
|
"scheduling periodic_stun to run immediately and in {}s",
|
||
|
|
d.as_secs()
|
||
|
|
);
|
||
|
|
time::interval(d)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The discovered direct addresses of this [`Socket`].
|
||
|
|
///
|
||
|
|
/// These are all the [`DirectAddr`]s that this [`Socket`] is aware of for itself.
|
||
|
|
/// They include all locally bound ones as well as those discovered by other mechanisms like
|
||
|
|
/// QAD.
|
||
|
|
#[derive(derive_more::Debug, Clone, Default)]
|
||
|
|
struct DiscoveredDirectAddrs {
|
||
|
|
/// The last set of discovered direct addresses.
|
||
|
|
addrs: Watchable<BTreeSet<DirectAddr>>,
|
||
|
|
|
||
|
|
/// The last time the direct addresses were updated, even if there was no change.
|
||
|
|
///
|
||
|
|
/// This is only ever None at startup.
|
||
|
|
updated_at: Arc<RwLock<Option<Instant>>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl DiscoveredDirectAddrs {
|
||
|
|
/// Updates the direct addresses, returns `true` if they changed, `false` if not.
|
||
|
|
fn update(&self, addrs: BTreeSet<DirectAddr>) -> bool {
|
||
|
|
*self.updated_at.write().expect("poisoned") = Some(Instant::now());
|
||
|
|
let updated = self.addrs.set(addrs).is_ok();
|
||
|
|
if updated {
|
||
|
|
event!(
|
||
|
|
target: "iroh::_events::direct_addrs",
|
||
|
|
Level::DEBUG,
|
||
|
|
addrs = ?self.addrs.get(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
updated
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sockaddrs(&self) -> impl Iterator<Item = SocketAddr> {
|
||
|
|
self.addrs.get().into_iter().map(|da| da.addr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A *direct address* on which an iroh-endpoint might be contactable.
|
||
|
|
///
|
||
|
|
/// Direct addresses are UDP socket addresses on which an iroh endpoint could potentially be
|
||
|
|
/// contacted. These can come from various sources depending on the network topology of the
|
||
|
|
/// iroh endpoint, see [`DirectAddrType`] for the several kinds of sources.
|
||
|
|
///
|
||
|
|
/// This is essentially a combination of our local addresses combined with any reflexive
|
||
|
|
/// transport addresses we discovered using QAD.
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||
|
|
pub struct DirectAddr {
|
||
|
|
/// The address.
|
||
|
|
pub addr: SocketAddr,
|
||
|
|
/// The origin of this direct address.
|
||
|
|
pub typ: DirectAddrType,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The type of direct address.
|
||
|
|
///
|
||
|
|
/// These are the various sources or origins from which an iroh endpoint might have found a
|
||
|
|
/// possible [`DirectAddr`].
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||
|
|
#[non_exhaustive]
|
||
|
|
pub enum DirectAddrType {
|
||
|
|
/// Not yet determined..
|
||
|
|
Unknown,
|
||
|
|
/// A locally bound socket address.
|
||
|
|
Local,
|
||
|
|
/// Public internet address discovered via QAD.
|
||
|
|
///
|
||
|
|
/// When possible an iroh endpoint will perform QAD to discover which is the address
|
||
|
|
/// from which it sends data on the public internet. This can be different from locally
|
||
|
|
/// bound addresses when the endpoint is on a local network which performs NAT or similar.
|
||
|
|
Qad,
|
||
|
|
/// An address assigned by the router using port mapping.
|
||
|
|
///
|
||
|
|
/// When possible an iroh endpoint will request a port mapping from the local router to
|
||
|
|
/// get a publicly routable direct address.
|
||
|
|
Portmapped,
|
||
|
|
/// Hard NAT: QAD'ed IPv4 address + local fixed port.
|
||
|
|
///
|
||
|
|
/// It is possible to configure iroh to bound to a specific port and independently
|
||
|
|
/// configure the router to forward this port to the iroh endpoint. This indicates a
|
||
|
|
/// situation like this, which still uses QAD to discover the public address.
|
||
|
|
Qad4LocalPort,
|
||
|
|
/// An address explicitly provided by the user via configuration.
|
||
|
|
Config,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Display for DirectAddrType {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
DirectAddrType::Unknown => write!(f, "?"),
|
||
|
|
DirectAddrType::Local => write!(f, "local"),
|
||
|
|
DirectAddrType::Qad => write!(f, "qad"),
|
||
|
|
DirectAddrType::Portmapped => write!(f, "portmap"),
|
||
|
|
DirectAddrType::Qad4LocalPort => write!(f, "qad4localport"),
|
||
|
|
DirectAddrType::Config => write!(f, "config"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(all(test, with_crypto_provider))]
|
||
|
|
mod tests {
|
||
|
|
use std::{net::SocketAddrV4, sync::Arc, time::Duration};
|
||
|
|
|
||
|
|
use data_encoding::HEXLOWER;
|
||
|
|
use iroh_base::{EndpointAddr, EndpointId, TransportAddr};
|
||
|
|
use iroh_relay::tls::{CaTlsConfig, default_provider};
|
||
|
|
use n0_error::{Result, StackResultExt, StdResultExt};
|
||
|
|
use n0_future::{MergeBounded, StreamExt, time};
|
||
|
|
use n0_tracing_test::traced_test;
|
||
|
|
use n0_watcher::Watcher;
|
||
|
|
use rand::{CryptoRng, Rng, RngExt, SeedableRng};
|
||
|
|
use tokio_util::task::AbortOnDropHandle;
|
||
|
|
use tracing::{Instrument, error, info, info_span, instrument};
|
||
|
|
|
||
|
|
use super::Options;
|
||
|
|
use crate::{
|
||
|
|
Endpoint, SecretKey,
|
||
|
|
address_lookup::memory::MemoryLookup,
|
||
|
|
dns::DnsResolver,
|
||
|
|
endpoint::{QuicTransportConfig, presets},
|
||
|
|
socket::{
|
||
|
|
EndpointInner, StaticConfig, TransportConfig,
|
||
|
|
biased_rtt_path_selector::BiasedRttPathSelector,
|
||
|
|
mapped_addrs::{EndpointIdMappedAddr, MappedAddr},
|
||
|
|
},
|
||
|
|
tls::{self, DEFAULT_MAX_TLS_TICKETS, misc::RustlsTokenKey},
|
||
|
|
};
|
||
|
|
|
||
|
|
const ALPN: &[u8] = b"n0/test/1";
|
||
|
|
|
||
|
|
fn default_options(rng: &mut impl CryptoRng) -> Options {
|
||
|
|
let crypto_provider = default_provider();
|
||
|
|
let secret_key = SecretKey::from_bytes(&rng.random());
|
||
|
|
let tls_config = tls::TlsConfig::new(
|
||
|
|
secret_key.clone(),
|
||
|
|
DEFAULT_MAX_TLS_TICKETS,
|
||
|
|
crypto_provider.clone(),
|
||
|
|
);
|
||
|
|
let static_config = StaticConfig {
|
||
|
|
server_config: tls_config.make_server_config(false).unwrap(),
|
||
|
|
client_config: tls_config.make_client_config(false).unwrap(),
|
||
|
|
tls_config,
|
||
|
|
token_key: Arc::new(RustlsTokenKey::new(rng, &crypto_provider).unwrap()),
|
||
|
|
token_store: Arc::new(noq::TokenMemoryCache::default()),
|
||
|
|
transport_config: QuicTransportConfig::default(),
|
||
|
|
};
|
||
|
|
let server_config = static_config.create_server_config(vec![]);
|
||
|
|
Options {
|
||
|
|
transports: vec![
|
||
|
|
TransportConfig::default_ipv4(),
|
||
|
|
TransportConfig::default_ipv6(),
|
||
|
|
],
|
||
|
|
secret_key,
|
||
|
|
proxy_url: None,
|
||
|
|
dns_resolver: DnsResolver::new(),
|
||
|
|
server_config,
|
||
|
|
tls_config: CaTlsConfig::default()
|
||
|
|
.client_config(crypto_provider.clone())
|
||
|
|
.unwrap(),
|
||
|
|
#[cfg(any(test, feature = "test-utils"))]
|
||
|
|
address_lookup_user_data: None,
|
||
|
|
metrics: Default::default(),
|
||
|
|
hooks: Default::default(),
|
||
|
|
path_selector: Arc::new(BiasedRttPathSelector::default()),
|
||
|
|
portmapper_config: Default::default(),
|
||
|
|
net_report_config: Default::default(),
|
||
|
|
static_config,
|
||
|
|
configured_addrs: Default::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[instrument(skip_all, fields(me = %ep.id().fmt_short()))]
|
||
|
|
async fn echo_receiver(ep: Endpoint, loss: ExpectedLoss) -> Result {
|
||
|
|
info!("accepting conn");
|
||
|
|
let conn = ep.accept().await.expect("no conn");
|
||
|
|
|
||
|
|
info!("accepting");
|
||
|
|
let conn = conn.await.context("accepting")?;
|
||
|
|
info!("accepting bi");
|
||
|
|
let (mut send_bi, mut recv_bi) = conn.accept_bi().await.std_context("accept bi")?;
|
||
|
|
|
||
|
|
info!("reading");
|
||
|
|
let val = recv_bi
|
||
|
|
.read_to_end(usize::MAX)
|
||
|
|
.await
|
||
|
|
.std_context("read to end")?;
|
||
|
|
|
||
|
|
info!("replying");
|
||
|
|
for chunk in val.chunks(12) {
|
||
|
|
send_bi.write_all(chunk).await.std_context("write all")?;
|
||
|
|
}
|
||
|
|
|
||
|
|
info!("finishing");
|
||
|
|
send_bi.finish().std_context("finish")?;
|
||
|
|
|
||
|
|
let stats = conn.stats();
|
||
|
|
info!("stats: {:#?}", stats);
|
||
|
|
if matches!(loss, ExpectedLoss::AlmostNone) {
|
||
|
|
for info in conn.paths().iter() {
|
||
|
|
assert!(
|
||
|
|
info.stats().lost_packets < 10,
|
||
|
|
"[receiver] path {:?} should not loose many packets",
|
||
|
|
info.remote_addr()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
conn.closed().await;
|
||
|
|
info!("closed");
|
||
|
|
ep.inner()?.noq_endpoint().wait_idle().await;
|
||
|
|
info!("idle");
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[instrument(skip_all, fields(me = %ep.id().fmt_short()))]
|
||
|
|
async fn echo_sender(
|
||
|
|
ep: Endpoint,
|
||
|
|
dest_id: EndpointId,
|
||
|
|
msg: &[u8],
|
||
|
|
loss: ExpectedLoss,
|
||
|
|
) -> Result {
|
||
|
|
info!("connecting to {}", dest_id.fmt_short());
|
||
|
|
let dest = EndpointAddr::new(dest_id);
|
||
|
|
let conn = ep.connect(dest, ALPN).await?;
|
||
|
|
|
||
|
|
info!("opening bi");
|
||
|
|
let (mut send_bi, mut recv_bi) = conn.open_bi().await.std_context("open bi")?;
|
||
|
|
|
||
|
|
info!("writing message");
|
||
|
|
send_bi.write_all(msg).await.std_context("write all")?;
|
||
|
|
|
||
|
|
info!("finishing");
|
||
|
|
send_bi.finish().std_context("finish")?;
|
||
|
|
|
||
|
|
info!("reading_to_end");
|
||
|
|
let val = recv_bi
|
||
|
|
.read_to_end(usize::MAX)
|
||
|
|
.await
|
||
|
|
.std_context("read to end")?;
|
||
|
|
assert_eq!(
|
||
|
|
val,
|
||
|
|
msg,
|
||
|
|
"[sender] expected {}, got {}",
|
||
|
|
HEXLOWER.encode(msg),
|
||
|
|
HEXLOWER.encode(&val)
|
||
|
|
);
|
||
|
|
|
||
|
|
let stats = conn.stats();
|
||
|
|
info!("stats: {:#?}", stats);
|
||
|
|
if matches!(loss, ExpectedLoss::AlmostNone) {
|
||
|
|
for info in conn.paths().iter() {
|
||
|
|
assert!(
|
||
|
|
info.stats().lost_packets < 10,
|
||
|
|
"[sender] path {:?} should not loose many packets",
|
||
|
|
info.remote_addr()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
conn.close(0u32.into(), b"done");
|
||
|
|
info!("closed");
|
||
|
|
ep.inner()?.noq_endpoint().wait_idle().await;
|
||
|
|
info!("idle");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Copy, Clone)]
|
||
|
|
enum ExpectedLoss {
|
||
|
|
AlmostNone,
|
||
|
|
YeahSure,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Runs a roundtrip between the [`echo_sender`] and [`echo_receiver`].
|
||
|
|
async fn run_roundtrip(
|
||
|
|
sender: Endpoint,
|
||
|
|
receiver: Endpoint,
|
||
|
|
payload: &[u8],
|
||
|
|
loss: ExpectedLoss,
|
||
|
|
) -> Result<()> {
|
||
|
|
tokio::time::timeout(Duration::from_secs(20), async move {
|
||
|
|
let send_endpoint_id = sender.id();
|
||
|
|
let recv_endpoint_id = receiver.id();
|
||
|
|
info!("\nroundtrip: {send_endpoint_id:#} -> {recv_endpoint_id:#}");
|
||
|
|
|
||
|
|
let receiver_task = AbortOnDropHandle::new(tokio::spawn(echo_receiver(receiver, loss)));
|
||
|
|
let sender_res = echo_sender(sender, recv_endpoint_id, payload, loss).await;
|
||
|
|
let sender_is_err = match sender_res {
|
||
|
|
Ok(()) => false,
|
||
|
|
Err(err) => {
|
||
|
|
error!("[sender] Error:\n{err:#?}");
|
||
|
|
true
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let receiver_is_err = match receiver_task.await {
|
||
|
|
Ok(Ok(())) => false,
|
||
|
|
Ok(Err(err)) => {
|
||
|
|
error!("[receiver] Error:\n{err:#?}");
|
||
|
|
true
|
||
|
|
}
|
||
|
|
Err(joinerr) => {
|
||
|
|
if joinerr.is_panic() {
|
||
|
|
std::panic::resume_unwind(joinerr.into_panic());
|
||
|
|
} else {
|
||
|
|
error!("[receiver] Error:\n{joinerr:#?}");
|
||
|
|
}
|
||
|
|
true
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if sender_is_err || receiver_is_err {
|
||
|
|
panic!("Sender or receiver errored");
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
.std_context("timeout")?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a pair of endpoints with a shared [`MemoryLookup`].
|
||
|
|
///
|
||
|
|
/// The endpoints do not use a relay server but can connect to each other via local
|
||
|
|
/// addresses. Dialing by [`EndpointId`] is possible, and the addresses get updated even if
|
||
|
|
/// the endpoints rebind.
|
||
|
|
async fn endpoint_pair() -> (AbortOnDropHandle<()>, Endpoint, Endpoint) {
|
||
|
|
let address_lookup = MemoryLookup::new();
|
||
|
|
let ep1 = Endpoint::builder(presets::Minimal)
|
||
|
|
.alpns(vec![ALPN.to_vec()])
|
||
|
|
.address_lookup(address_lookup.clone())
|
||
|
|
.bind()
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let ep2 = Endpoint::builder(presets::Minimal)
|
||
|
|
.alpns(vec![ALPN.to_vec()])
|
||
|
|
.address_lookup(address_lookup.clone())
|
||
|
|
.bind()
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
address_lookup.add_endpoint_info(ep1.addr());
|
||
|
|
address_lookup.add_endpoint_info(ep2.addr());
|
||
|
|
|
||
|
|
let ep1_addr_stream = ep1.watch_addr().stream();
|
||
|
|
let ep2_addr_stream = ep2.watch_addr().stream();
|
||
|
|
let mut addr_stream = MergeBounded::from_iter([ep1_addr_stream, ep2_addr_stream]);
|
||
|
|
let task = tokio::spawn(async move {
|
||
|
|
while let Some(addr) = addr_stream.next().await {
|
||
|
|
address_lookup.add_endpoint_info(addr);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
(AbortOnDropHandle::new(task), ep1, ep2)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test(flavor = "multi_thread")]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_two_devices_roundtrip_noq_small() -> Result {
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
|
||
|
|
run_roundtrip(
|
||
|
|
m1.clone(),
|
||
|
|
m2.clone(),
|
||
|
|
b"hello m1",
|
||
|
|
ExpectedLoss::AlmostNone,
|
||
|
|
)
|
||
|
|
.await?;
|
||
|
|
run_roundtrip(
|
||
|
|
m2.clone(),
|
||
|
|
m1.clone(),
|
||
|
|
b"hello m2",
|
||
|
|
ExpectedLoss::AlmostNone,
|
||
|
|
)
|
||
|
|
.await?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test(flavor = "multi_thread")]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_two_devices_roundtrip_noq_large() -> Result {
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
let mut data = vec![0u8; 10 * 1024];
|
||
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
|
||
|
|
rng.fill_bytes(&mut data);
|
||
|
|
run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::AlmostNone).await?;
|
||
|
|
run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::AlmostNone).await?;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
// EADDRINUSE on the GitHub Android emulator persists past
|
||
|
|
// `force_network_change()`, so the rebind fails and connect() never
|
||
|
|
// wakes the connection driver. Passes locally.
|
||
|
|
#[cfg_attr(
|
||
|
|
target_os = "android",
|
||
|
|
ignore = "rebind flakes against the GitHub Android emulator"
|
||
|
|
)]
|
||
|
|
#[tokio::test]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_regression_network_change_rebind_wakes_connection_driver() -> Result {
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
|
||
|
|
println!("Net change");
|
||
|
|
m1.inner()?.force_network_change(true).await;
|
||
|
|
tokio::time::sleep(Duration::from_secs(1)).await; // wait for socket rebinding
|
||
|
|
|
||
|
|
let _handle = AbortOnDropHandle::new(tokio::spawn({
|
||
|
|
let endpoint = m2.clone();
|
||
|
|
async move {
|
||
|
|
while let Some(incoming) = endpoint.accept().await {
|
||
|
|
println!("Incoming first conn!");
|
||
|
|
let conn = incoming.await.anyerr()?;
|
||
|
|
conn.closed().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
n0_error::Ok(())
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
println!("first conn!");
|
||
|
|
let conn = m1.connect(m2.addr(), ALPN).await?;
|
||
|
|
println!("Closing first conn");
|
||
|
|
conn.close(0u32.into(), b"bye lolz");
|
||
|
|
conn.closed().await;
|
||
|
|
println!("Closed first conn");
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn offset(rng: &mut rand_chacha::ChaCha8Rng) -> Duration {
|
||
|
|
let delay = rng.random_range(1..=5);
|
||
|
|
Duration::from_millis(delay * 50)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Same structure as `test_two_devices_roundtrip_noq`, but interrupts regularly
|
||
|
|
/// with (simulated) network changes.
|
||
|
|
/// Regular network changes to m1 only.
|
||
|
|
#[tokio::test(flavor = "multi_thread")]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_two_devices_roundtrip_network_change_only_a() -> Result {
|
||
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
|
||
|
|
let _network_change_guard = {
|
||
|
|
let m1 = m1.clone();
|
||
|
|
let mut rng = rng.clone();
|
||
|
|
let task = tokio::spawn(async move {
|
||
|
|
loop {
|
||
|
|
info!("[m1] network change");
|
||
|
|
m1.inner()
|
||
|
|
.expect("haven't closed the endpoint yet")
|
||
|
|
.force_network_change(true)
|
||
|
|
.await;
|
||
|
|
time::sleep(offset(&mut rng)).await;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
AbortOnDropHandle::new(task)
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut data = vec![0u8; 10 * 1024];
|
||
|
|
rng.fill_bytes(&mut data);
|
||
|
|
run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::YeahSure).await?;
|
||
|
|
run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::YeahSure).await?;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Regular network changes to both m1 and m2.
|
||
|
|
#[tokio::test(flavor = "multi_thread")]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_two_devices_roundtrip_network_change_a_and_b() -> Result {
|
||
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
|
||
|
|
let _network_change_guard = {
|
||
|
|
let m1 = m1.clone();
|
||
|
|
let m2 = m2.clone();
|
||
|
|
let mut rng = rng.clone();
|
||
|
|
let task = tokio::spawn(async move {
|
||
|
|
info!("-- [m1] network change");
|
||
|
|
m1.inner()
|
||
|
|
.expect("haven't closed the endpoint yet")
|
||
|
|
.force_network_change(true)
|
||
|
|
.await;
|
||
|
|
info!("-- [m2] network change");
|
||
|
|
m2.inner()
|
||
|
|
.expect("haven't closed the endpoint yet")
|
||
|
|
.force_network_change(true)
|
||
|
|
.await;
|
||
|
|
time::sleep(offset(&mut rng)).await;
|
||
|
|
});
|
||
|
|
AbortOnDropHandle::new(task)
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut data = vec![0u8; 10 * 1024];
|
||
|
|
rng.fill_bytes(&mut data);
|
||
|
|
run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::YeahSure).await?;
|
||
|
|
run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::YeahSure).await?;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test(flavor = "multi_thread")]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_two_devices_setup_teardown() -> Result {
|
||
|
|
for i in 0..10 {
|
||
|
|
info!("-- round {i}");
|
||
|
|
info!("setting up stack");
|
||
|
|
let (_guard, m1, m2) = endpoint_pair().await;
|
||
|
|
|
||
|
|
info!("closing endpoints");
|
||
|
|
let sock1 = m1.inner()?;
|
||
|
|
let sock2 = m2.inner()?;
|
||
|
|
m1.close().await;
|
||
|
|
m2.close().await;
|
||
|
|
|
||
|
|
assert!(sock1.is_closed());
|
||
|
|
assert!(sock2.is_closed());
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_direct_addresses() {
|
||
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
|
||
|
|
let sock = EndpointInner::bind(default_options(&mut rng))
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
// See if we can get endpoints.
|
||
|
|
let eps0 = sock.ip_addrs().get();
|
||
|
|
info!("{eps0:?}");
|
||
|
|
assert!(!eps0.is_empty());
|
||
|
|
|
||
|
|
// Getting the endpoints again immediately should give the same results.
|
||
|
|
let eps1 = sock.ip_addrs().get();
|
||
|
|
info!("{eps1:?}");
|
||
|
|
assert_eq!(eps0, eps1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Creates a new [`noq::Endpoint`] hooked up to a [`Socket`].
|
||
|
|
///
|
||
|
|
/// This is without involving [`crate::endpoint::Endpoint`]. The socket will accept
|
||
|
|
/// connections using [`ALPN`].
|
||
|
|
///
|
||
|
|
/// Use [`socket_connect`] to establish connections.
|
||
|
|
#[instrument(name = "ep", skip_all, fields(me = %secret_key.public().fmt_short()))]
|
||
|
|
async fn socket_ep(secret_key: SecretKey) -> Result<EndpointInner> {
|
||
|
|
let crypto_provider = default_provider();
|
||
|
|
let tls_config = tls::TlsConfig::new(
|
||
|
|
secret_key.clone(),
|
||
|
|
DEFAULT_MAX_TLS_TICKETS,
|
||
|
|
crypto_provider.clone(),
|
||
|
|
);
|
||
|
|
let keylog = true;
|
||
|
|
let static_config = StaticConfig {
|
||
|
|
server_config: tls_config.make_server_config(keylog).unwrap(),
|
||
|
|
client_config: tls_config.make_client_config(keylog).unwrap(),
|
||
|
|
tls_config,
|
||
|
|
token_key: Arc::new(RustlsTokenKey::new(&mut rand::rng(), &crypto_provider).unwrap()),
|
||
|
|
token_store: Arc::new(noq::TokenMemoryCache::default()),
|
||
|
|
transport_config: QuicTransportConfig::default(),
|
||
|
|
};
|
||
|
|
let server_config = static_config.create_server_config(vec![ALPN.to_vec()]);
|
||
|
|
|
||
|
|
let dns_resolver = DnsResolver::new();
|
||
|
|
let opts = Options {
|
||
|
|
transports: vec![
|
||
|
|
TransportConfig::default_ipv4(),
|
||
|
|
TransportConfig::default_ipv6(),
|
||
|
|
],
|
||
|
|
secret_key: secret_key.clone(),
|
||
|
|
address_lookup_user_data: None,
|
||
|
|
dns_resolver,
|
||
|
|
proxy_url: None,
|
||
|
|
server_config,
|
||
|
|
tls_config: CaTlsConfig::default()
|
||
|
|
.client_config(crypto_provider.clone())
|
||
|
|
.unwrap(),
|
||
|
|
metrics: Default::default(),
|
||
|
|
hooks: Default::default(),
|
||
|
|
path_selector: Arc::new(BiasedRttPathSelector::default()),
|
||
|
|
portmapper_config: Default::default(),
|
||
|
|
net_report_config: Default::default(),
|
||
|
|
static_config,
|
||
|
|
configured_addrs: Default::default(),
|
||
|
|
};
|
||
|
|
let sock = EndpointInner::bind(opts).await?;
|
||
|
|
Ok(sock)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Connects from `ep` returned by [`socket_ep`] to the `endpoint_id`.
|
||
|
|
///
|
||
|
|
/// Uses [`ALPN`], `endpoint_id`, must match `addr`.
|
||
|
|
#[instrument(name = "connect", skip_all, fields(me = %ep_secret_key.public().fmt_short()))]
|
||
|
|
async fn socket_connect(
|
||
|
|
ep: noq::Endpoint,
|
||
|
|
ep_secret_key: SecretKey,
|
||
|
|
addr: EndpointIdMappedAddr,
|
||
|
|
endpoint_id: EndpointId,
|
||
|
|
) -> Result<noq::Connection> {
|
||
|
|
// Endpoint::connect sets this, do the same to have similar behaviour.
|
||
|
|
let mut transport_config = noq::TransportConfig::default();
|
||
|
|
transport_config.server_handshake_migration(true);
|
||
|
|
transport_config.keep_alive_interval(Some(Duration::from_secs(1)));
|
||
|
|
|
||
|
|
socket_connect_with_transport_config(
|
||
|
|
ep,
|
||
|
|
ep_secret_key,
|
||
|
|
addr,
|
||
|
|
endpoint_id,
|
||
|
|
Arc::new(transport_config),
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Connects from `ep` returned by [`socket_ep`] to the `endpoint_id`.
|
||
|
|
///
|
||
|
|
/// This version allows customising the transport config.
|
||
|
|
///
|
||
|
|
/// Uses [`ALPN`], `endpoint_id`, must match `addr`.
|
||
|
|
#[instrument(name = "connect", skip_all, fields(me = %ep_secret_key.public().fmt_short()))]
|
||
|
|
async fn socket_connect_with_transport_config(
|
||
|
|
ep: noq::Endpoint,
|
||
|
|
ep_secret_key: SecretKey,
|
||
|
|
mapped_addr: EndpointIdMappedAddr,
|
||
|
|
endpoint_id: EndpointId,
|
||
|
|
transport_config: Arc<noq::TransportConfig>,
|
||
|
|
) -> Result<noq::Connection> {
|
||
|
|
let mut quic_client_config = tls::TlsConfig::new(
|
||
|
|
ep_secret_key.clone(),
|
||
|
|
DEFAULT_MAX_TLS_TICKETS,
|
||
|
|
default_provider(),
|
||
|
|
)
|
||
|
|
.make_client_config(true)?;
|
||
|
|
quic_client_config.set_alpn_protocols(vec![ALPN.to_vec()]);
|
||
|
|
let mut client_config = noq::ClientConfig::new(Arc::new(quic_client_config));
|
||
|
|
client_config.transport_config(transport_config);
|
||
|
|
let connect = ep
|
||
|
|
.connect_with(
|
||
|
|
client_config,
|
||
|
|
mapped_addr.private_socket_addr(),
|
||
|
|
&tls::name::encode(endpoint_id),
|
||
|
|
)
|
||
|
|
.std_context("connect")?;
|
||
|
|
let connection = connect.await.anyerr()?;
|
||
|
|
Ok(connection)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_try_send_no_send_addr() {
|
||
|
|
// Regression test: if there is no send_addr we should keep being able to use the
|
||
|
|
// Endpoint.
|
||
|
|
|
||
|
|
let secret_key_1 = SecretKey::from_bytes(&[1u8; 32]);
|
||
|
|
let secret_key_2 = SecretKey::from_bytes(&[2u8; 32]);
|
||
|
|
let endpoint_id_2 = secret_key_2.public();
|
||
|
|
let secret_key_missing_endpoint = SecretKey::from_bytes(&[255u8; 32]);
|
||
|
|
let endpoint_id_missing_endpoint = secret_key_missing_endpoint.public();
|
||
|
|
|
||
|
|
let sock_1 = socket_ep(secret_key_1.clone()).await.unwrap();
|
||
|
|
|
||
|
|
// Generate an address not present in the RemoteMap.
|
||
|
|
let bad_addr = EndpointIdMappedAddr::generate();
|
||
|
|
|
||
|
|
// 500ms is rather fast here. Running this locally it should always be the correct
|
||
|
|
// timeout. If this is too slow however the test will not become flaky as we are
|
||
|
|
// expecting the timeout, we might just get the timeout for the wrong reason. But
|
||
|
|
// this speeds up the test.
|
||
|
|
let res = tokio::time::timeout(
|
||
|
|
Duration::from_millis(500),
|
||
|
|
socket_connect(
|
||
|
|
sock_1.noq_endpoint().clone(),
|
||
|
|
secret_key_1.clone(),
|
||
|
|
bad_addr,
|
||
|
|
endpoint_id_missing_endpoint,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
assert!(res.is_err(), "expecting timeout");
|
||
|
|
|
||
|
|
// Now check we can still create another connection with this endpoint.
|
||
|
|
let sock_2 = socket_ep(secret_key_2.clone()).await.unwrap();
|
||
|
|
|
||
|
|
// This needs an accept task
|
||
|
|
let accept_task = tokio::spawn({
|
||
|
|
async fn accept(ep: noq::Endpoint) -> Result<()> {
|
||
|
|
let incoming = ep.accept().await.std_context("no incoming")?;
|
||
|
|
let _conn = incoming
|
||
|
|
.accept()
|
||
|
|
.std_context("accept")?
|
||
|
|
.await
|
||
|
|
.std_context("accepting")?;
|
||
|
|
|
||
|
|
// Keep this connection alive for a while
|
||
|
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||
|
|
info!("accept finished");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
let ep = sock_2.noq_endpoint().clone();
|
||
|
|
async move {
|
||
|
|
if let Err(err) = accept(ep).await {
|
||
|
|
error!("{err:#}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.instrument(info_span!("ep2.accept, me = endpoint_id_2.fmt_short()"))
|
||
|
|
});
|
||
|
|
let _accept_task = AbortOnDropHandle::new(accept_task);
|
||
|
|
|
||
|
|
let addrs = sock_2
|
||
|
|
.ip_addrs()
|
||
|
|
.get()
|
||
|
|
.into_iter()
|
||
|
|
.map(|x| TransportAddr::Ip(x.addr));
|
||
|
|
let endpoint_addr_2 = EndpointAddr::from_parts(endpoint_id_2, addrs);
|
||
|
|
let addr = sock_1
|
||
|
|
.resolve_remote(endpoint_addr_2)
|
||
|
|
.await
|
||
|
|
.unwrap()
|
||
|
|
.unwrap();
|
||
|
|
let res = tokio::time::timeout(
|
||
|
|
Duration::from_secs(10),
|
||
|
|
socket_connect(
|
||
|
|
sock_1.noq_endpoint().clone(),
|
||
|
|
secret_key_1.clone(),
|
||
|
|
addr,
|
||
|
|
endpoint_id_2,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.expect("timeout while connecting");
|
||
|
|
|
||
|
|
// aka assert!(res.is_ok()) but with nicer error reporting.
|
||
|
|
res.unwrap();
|
||
|
|
|
||
|
|
// TODO: Now check if we can connect to a repaired ep_3, but we can't modify that
|
||
|
|
// much internal state for now.
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
#[traced_test]
|
||
|
|
async fn test_try_send_no_udp_addr_or_relay_url() {
|
||
|
|
// This specifically tests the `if udp_addr.is_none() && relay_url.is_none()`
|
||
|
|
// behaviour of Socket::try_send.
|
||
|
|
|
||
|
|
let secret_key_1 = SecretKey::from_bytes(&[1u8; 32]);
|
||
|
|
let secret_key_2 = SecretKey::from_bytes(&[2u8; 32]);
|
||
|
|
let endpoint_id_2 = secret_key_2.public();
|
||
|
|
|
||
|
|
let sock_1 = socket_ep(secret_key_1.clone()).await.unwrap();
|
||
|
|
let sock_2 = socket_ep(secret_key_2.clone()).await.unwrap();
|
||
|
|
let ep_2 = sock_2.noq_endpoint().clone();
|
||
|
|
|
||
|
|
// We need a task to accept the connection.
|
||
|
|
let accept_task = tokio::spawn({
|
||
|
|
async fn accept(ep: noq::Endpoint) -> Result<()> {
|
||
|
|
let incoming = ep.accept().await.std_context("no incoming")?;
|
||
|
|
let conn = incoming
|
||
|
|
.accept()
|
||
|
|
.std_context("accept")?
|
||
|
|
.await
|
||
|
|
.std_context("connecting")?;
|
||
|
|
let mut stream = conn.accept_uni().await.std_context("accept uni")?;
|
||
|
|
stream
|
||
|
|
.read_to_end(1 << 16)
|
||
|
|
.await
|
||
|
|
.std_context("read to end")?;
|
||
|
|
info!("accept finished");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
async move {
|
||
|
|
if let Err(err) = accept(ep_2).await {
|
||
|
|
error!("{err:#}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.instrument(info_span!("ep2.accept", me = %endpoint_id_2.fmt_short()))
|
||
|
|
});
|
||
|
|
let _accept_task = AbortOnDropHandle::new(accept_task);
|
||
|
|
|
||
|
|
// Add an entry in the RemoteMap of ep_1 with an invalid socket address
|
||
|
|
let empty_addr_2 = EndpointAddr::from_parts(
|
||
|
|
endpoint_id_2,
|
||
|
|
[TransportAddr::Ip(
|
||
|
|
// Reserved IP range for documentation (unreachable)
|
||
|
|
SocketAddrV4::new([192, 0, 2, 1].into(), 12345).into(),
|
||
|
|
)],
|
||
|
|
);
|
||
|
|
let addr_2 = sock_1.resolve_remote(empty_addr_2).await.unwrap().unwrap();
|
||
|
|
|
||
|
|
// Set a low max_idle_timeout so noq gives up on this quickly and our test does
|
||
|
|
// not take forever. You need to check the log output to verify this is really
|
||
|
|
// triggering the correct error.
|
||
|
|
// In test_try_send_no_send_addr() above you may have noticed we used
|
||
|
|
// tokio::time::timeout() on the connection attempt instead. Here however we want
|
||
|
|
// Noq itself to have fully given up on the connection attempt because we will
|
||
|
|
// later connect to **the same** endpoint. If Noq did not give up on the connection
|
||
|
|
// we'd close it on drop, and the retransmits of the close packets would interfere
|
||
|
|
// with the next handshake, closing it during the handshake. This makes the test a
|
||
|
|
// little slower though.
|
||
|
|
let mut transport_config = noq::TransportConfig::default();
|
||
|
|
transport_config.server_handshake_migration(true);
|
||
|
|
transport_config.max_idle_timeout(Some(Duration::from_millis(200).try_into().unwrap()));
|
||
|
|
let res = socket_connect_with_transport_config(
|
||
|
|
sock_1.noq_endpoint().clone(),
|
||
|
|
secret_key_1.clone(),
|
||
|
|
addr_2,
|
||
|
|
endpoint_id_2,
|
||
|
|
Arc::new(transport_config),
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
assert!(res.is_err(), "expected timeout");
|
||
|
|
info!("first connect timed out as expected");
|
||
|
|
|
||
|
|
// Provide correct addressing information
|
||
|
|
let correct_addr_2 = EndpointAddr::from_parts(
|
||
|
|
endpoint_id_2,
|
||
|
|
sock_2
|
||
|
|
.ip_addrs()
|
||
|
|
.get()
|
||
|
|
.into_iter()
|
||
|
|
.map(|x| TransportAddr::Ip(x.addr)),
|
||
|
|
);
|
||
|
|
let addr_2a = sock_1
|
||
|
|
.resolve_remote(correct_addr_2)
|
||
|
|
.await
|
||
|
|
.unwrap()
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(addr_2, addr_2a);
|
||
|
|
|
||
|
|
// We can now connect
|
||
|
|
tokio::time::timeout(Duration::from_secs(10), async move {
|
||
|
|
info!("establishing new connection");
|
||
|
|
let conn = socket_connect(
|
||
|
|
sock_1.noq_endpoint().clone(),
|
||
|
|
secret_key_1.clone(),
|
||
|
|
addr_2,
|
||
|
|
endpoint_id_2,
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
info!("have connection");
|
||
|
|
let mut stream = conn.open_uni().await.unwrap();
|
||
|
|
stream.write_all(b"hello").await.unwrap();
|
||
|
|
stream.finish().unwrap();
|
||
|
|
stream.stopped().await.unwrap();
|
||
|
|
info!("finished stream");
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
.expect("connection timed out");
|
||
|
|
|
||
|
|
// TODO: could remove the addresses again, send, add it back and see it recover.
|
||
|
|
// But we don't have that much private access to the RemoteMap. This will do for now.
|
||
|
|
}
|
||
|
|
}
|