源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s

This commit is contained in:
agent-radar
2026-09-01 19:05:14 +08:00
commit 7be535059e
3358 changed files with 1306357 additions and 0 deletions
+4345
View File
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
# 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.
[package]
name = "maka-runtime-host-peer"
version = "0.0.0"
edition = "2024"
license = "Apache-2.0"
rust-version = "1.98"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
async-trait = "0.1"
bytes = "1"
futures = "0.3"
libp2p = { version = "0.56", features = [
"dcutr",
"dns",
"identify",
"kad",
"macros",
"noise",
"ping",
"quic",
"relay",
"rsa",
"tcp",
"tokio",
"yamux",
] }
libp2p-webrtc-utils = "0.4"
napi = { version = "3.12", features = ["napi8", "tokio_rt"] }
napi-derive = "3.5"
prost = "0.14"
serde_json = "1"
thiserror = "2"
tokio = { version = "1.53", features = ["fs", "io-util", "rt-multi-thread", "sync", "time"] }
tokio-util = { version = "0.7", features = ["compat", "rt"] }
unsigned-varint = "0.8"
# v0.21.0-rc.1 predates the rtc fixes qualified in apache/maka#4382.
# Keep this immutable until an official release contains those fixes.
webrtc = { git = "https://github.com/webrtc-rs/webrtc", rev = "e132552fc67b84c30e63c5ce916a9a63e2484b6f" }
[build-dependencies]
napi-build = "2.4"
[profile.release]
# build.mjs strips the copied addon once; Cargo stripping every intermediate artifact is redundant.
strip = "none"
# The CLI ships all four platform addons in one immutable tarball. Optimize the
# network helper for deployed size while preserving unwind semantics at the N-API boundary.
opt-level = "s"
lto = "thin"
codegen-units = 1
+101
View File
@@ -0,0 +1,101 @@
/*
* 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.
*/
import { copyFile, mkdir, readFile } from 'node:fs/promises';
import { spawn } from 'node:child_process';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = dirname(fileURLToPath(import.meta.url));
const encodedRustflags = [
process.env.CARGO_ENCODED_RUSTFLAGS,
`--remap-path-prefix=${root}=native/runtime-host-peer`,
...(process.platform === 'win32' ? ['-Clink-arg=/PDBALTPATH:maka_runtime_host_peer.pdb'] : []),
]
.filter(Boolean)
.join('\x1f');
const cargoSubcommand = process.env.MAKA_RUNTIME_HOST_PEER_CARGO_SUBCOMMAND?.trim() || 'build';
const cargoTarget = process.env.MAKA_RUNTIME_HOST_PEER_CARGO_TARGET?.trim();
await run(
'cargo',
[cargoSubcommand, '--release', '--locked', ...(cargoTarget ? ['--target', cargoTarget] : [])],
root,
{
...process.env,
CARGO_ENCODED_RUSTFLAGS: encodedRustflags,
},
);
const cargoArtifactTarget = cargoTarget?.replace(/\.\d+\.\d+$/u, '');
const releaseDirectory = cargoArtifactTarget
? join(root, 'target', cargoArtifactTarget, 'release')
: join(root, 'target', 'release');
const targetPlatform = cargoTarget
? cargoTarget.includes('windows')
? 'win32'
: cargoTarget.includes('apple')
? 'darwin'
: 'linux'
: process.platform;
const targetArch = cargoTarget
? cargoTarget.startsWith('aarch64-')
? 'arm64'
: cargoTarget.startsWith('x86_64-')
? 'x64'
: undefined
: process.arch;
const library =
targetPlatform === 'win32'
? 'maka_runtime_host_peer.dll'
: targetPlatform === 'darwin'
? 'libmaka_runtime_host_peer.dylib'
: 'libmaka_runtime_host_peer.so';
const destination = process.env.MAKA_RUNTIME_HOST_PEER_OUTPUT?.trim()
? resolve(process.env.MAKA_RUNTIME_HOST_PEER_OUTPUT.trim())
: join(root, 'target', 'release', 'maka_runtime_host_peer.node');
await mkdir(dirname(destination), { recursive: true });
await copyFile(join(releaseDirectory, library), destination);
if (targetPlatform === 'darwin' && process.platform === 'darwin') {
await run('install_name_tool', ['-id', '@rpath/maka_runtime_host_peer.node', destination], root, {
...process.env,
});
await run('strip', ['-x', destination], root, { ...process.env });
} else if (
targetPlatform === 'linux' &&
process.platform === 'linux' &&
targetArch === process.arch
) {
await run('strip', ['--strip-unneeded', destination], root, { ...process.env });
}
if ((await readFile(destination)).includes(Buffer.from(root))) {
throw new Error('The Runtime Host peer addon contains its build path');
}
process.stdout.write(`${destination}\n`);
function run(command, args, cwd, env) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, env, stdio: 'inherit' });
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) resolve();
else reject(new Error(`${command} failed: ${signal ?? code}`));
});
});
}
+22
View File
@@ -0,0 +1,22 @@
/*
* 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.
*/
fn main() {
napi_build::setup();
}
@@ -0,0 +1,21 @@
# 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.
[toolchain]
channel = "stable"
components = ["clippy", "rustfmt"]
profile = "minimal"
+679
View File
@@ -0,0 +1,679 @@
/*
* 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_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 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 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]
pub struct PeerEndpoint {
peer_id: String,
listen_addresses: Vec<String>,
active_coordination_relays: Arc<RwLock<Vec<Multiaddr>>>,
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 listen_addresses(&self) -> Vec<String> {
self.listen_addresses.clone()
}
#[napi(getter)]
pub fn active_coordination_relays(&self) -> Vec<String> {
self.active_coordination_relays
.read()
.map(|addresses| addresses.iter().map(ToString::to_string).collect())
.unwrap_or_default()
}
#[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 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_addresses(options.route_hints, "route hint")?;
let coordination_relays = parse_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),
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(),
listen_addresses: started
.listen_addresses
.into_iter()
.map(|address| address.to_string())
.collect(),
active_coordination_relays: started.active_coordination_relays,
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))),
})
}
#[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_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::*;
#[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());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
/*
* 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 libp2p::{Multiaddr, PeerId, multiaddr::Protocol};
use super::PeerError;
pub(super) fn address_with_expected_peer(
address: &Multiaddr,
expected_peer: PeerId,
) -> Result<Multiaddr, PeerError> {
match peer_id_from_address(address) {
Some(actual) if actual != expected_peer => Err(PeerError::new(
"peer_identity_mismatch",
"route hint names a different peer identity",
)),
Some(_) => Ok(address.clone()),
None => Ok(address.clone().with(Protocol::P2p(expected_peer))),
}
}
pub(super) fn address_with_peer(address: Multiaddr, peer_id: PeerId) -> Multiaddr {
if peer_id_from_address(&address) == Some(peer_id) {
address
} else {
address.with(Protocol::P2p(peer_id))
}
}
pub(super) fn peer_id_from_address(address: &Multiaddr) -> Option<PeerId> {
address.iter().find_map(|protocol| match protocol {
Protocol::P2p(peer_id) => Some(peer_id),
_ => None,
})
}
pub(crate) fn coordination_relay_peer_id(address: &Multiaddr) -> Result<PeerId, PeerError> {
base_relay_peer_id(address, "coordination_unavailable", "coordination relay")
}
pub(crate) fn transit_relay_peer_id(address: &Multiaddr) -> Result<PeerId, PeerError> {
base_relay_peer_id(address, "transit_unavailable", "transit relay")
}
fn base_relay_peer_id(
address: &Multiaddr,
error_code: &'static str,
label: &str,
) -> Result<PeerId, PeerError> {
let mut peer_id = None;
for protocol in address.iter() {
match protocol {
Protocol::P2p(_) if peer_id.is_some() => {
return Err(PeerError::new(
error_code,
format!("{label} address must name exactly one peer"),
));
}
Protocol::P2p(value) => peer_id = Some(value),
Protocol::P2pCircuit => {
return Err(PeerError::new(
error_code,
format!("{label} address must be a base relay address"),
));
}
_ => {}
}
}
match (peer_id, address.iter().last()) {
(Some(peer_id), Some(Protocol::P2p(terminal))) if peer_id == terminal => Ok(peer_id),
_ => Err(PeerError::new(
error_code,
format!("{label} address must end with its peer identity"),
)),
}
}
pub(super) fn is_relayed_address(address: &Multiaddr) -> bool {
address
.iter()
.any(|protocol| matches!(protocol, Protocol::P2pCircuit))
}
pub(super) fn relay_peer_id_from_circuit_address(address: &Multiaddr) -> Option<PeerId> {
let mut previous_peer = None;
for protocol in address.iter() {
match protocol {
Protocol::P2p(peer_id) => previous_peer = Some(peer_id),
Protocol::P2pCircuit => return previous_peer,
_ => {}
}
}
None
}
@@ -0,0 +1,747 @@
/*
* 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::{HashMap, HashSet},
convert::Infallible,
future::{Ready, ready},
io,
sync::{Arc, Mutex, MutexGuard},
task::{Context, Poll},
};
use libp2p::{
Multiaddr, PeerId,
core::{
Endpoint,
transport::PortUse,
upgrade::{InboundUpgrade, OutboundUpgrade, UpgradeInfo},
},
swarm::{
ConnectionDenied, ConnectionHandler, ConnectionId, FromSwarm, NetworkBehaviour, Stream,
StreamProtocol, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
behaviour::ConnectionClosed,
handler::{
ConnectionEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound,
},
},
};
use tokio::sync::{mpsc, oneshot};
use super::{
address::relay_peer_id_from_circuit_address,
peer_stream::{DirectTransport, PeerConnectionPath},
};
const OUTBOUND_COMMAND_CAPACITY: usize = 1;
const OUTBOUND_STREAM_OPEN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
pub(super) struct Behaviour {
protocol: StreamProtocol,
trusted_transit_relays: Option<Arc<std::sync::RwLock<HashSet<PeerId>>>>,
incoming: mpsc::Sender<InboundStream>,
shared: Arc<Mutex<DirectConnections>>,
}
pub(super) struct InboundStream {
pub(super) peer_id: PeerId,
pub(super) connection_id: ConnectionId,
pub(super) path: PeerConnectionPath,
pub(super) stream: Stream,
}
impl Behaviour {
pub(super) fn new(
protocol: StreamProtocol,
incoming_capacity: usize,
trusted_transit_relays: Option<Arc<std::sync::RwLock<HashSet<PeerId>>>>,
) -> (Self, Control, mpsc::Receiver<InboundStream>) {
let (incoming, receiver) = mpsc::channel(incoming_capacity);
let shared = Arc::new(Mutex::new(DirectConnections::default()));
(
Self {
protocol,
trusted_transit_relays,
incoming,
shared: shared.clone(),
},
Control { shared },
receiver,
)
}
fn handler(
&mut self,
connection_id: ConnectionId,
peer_id: PeerId,
path: PeerConnectionPath,
allow_relayed: bool,
) -> Handler {
let relay_peer_id = path.relay_peer_id();
if relay_peer_id.is_some()
&& !allow_relayed
&& self.trusted_transit_relays.as_ref().is_some_and(|trusted| {
!trusted
.read()
.map(|peers| relay_peer_id.is_some_and(|peer| peers.contains(&peer)))
.unwrap_or(false)
})
{
lock(&self.shared).insert(connection_id, peer_id, path, None);
return Handler::relayed();
}
let (sender, receiver) = mpsc::channel(OUTBOUND_COMMAND_CAPACITY);
lock(&self.shared).insert(connection_id, peer_id, path.clone(), Some(sender));
Handler::direct(
connection_id,
peer_id,
path,
self.protocol.clone(),
self.incoming.clone(),
receiver,
)
}
}
impl NetworkBehaviour for Behaviour {
type ConnectionHandler = Handler;
type ToSwarm = Infallible;
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer_id: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(self.handler(
connection_id,
peer_id,
connection_path(local_addr, remote_addr),
false,
))
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer_id: PeerId,
address: &Multiaddr,
_: Endpoint,
_: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
Ok(self.handler(
connection_id,
peer_id,
connection_path(address, address),
true,
))
}
fn on_swarm_event(&mut self, event: FromSwarm) {
if let FromSwarm::ConnectionClosed(ConnectionClosed { connection_id, .. }) = event {
lock(&self.shared).remove(connection_id);
}
}
fn on_connection_handler_event(
&mut self,
_: PeerId,
_: ConnectionId,
event: THandlerOutEvent<Self>,
) {
libp2p::core::util::unreachable(event);
}
fn poll(&mut self, _: &mut Context<'_>) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
Poll::Pending
}
}
#[derive(Clone)]
pub(super) struct Control {
shared: Arc<Mutex<DirectConnections>>,
}
impl Control {
pub(super) fn relays_with_active_streams(
&self,
active_streams: &HashMap<ConnectionId, usize>,
) -> HashSet<PeerId> {
lock(&self.shared)
.connections
.iter()
.filter_map(|(connection_id, connection)| {
active_streams
.contains_key(connection_id)
.then_some(connection.path.relay_peer_id())
.flatten()
})
.collect()
}
pub(super) fn connections_via(&self, relays: &HashSet<PeerId>) -> Vec<ConnectionId> {
lock(&self.shared)
.connections
.iter()
.filter_map(|(connection_id, connection)| {
connection
.path
.relay_peer_id()
.is_some_and(|relay| relays.contains(&relay))
.then_some(*connection_id)
})
.collect()
}
pub(super) fn has_connection(
&self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> bool {
!self
.eligible_connections(peer_id, excluded, allowed_relays)
.is_empty()
}
pub(super) fn eligible_connections(
&self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> Vec<(ConnectionId, PeerConnectionPath)> {
lock(&self.shared).eligible_connections(peer_id, excluded, allowed_relays)
}
pub(super) fn has_relayed_connection(&self, peer_id: PeerId) -> bool {
lock(&self.shared).connections.values().any(|connection| {
connection.peer_id == peer_id && connection.path.relay_peer_id().is_some()
})
}
pub(super) fn has_relayed_connection_via(
&self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> bool {
lock(&self.shared)
.relayed_connection(peer_id, excluded, allowed_relays)
.is_some()
}
pub(super) async fn open_stream_on(
&mut self,
connection_id: ConnectionId,
peer_id: PeerId,
allowed_relays: &HashSet<PeerId>,
) -> Result<OpenedStream, OpenStreamError> {
let (path, sender) = lock(&self.shared)
.connection_by_id(connection_id, peer_id, allowed_relays)
.ok_or(OpenStreamError::NoEligibleConnection)?;
let (result, receiver) = oneshot::channel();
sender
.send(NewStream { result })
.await
.map_err(|_| OpenStreamError::ConnectionClosed)?;
let stream = receiver
.await
.map_err(|_| OpenStreamError::ConnectionClosed)??;
Ok(OpenedStream { path, stream })
}
pub(super) async fn open_relayed_stream(
&mut self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> Result<OpenedStream, OpenStreamError> {
let (relay_peer_id, sender) = lock(&self.shared)
.relayed_connection(peer_id, excluded, allowed_relays)
.ok_or(OpenStreamError::NoEligibleConnection)?;
let (result, receiver) = oneshot::channel();
sender
.send(NewStream { result })
.await
.map_err(|_| OpenStreamError::ConnectionClosed)?;
let stream = receiver
.await
.map_err(|_| OpenStreamError::ConnectionClosed)??;
Ok(OpenedStream {
path: PeerConnectionPath::Transit { relay_peer_id },
stream,
})
}
}
pub(super) struct OpenedStream {
pub(super) path: PeerConnectionPath,
pub(super) stream: Stream,
}
#[derive(Debug)]
pub(super) enum OpenStreamError {
NoEligibleConnection,
ConnectionClosed,
UnsupportedProtocol,
Io(io::Error),
}
impl std::fmt::Display for OpenStreamError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoEligibleConnection => {
write!(
formatter,
"peer has no verified direct or approved transit connection"
)
}
Self::ConnectionClosed => write!(formatter, "direct connection closed"),
Self::UnsupportedProtocol => {
write!(formatter, "peer does not support the application protocol")
}
Self::Io(error) => error.fmt(formatter),
}
}
}
#[derive(Default)]
struct DirectConnections {
connections: HashMap<ConnectionId, DirectConnection>,
}
struct DirectConnection {
peer_id: PeerId,
path: PeerConnectionPath,
sender: Option<mpsc::Sender<NewStream>>,
}
impl DirectConnections {
fn insert(
&mut self,
connection_id: ConnectionId,
peer_id: PeerId,
path: PeerConnectionPath,
sender: Option<mpsc::Sender<NewStream>>,
) {
self.connections.insert(
connection_id,
DirectConnection {
peer_id,
path,
sender,
},
);
}
fn remove(&mut self, connection_id: ConnectionId) {
self.connections.remove(&connection_id);
}
fn eligible_connections(
&self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> Vec<(ConnectionId, PeerConnectionPath)> {
self.connections
.iter()
.filter_map(|(connection_id, connection)| {
let sender = connection.sender.as_ref()?;
(connection.peer_id == peer_id
&& !excluded.contains(connection_id)
&& connection
.path
.relay_peer_id()
.is_none_or(|relay| allowed_relays.contains(&relay))
&& !sender.is_closed())
.then(|| (*connection_id, connection.path.clone()))
})
.collect()
}
fn connection_by_id(
&self,
connection_id: ConnectionId,
peer_id: PeerId,
allowed_relays: &HashSet<PeerId>,
) -> Option<(PeerConnectionPath, mpsc::Sender<NewStream>)> {
let connection = self.connections.get(&connection_id)?;
let sender = connection.sender.as_ref()?;
(connection.peer_id == peer_id
&& connection
.path
.relay_peer_id()
.is_none_or(|relay| allowed_relays.contains(&relay))
&& !sender.is_closed())
.then(|| (connection.path.clone(), sender.clone()))
}
fn relayed_connection(
&self,
peer_id: PeerId,
excluded: &HashSet<ConnectionId>,
allowed_relays: &HashSet<PeerId>,
) -> Option<(PeerId, mpsc::Sender<NewStream>)> {
self.connections
.iter()
.find_map(|(connection_id, connection)| {
let sender = connection.sender.as_ref()?;
let relay = connection.path.relay_peer_id()?;
(connection.peer_id == peer_id
&& !excluded.contains(connection_id)
&& allowed_relays.contains(&relay)
&& !sender.is_closed())
.then(|| (relay, sender.clone()))
})
}
}
fn lock(shared: &Arc<Mutex<DirectConnections>>) -> MutexGuard<'_, DirectConnections> {
shared
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn connection_path(local_addr: &Multiaddr, remote_addr: &Multiaddr) -> PeerConnectionPath {
if let Some(relay_peer_id) = relay_peer_id_from_circuit_address(local_addr)
.or_else(|| relay_peer_id_from_circuit_address(remote_addr))
{
return PeerConnectionPath::Transit { relay_peer_id };
}
let transport = local_addr
.iter()
.chain(remote_addr.iter())
.find_map(|protocol| match protocol {
libp2p::multiaddr::Protocol::WebRTC => Some(DirectTransport::WebRtc),
libp2p::multiaddr::Protocol::QuicV1 => Some(DirectTransport::Quic),
libp2p::multiaddr::Protocol::Tcp(_) => Some(DirectTransport::Tcp),
_ => None,
})
.unwrap_or(DirectTransport::Other);
PeerConnectionPath::Direct(transport)
}
pub(super) struct Handler {
connection_id: Option<ConnectionId>,
peer_id: Option<PeerId>,
path: Option<PeerConnectionPath>,
protocol: Option<StreamProtocol>,
incoming: Option<mpsc::Sender<InboundStream>>,
commands: Option<mpsc::Receiver<NewStream>>,
pending: HashMap<u64, oneshot::Sender<Result<Stream, OpenStreamError>>>,
next_request_id: u64,
}
impl Handler {
fn direct(
connection_id: ConnectionId,
peer_id: PeerId,
path: PeerConnectionPath,
protocol: StreamProtocol,
incoming: mpsc::Sender<InboundStream>,
commands: mpsc::Receiver<NewStream>,
) -> Self {
Self {
connection_id: Some(connection_id),
peer_id: Some(peer_id),
path: Some(path),
protocol: Some(protocol),
incoming: Some(incoming),
commands: Some(commands),
pending: HashMap::new(),
next_request_id: 0,
}
}
fn relayed() -> Self {
Self {
connection_id: None,
peer_id: None,
path: None,
protocol: None,
incoming: None,
commands: None,
pending: HashMap::new(),
next_request_id: 0,
}
}
}
impl ConnectionHandler for Handler {
type FromBehaviour = Infallible;
type ToBehaviour = Infallible;
type InboundProtocol = ProtocolUpgrade;
type OutboundProtocol = ProtocolUpgrade;
type InboundOpenInfo = ();
type OutboundOpenInfo = u64;
fn listen_protocol(
&self,
) -> libp2p::swarm::SubstreamProtocol<Self::InboundProtocol, Self::InboundOpenInfo> {
libp2p::swarm::SubstreamProtocol::new(
ProtocolUpgrade(self.protocol.iter().cloned().collect()),
(),
)
}
fn on_behaviour_event(&mut self, event: Self::FromBehaviour) {
libp2p::core::util::unreachable(event);
}
fn poll(
&mut self,
context: &mut Context<'_>,
) -> Poll<libp2p::swarm::ConnectionHandlerEvent<Self::OutboundProtocol, u64, Self::ToBehaviour>>
{
let Some(commands) = self.commands.as_mut() else {
return Poll::Pending;
};
loop {
match commands.poll_recv(context) {
Poll::Ready(Some(command)) if command.result.is_closed() => continue,
Poll::Ready(Some(command)) => {
let protocol = self
.protocol
.clone()
.expect("only direct handlers receive stream commands");
let request_id = self.next_request_id;
self.next_request_id = self.next_request_id.wrapping_add(1);
self.pending.insert(request_id, command.result);
return Poll::Ready(
libp2p::swarm::ConnectionHandlerEvent::OutboundSubstreamRequest {
protocol: libp2p::swarm::SubstreamProtocol::new(
ProtocolUpgrade(vec![protocol]),
request_id,
)
.with_timeout(OUTBOUND_STREAM_OPEN_TIMEOUT),
},
);
}
Poll::Ready(None) | Poll::Pending => return Poll::Pending,
}
}
}
fn on_connection_event(
&mut self,
event: ConnectionEvent<
Self::InboundProtocol,
Self::OutboundProtocol,
Self::InboundOpenInfo,
Self::OutboundOpenInfo,
>,
) {
match event {
ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
protocol: (stream, _),
..
}) => {
if let Some(incoming) = self.incoming.as_ref() {
let _ = incoming.try_send(InboundStream {
connection_id: self
.connection_id
.expect("direct handlers have a connection id"),
peer_id: self.peer_id.expect("direct handlers have a peer id"),
path: self.path.clone().expect("direct handlers have a path"),
stream,
});
}
}
ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound {
protocol: (stream, _),
info,
}) => {
let Some(result) = self.pending.remove(&info) else {
return;
};
let _ = result.send(Ok(stream));
}
ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => {
let Some(result) = self.pending.remove(&info) else {
return;
};
let error = match error {
libp2p::swarm::StreamUpgradeError::Timeout => {
OpenStreamError::Io(io::Error::from(io::ErrorKind::TimedOut))
}
libp2p::swarm::StreamUpgradeError::Apply(value) => {
libp2p::core::util::unreachable(value)
}
libp2p::swarm::StreamUpgradeError::NegotiationFailed => {
OpenStreamError::UnsupportedProtocol
}
libp2p::swarm::StreamUpgradeError::Io(error) => OpenStreamError::Io(error),
};
let _ = result.send(Err(error));
}
_ => {}
}
}
}
struct NewStream {
result: oneshot::Sender<Result<Stream, OpenStreamError>>,
}
#[derive(Clone)]
pub(super) struct ProtocolUpgrade(Vec<StreamProtocol>);
impl UpgradeInfo for ProtocolUpgrade {
type Info = StreamProtocol;
type InfoIter = std::vec::IntoIter<StreamProtocol>;
fn protocol_info(&self) -> Self::InfoIter {
self.0.clone().into_iter()
}
}
impl InboundUpgrade<Stream> for ProtocolUpgrade {
type Output = (Stream, StreamProtocol);
type Error = Infallible;
type Future = Ready<Result<Self::Output, Self::Error>>;
fn upgrade_inbound(self, stream: Stream, protocol: StreamProtocol) -> Self::Future {
ready(Ok((stream, protocol)))
}
}
impl OutboundUpgrade<Stream> for ProtocolUpgrade {
type Output = (Stream, StreamProtocol);
type Error = Infallible;
type Future = Ready<Result<Self::Output, Self::Error>>;
fn upgrade_outbound(self, stream: Stream, protocol: StreamProtocol) -> Self::Future {
ready(Ok((stream, protocol)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn application_protocol_is_registered_only_on_direct_or_trusted_transit_connections() {
let protocol = StreamProtocol::new("/maka/test/1");
let peer_id = PeerId::random();
let relay_peer_id = PeerId::random();
let trusted = Arc::new(std::sync::RwLock::new(HashSet::new()));
let (mut behaviour, control, _) =
Behaviour::new(protocol.clone(), 1, Some(Arc::clone(&trusted)));
let relay_address: Multiaddr =
format!("/ip4/127.0.0.1/udp/1/quic-v1/p2p/{relay_peer_id}/p2p-circuit")
.parse()
.expect("relay address");
let relayed = behaviour
.handle_established_inbound_connection(
ConnectionId::new_unchecked(1),
peer_id,
&relay_address,
&relay_address,
)
.expect("relayed handler");
assert_eq!(
relayed.listen_protocol().upgrade().protocol_info().count(),
0
);
assert!(
lock(&control.shared)
.connection_by_id(ConnectionId::new_unchecked(1), peer_id, &HashSet::new(),)
.is_none()
);
assert!(!control.has_connection(peer_id, &HashSet::new(), &HashSet::new()));
assert!(control.has_relayed_connection(peer_id));
trusted
.write()
.expect("trusted relays")
.insert(relay_peer_id);
let trusted_relay = behaviour
.handle_established_inbound_connection(
ConnectionId::new_unchecked(2),
peer_id,
&relay_address,
&relay_address,
)
.expect("trusted relayed handler");
assert_eq!(
trusted_relay
.listen_protocol()
.upgrade()
.protocol_info()
.collect::<Vec<_>>(),
vec![protocol.clone()]
);
let (relay_path, _) = lock(&control.shared)
.connection_by_id(
ConnectionId::new_unchecked(2),
peer_id,
&HashSet::from([relay_peer_id]),
)
.expect("trusted relay connection");
assert_eq!(relay_path, PeerConnectionPath::Transit { relay_peer_id });
let relay_connections = control.connections_via(&HashSet::from([relay_peer_id]));
assert_eq!(relay_connections.len(), 2);
assert!(relay_connections.contains(&ConnectionId::new_unchecked(1)));
assert!(relay_connections.contains(&ConnectionId::new_unchecked(2)));
let direct = behaviour
.handle_established_outbound_connection(
ConnectionId::new_unchecked(3),
peer_id,
&"/ip4/127.0.0.1/udp/1/quic-v1"
.parse()
.expect("direct address"),
Endpoint::Dialer,
PortUse::Reuse,
)
.expect("direct handler");
assert_eq!(
direct
.listen_protocol()
.upgrade()
.protocol_info()
.collect::<Vec<_>>(),
vec![protocol]
);
assert!(
lock(&control.shared)
.connection_by_id(ConnectionId::new_unchecked(3), peer_id, &HashSet::new(),)
.is_some()
);
let (direct_path, _) = lock(&control.shared)
.connection_by_id(ConnectionId::new_unchecked(3), peer_id, &HashSet::new())
.expect("direct connection");
assert_eq!(
direct_path,
PeerConnectionPath::Direct(DirectTransport::Quic)
);
assert!(control.has_connection(peer_id, &HashSet::new(), &HashSet::new()));
assert!(!control.has_connection(
peer_id,
&HashSet::from([
ConnectionId::new_unchecked(2),
ConnectionId::new_unchecked(3),
]),
&HashSet::new(),
));
}
}
@@ -0,0 +1,123 @@
/*
* 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::{
path::Path,
sync::atomic::{AtomicU64, Ordering},
};
use libp2p::identity;
use tokio::io::AsyncWriteExt as _;
use super::{PeerError, native_error};
static TEMPORARY_KEY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
pub(super) async fn load_or_create_key(path: &Path) -> Result<identity::Keypair, PeerError> {
match tokio::fs::read(path).await {
Ok(bytes) => identity::Keypair::from_protobuf_encoding(&bytes)
.map_err(|error| PeerError::new("peer_native_failed", error.to_string())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
tokio::fs::create_dir_all(parent)
.await
.map_err(native_error)?;
}
let key = identity::Keypair::generate_ed25519();
let encoded = key.to_protobuf_encoding().map_err(native_error)?;
match publish_private_file(path, &encoded).await {
Ok(()) => Ok(key),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let bytes = tokio::fs::read(path).await.map_err(native_error)?;
identity::Keypair::from_protobuf_encoding(&bytes)
.map_err(|error| PeerError::new("peer_native_failed", error.to_string()))
}
Err(error) => Err(native_error(error)),
}
}
Err(error) => Err(native_error(error)),
}
}
pub(super) async fn load_key(path: &Path) -> Result<identity::Keypair, PeerError> {
let bytes = tokio::fs::read(path).await.map_err(native_error)?;
identity::Keypair::from_protobuf_encoding(&bytes)
.map_err(|error| PeerError::new("peer_native_failed", error.to_string()))
}
async fn publish_private_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new(""));
let temporary = loop {
let sequence = TEMPORARY_KEY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let candidate = parent.join(format!(
".maka-peer-key-{}-{sequence}.tmp",
std::process::id()
));
let mut options = tokio::fs::OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
options.mode(0o600);
}
match options.open(&candidate).await {
Ok(file) => break (candidate, file),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
};
let (temporary_path, mut file) = temporary;
let publish = async {
file.write_all(bytes).await?;
file.sync_all().await?;
drop(file);
tokio::fs::hard_link(&temporary_path, path).await
}
.await;
let cleanup = tokio::fs::remove_file(&temporary_path).await;
publish?;
cleanup
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn concurrent_creation_adopts_one_published_identity() {
let path = std::env::temp_dir().join(format!(
"maka-peer-key-test-{}-{}",
std::process::id(),
TEMPORARY_KEY_SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime");
let (first, second) = runtime.block_on(async {
futures::future::join(load_or_create_key(&path), load_or_create_key(&path)).await
});
let first = first.expect("first identity");
let second = second.expect("second identity");
assert_eq!(first.public().to_peer_id(), second.public().to_peer_id());
std::fs::remove_file(path).expect("remove test identity");
}
}
@@ -0,0 +1,174 @@
/*
* 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 futures::{AsyncReadExt as _, AsyncWriteExt as _};
use libp2p::PeerId;
use tokio::sync::{mpsc, oneshot, watch};
use super::{CompletedStream, PeerError, StreamCompletion};
const QUEUE_CAPACITY: usize = 64;
const CHUNK_BYTES: usize = 64 * 1024;
pub struct PeerStream {
pub peer_id: PeerId,
pub path: PeerConnectionPath,
pub incoming: mpsc::Receiver<Result<Vec<u8>, PeerError>>,
pub commands: mpsc::Sender<StreamCommand>,
pub abort: watch::Sender<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DirectTransport {
Quic,
Tcp,
WebRtc,
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PeerConnectionPath {
Direct(DirectTransport),
Transit { relay_peer_id: PeerId },
}
impl PeerConnectionPath {
pub(super) fn relay_peer_id(&self) -> Option<PeerId> {
match self {
Self::Direct(_) => None,
Self::Transit { relay_peer_id } => Some(*relay_peer_id),
}
}
}
pub enum StreamCommand {
Write {
bytes: Vec<u8>,
result: oneshot::Sender<Result<(), PeerError>>,
},
Close {
result: oneshot::Sender<Result<(), PeerError>>,
},
}
pub(super) fn spawn_stream(
peer_id: PeerId,
path: PeerConnectionPath,
stream: libp2p::swarm::Stream,
completion: Option<(StreamCompletion, mpsc::Sender<CompletedStream>)>,
) -> PeerStream {
let (incoming_tx, incoming_rx) = mpsc::channel(QUEUE_CAPACITY);
let (command_tx, mut command_rx) = mpsc::channel(QUEUE_CAPACITY);
let (abort_tx, mut abort_rx) = watch::channel(false);
let abort_guard = abort_tx.clone();
tokio::spawn(async move {
let _abort_guard = abort_guard;
let (mut reader, mut writer) = stream.split();
let mut buffer = vec![0_u8; CHUNK_BYTES];
let mut pending_read: Option<Result<Vec<u8>, PeerError>> = None;
let mut finish_after_delivery = false;
let mut close_result = None;
loop {
tokio::select! {
biased;
changed = abort_rx.changed() => {
let _ = changed;
break;
}
command = command_rx.recv() => match command {
Some(StreamCommand::Write { bytes, result }) => {
let outcome = tokio::select! {
biased;
_ = abort_rx.changed() => Err(PeerError::new(
"peer_stream_aborted",
"peer stream was aborted",
)),
outcome = async {
writer.write_all(&bytes).await?;
// Framed transports may buffer a complete write until flush.
writer.flush().await
} => outcome.map_err(|error| {
PeerError::new("peer_native_failed", error.to_string())
}),
};
let failed = outcome.is_err();
let _ = result.send(outcome);
if failed { break; }
}
Some(StreamCommand::Close { result }) => {
let outcome = tokio::select! {
biased;
_ = abort_rx.changed() => Err(PeerError::new(
"peer_stream_aborted",
"peer stream was aborted",
)),
outcome = writer.close() => outcome.map_err(|error| {
PeerError::new("peer_native_failed", error.to_string())
}),
};
close_result = Some((result, outcome));
break;
}
None => break,
},
permit = incoming_tx.reserve(), if pending_read.is_some() => match permit {
Ok(permit) => {
permit.send(pending_read.take().expect("read is pending"));
if finish_after_delivery { break; }
}
Err(_) => break,
},
read = reader.read(&mut buffer), if pending_read.is_none() => match read {
Ok(0) => break,
Ok(size) => pending_read = Some(Ok(buffer[..size].to_vec())),
Err(error) => {
pending_read = Some(Err(PeerError::new(
"peer_native_failed",
error.to_string(),
)));
finish_after_delivery = true;
}
},
}
}
if let Some((completion, completed)) = completion {
let (acknowledged, acknowledgment) = oneshot::channel();
if completed
.send(CompletedStream {
kind: completion,
acknowledged,
})
.await
.is_ok()
{
let _ = acknowledgment.await;
}
}
if let Some((result, outcome)) = close_result {
let _ = result.send(outcome);
}
});
PeerStream {
peer_id,
path,
incoming: incoming_rx,
commands: command_tx,
abort: abort_tx,
}
}
@@ -0,0 +1,504 @@
/*
* 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::{HashMap, HashSet, VecDeque, hash_map::Entry},
ops::{Deref, DerefMut},
task::{Context, Poll},
time::{Duration, Instant},
};
use futures::StreamExt;
use libp2p::{
Multiaddr, PeerId, StreamProtocol, Swarm, SwarmBuilder, connection_limits,
core::{Endpoint, transport::PortUse},
identify, identity,
kad::{self, store::MemoryStore},
noise, ping,
swarm::{
ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, SwarmEvent, THandler,
THandlerInEvent, THandlerOutEvent, ToSwarm, dial_opts::DialOpts,
},
tcp, yamux,
};
use tokio::sync::mpsc;
use super::{address::address_with_peer, supported_public_relay_address};
const AMINO_PROTOCOL: &str = "/ipfs/kad/1.0.0";
const IDENTIFY_PROTOCOL: &str = "/maka/runtime-host/relay-discovery/1";
const RELAY_HOP_PROTOCOL: &str = "/libp2p/circuit/relay/0.2.0/hop";
const DISCOVERY_INTERVAL: Duration = Duration::from_secs(30);
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
const MAX_PENDING_PROBES: usize = 8;
const MAX_KNOWN_PEERS: usize = 256;
const MAX_ADDRESSES_PER_PEER: usize = 8;
const PROBE_REVISIT_INTERVAL: Duration = Duration::from_secs(10 * 60);
const AMINO_BOOTSTRAPS: &[&str] = &[
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
"/dnsaddr/va1.bootstrap.libp2p.io/p2p/12D3KooWKnDdG3iXw9eTFijk3EWSunZcFi54Zka4wmtqtt6rPxc8",
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
"/ip4/104.131.131.82/udp/4001/quic-v1/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
];
struct KnownPeer {
addresses: Vec<Multiaddr>,
first_seen: Instant,
}
#[derive(Debug)]
pub(super) struct RelayCandidate {
pub peer_id: PeerId,
pub addresses: Vec<Multiaddr>,
}
#[derive(NetworkBehaviour)]
struct Behaviour {
connection_limits: connection_limits::Behaviour,
kad: PublicKad,
identify: identify::Behaviour,
ping: ping::Behaviour,
}
struct PublicKad(kad::Behaviour<MemoryStore>);
impl Deref for PublicKad {
type Target = kad::Behaviour<MemoryStore>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PublicKad {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl NetworkBehaviour for PublicKad {
type ConnectionHandler = <kad::Behaviour<MemoryStore> as NetworkBehaviour>::ConnectionHandler;
type ToSwarm = <kad::Behaviour<MemoryStore> as NetworkBehaviour>::ToSwarm;
fn handle_pending_inbound_connection(
&mut self,
connection_id: ConnectionId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<(), ConnectionDenied> {
self.0
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
}
fn handle_established_inbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
local_addr: &Multiaddr,
remote_addr: &Multiaddr,
) -> Result<THandler<Self>, ConnectionDenied> {
self.0
.handle_established_inbound_connection(connection_id, peer, local_addr, remote_addr)
}
fn handle_pending_outbound_connection(
&mut self,
connection_id: ConnectionId,
maybe_peer: Option<PeerId>,
addresses: &[Multiaddr],
effective_role: Endpoint,
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
let addresses = self.0.handle_pending_outbound_connection(
connection_id,
maybe_peer,
addresses,
effective_role,
)?;
Ok(addresses
.into_iter()
.filter(|address| allowed_kad_dial_address(maybe_peer, address))
.collect())
}
fn handle_established_outbound_connection(
&mut self,
connection_id: ConnectionId,
peer: PeerId,
address: &Multiaddr,
role_override: Endpoint,
port_use: PortUse,
) -> Result<THandler<Self>, ConnectionDenied> {
self.0.handle_established_outbound_connection(
connection_id,
peer,
address,
role_override,
port_use,
)
}
fn on_swarm_event(&mut self, event: FromSwarm) {
self.0.on_swarm_event(event);
}
fn on_connection_handler_event(
&mut self,
peer_id: PeerId,
connection_id: ConnectionId,
event: THandlerOutEvent<Self>,
) {
self.0
.on_connection_handler_event(peer_id, connection_id, event);
}
fn poll(
&mut self,
cx: &mut Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
self.0.poll(cx)
}
}
pub(super) fn spawn() -> mpsc::Receiver<RelayCandidate> {
let (sender, receiver) = mpsc::channel(32);
tokio::spawn(async move {
match build_swarm() {
Ok(mut swarm) => run(&mut swarm, sender).await,
Err(error) => eprintln!("[peer-relay-discovery] unavailable: {error}"),
}
});
receiver
}
fn build_swarm() -> Result<Swarm<Behaviour>, Box<dyn std::error::Error + Send + Sync>> {
let key = identity::Keypair::generate_ed25519();
let local_peer_id = key.public().to_peer_id();
let swarm = SwarmBuilder::with_existing_identity(key)
.with_tokio()
.with_tcp(
tcp::Config::default().nodelay(true),
noise::Config::new,
yamux::Config::default,
)?
.with_quic()
.with_dns()?
.with_behaviour(move |key| {
let mut config = kad::Config::new(StreamProtocol::new(AMINO_PROTOCOL));
config.set_query_timeout(Duration::from_secs(15));
let mut kad =
kad::Behaviour::with_config(local_peer_id, MemoryStore::new(local_peer_id), config);
kad.set_mode(Some(kad::Mode::Client));
Behaviour {
connection_limits: connection_limits::Behaviour::new(
connection_limits::ConnectionLimits::default()
.with_max_pending_outgoing(Some(MAX_PENDING_PROBES as u32))
.with_max_established_outgoing(Some(32))
.with_max_established(Some(32))
.with_max_established_per_peer(Some(1)),
),
kad: PublicKad(kad),
identify: identify::Behaviour::new(discovery_identify_config(key.public())),
ping: ping::Behaviour::new(ping::Config::new()),
}
})?
.with_swarm_config(|config| config.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
.build();
Ok(swarm)
}
fn discovery_identify_config(public_key: identity::PublicKey) -> identify::Config {
identify::Config::new(IDENTIFY_PROTOCOL.to_owned(), public_key).with_cache_size(0)
}
async fn run(swarm: &mut Swarm<Behaviour>, sender: mpsc::Sender<RelayCandidate>) {
let mut known = HashMap::<PeerId, KnownPeer>::new();
let mut pending = HashSet::<PeerId>::new();
let mut queue = VecDeque::<PeerId>::new();
for value in AMINO_BOOTSTRAPS {
let Ok(address) = value.parse::<Multiaddr>() else {
continue;
};
let Some(peer_id) = terminal_peer_id(&address) else {
continue;
};
remember_canonical(
peer_id,
vec![address_with_peer(address, peer_id)],
&mut known,
&mut queue,
Instant::now(),
);
for address in known
.get(&peer_id)
.into_iter()
.flat_map(|peer| &peer.addresses)
{
swarm
.behaviour_mut()
.kad
.add_address(&peer_id, address.clone());
}
}
let _ = swarm.behaviour_mut().kad.bootstrap();
let mut walk = tokio::time::interval(DISCOVERY_INTERVAL);
walk.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
start_probes(swarm, &known, &mut queue, &mut pending);
tokio::select! {
_ = walk.tick() => {
expire_known_peers(&mut known, &pending, Instant::now());
swarm.behaviour_mut().kad.get_closest_peers(PeerId::random().to_bytes());
}
event = swarm.select_next_some() => match event {
SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::OutboundQueryProgressed {
result: kad::QueryResult::GetClosestPeers(result),
..
})) => {
let peers = match result {
Ok(result) => result.peers,
Err(kad::GetClosestPeersError::Timeout { peers, .. }) => peers,
};
debug(format_args!("DHT walk returned {} peers", peers.len()));
for peer in peers {
remember(
peer.peer_id,
peer.addrs,
&mut known,
&mut queue,
Instant::now(),
);
}
}
SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::RoutingUpdated {
peer,
addresses: discovered,
..
})) => {
remember(
peer,
discovered.into_vec(),
&mut known,
&mut queue,
Instant::now(),
);
}
SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
peer_id,
info,
..
})) => {
pending.remove(&peer_id);
let mut candidate_addresses = info.listen_addrs;
if let Some(discovered) = known.get(&peer_id) {
candidate_addresses.extend(discovered.addresses.iter().cloned());
}
candidate_addresses = canonical_addresses(peer_id, candidate_addresses);
debug(format_args!(
"identified {peer_id}; relay={} addresses={}",
info.protocols.iter().any(|protocol| protocol.as_ref() == RELAY_HOP_PROTOCOL),
candidate_addresses.len(),
));
if info.protocols.iter().any(|protocol| protocol.as_ref() == RELAY_HOP_PROTOCOL)
&& !candidate_addresses.is_empty()
&& sender.send(RelayCandidate {
peer_id,
addresses: candidate_addresses,
}).await.is_err()
{
return;
}
}
SwarmEvent::OutgoingConnectionError { peer_id: Some(peer_id), error, .. } => {
debug(format_args!("dial to {peer_id} failed: {error}"));
pending.remove(&peer_id);
}
SwarmEvent::ConnectionClosed { peer_id, .. } => {
pending.remove(&peer_id);
}
_ => {}
}
}
}
}
fn debug(message: std::fmt::Arguments<'_>) {
if std::env::var_os("MAKA_PEER_DISCOVERY_DEBUG").is_some() {
eprintln!("[peer-relay-discovery] {message}");
}
}
fn remember(
peer_id: PeerId,
discovered: Vec<Multiaddr>,
known: &mut HashMap<PeerId, KnownPeer>,
queue: &mut VecDeque<PeerId>,
now: Instant,
) {
let discovered = canonical_addresses(peer_id, discovered);
remember_canonical(peer_id, discovered, known, queue, now);
}
fn remember_canonical(
peer_id: PeerId,
mut discovered: Vec<Multiaddr>,
known: &mut HashMap<PeerId, KnownPeer>,
queue: &mut VecDeque<PeerId>,
now: Instant,
) {
discovered.truncate(MAX_ADDRESSES_PER_PEER);
if discovered.is_empty() || !known.contains_key(&peer_id) && known.len() >= MAX_KNOWN_PEERS {
return;
}
match known.entry(peer_id) {
Entry::Occupied(mut entry) => {
for address in discovered {
if entry.get().addresses.len() >= MAX_ADDRESSES_PER_PEER {
break;
}
if !entry.get().addresses.contains(&address) {
entry.get_mut().addresses.push(address);
}
}
}
Entry::Vacant(entry) => {
entry.insert(KnownPeer {
addresses: discovered,
first_seen: now,
});
queue.push_back(peer_id);
}
}
}
fn expire_known_peers(
known: &mut HashMap<PeerId, KnownPeer>,
pending: &HashSet<PeerId>,
now: Instant,
) {
known.retain(|peer_id, peer| {
pending.contains(peer_id) || now.duration_since(peer.first_seen) < PROBE_REVISIT_INTERVAL
});
}
fn start_probes(
swarm: &mut Swarm<Behaviour>,
known: &HashMap<PeerId, KnownPeer>,
queue: &mut VecDeque<PeerId>,
pending: &mut HashSet<PeerId>,
) {
while pending.len() < MAX_PENDING_PROBES {
let Some(peer_id) = queue.pop_front() else {
break;
};
let Some(peer) = known.get(&peer_id) else {
continue;
};
let options = DialOpts::peer_id(peer_id)
.addresses(peer.addresses.clone())
.build();
if swarm.dial(options).is_ok() {
pending.insert(peer_id);
}
}
}
fn canonical_addresses(peer_id: PeerId, addresses: Vec<Multiaddr>) -> Vec<Multiaddr> {
let mut result = Vec::new();
for address in addresses {
if address
.iter()
.any(|protocol| matches!(protocol, libp2p::multiaddr::Protocol::P2pCircuit))
{
continue;
}
let address = address_with_peer(address, peer_id);
if has_literal_ip_host(&address)
&& supported_public_relay_address(&address)
&& !result.contains(&address)
{
result.push(address);
}
}
result
}
fn allowed_kad_dial_address(peer_id: Option<PeerId>, address: &Multiaddr) -> bool {
let Some(peer_id) = peer_id else {
return false;
};
let canonical = address_with_peer(address.clone(), peer_id);
if supported_public_relay_address(&canonical) {
return true;
}
AMINO_BOOTSTRAPS.iter().any(|value| {
value
.parse::<Multiaddr>()
.is_ok_and(|bootstrap| bootstrap == canonical)
})
}
fn terminal_peer_id(address: &Multiaddr) -> Option<PeerId> {
match address.iter().last()? {
libp2p::multiaddr::Protocol::P2p(peer_id) => Some(peer_id),
_ => None,
}
}
fn has_literal_ip_host(address: &Multiaddr) -> bool {
matches!(
address.iter().next(),
Some(libp2p::multiaddr::Protocol::Ip4(_) | libp2p::multiaddr::Protocol::Ip6(_))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kad_dials_only_public_addresses_or_exact_bootstraps() {
let peer = PeerId::random();
let public: Multiaddr = "/ip4/1.1.1.1/tcp/4001".parse().expect("public address");
let private: Multiaddr = "/ip4/127.0.0.1/tcp/4001".parse().expect("private address");
let dns: Multiaddr = "/dns4/attacker.example/tcp/4001"
.parse()
.expect("DNS address");
assert!(allowed_kad_dial_address(Some(peer), &public));
assert!(!allowed_kad_dial_address(Some(peer), &private));
assert!(!allowed_kad_dial_address(Some(peer), &dns));
let bootstrap: Multiaddr = AMINO_BOOTSTRAPS[0]
.parse()
.expect("constant bootstrap address");
let bootstrap_peer = terminal_peer_id(&bootstrap).expect("bootstrap peer identity");
assert!(allowed_kad_dial_address(Some(bootstrap_peer), &bootstrap,));
assert!(!allowed_kad_dial_address(Some(peer), &bootstrap));
assert_eq!(
discovery_identify_config(identity::Keypair::generate_ed25519().public()).cache_size(),
0,
);
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* 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.
*/
mod bindings;
mod engine;
mod webrtc_direct;
pub use bindings::{
ConfigurePeerTransitOptions, ConnectPeerOptions, PeerEndpoint, PeerIdentitySignature,
PeerStream, PeerTransitRelayCandidate, PeerTransitSnapshot, StartPeerEndpointOptions,
ensure_peer_identity, sign_peer_identity, start_peer_endpoint, verify_peer_identity,
};
@@ -0,0 +1,33 @@
/*
* 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.
*/
mod muxer;
mod signaling;
mod transport;
mod upgrade;
pub(crate) use muxer::WebRtcConnection;
pub(crate) use signaling::{Signal, SignalingError, read_signal, write_signal};
pub(crate) use transport::{WebRtcTransport, WebRtcTransportControl};
pub(crate) use upgrade::{UpgradeOptions, UpgradeRole, upgrade_connection};
pub(crate) const SIGNALING_PROTOCOL: &str = "/webrtc-signaling/0.0.1";
#[cfg(test)]
mod tests;
@@ -0,0 +1,527 @@
/*
* 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::{
io,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
time::Duration,
};
use bytes::BytesMut;
use futures::{
AsyncRead, AsyncWrite, FutureExt, Sink, SinkExt as _, Stream, StreamExt as _,
channel::{mpsc, oneshot},
future::BoxFuture,
ready,
stream::FuturesUnordered,
};
use libp2p::core::muxing::{StreamMuxer, StreamMuxerEvent};
use libp2p_webrtc_utils::{DropListener, Stream as Libp2pWebRtcStream};
use tokio::sync::OwnedSemaphorePermit;
use webrtc::{
data_channel::{DataChannel, DataChannelEvent, RTCDataChannelState},
peer_connection::{PeerConnection, RTCPeerConnectionState},
};
const DATA_CHANNEL_QUEUE_CAPACITY: usize = 16;
const MAX_DATA_CHANNEL_MESSAGE_BYTES: usize = 16 * 1024;
pub(crate) type ReadySubstream = (WebRtcSubstream, DropListener<MessageIo>);
pub struct WebRtcConnection {
peer_connection: Arc<dyn PeerConnection>,
incoming: mpsc::Receiver<Result<ReadySubstream, io::Error>>,
states: mpsc::Receiver<RTCPeerConnectionState>,
outbound: Option<BoxFuture<'static, Result<ReadySubstream, io::Error>>>,
closing: Option<BoxFuture<'static, Result<(), io::Error>>>,
drop_listeners: FuturesUnordered<DropListener<MessageIo>>,
no_drop_listener_waker: Option<Waker>,
}
impl WebRtcConnection {
pub(crate) fn new(
peer_connection: Arc<dyn PeerConnection>,
incoming: mpsc::Receiver<Result<ReadySubstream, io::Error>>,
states: mpsc::Receiver<RTCPeerConnectionState>,
) -> Self {
Self {
peer_connection,
incoming,
states,
outbound: None,
closing: None,
drop_listeners: FuturesUnordered::new(),
no_drop_listener_waker: None,
}
}
fn track_substream(&mut self, (stream, listener): ReadySubstream) -> WebRtcSubstream {
self.drop_listeners.push(listener);
if let Some(waker) = self.no_drop_listener_waker.take() {
waker.wake();
}
stream
}
pub(crate) fn close_in_background(self) {
let peer_connection = Arc::clone(&self.peer_connection);
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
runtime.spawn(async move {
let _ = peer_connection.close().await;
});
}
}
}
impl StreamMuxer for WebRtcConnection {
type Substream = WebRtcSubstream;
type Error = io::Error;
fn poll_inbound(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<Self::Substream, Self::Error>> {
match ready!(self.incoming.poll_next_unpin(cx)) {
Some(Ok(stream)) => Poll::Ready(Ok(self.track_substream(stream))),
Some(Err(error)) => Poll::Ready(Err(error)),
None => Poll::Ready(Err(io::Error::new(
io::ErrorKind::ConnectionReset,
"WebRTC inbound stream channel closed",
))),
}
}
fn poll_outbound(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<Self::Substream, Self::Error>> {
let peer_connection = Arc::clone(&self.peer_connection);
let future = self.outbound.get_or_insert_with(|| {
async move {
let channel = peer_connection
.create_data_channel("", None)
.await
.map_err(webrtc_io_error)?;
data_channel_diagnostic("created", channel.id(), Some("outbound"));
ready_substream(channel, "outbound", None).await
}
.boxed()
});
match ready!(future.poll_unpin(cx)) {
Ok(stream) => {
self.outbound = None;
Poll::Ready(Ok(self.track_substream(stream)))
}
Err(error) => {
self.outbound = None;
Poll::Ready(Err(error))
}
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let peer_connection = Arc::clone(&self.peer_connection);
let future = self.closing.get_or_insert_with(|| {
async move { peer_connection.close().await.map_err(webrtc_io_error) }.boxed()
});
future.poll_unpin(cx)
}
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<StreamMuxerEvent, Self::Error>> {
loop {
match self.states.poll_next_unpin(cx) {
Poll::Ready(Some(RTCPeerConnectionState::Failed))
| Poll::Ready(Some(RTCPeerConnectionState::Closed)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::ConnectionReset,
"WebRTC peer connection closed",
)));
}
Poll::Ready(Some(_)) => continue,
Poll::Ready(None) | Poll::Pending => break,
}
}
loop {
match self.drop_listeners.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(()))) => continue,
Poll::Ready(Some(Err(error))) => {
muxer_diagnostic("substream-reset-failed", Some(&error.to_string()));
continue;
}
Poll::Ready(None) => {
self.no_drop_listener_waker = Some(cx.waker().clone());
return Poll::Pending;
}
Poll::Pending => return Poll::Pending,
}
}
}
}
pub struct WebRtcSubstream {
inner: Libp2pWebRtcStream<MessageIo>,
_inbound_permit: Option<OwnedSemaphorePermit>,
}
impl AsyncRead for WebRtcSubstream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buffer: &mut [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_read(cx, buffer)
}
}
impl AsyncWrite for WebRtcSubstream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buffer: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buffer)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_close(cx)
}
}
pub(crate) async fn ready_substream(
data_channel: Arc<dyn DataChannel>,
origin: &'static str,
inbound_permit: Option<OwnedSemaphorePermit>,
) -> Result<ReadySubstream, io::Error> {
let (outgoing, outgoing_receiver) = mpsc::channel(DATA_CHANNEL_QUEUE_CAPACITY);
let (incoming_sender, incoming) = mpsc::channel(DATA_CHANNEL_QUEUE_CAPACITY);
let io = MessageIo::new(outgoing, incoming);
let (ready_sender, ready_receiver) = oneshot::channel();
tokio::spawn(drive_data_channel(
data_channel,
outgoing_receiver,
incoming_sender,
ready_sender,
origin,
));
ready_receiver
.await
.map_err(|_| io::Error::new(io::ErrorKind::ConnectionAborted, "data channel stopped"))??;
let (stream, listener) = Libp2pWebRtcStream::new(io);
Ok((
WebRtcSubstream {
inner: stream,
_inbound_permit: inbound_permit,
},
listener,
))
}
pub(crate) fn keep_init_channel(
data_channel: Arc<dyn DataChannel>,
mut opened: mpsc::Sender<Result<(), io::Error>>,
) {
tokio::spawn(async move {
data_channel_diagnostic("waiting", data_channel.id(), Some("init"));
let result = wait_for_data_channel_open(&data_channel).await;
let is_open = result.is_ok();
data_channel_diagnostic(
if is_open { "opened" } else { "open-failed" },
data_channel.id(),
Some("init"),
);
let _ = opened.try_send(result);
if is_open {
// Keep the initial channel allocated for the connection lifetime. Closing it lets
// current native WebRTC stacks reuse its SCTP stream id before both peers have
// retired it, causing the first real libp2p substream to fail immediately.
while data_channel.poll().await.is_some() {}
}
});
}
async fn drive_data_channel(
data_channel: Arc<dyn DataChannel>,
mut outgoing: mpsc::Receiver<BytesMut>,
mut incoming: mpsc::Sender<Result<BytesMut, io::Error>>,
ready: oneshot::Sender<Result<(), io::Error>>,
origin: &'static str,
) {
data_channel_diagnostic("waiting", data_channel.id(), Some(origin));
if let Err(error) = wait_for_data_channel_open(&data_channel).await {
data_channel_diagnostic("open-failed", data_channel.id(), Some(origin));
let _ = ready.send(Err(error));
return;
}
data_channel_diagnostic("opened", data_channel.id(), Some(origin));
let _ = ready.send(Ok(()));
let mut pending_incoming = None;
loop {
if pending_incoming.is_some() {
tokio::select! {
ready = futures::future::poll_fn(|context| {
Pin::new(&mut incoming).poll_ready(context)
}) => {
if ready.is_err() {
let _ = data_channel.close().await;
return;
}
let message = pending_incoming.take().expect("pending message exists");
if Pin::new(&mut incoming).start_send(Ok(message)).is_err() {
let _ = data_channel.close().await;
return;
}
}
message = outgoing.next() => {
if !send_outgoing_message(&data_channel, message).await {
return;
}
}
}
continue;
}
tokio::select! {
event = data_channel.poll() => {
match event {
Some(DataChannelEvent::OnMessage(message)) => {
data_channel_diagnostic(
"received",
data_channel.id(),
Some(&message.data.len().to_string()),
);
pending_incoming = Some(message.data);
}
Some(DataChannelEvent::OnClose) => {
data_channel_diagnostic("closed", data_channel.id(), Some(origin));
return;
}
Some(DataChannelEvent::OnError) => {
data_channel_diagnostic("error", data_channel.id(), Some(origin));
send_channel_reset(&mut incoming, "WebRTC data channel failed").await;
return;
}
None => {
data_channel_diagnostic("stopped", data_channel.id(), Some(origin));
send_channel_reset(&mut incoming, "WebRTC data channel stopped").await;
return;
}
_ => {}
}
}
message = outgoing.next() => {
if !send_outgoing_message(&data_channel, message).await {
return;
}
}
}
}
}
async fn send_outgoing_message(
data_channel: &Arc<dyn DataChannel>,
message: Option<BytesMut>,
) -> bool {
let Some(message) = message else {
drain_data_channel(data_channel).await;
let _ = data_channel.close().await;
return false;
};
let message_bytes = message.len().to_string();
if message.len() > MAX_DATA_CHANNEL_MESSAGE_BYTES || data_channel.send(message).await.is_err() {
let _ = data_channel.close().await;
return false;
}
data_channel_diagnostic("sent", data_channel.id(), Some(&message_bytes));
true
}
async fn send_channel_reset(
incoming: &mut mpsc::Sender<Result<BytesMut, io::Error>>,
message: &'static str,
) {
let _ = incoming
.send(Err(io::Error::new(io::ErrorKind::ConnectionReset, message)))
.await;
}
async fn drain_data_channel(data_channel: &Arc<dyn DataChannel>) {
let _ = tokio::time::timeout(Duration::from_secs(5), async {
loop {
match data_channel.outstanding_bytes().await {
Ok(0) | Err(_) => return,
Ok(_) => tokio::time::sleep(Duration::from_millis(1)).await,
}
}
})
.await;
}
async fn wait_for_data_channel_open(data_channel: &Arc<dyn DataChannel>) -> Result<(), io::Error> {
match data_channel.ready_state().await.map_err(webrtc_io_error)? {
RTCDataChannelState::Open => return Ok(()),
RTCDataChannelState::Closing | RTCDataChannelState::Closed => {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"data channel closed before opening",
));
}
RTCDataChannelState::Unspecified | RTCDataChannelState::Connecting => {}
_ => {}
}
while let Some(event) = data_channel.poll().await {
match event {
DataChannelEvent::OnOpen => return Ok(()),
DataChannelEvent::OnClose | DataChannelEvent::OnError => {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"data channel failed to open",
));
}
_ => {}
}
}
Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"data channel stopped before opening",
))
}
#[derive(Clone)]
pub(crate) struct MessageIo {
inner: Arc<Mutex<MessageIoInner>>,
}
struct MessageIoInner {
outgoing: mpsc::Sender<BytesMut>,
incoming: mpsc::Receiver<Result<BytesMut, io::Error>>,
read_buffer: BytesMut,
}
impl MessageIo {
fn new(
outgoing: mpsc::Sender<BytesMut>,
incoming: mpsc::Receiver<Result<BytesMut, io::Error>>,
) -> Self {
Self {
inner: Arc::new(Mutex::new(MessageIoInner {
outgoing,
incoming,
read_buffer: BytesMut::new(),
})),
}
}
}
impl AsyncRead for MessageIo {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buffer: &mut [u8],
) -> Poll<io::Result<usize>> {
let mut inner = self.inner.lock().expect("message I/O mutex poisoned");
if inner.read_buffer.is_empty() {
match ready!(Pin::new(&mut inner.incoming).poll_next(cx)) {
Some(Ok(message)) => inner.read_buffer = message,
Some(Err(error)) => return Poll::Ready(Err(error)),
None => return Poll::Ready(Ok(0)),
}
}
let length = buffer.len().min(inner.read_buffer.len());
buffer[..length].copy_from_slice(&inner.read_buffer.split_to(length));
Poll::Ready(Ok(length))
}
}
impl AsyncWrite for MessageIo {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buffer: &[u8],
) -> Poll<io::Result<usize>> {
if buffer.len() > MAX_DATA_CHANNEL_MESSAGE_BYTES {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"data channel message exceeds the 16 KiB libp2p limit",
)));
}
let mut inner = self.inner.lock().expect("message I/O mutex poisoned");
ready!(Pin::new(&mut inner.outgoing).poll_ready(cx))
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "data channel closed"))?;
Pin::new(&mut inner.outgoing)
.start_send(BytesMut::from(buffer))
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "data channel closed"))?;
Poll::Ready(Ok(buffer.len()))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut inner = self.inner.lock().expect("message I/O mutex poisoned");
Pin::new(&mut inner.outgoing)
.poll_flush(cx)
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "data channel closed"))
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut inner = self.inner.lock().expect("message I/O mutex poisoned");
Pin::new(&mut inner.outgoing)
.poll_close(cx)
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "data channel closed"))
}
}
fn webrtc_io_error(error: impl std::fmt::Display) -> io::Error {
io::Error::other(error.to_string())
}
pub(crate) fn data_channel_diagnostic(event: &str, channel: usize, detail: Option<&str>) {
if std::env::var_os("MAKA_WEBRTC_DIAGNOSTICS").is_none() {
return;
}
eprintln!(
"{}",
serde_json::json!({
"dataChannel": event,
"channel": channel,
"detail": detail,
})
);
}
fn muxer_diagnostic(event: &str, detail: Option<&str>) {
if std::env::var_os("MAKA_WEBRTC_DIAGNOSTICS").is_none() {
return;
}
eprintln!(
"{}",
serde_json::json!({
"webRtcMuxer": event,
"detail": detail,
})
);
}
@@ -0,0 +1,225 @@
/*
* 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::io;
use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use prost::Message as _;
const MAX_SIGNAL_FRAME_BYTES: usize = 64 * 1024;
const MAX_SDP_BYTES: usize = 48 * 1024;
const MAX_CANDIDATE_BYTES: usize = 4 * 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Signal {
Offer(String),
Answer(String),
IceCandidate(String),
}
impl Signal {
fn into_wire(self) -> Result<WireMessage, SignalingError> {
let (kind, data, limit) = match self {
Self::Offer(data) => (WireType::SdpOffer, data, MAX_SDP_BYTES),
Self::Answer(data) => (WireType::SdpAnswer, data, MAX_SDP_BYTES),
Self::IceCandidate(data) => (WireType::IceCandidate, data, MAX_CANDIDATE_BYTES),
};
if data.len() > limit {
return Err(SignalingError::PayloadTooLarge {
actual: data.len(),
limit,
});
}
Ok(WireMessage {
kind: Some(kind as i32),
data: Some(data),
})
}
fn from_wire(message: WireMessage) -> Result<Self, SignalingError> {
let kind = message
.kind
.and_then(|kind| WireType::try_from(kind).ok())
.ok_or(SignalingError::MissingOrInvalidType)?;
let data = message.data.ok_or(SignalingError::MissingData)?;
match kind {
WireType::SdpOffer if data.len() <= MAX_SDP_BYTES => Ok(Self::Offer(data)),
WireType::SdpAnswer if data.len() <= MAX_SDP_BYTES => Ok(Self::Answer(data)),
WireType::IceCandidate if data.len() <= MAX_CANDIDATE_BYTES => {
Ok(Self::IceCandidate(data))
}
WireType::SdpOffer | WireType::SdpAnswer => Err(SignalingError::PayloadTooLarge {
actual: data.len(),
limit: MAX_SDP_BYTES,
}),
WireType::IceCandidate => Err(SignalingError::PayloadTooLarge {
actual: data.len(),
limit: MAX_CANDIDATE_BYTES,
}),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SignalingError {
#[error("signaling I/O failed: {0}")]
Io(#[from] io::Error),
#[error("signaling protobuf was invalid: {0}")]
Protobuf(#[from] prost::DecodeError),
#[error("signaling frame length was invalid")]
InvalidFrameLength,
#[error("signaling frame is {actual} bytes; limit is {limit}")]
FrameTooLarge { actual: usize, limit: usize },
#[error("signaling payload is {actual} bytes; limit is {limit}")]
PayloadTooLarge { actual: usize, limit: usize },
#[error("signaling message has no valid type")]
MissingOrInvalidType,
#[error("signaling message has no data")]
MissingData,
}
pub async fn write_signal<W>(writer: &mut W, signal: Signal) -> Result<(), SignalingError>
where
W: AsyncWrite + Unpin,
{
let message = signal.into_wire()?;
let mut payload = Vec::with_capacity(message.encoded_len());
message
.encode(&mut payload)
.expect("encoding protobuf into Vec cannot fail");
if payload.len() > MAX_SIGNAL_FRAME_BYTES {
return Err(SignalingError::FrameTooLarge {
actual: payload.len(),
limit: MAX_SIGNAL_FRAME_BYTES,
});
}
frame_diagnostic("write", &payload);
let mut prefix = unsigned_varint::encode::usize_buffer();
let prefix = unsigned_varint::encode::usize(payload.len(), &mut prefix);
let mut frame = Vec::with_capacity(prefix.len() + payload.len());
frame.extend_from_slice(prefix);
frame.extend_from_slice(&payload);
writer.write_all(&frame).await?;
writer.flush().await?;
Ok(())
}
pub async fn read_signal<R>(reader: &mut R) -> Result<Option<Signal>, SignalingError>
where
R: AsyncRead + Unpin,
{
let Some(length) = read_frame_length(reader).await? else {
return Ok(None);
};
if length > MAX_SIGNAL_FRAME_BYTES {
return Err(SignalingError::FrameTooLarge {
actual: length,
limit: MAX_SIGNAL_FRAME_BYTES,
});
}
let mut payload = vec![0; length];
reader.read_exact(&mut payload).await?;
frame_diagnostic("read", &payload);
Signal::from_wire(WireMessage::decode(payload.as_slice())?).map(Some)
}
fn frame_diagnostic(direction: &str, payload: &[u8]) {
if std::env::var_os("MAKA_WEBRTC_DIAGNOSTICS").is_none() {
return;
}
eprintln!(
"{}",
serde_json::json!({
"signaling": direction,
"bytes": payload.len(),
"firstTag": payload.first(),
})
);
}
async fn read_frame_length<R>(reader: &mut R) -> Result<Option<usize>, SignalingError>
where
R: AsyncRead + Unpin,
{
let mut prefix = unsigned_varint::encode::usize_buffer();
for index in 0..prefix.len() {
match reader.read_exact(&mut prefix[index..=index]).await {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof && index == 0 => {
return Ok(None);
}
Err(error) => return Err(error.into()),
}
if unsigned_varint::decode::is_last(prefix[index]) {
let (length, remainder) = unsigned_varint::decode::usize(&prefix[..=index])
.map_err(|_| SignalingError::InvalidFrameLength)?;
if !remainder.is_empty() {
return Err(SignalingError::InvalidFrameLength);
}
return Ok(Some(length));
}
}
Err(SignalingError::InvalidFrameLength)
}
#[derive(Clone, PartialEq, prost::Message)]
struct WireMessage {
#[prost(enumeration = "WireType", optional, tag = "1")]
kind: Option<i32>,
#[prost(string, optional, tag = "2")]
data: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, prost::Enumeration)]
enum WireType {
SdpOffer = 0,
SdpAnswer = 1,
IceCandidate = 2,
}
#[cfg(test)]
mod tests {
use futures::io::Cursor;
use super::*;
#[tokio::test]
async fn standard_signal_round_trip_and_bounds() {
let mut bytes = Cursor::new(Vec::new());
write_signal(&mut bytes, Signal::Offer("v=0".to_owned()))
.await
.unwrap();
bytes.set_position(0);
assert_eq!(
read_signal(&mut bytes).await.unwrap(),
Some(Signal::Offer("v=0".to_owned()))
);
assert_eq!(read_signal(&mut bytes).await.unwrap(), None);
let error = write_signal(
&mut Cursor::new(Vec::new()),
Signal::IceCandidate("x".repeat(MAX_CANDIDATE_BYTES + 1)),
)
.await
.unwrap_err();
assert!(matches!(error, SignalingError::PayloadTooLarge { .. }));
}
}
@@ -0,0 +1,196 @@
/*
* 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::{pin::Pin, time::Duration};
use futures::{AsyncReadExt as _, AsyncWriteExt as _, future::poll_fn};
use libp2p::{
PeerId,
core::{
Endpoint,
muxing::StreamMuxer,
transport::{DialOpts, PortUse, Transport},
},
};
use tokio_util::compat::TokioAsyncReadCompatExt as _;
use super::{
UpgradeOptions, UpgradeRole, WebRtcTransport, upgrade::MAX_INBOUND_SUBSTREAMS,
upgrade_connection,
};
#[tokio::test]
async fn authenticated_signaling_yields_a_libp2p_stream() {
let peer_a = PeerId::random();
let peer_b = PeerId::random();
let (signaling_a, signaling_b) = tokio::io::duplex(256 * 1024);
let options = UpgradeOptions {
deadline: Duration::from_secs(10),
..UpgradeOptions::default()
};
let (offer, answer) = tokio::join!(
upgrade_connection(
signaling_a.compat(),
peer_b,
peer_b,
UpgradeRole::Offerer,
options.clone(),
),
upgrade_connection(
signaling_b.compat(),
peer_a,
peer_a,
UpgradeRole::Answerer,
options,
)
);
let (_, connection_a) = offer.expect("offerer upgrade");
let (_, mut connection_b) = answer.expect("answerer upgrade");
let (mut transport, control) = WebRtcTransport::new();
let address = control
.register_outbound(peer_b, connection_a)
.expect("register outbound connection");
let (_, mut connection_a) = transport
.dial(
address,
DialOpts {
role: Endpoint::Dialer,
port_use: PortUse::Reuse,
},
)
.expect("dial prepared WebRTC connection")
.await
.expect("prepared WebRTC connection");
let (outbound, inbound) = tokio::join!(
poll_fn(|cx| Pin::new(&mut connection_a).poll_outbound(cx)),
poll_fn(|cx| Pin::new(&mut connection_b).poll_inbound(cx)),
);
let mut outbound = outbound.expect("outbound WebRTC stream");
let mut inbound = inbound.expect("inbound WebRTC stream");
let payload = b"maka-runtime-host-webrtc";
outbound.write_all(payload).await.expect("write payload");
outbound.flush().await.expect("flush payload");
let mut received = vec![0; payload.len()];
inbound
.read_exact(&mut received)
.await
.expect("read payload");
assert_eq!(received, payload);
}
#[tokio::test]
async fn a_slow_reader_backpressures_without_losing_bytes() {
let (mut connection_a, mut connection_b) = connected_pair().await;
let (outbound, inbound) = tokio::join!(
poll_fn(|cx| Pin::new(&mut connection_a).poll_outbound(cx)),
poll_fn(|cx| Pin::new(&mut connection_b).poll_inbound(cx)),
);
let mut outbound = outbound.expect("outbound WebRTC stream");
let mut inbound = inbound.expect("inbound WebRTC stream");
let payload = vec![0x5a; 20 * 16 * 1024];
let expected = payload.clone();
let writer = tokio::spawn(async move {
outbound.write_all(&payload).await.expect("write payload");
outbound.flush().await.expect("flush payload");
});
tokio::time::sleep(Duration::from_millis(100)).await;
let mut received = vec![0; expected.len()];
tokio::time::timeout(Duration::from_secs(10), inbound.read_exact(&mut received))
.await
.expect("slow-reader receive timeout")
.expect("slow-reader receive failed");
tokio::time::timeout(Duration::from_secs(10), writer)
.await
.expect("slow-reader writer timeout")
.expect("slow-reader writer task failed");
assert_eq!(received, expected);
}
#[tokio::test]
async fn inbound_substreams_are_bounded_per_connection() {
let (mut connection_a, mut connection_b) = connected_pair().await;
let mut outbound = Vec::new();
for _ in 0..=MAX_INBOUND_SUBSTREAMS {
outbound.push(
tokio::time::timeout(
Duration::from_secs(5),
poll_fn(|cx| Pin::new(&mut connection_a).poll_outbound(cx)),
)
.await
.expect("outbound substream timeout")
.expect("outbound substream failed"),
);
}
let mut inbound = Vec::new();
for _ in 0..MAX_INBOUND_SUBSTREAMS {
inbound.push(
tokio::time::timeout(
Duration::from_secs(5),
poll_fn(|cx| Pin::new(&mut connection_b).poll_inbound(cx)),
)
.await
.expect("inbound substream timeout")
.expect("inbound substream failed"),
);
}
assert!(
tokio::time::timeout(
Duration::from_millis(250),
poll_fn(|cx| Pin::new(&mut connection_b).poll_inbound(cx)),
)
.await
.is_err(),
"the excess inbound substream escaped the per-connection bound",
);
drop((outbound, inbound));
}
async fn connected_pair() -> (super::WebRtcConnection, super::WebRtcConnection) {
let peer_a = PeerId::random();
let peer_b = PeerId::random();
let (signaling_a, signaling_b) = tokio::io::duplex(256 * 1024);
let options = UpgradeOptions {
deadline: Duration::from_secs(10),
..UpgradeOptions::default()
};
let (offer, answer) = tokio::join!(
upgrade_connection(
signaling_a.compat(),
peer_b,
peer_b,
UpgradeRole::Offerer,
options.clone(),
),
upgrade_connection(
signaling_b.compat(),
peer_a,
peer_a,
UpgradeRole::Answerer,
options,
)
);
(
offer.expect("offerer upgrade").1,
answer.expect("answerer upgrade").1,
)
}
@@ -0,0 +1,259 @@
/*
* 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::{HashMap, VecDeque},
io,
pin::Pin,
sync::{Arc, Mutex, MutexGuard},
task::{Context, Poll},
};
use futures::{Stream as _, channel::mpsc, future};
use libp2p::{
Multiaddr, PeerId,
core::transport::{DialOpts, ListenerId, Transport, TransportError, TransportEvent},
multiaddr::Protocol,
};
use super::muxer::WebRtcConnection;
const INCOMING_CONNECTION_CAPACITY: usize = 4;
const OUTGOING_CONNECTION_CAPACITY: usize = 32;
type TransportOutput = (PeerId, WebRtcConnection);
pub struct WebRtcTransport {
incoming: mpsc::Receiver<TransportOutput>,
listeners: HashMap<ListenerId, Multiaddr>,
pending_events:
VecDeque<TransportEvent<future::Ready<Result<TransportOutput, io::Error>>, io::Error>>,
pending_incoming: VecDeque<TransportOutput>,
outgoing: Arc<Mutex<HashMap<PeerId, WebRtcConnection>>>,
}
#[derive(Clone)]
pub struct WebRtcTransportControl {
incoming: mpsc::Sender<TransportOutput>,
outgoing: Arc<Mutex<HashMap<PeerId, WebRtcConnection>>>,
}
impl WebRtcTransport {
pub fn new() -> (Self, WebRtcTransportControl) {
let (incoming_sender, incoming) = mpsc::channel(INCOMING_CONNECTION_CAPACITY);
let outgoing = Arc::new(Mutex::new(HashMap::new()));
(
Self {
incoming,
listeners: HashMap::new(),
pending_events: VecDeque::new(),
pending_incoming: VecDeque::new(),
outgoing: Arc::clone(&outgoing),
},
WebRtcTransportControl {
incoming: incoming_sender,
outgoing,
},
)
}
}
impl WebRtcTransportControl {
pub fn inject_inbound(
&self,
peer: PeerId,
connection: WebRtcConnection,
) -> Result<(), io::Error> {
let mut incoming = self.incoming.clone();
incoming.try_send((peer, connection)).map_err(|error| {
let full = error.is_full();
let (_, connection) = error.into_inner();
connection.close_in_background();
io::Error::new(
if full {
io::ErrorKind::WouldBlock
} else {
io::ErrorKind::BrokenPipe
},
if full {
"WebRTC transport input is full"
} else {
"WebRTC transport stopped"
},
)
})
}
pub fn register_outbound(
&self,
peer: PeerId,
connection: WebRtcConnection,
) -> Result<Multiaddr, io::Error> {
let mut outgoing = lock(&self.outgoing);
if outgoing.contains_key(&peer) {
connection.close_in_background();
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"WebRTC transport already has a pending connection for this peer",
));
}
if outgoing.len() >= OUTGOING_CONNECTION_CAPACITY {
connection.close_in_background();
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"WebRTC transport outbound queue is full",
));
}
outgoing.insert(peer, connection);
Ok(webrtc_peer_address(peer))
}
pub fn discard_outbound(&self, peer: PeerId) {
if let Some(connection) = lock(&self.outgoing).remove(&peer) {
connection.close_in_background();
}
}
}
impl Transport for WebRtcTransport {
type Output = TransportOutput;
type Error = io::Error;
type ListenerUpgrade = future::Ready<Result<Self::Output, Self::Error>>;
type Dial = future::Ready<Result<Self::Output, Self::Error>>;
fn listen_on(
&mut self,
id: ListenerId,
address: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
if !is_webrtc_listener_address(&address) {
return Err(TransportError::MultiaddrNotSupported(address));
}
if self.listeners.insert(id, address.clone()).is_some() {
return Err(TransportError::Other(io::Error::new(
io::ErrorKind::AlreadyExists,
"WebRTC listener ID is already registered",
)));
}
self.pending_events.push_back(TransportEvent::NewAddress {
listener_id: id,
listen_addr: address,
});
Ok(())
}
fn remove_listener(&mut self, id: ListenerId) -> bool {
let Some(address) = self.listeners.remove(&id) else {
return false;
};
self.pending_events
.push_back(TransportEvent::AddressExpired {
listener_id: id,
listen_addr: address,
});
true
}
fn dial(
&mut self,
address: Multiaddr,
_options: DialOpts,
) -> Result<Self::Dial, TransportError<Self::Error>> {
let Some(peer) = webrtc_peer_id(&address) else {
return Err(TransportError::MultiaddrNotSupported(address));
};
let Some(connection) = lock(&self.outgoing).remove(&peer) else {
return Err(TransportError::Other(io::Error::new(
io::ErrorKind::NotFound,
"WebRTC transport has no prepared connection for this peer",
)));
};
Ok(future::ready(Ok((peer, connection))))
}
fn poll(
mut self: Pin<&mut Self>,
context: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
if let Some(event) = self.pending_events.pop_front() {
return Poll::Ready(event);
}
while let Poll::Ready(Some(incoming)) = Pin::new(&mut self.incoming).poll_next(context) {
self.pending_incoming.push_back(incoming);
}
let Some((listener_id, local_address)) = self
.listeners
.iter()
.next()
.map(|(id, address)| (*id, address.clone()))
else {
return Poll::Pending;
};
let Some((peer, connection)) = self.pending_incoming.pop_front() else {
return Poll::Pending;
};
Poll::Ready(TransportEvent::Incoming {
listener_id,
upgrade: future::ready(Ok((peer, connection))),
local_addr: local_address,
send_back_addr: webrtc_peer_address(peer),
})
}
}
pub fn webrtc_peer_address(peer: PeerId) -> Multiaddr {
Multiaddr::empty()
.with(Protocol::WebRTC)
.with(Protocol::P2p(peer))
}
fn webrtc_peer_id(address: &Multiaddr) -> Option<PeerId> {
let mut protocols = address.iter();
match (protocols.next(), protocols.next(), protocols.next()) {
(Some(Protocol::WebRTC), Some(Protocol::P2p(peer)), None) => Some(peer),
_ => None,
}
}
fn is_webrtc_listener_address(address: &Multiaddr) -> bool {
let mut protocols = address.iter();
matches!(protocols.next(), Some(Protocol::WebRTC)) && protocols.next().is_none()
}
fn lock(
shared: &Arc<Mutex<HashMap<PeerId, WebRtcConnection>>>,
) -> MutexGuard<'_, HashMap<PeerId, WebRtcConnection>> {
shared
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_webrtc_address_is_closed_and_unambiguous() {
let peer = PeerId::random();
assert_eq!(
webrtc_peer_address(peer).to_string(),
format!("/webrtc/p2p/{peer}"),
);
}
}
@@ -0,0 +1,519 @@
/*
* 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::{
io,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use futures::{
AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _, SinkExt as _, StreamExt as _,
channel::mpsc,
};
use libp2p::PeerId;
use tokio::sync::Semaphore;
use tokio_util::sync::CancellationToken;
use webrtc::{
data_channel::DataChannel,
peer_connection::{
PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCConfigurationBuilder,
RTCIceCandidateInit, RTCIceServer, RTCPeerConnectionIceEvent, RTCPeerConnectionState,
RTCSessionDescription,
},
};
use super::{
Signal, SignalingError, WebRtcConnection,
muxer::{ReadySubstream, data_channel_diagnostic, keep_init_channel, ready_substream},
read_signal, write_signal,
};
const EVENT_CAPACITY: usize = 64;
const MAX_CANDIDATES: usize = 64;
const DATA_CHANNEL_SEND_BUFFER_BYTES: usize = 1024 * 1024;
pub(super) const MAX_INBOUND_SUBSTREAMS: usize = 16;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpgradeRole {
Offerer,
Answerer,
}
#[derive(Clone, Debug)]
pub struct UpgradeOptions {
pub stun_urls: Vec<String>,
pub udp_bind_addresses: Vec<String>,
pub deadline: Duration,
pub cancellation: CancellationToken,
}
impl Default for UpgradeOptions {
fn default() -> Self {
Self {
stun_urls: Vec::new(),
udp_bind_addresses: vec!["127.0.0.1:0".to_owned()],
deadline: Duration::from_secs(10),
cancellation: CancellationToken::new(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum UpgradeError {
#[error("authenticated signaling peer {authenticated} did not match expected peer {expected}")]
IdentityMismatch {
authenticated: String,
expected: String,
},
#[error("WebRTC signaling failed: {0}")]
Signaling(#[from] SignalingError),
#[error("WebRTC operation failed: {0}")]
WebRtc(String),
#[error("WebRTC signaling stream ended before the direct connection was ready")]
SignalingEnded,
#[error("WebRTC signaling message arrived in the wrong phase")]
UnexpectedSignal,
#[error("WebRTC signaling exceeded the candidate limit")]
TooManyCandidates,
#[error("WebRTC event delivery failed: {0}")]
EventDelivery(&'static str),
#[error("WebRTC direct upgrade reached its deadline")]
Deadline,
#[error("WebRTC direct upgrade was cancelled")]
Cancelled,
}
struct PeerConnectionGuard(Option<Arc<dyn PeerConnection>>);
impl PeerConnectionGuard {
fn new(peer_connection: Arc<dyn PeerConnection>) -> Self {
Self(Some(peer_connection))
}
fn disarm(&mut self) {
self.0 = None;
}
}
impl Drop for PeerConnectionGuard {
fn drop(&mut self) {
let Some(peer_connection) = self.0.take() else {
return;
};
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
runtime.spawn(async move {
let _ = peer_connection.close().await;
});
}
}
}
pub async fn upgrade_connection<S>(
signaling: S,
authenticated_peer: PeerId,
expected_peer: PeerId,
role: UpgradeRole,
options: UpgradeOptions,
) -> Result<(PeerId, WebRtcConnection), UpgradeError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
if authenticated_peer != expected_peer {
return Err(UpgradeError::IdentityMismatch {
authenticated: authenticated_peer.to_string(),
expected: expected_peer.to_string(),
});
}
let channels = EventChannels::new();
let peer_connection = build_peer_connection(&options, channels.handler()).await?;
let mut peer_connection_guard = PeerConnectionGuard::new(Arc::clone(&peer_connection));
let negotiation = negotiate(signaling, role, Arc::clone(&peer_connection), channels);
tokio::pin!(negotiation);
let deadline = tokio::time::sleep(options.deadline);
tokio::pin!(deadline);
let result = tokio::select! {
biased;
_ = options.cancellation.cancelled() => Err(UpgradeError::Cancelled),
result = &mut negotiation => result,
_ = &mut deadline => Err(UpgradeError::Deadline),
};
match result {
Ok(connection) => {
peer_connection_guard.disarm();
Ok((authenticated_peer, connection))
}
Err(error) => {
let _ = peer_connection.close().await;
peer_connection_guard.disarm();
Err(error)
}
}
}
async fn build_peer_connection(
options: &UpgradeOptions,
handler: Arc<EventHandler>,
) -> Result<Arc<dyn PeerConnection>, UpgradeError> {
let configuration = RTCConfigurationBuilder::new()
.with_ice_servers(if options.stun_urls.is_empty() {
Vec::new()
} else {
vec![RTCIceServer {
urls: options.stun_urls.clone(),
..Default::default()
}]
})
.build();
let connection = PeerConnectionBuilder::new()
.with_configuration(configuration)
.with_handler(handler)
.with_udp_addrs(options.udp_bind_addresses.clone())
.with_data_channel_send_buffer_limit(DATA_CHANNEL_SEND_BUFFER_BYTES)
.build()
.await
.map_err(webrtc_upgrade_error)?;
Ok(Arc::new(connection))
}
async fn negotiate<S>(
signaling: S,
role: UpgradeRole,
peer_connection: Arc<dyn PeerConnection>,
channels: EventChannels,
) -> Result<WebRtcConnection, UpgradeError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let (mut reader, mut writer) = signaling.split();
let EventChannels {
mut candidates,
mut states,
incoming,
init_sender,
mut init_opened,
mut failures,
init_claimed,
..
} = channels;
let mut remote_description_set = false;
let mut pending_candidates = Vec::new();
let mut local_candidate_count = 0;
let mut remote_candidate_count = 0;
let mut local_signaling_open = true;
let mut remote_signaling_open = true;
let mut peer_connection_connected = false;
let mut init_channel_opened = false;
match role {
UpgradeRole::Offerer => {
init_claimed.store(true, Ordering::Release);
let init = peer_connection
.create_data_channel("init", None)
.await
.map_err(webrtc_upgrade_error)?;
keep_init_channel(init, init_sender);
let offer = peer_connection
.create_offer(None)
.await
.map_err(webrtc_upgrade_error)?;
peer_connection
.set_local_description(offer.clone())
.await
.map_err(webrtc_upgrade_error)?;
write_signal(&mut writer, Signal::Offer(offer.sdp)).await?;
diagnostic(role, "offer-sent", None);
}
UpgradeRole::Answerer => {
let offer = match read_signal(&mut reader).await? {
Some(Signal::Offer(sdp)) => RTCSessionDescription::offer(sdp),
Some(_) => return Err(UpgradeError::UnexpectedSignal),
None => return Err(UpgradeError::SignalingEnded),
}
.map_err(webrtc_upgrade_error)?;
peer_connection
.set_remote_description(offer)
.await
.map_err(webrtc_upgrade_error)?;
remote_description_set = true;
let answer = peer_connection
.create_answer(None)
.await
.map_err(webrtc_upgrade_error)?;
peer_connection
.set_local_description(answer.clone())
.await
.map_err(webrtc_upgrade_error)?;
write_signal(&mut writer, Signal::Answer(answer.sdp)).await?;
diagnostic(role, "answer-sent", None);
}
}
loop {
let direct_connection_ready = peer_connection_connected && init_channel_opened;
if direct_connection_ready && local_signaling_open {
writer.close().await.map_err(SignalingError::Io)?;
local_signaling_open = false;
diagnostic(role, "signaling-write-closed", None);
}
if direct_connection_ready && !remote_signaling_open {
return Ok(WebRtcConnection::new(peer_connection, incoming, states));
}
tokio::select! {
signal = read_signal(&mut reader), if remote_signaling_open => {
match signal? {
Some(Signal::Answer(sdp)) if role == UpgradeRole::Offerer && !remote_description_set => {
let answer = RTCSessionDescription::answer(sdp).map_err(webrtc_upgrade_error)?;
peer_connection.set_remote_description(answer).await.map_err(webrtc_upgrade_error)?;
remote_description_set = true;
for candidate in pending_candidates.drain(..) {
peer_connection.add_ice_candidate(candidate).await.map_err(webrtc_upgrade_error)?;
}
}
Some(Signal::IceCandidate(candidate)) => {
remote_candidate_count += 1;
if remote_candidate_count > MAX_CANDIDATES {
return Err(UpgradeError::TooManyCandidates);
}
let candidate = serde_json::from_str::<RTCIceCandidateInit>(&candidate)
.map_err(|error| UpgradeError::WebRtc(error.to_string()))?;
diagnostic(role, "remote-candidate", candidate_kind(&candidate));
if remote_description_set {
peer_connection.add_ice_candidate(candidate).await.map_err(webrtc_upgrade_error)?;
} else {
pending_candidates.push(candidate);
}
}
Some(_) => return Err(UpgradeError::UnexpectedSignal),
None if remote_description_set => {
remote_signaling_open = false;
diagnostic(role, "signaling-read-closed", None);
}
None => return Err(UpgradeError::SignalingEnded),
}
}
candidate = candidates.next() => {
let candidate = candidate.ok_or(UpgradeError::EventDelivery("candidate channel closed"))?;
local_candidate_count += 1;
if local_candidate_count > MAX_CANDIDATES {
return Err(UpgradeError::TooManyCandidates);
}
diagnostic(role, "local-candidate", candidate_kind(&candidate));
let candidate = serde_json::to_string(&candidate)
.map_err(|error| UpgradeError::WebRtc(error.to_string()))?;
if local_signaling_open {
write_signal(&mut writer, Signal::IceCandidate(candidate)).await?;
}
}
state = states.next() => {
let state = state.ok_or(UpgradeError::EventDelivery("connection state channel closed"))?;
diagnostic(role, "connection-state", Some(&format!("{state:?}")));
match state {
RTCPeerConnectionState::Connected => {
peer_connection_connected = true;
}
RTCPeerConnectionState::Failed | RTCPeerConnectionState::Closed => {
return Err(UpgradeError::WebRtc("peer connection failed".to_owned()));
}
_ => {}
}
}
opened = init_opened.next() => {
opened
.ok_or(UpgradeError::EventDelivery("init data channel event channel closed"))?
.map_err(|error| UpgradeError::WebRtc(error.to_string()))?;
init_channel_opened = true;
diagnostic(role, "init-channel-open", None);
}
failure = failures.next() => {
return Err(UpgradeError::WebRtc(
failure.unwrap_or_else(|| "event handler stopped".to_owned())
));
}
}
}
}
fn candidate_kind(candidate: &RTCIceCandidateInit) -> Option<&str> {
let mut parts = candidate.candidate.split_whitespace();
while let Some(part) = parts.next() {
if part == "typ" {
return parts.next();
}
}
None
}
fn diagnostic(role: UpgradeRole, event: &str, detail: Option<&str>) {
if std::env::var_os("MAKA_WEBRTC_DIAGNOSTICS").is_none() {
return;
}
eprintln!(
"{}",
serde_json::json!({
"webrtc": event,
"role": match role {
UpgradeRole::Offerer => "offerer",
UpgradeRole::Answerer => "answerer",
},
"detail": detail,
})
);
}
struct EventChannels {
candidate_sender: mpsc::Sender<RTCIceCandidateInit>,
candidates: mpsc::Receiver<RTCIceCandidateInit>,
state_sender: mpsc::Sender<RTCPeerConnectionState>,
states: mpsc::Receiver<RTCPeerConnectionState>,
incoming_sender: mpsc::Sender<Result<ReadySubstream, io::Error>>,
incoming: mpsc::Receiver<Result<ReadySubstream, io::Error>>,
init_sender: mpsc::Sender<Result<(), io::Error>>,
init_opened: mpsc::Receiver<Result<(), io::Error>>,
failure_sender: mpsc::Sender<String>,
failures: mpsc::Receiver<String>,
init_claimed: Arc<AtomicBool>,
inbound_substreams: Arc<Semaphore>,
}
impl EventChannels {
fn new() -> Self {
let (candidate_sender, candidates) = mpsc::channel(EVENT_CAPACITY);
let (state_sender, states) = mpsc::channel(EVENT_CAPACITY);
let (incoming_sender, incoming) = mpsc::channel(EVENT_CAPACITY);
let (init_sender, init_opened) = mpsc::channel(1);
let (failure_sender, failures) = mpsc::channel(EVENT_CAPACITY);
Self {
candidate_sender,
candidates,
state_sender,
states,
incoming_sender,
incoming,
init_sender,
init_opened,
failure_sender,
failures,
init_claimed: Arc::new(AtomicBool::new(false)),
inbound_substreams: Arc::new(Semaphore::new(MAX_INBOUND_SUBSTREAMS)),
}
}
fn handler(&self) -> Arc<EventHandler> {
Arc::new(EventHandler {
candidates: self.candidate_sender.clone(),
states: self.state_sender.clone(),
incoming: self.incoming_sender.clone(),
init_opened: self.init_sender.clone(),
failures: self.failure_sender.clone(),
init_claimed: Arc::clone(&self.init_claimed),
inbound_substreams: Arc::clone(&self.inbound_substreams),
})
}
}
#[derive(Clone)]
struct EventHandler {
candidates: mpsc::Sender<RTCIceCandidateInit>,
states: mpsc::Sender<RTCPeerConnectionState>,
incoming: mpsc::Sender<Result<ReadySubstream, io::Error>>,
init_opened: mpsc::Sender<Result<(), io::Error>>,
failures: mpsc::Sender<String>,
init_claimed: Arc<AtomicBool>,
inbound_substreams: Arc<Semaphore>,
}
#[async_trait::async_trait]
impl PeerConnectionEventHandler for EventHandler {
async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
match event.candidate.to_json() {
Ok(candidate) => {
let mut candidates = self.candidates.clone();
if candidates.try_send(candidate).is_err() {
self.report_failure("local ICE candidate limit exceeded");
}
}
Err(error) => self.report_failure(&format!("could not encode ICE candidate: {error}")),
}
}
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
let mut states = self.states.clone();
if states.try_send(state).is_err() {
self.report_failure("connection state delivery overflowed");
}
}
async fn on_data_channel(&self, data_channel: Arc<dyn DataChannel>) {
data_channel_diagnostic("received", data_channel.id(), Some("incoming"));
match data_channel.label().await {
Ok(label) if label == "init" => {
if self
.init_claimed
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
keep_init_channel(data_channel, self.init_opened.clone());
} else {
data_channel_diagnostic("rejected", data_channel.id(), Some("duplicate-init"));
let _ = data_channel.close().await;
}
}
Ok(_) => {
let permit = match Arc::clone(&self.inbound_substreams).try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
data_channel_diagnostic(
"rejected",
data_channel.id(),
Some("inbound-limit"),
);
let _ = data_channel.close().await;
return;
}
};
let mut incoming = self.incoming.clone();
tokio::spawn(async move {
let stream = ready_substream(data_channel, "incoming", Some(permit)).await;
let _ = incoming.send(stream).await;
});
}
Err(error) => {
self.report_failure(&format!("could not read data channel label: {error}"))
}
}
}
}
impl EventHandler {
fn report_failure(&self, message: &str) {
let mut failures = self.failures.clone();
let _ = failures.try_send(message.to_owned());
}
}
fn webrtc_upgrade_error(error: impl std::fmt::Display) -> UpgradeError {
UpgradeError::WebRtc(error.to_string())
}