blob: c0cc2dce9bb7cc3e78f672121934360e9a38f57e [file]
/*
* 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());
}
}