| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use std::{ | |
| collections::HashSet, | |
| path::PathBuf, | |
| sync::{Arc, Mutex, RwLock}, | |
| time::Duration, | |
| }; | |
| use libp2p::{Multiaddr, PeerId}; | |
| use napi::bindgen_prelude::{Buffer, Error, Result, Status}; | |
| use napi_derive::napi; | |
| use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot, watch}; | |
| use crate::engine::{ | |
| self, DirectTransport, EngineCommand, PeerConnectionPath, PeerError, StreamCommand, | |
| }; | |
| type IncomingStreamReceiver = mpsc::Receiver<std::result::Result<Vec<u8>, PeerError>>; | |
| const IDENTITY_PAYLOAD_MAX_BYTES: usize = 8 * 1024; | |
| const MAX_CONNECT_ROUTES_PER_CLASS: usize = 32; | |
| const MAX_TRANSIT_PEERS: usize = 64; | |
| const MAX_TRANSIT_RELAY_ADDRESSES: usize = 256; | |
| const MAX_WEBRTC_STUN_URLS: usize = 8; | |
| const MAX_WEBRTC_STUN_URL_BYTES: usize = 512; | |
| #[napi(object)] | |
| pub struct StartPeerEndpointOptions { | |
| pub key_path: String, | |
| pub relay_anchor_path: Option<String>, | |
| pub expected_peer_id: Option<String>, | |
| pub listen_addresses: Option<Vec<String>>, | |
| pub coordination_relays: Option<Vec<String>>, | |
| pub automatic_relay_discovery: Option<bool>, | |
| pub web_rtc_stun_urls: Option<Vec<String>>, | |
| } | |
| #[napi(object)] | |
| pub struct ConnectPeerOptions { | |
| pub request_id: u32, | |
| pub peer_id: String, | |
| pub route_hints: Vec<String>, | |
| pub coordination_relays: Option<Vec<String>>, | |
| pub transit_relay_peer_ids: Option<Vec<String>>, | |
| pub direct_deadline_ms: u32, | |
| } | |
| #[napi(object)] | |
| pub struct UpdatePeerConnectOptions { | |
| pub request_id: u32, | |
| pub route_hints: Vec<String>, | |
| pub coordination_relays: Option<Vec<String>>, | |
| pub transit_relay_peer_ids: Option<Vec<String>>, | |
| } | |
| #[napi(object)] | |
| pub struct ConfigurePeerTransitOptions { | |
| pub allowed_peer_ids: Vec<String>, | |
| pub approved_relay_peer_ids: Vec<String>, | |
| pub relay_candidates: Vec<PeerTransitRelayCandidate>, | |
| } | |
| #[napi(object)] | |
| pub struct PeerTransitRelayCandidate { | |
| pub peer_id: String, | |
| pub addresses: Vec<String>, | |
| pub coordination_relays: Vec<String>, | |
| } | |
| #[napi(object)] | |
| pub struct PeerTransitSnapshot { | |
| pub allowed_peer_count: u32, | |
| pub active_reservation_count: u32, | |
| pub active_circuit_count: u32, | |
| pub max_reservation_count: u32, | |
| pub max_circuit_count: u32, | |
| pub max_circuits_per_peer: u32, | |
| pub max_circuit_duration_seconds: u32, | |
| pub max_circuit_bytes: u32, | |
| } | |
| #[napi(object)] | |
| pub struct PeerIdentitySignature { | |
| pub public_key: Buffer, | |
| pub signature: Buffer, | |
| } | |
| #[napi(object)] | |
| #[derive(Clone)] | |
| pub struct PeerReachabilitySnapshot { | |
| pub generation: u32, | |
| pub listen_addresses: Vec<String>, | |
| pub active_coordination_relays: Vec<String>, | |
| } | |
| #[napi(object)] | |
| pub struct PeerConnectivitySnapshot { | |
| pub generation: u32, | |
| pub connected_peer_ids: Vec<String>, | |
| } | |
| #[napi] | |
| pub struct PeerEndpoint { | |
| peer_id: String, | |
| reachability: watch::Receiver<engine::ReachabilitySnapshot>, | |
| connectivity: watch::Receiver<engine::ConnectivitySnapshot>, | |
| transit_snapshot: Arc<RwLock<engine::TransitSnapshot>>, | |
| commands: mpsc::Sender<EngineCommand>, | |
| incoming: Arc<AsyncMutex<mpsc::Receiver<engine::PeerStream>>>, | |
| mesh_incoming: Arc<AsyncMutex<mpsc::Receiver<engine::PeerStream>>>, | |
| terminal: Arc<AsyncMutex<mpsc::Receiver<PeerError>>>, | |
| thread: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>, | |
| } | |
| #[napi] | |
| impl PeerEndpoint { | |
| #[napi(getter)] | |
| pub fn peer_id(&self) -> String { | |
| self.peer_id.clone() | |
| } | |
| #[napi(getter)] | |
| pub fn reachability_snapshot(&self) -> PeerReachabilitySnapshot { | |
| reachability_snapshot(&self.reachability.borrow()) | |
| } | |
| #[napi] | |
| pub async fn watch_reachability(&self, after_generation: u32, timeout_ms: u32) -> Result<u32> { | |
| if !(1..=300_000).contains(&timeout_ms) { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "reachability watch timeout must be between 1 and 300000 milliseconds", | |
| )); | |
| } | |
| let mut receiver = self.reachability.clone(); | |
| match tokio::time::timeout( | |
| Duration::from_millis(u64::from(timeout_ms)), | |
| receiver.wait_for(|snapshot| snapshot.generation != after_generation), | |
| ) | |
| .await | |
| { | |
| Ok(Ok(snapshot)) => return Ok(snapshot.generation), | |
| Ok(Err(_)) => return Err(native_closed_error()), | |
| Err(_) => {} | |
| } | |
| Ok(receiver.borrow().generation) | |
| } | |
| #[napi(getter)] | |
| pub fn connectivity_snapshot(&self) -> PeerConnectivitySnapshot { | |
| connectivity_snapshot(&self.connectivity.borrow()) | |
| } | |
| #[napi] | |
| pub async fn watch_connectivity( | |
| &self, | |
| after_generation: u32, | |
| timeout_ms: u32, | |
| ) -> Result<PeerConnectivitySnapshot> { | |
| if !(1..=300_000).contains(&timeout_ms) { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "connectivity watch timeout must be between 1 and 300000 milliseconds", | |
| )); | |
| } | |
| let mut receiver = self.connectivity.clone(); | |
| match tokio::time::timeout( | |
| Duration::from_millis(u64::from(timeout_ms)), | |
| receiver.wait_for(|snapshot| snapshot.generation != after_generation), | |
| ) | |
| .await | |
| { | |
| Ok(Ok(snapshot)) => return Ok(connectivity_snapshot(&snapshot)), | |
| Ok(Err(_)) => return Err(native_closed_error()), | |
| Err(_) => {} | |
| } | |
| Ok(connectivity_snapshot(&receiver.borrow())) | |
| } | |
| #[napi(getter)] | |
| pub fn transit_snapshot(&self) -> PeerTransitSnapshot { | |
| let snapshot = self | |
| .transit_snapshot | |
| .read() | |
| .map(|snapshot| snapshot.clone()) | |
| .unwrap_or_default(); | |
| PeerTransitSnapshot { | |
| allowed_peer_count: snapshot.allowed_peer_count as u32, | |
| active_reservation_count: snapshot.active_reservation_count as u32, | |
| active_circuit_count: snapshot.active_circuit_count as u32, | |
| max_reservation_count: snapshot.max_reservation_count as u32, | |
| max_circuit_count: snapshot.max_circuit_count as u32, | |
| max_circuits_per_peer: snapshot.max_circuits_per_peer as u32, | |
| max_circuit_duration_seconds: snapshot.max_circuit_duration_seconds as u32, | |
| max_circuit_bytes: snapshot.max_circuit_bytes as u32, | |
| } | |
| } | |
| #[napi] | |
| pub async fn configure_transit(&self, options: ConfigurePeerTransitOptions) -> Result<()> { | |
| let allowed_peers = parse_peer_ids(options.allowed_peer_ids)?; | |
| let approved_relays = parse_peer_ids(options.approved_relay_peer_ids)?; | |
| let relays = parse_transit_relay_candidates(options.relay_candidates)?; | |
| let trusted_relays = relays | |
| .iter() | |
| .map(|candidate| candidate.peer_id) | |
| .collect::<HashSet<_>>(); | |
| let local_peer_id = parse_peer_id(&self.peer_id)?; | |
| if allowed_peers.contains(&local_peer_id) | |
| || approved_relays.contains(&local_peer_id) | |
| || trusted_relays.contains(&local_peer_id) | |
| { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "peer endpoint cannot configure itself as a transit peer", | |
| )); | |
| } | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| self.commands | |
| .send(EngineCommand::ConfigureTransit { | |
| policy: engine::TransitPolicy { | |
| allowed_peers, | |
| approved_relays, | |
| relays, | |
| }, | |
| result: result_tx, | |
| }) | |
| .await | |
| .map_err(|_| native_closed_error())?; | |
| result_rx.await.map_err(|_| native_closed_error()) | |
| } | |
| #[napi] | |
| pub async fn connect(&self, options: ConnectPeerOptions) -> Result<PeerStream> { | |
| connect_peer(self, options, engine::StreamKind::Application).await | |
| } | |
| #[napi] | |
| pub async fn connect_mesh_control(&self, options: ConnectPeerOptions) -> Result<PeerStream> { | |
| connect_peer(self, options, engine::StreamKind::MeshControl).await | |
| } | |
| #[napi] | |
| pub async fn accept_mesh_control(&self) -> Result<Option<PeerStream>> { | |
| self.mesh_incoming | |
| .lock() | |
| .await | |
| .recv() | |
| .await | |
| .map(wrap_stream) | |
| .transpose() | |
| } | |
| #[napi] | |
| pub async fn cancel_connect(&self, request_id: u32) -> Result<bool> { | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| self.commands | |
| .send(EngineCommand::CancelConnect { | |
| request_id, | |
| result: result_tx, | |
| }) | |
| .await | |
| .map_err(|_| native_closed_error())?; | |
| result_rx.await.map_err(|_| native_closed_error()) | |
| } | |
| #[napi] | |
| pub async fn update_connect(&self, options: UpdatePeerConnectOptions) -> Result<bool> { | |
| let route_hints = parse_connect_addresses(options.route_hints, "route hint")?; | |
| let coordination_relays = parse_connect_addresses( | |
| options.coordination_relays.unwrap_or_default(), | |
| "coordination relay", | |
| )?; | |
| let transit_relay_peers = | |
| parse_peer_id_list(options.transit_relay_peer_ids.unwrap_or_default())?; | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| self.commands | |
| .send(EngineCommand::UpdateConnect { | |
| request_id: options.request_id, | |
| candidates: engine::ConnectCandidates { | |
| route_hints, | |
| coordination_relays, | |
| transit_relay_peers, | |
| }, | |
| result: result_tx, | |
| }) | |
| .await | |
| .map_err(|_| native_closed_error())?; | |
| result_rx | |
| .await | |
| .map_err(|_| native_closed_error())? | |
| .map_err(peer_error) | |
| } | |
| #[napi] | |
| pub async fn accept(&self) -> Result<Option<PeerStream>> { | |
| let mut incoming = self.incoming.lock().await; | |
| let mut terminal = self.terminal.lock().await; | |
| tokio::select! { | |
| error = terminal.recv() => match error { | |
| Some(error) => Err(peer_error(error)), | |
| None => Ok(None), | |
| }, | |
| stream = incoming.recv() => stream.map(wrap_stream).transpose(), | |
| } | |
| } | |
| #[napi] | |
| pub async fn close(&self) -> Result<()> { | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| if self | |
| .commands | |
| .send(EngineCommand::Stop { result: result_tx }) | |
| .await | |
| .is_ok() | |
| { | |
| let _ = result_rx.await; | |
| } | |
| let thread = self | |
| .thread | |
| .lock() | |
| .map_err(|_| Error::new(Status::GenericFailure, "peer endpoint lock poisoned"))? | |
| .take(); | |
| if let Some(thread) = thread { | |
| tokio::task::spawn_blocking(move || thread.join()) | |
| .await | |
| .map_err(|error| Error::new(Status::GenericFailure, error.to_string()))? | |
| .map_err(|_| Error::new(Status::GenericFailure, "peer endpoint thread panicked"))?; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| async fn connect_peer( | |
| endpoint: &PeerEndpoint, | |
| options: ConnectPeerOptions, | |
| stream_kind: engine::StreamKind, | |
| ) -> Result<PeerStream> { | |
| let peer_id = parse_peer_id(&options.peer_id)?; | |
| let route_hints = parse_connect_addresses(options.route_hints, "route hint")?; | |
| let coordination_relays = parse_connect_addresses( | |
| options.coordination_relays.unwrap_or_default(), | |
| "coordination relay", | |
| )?; | |
| let transit_relay_peers = | |
| parse_peer_id_list(options.transit_relay_peer_ids.unwrap_or_default())?; | |
| if !(1..=120_000).contains(&options.direct_deadline_ms) { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "direct deadline must be between 1 and 120000 milliseconds", | |
| )); | |
| } | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| endpoint | |
| .commands | |
| .send(EngineCommand::Connect { | |
| options: engine::ConnectOptions { | |
| request_id: options.request_id, | |
| peer_id, | |
| route_hints, | |
| coordination_relays, | |
| transit_relay_peers, | |
| deadline: Duration::from_millis(u64::from(options.direct_deadline_ms)), | |
| }, | |
| stream_kind, | |
| result: result_tx, | |
| }) | |
| .await | |
| .map_err(|_| { | |
| peer_error(PeerError { | |
| code: "peer_native_failed", | |
| message: "peer endpoint is closed".to_owned(), | |
| }) | |
| })?; | |
| wrap_stream( | |
| result_rx | |
| .await | |
| .map_err(|_| native_closed_error())? | |
| .map_err(peer_error)?, | |
| ) | |
| } | |
| impl Drop for PeerEndpoint { | |
| fn drop(&mut self) { | |
| if Arc::strong_count(&self.thread) == 1 { | |
| let (result, _) = oneshot::channel(); | |
| let _ = self.commands.try_send(EngineCommand::Stop { result }); | |
| } | |
| } | |
| } | |
| #[napi] | |
| pub struct PeerStream { | |
| peer_id: String, | |
| path: PeerStreamPath, | |
| incoming: Arc<AsyncMutex<IncomingStreamReceiver>>, | |
| commands: mpsc::Sender<StreamCommand>, | |
| abort: watch::Sender<bool>, | |
| } | |
| #[napi(object)] | |
| #[derive(Clone)] | |
| pub struct PeerStreamPath { | |
| pub kind: String, | |
| pub transport: Option<String>, | |
| pub relay_peer_id: Option<String>, | |
| } | |
| #[napi] | |
| impl PeerStream { | |
| #[napi(getter)] | |
| pub fn peer_id(&self) -> String { | |
| self.peer_id.clone() | |
| } | |
| #[napi(getter)] | |
| pub fn path(&self) -> PeerStreamPath { | |
| self.path.clone() | |
| } | |
| #[napi] | |
| pub async fn read(&self) -> Result<Option<Buffer>> { | |
| match self.incoming.lock().await.recv().await { | |
| Some(Ok(bytes)) => Ok(Some(bytes.into())), | |
| Some(Err(error)) => Err(peer_error(error)), | |
| None => Ok(None), | |
| } | |
| } | |
| #[napi] | |
| pub async fn write(&self, bytes: Buffer) -> Result<()> { | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| self.commands | |
| .send(StreamCommand::Write { | |
| bytes: bytes.to_vec(), | |
| result: result_tx, | |
| }) | |
| .await | |
| .map_err(|_| native_closed_error())?; | |
| result_rx | |
| .await | |
| .map_err(|_| native_closed_error())? | |
| .map_err(peer_error) | |
| } | |
| #[napi] | |
| pub async fn close(&self) -> Result<()> { | |
| let (result_tx, result_rx) = oneshot::channel(); | |
| if self | |
| .commands | |
| .send(StreamCommand::Close { result: result_tx }) | |
| .await | |
| .is_err() | |
| { | |
| return Ok(()); | |
| } | |
| match result_rx.await { | |
| Ok(result) => result.map_err(peer_error), | |
| Err(_) => Ok(()), | |
| } | |
| } | |
| #[napi] | |
| pub fn abort(&self) { | |
| self.abort.send_replace(true); | |
| } | |
| } | |
| #[napi] | |
| pub fn start_peer_endpoint(options: StartPeerEndpointOptions) -> Result<PeerEndpoint> { | |
| let started = engine::start(engine::StartOptions { | |
| key_path: PathBuf::from(options.key_path), | |
| relay_anchor_path: options.relay_anchor_path.map(PathBuf::from), | |
| expected_peer_id: options | |
| .expected_peer_id | |
| .map(|value| parse_peer_id(&value)) | |
| .transpose()?, | |
| listen_addresses: parse_addresses(options.listen_addresses.unwrap_or_default(), "listen")?, | |
| coordination_relays: parse_addresses( | |
| options.coordination_relays.unwrap_or_default(), | |
| "coordination relay", | |
| )?, | |
| automatic_relay_discovery: options.automatic_relay_discovery.unwrap_or(false), | |
| web_rtc_stun_urls: options | |
| .web_rtc_stun_urls | |
| .map(parse_webrtc_stun_urls) | |
| .transpose()?, | |
| }) | |
| .map_err(peer_error)?; | |
| Ok(PeerEndpoint { | |
| peer_id: started.peer_id.to_string(), | |
| reachability: started.reachability, | |
| connectivity: started.connectivity, | |
| transit_snapshot: started.transit_snapshot, | |
| commands: started.commands, | |
| incoming: Arc::new(AsyncMutex::new(started.incoming)), | |
| mesh_incoming: Arc::new(AsyncMutex::new(started.mesh_incoming)), | |
| terminal: Arc::new(AsyncMutex::new(started.terminal)), | |
| thread: Arc::new(Mutex::new(Some(started.thread))), | |
| }) | |
| } | |
| fn reachability_snapshot(snapshot: &engine::ReachabilitySnapshot) -> PeerReachabilitySnapshot { | |
| PeerReachabilitySnapshot { | |
| generation: snapshot.generation, | |
| listen_addresses: snapshot | |
| .listen_addresses | |
| .iter() | |
| .map(ToString::to_string) | |
| .collect(), | |
| active_coordination_relays: snapshot | |
| .active_coordination_relays | |
| .iter() | |
| .map(ToString::to_string) | |
| .collect(), | |
| } | |
| } | |
| fn connectivity_snapshot(snapshot: &engine::ConnectivitySnapshot) -> PeerConnectivitySnapshot { | |
| PeerConnectivitySnapshot { | |
| generation: snapshot.generation, | |
| connected_peer_ids: snapshot | |
| .connected_peers | |
| .iter() | |
| .map(ToString::to_string) | |
| .collect(), | |
| } | |
| } | |
| #[napi] | |
| pub async fn ensure_peer_identity(key_path: String) -> Result<String> { | |
| engine::ensure_identity(PathBuf::from(key_path)) | |
| .await | |
| .map(|peer_id| peer_id.to_string()) | |
| .map_err(peer_error) | |
| } | |
| #[napi] | |
| pub async fn sign_peer_identity( | |
| key_path: String, | |
| expected_peer_id: String, | |
| payload: Buffer, | |
| ) -> Result<PeerIdentitySignature> { | |
| validate_identity_payload(&payload)?; | |
| let signed = engine::sign_identity( | |
| PathBuf::from(key_path), | |
| parse_peer_id(&expected_peer_id)?, | |
| &payload, | |
| ) | |
| .await | |
| .map_err(peer_error)?; | |
| Ok(PeerIdentitySignature { | |
| public_key: signed.public_key.into(), | |
| signature: signed.signature.into(), | |
| }) | |
| } | |
| #[napi] | |
| pub fn verify_peer_identity( | |
| peer_id: String, | |
| public_key: Buffer, | |
| payload: Buffer, | |
| signature: Buffer, | |
| ) -> Result<bool> { | |
| validate_identity_payload(&payload)?; | |
| engine::verify_identity(parse_peer_id(&peer_id)?, &public_key, &payload, &signature) | |
| .map_err(peer_error) | |
| } | |
| fn wrap_stream(stream: engine::PeerStream) -> Result<PeerStream> { | |
| Ok(PeerStream { | |
| peer_id: stream.peer_id.to_string(), | |
| path: peer_stream_path(stream.path), | |
| incoming: Arc::new(AsyncMutex::new(stream.incoming)), | |
| commands: stream.commands, | |
| abort: stream.abort, | |
| }) | |
| } | |
| fn peer_stream_path(path: PeerConnectionPath) -> PeerStreamPath { | |
| match path { | |
| PeerConnectionPath::Direct(transport) => PeerStreamPath { | |
| kind: "direct".to_owned(), | |
| transport: Some( | |
| match transport { | |
| DirectTransport::Quic => "quic", | |
| DirectTransport::Tcp => "tcp", | |
| DirectTransport::WebRtc => "webrtc", | |
| DirectTransport::Other => "other", | |
| } | |
| .to_owned(), | |
| ), | |
| relay_peer_id: None, | |
| }, | |
| PeerConnectionPath::Transit { relay_peer_id } => PeerStreamPath { | |
| kind: "transit".to_owned(), | |
| transport: None, | |
| relay_peer_id: Some(relay_peer_id.to_string()), | |
| }, | |
| } | |
| } | |
| fn parse_peer_id(value: &str) -> Result<PeerId> { | |
| value | |
| .parse() | |
| .map_err(|_| Error::new(Status::InvalidArg, "peer id is invalid")) | |
| } | |
| fn parse_addresses(values: Vec<String>, label: &str) -> Result<Vec<Multiaddr>> { | |
| values | |
| .into_iter() | |
| .map(|value| { | |
| value.parse().map_err(|_| { | |
| Error::new(Status::InvalidArg, format!("{label} multiaddr is invalid")) | |
| }) | |
| }) | |
| .collect() | |
| } | |
| fn parse_connect_addresses(values: Vec<String>, label: &str) -> Result<Vec<Multiaddr>> { | |
| if values.len() > MAX_CONNECT_ROUTES_PER_CLASS { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| format!("peer connection cannot contain more than 32 {label}s"), | |
| )); | |
| } | |
| parse_addresses(values, label) | |
| } | |
| fn parse_webrtc_stun_urls(values: Vec<String>) -> Result<Vec<String>> { | |
| if values.len() > MAX_WEBRTC_STUN_URLS { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "WebRTC cannot use more than 8 STUN URLs", | |
| )); | |
| } | |
| for value in &values { | |
| if value.len() > MAX_WEBRTC_STUN_URL_BYTES | |
| || !value.starts_with("stun:") | |
| || value.chars().any(char::is_whitespace) | |
| { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "WebRTC STUN URL must use the stun: scheme and contain no whitespace", | |
| )); | |
| } | |
| } | |
| Ok(values) | |
| } | |
| fn parse_transit_relay_candidates( | |
| candidates: Vec<PeerTransitRelayCandidate>, | |
| ) -> Result<Vec<engine::TransitRelayCandidate>> { | |
| let address_count = candidates.iter().try_fold(0usize, |count, candidate| { | |
| count | |
| .checked_add(candidate.addresses.len())? | |
| .checked_add(candidate.coordination_relays.len()) | |
| }); | |
| if address_count.is_none_or(|count| count > MAX_TRANSIT_RELAY_ADDRESSES) { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "transit policy cannot contain more than 256 relay addresses", | |
| )); | |
| } | |
| let mut relays = Vec::new(); | |
| for candidate in candidates { | |
| let Ok(expected_peer) = candidate.peer_id.parse::<PeerId>() else { | |
| continue; | |
| }; | |
| let mut addresses = Vec::new(); | |
| for value in candidate.addresses { | |
| let Ok(address) = value.parse::<Multiaddr>() else { | |
| continue; | |
| }; | |
| if engine::transit_relay_peer_id(&address).ok() == Some(expected_peer) { | |
| addresses.push(address); | |
| } | |
| } | |
| let mut coordination_relays = candidate | |
| .coordination_relays | |
| .into_iter() | |
| .filter_map(|value| value.parse::<Multiaddr>().ok()) | |
| .filter(|address| { | |
| engine::coordination_relay_peer_id(address) | |
| .is_ok_and(|peer_id| peer_id != expected_peer) | |
| }) | |
| .collect::<Vec<_>>(); | |
| addresses.sort_unstable_by_key(ToString::to_string); | |
| addresses.dedup(); | |
| coordination_relays.sort_unstable_by_key(ToString::to_string); | |
| coordination_relays.dedup(); | |
| if !addresses.is_empty() || !coordination_relays.is_empty() { | |
| relays.push(engine::TransitRelayCandidate { | |
| peer_id: expected_peer, | |
| addresses, | |
| coordination_relays, | |
| }); | |
| } | |
| } | |
| relays.sort_unstable_by_key(|candidate| candidate.peer_id.to_string()); | |
| relays.dedup_by_key(|candidate| candidate.peer_id); | |
| Ok(relays) | |
| } | |
| fn parse_peer_ids(values: Vec<String>) -> Result<HashSet<PeerId>> { | |
| Ok(parse_peer_id_list(values)?.into_iter().collect()) | |
| } | |
| fn parse_peer_id_list(values: Vec<String>) -> Result<Vec<PeerId>> { | |
| if values.len() > MAX_TRANSIT_PEERS { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "transit policy cannot contain more than 64 peers", | |
| )); | |
| } | |
| let mut peers = Vec::new(); | |
| for value in values { | |
| let peer = parse_peer_id(&value)?; | |
| if !peers.contains(&peer) { | |
| peers.push(peer); | |
| } | |
| } | |
| Ok(peers) | |
| } | |
| fn validate_identity_payload(payload: &[u8]) -> Result<()> { | |
| if payload.is_empty() || payload.len() > IDENTITY_PAYLOAD_MAX_BYTES { | |
| return Err(Error::new( | |
| Status::InvalidArg, | |
| "identity payload must be between 1 and 8192 bytes", | |
| )); | |
| } | |
| Ok(()) | |
| } | |
| fn peer_error(error: PeerError) -> Error { | |
| Error::new( | |
| Status::GenericFailure, | |
| format!("{}: {}", error.code, error.message), | |
| ) | |
| } | |
| fn native_closed_error() -> Error { | |
| peer_error(PeerError { | |
| code: "peer_native_failed", | |
| message: "peer stream is closed".to_owned(), | |
| }) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| fn endpoint_for_watch_tests() -> ( | |
| PeerEndpoint, | |
| watch::Sender<engine::ReachabilitySnapshot>, | |
| watch::Sender<engine::ConnectivitySnapshot>, | |
| ) { | |
| let (reachability_tx, reachability) = watch::channel(Default::default()); | |
| let (connectivity_tx, connectivity) = watch::channel(Default::default()); | |
| let (commands, _command_rx) = mpsc::channel(1); | |
| let (_incoming_tx, incoming) = mpsc::channel(1); | |
| let (_mesh_incoming_tx, mesh_incoming) = mpsc::channel(1); | |
| let (_terminal_tx, terminal) = mpsc::channel(1); | |
| ( | |
| PeerEndpoint { | |
| peer_id: PeerId::random().to_string(), | |
| reachability, | |
| connectivity, | |
| transit_snapshot: Arc::new(RwLock::new(Default::default())), | |
| commands, | |
| incoming: Arc::new(AsyncMutex::new(incoming)), | |
| mesh_incoming: Arc::new(AsyncMutex::new(mesh_incoming)), | |
| terminal: Arc::new(AsyncMutex::new(terminal)), | |
| thread: Arc::new(Mutex::new(None)), | |
| }, | |
| reachability_tx, | |
| connectivity_tx, | |
| ) | |
| } | |
| #[tokio::test] | |
| async fn reachability_watch_waits_for_a_newer_generation() { | |
| let (endpoint, reachability, _) = endpoint_for_watch_tests(); | |
| reachability.send_replace(engine::ReachabilitySnapshot { | |
| generation: 1, | |
| ..Default::default() | |
| }); | |
| let watched = endpoint.watch_reachability(1, 1_000); | |
| tokio::pin!(watched); | |
| assert!( | |
| tokio::time::timeout(Duration::from_millis(10), &mut watched) | |
| .await | |
| .is_err() | |
| ); | |
| reachability.send_replace(engine::ReachabilitySnapshot { | |
| generation: 2, | |
| ..Default::default() | |
| }); | |
| assert_eq!(watched.await.expect("reachability watch"), 2); | |
| } | |
| #[tokio::test] | |
| async fn connectivity_watch_waits_for_a_newer_generation() { | |
| let (endpoint, _, connectivity) = endpoint_for_watch_tests(); | |
| connectivity.send_replace(engine::ConnectivitySnapshot { | |
| generation: 1, | |
| ..Default::default() | |
| }); | |
| let watched = endpoint.watch_connectivity(1, 1_000); | |
| tokio::pin!(watched); | |
| assert!( | |
| tokio::time::timeout(Duration::from_millis(10), &mut watched) | |
| .await | |
| .is_err() | |
| ); | |
| connectivity.send_replace(engine::ConnectivitySnapshot { | |
| generation: 2, | |
| ..Default::default() | |
| }); | |
| assert_eq!(watched.await.expect("connectivity watch").generation, 2); | |
| } | |
| #[test] | |
| fn transit_relay_addresses_are_bound_to_the_declared_peer() { | |
| let expected = PeerId::random(); | |
| let other = PeerId::random(); | |
| let accepted = format!("/ip4/192.0.2.1/tcp/4001/p2p/{expected}"); | |
| let coordination = format!("/ip4/198.51.100.1/tcp/4001/p2p/{other}"); | |
| let relays = parse_transit_relay_candidates(vec![PeerTransitRelayCandidate { | |
| peer_id: expected.to_string(), | |
| addresses: vec![ | |
| accepted.clone(), | |
| format!("/ip4/192.0.2.2/tcp/4001/p2p/{other}"), | |
| "not-a-multiaddr".to_owned(), | |
| ], | |
| coordination_relays: vec![ | |
| coordination.clone(), | |
| format!("/ip4/198.51.100.2/tcp/4001/p2p/{expected}"), | |
| ], | |
| }]) | |
| .expect("candidate policy"); | |
| assert_eq!(relays.len(), 1); | |
| assert_eq!( | |
| relays[0].addresses, | |
| vec![accepted.parse().expect("accepted multiaddr")], | |
| ); | |
| assert_eq!( | |
| relays[0].coordination_relays, | |
| vec![coordination.parse().expect("coordination multiaddr")], | |
| ); | |
| } | |
| #[test] | |
| fn webrtc_configuration_accepts_explicit_host_only_ice_and_rejects_turn() { | |
| assert_eq!( | |
| parse_webrtc_stun_urls(Vec::new()).expect("host-only ICE"), | |
| Vec::<String>::new() | |
| ); | |
| assert!(parse_webrtc_stun_urls(vec!["turn:relay.example:3478".to_owned()]).is_err()); | |
| } | |
| } |