源码快照 @ 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
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:
Generated
+4345
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user