Bound iroh path retries to prevent multi-connection OOM
A long-running rc.33 daemon reached OOM with roughly 22 GiB resident and 24 GiB swapped. Live inspection found 40 GiB and 10/20 GiB anonymous container mappings; the active worker stack and tracing metadata identified iroh's scheduled path-open retry loop. In iroh 1.2.0 every failed connection requeues the same path, while each retry visits every connection again. Separate control and data connections multiply the queue geometrically. Vendor the published iroh 1.2.0 source and select it with a workspace Cargo patch. Deduplicate retry admission and cap distinct pending paths at 64, evicting the oldest before inserting a new candidate. Keep the current and next retry batches bounded without combining control/data connections or changing wire protocols, identity derivation, authentication or state. Document source provenance, the live evidence and removal conditions. Upstream PR 4551 merged on October 6, but inspection of the official 1.3.0 crate still shows the unconditional enqueue, so that upgrade alone is not a fix. No version bump, release tag or installed-service change. The default workspace suite includes the exact vendored admission module. Three regressions failed with the original enqueue and pass with the fix: repeated failures across connections, duplicate admission at capacity, and 10,000 distinct candidates with bounded length/capacity and FIFO eviction. Transport integration tests continue using real endpoints and encryption. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with only the inherited TSUNAGI_CONTROL_SOCKET removed from the test environment - git diff --cached --check Initial full-suite runs failed six DNS readiness checks because the inherited system socket path overrode their temporary state directories. Clearing that environment variable resolves those failures without changing application code. Root/network-dependent ignored tests and other operating systems were not run.
This commit is contained in:
1 parent
4e235cbac6
commit
2b0c938487
86 files changed
+35810
-3
No files matched your search
Vendored
+181
@@ -0,0 +1,181 @@
|
||||
use std::{env, str::FromStr, time::Instant};
|
||||
|
||||
use clap::Parser;
|
||||
use data_encoding::HEXLOWER;
|
||||
use iroh::{
|
||||
EndpointId, SecretKey,
|
||||
endpoint::{RecvStream, SendStream, ZeroRttStatus, presets},
|
||||
};
|
||||
use n0_error::{Result, StackResultExt, StdResultExt};
|
||||
use n0_future::StreamExt;
|
||||
use tracing::{info, trace};
|
||||
|
||||
const PINGPONG_ALPN: &[u8] = b"0rtt-pingpong";
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
/// The endpoint id to connect to. If not set, the program will start a server.
|
||||
endpoint_id: Option<EndpointId>,
|
||||
/// Number of rounds to run.
|
||||
#[clap(long, default_value = "100")]
|
||||
rounds: u64,
|
||||
/// Run without 0-RTT for comparison.
|
||||
#[clap(long)]
|
||||
disable_0rtt: bool,
|
||||
}
|
||||
|
||||
/// Gets a secret key from the IROH_SECRET environment variable or generates a new random one.
|
||||
/// If the environment variable is set, it must be a valid string representation of a secret key.
|
||||
pub fn get_or_generate_secret_key() -> Result<SecretKey> {
|
||||
if let Ok(secret) = env::var("IROH_SECRET") {
|
||||
// Parse the secret key from string
|
||||
SecretKey::from_str(&secret).context("Invalid secret key format")
|
||||
} else {
|
||||
// Generate a new random key
|
||||
let secret_key = SecretKey::generate();
|
||||
println!(
|
||||
"Generated new secret key: {}",
|
||||
HEXLOWER.encode(&secret_key.to_bytes())
|
||||
);
|
||||
println!("To reuse this key, set the IROH_SECRET environment variable to this value");
|
||||
Ok(secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Do a simple ping-pong with the given connection.
|
||||
async fn pingpong(send: SendStream, recv: RecvStream, x: u64) -> Result<()> {
|
||||
ping(send, x).await?;
|
||||
pong(recv, x).await
|
||||
}
|
||||
|
||||
async fn ping(mut send: SendStream, x: u64) -> Result<()> {
|
||||
let data = x.to_be_bytes();
|
||||
send.write_all(&data).await.anyerr()?;
|
||||
send.finish().anyerr()
|
||||
}
|
||||
|
||||
async fn pong(mut recv: RecvStream, x: u64) -> Result<()> {
|
||||
let data = x.to_be_bytes();
|
||||
let echo = recv.read_to_end(8).await.anyerr()?;
|
||||
assert!(echo == data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect(args: Args) -> Result<()> {
|
||||
let remote_id = args.endpoint_id.unwrap();
|
||||
let endpoint = iroh::Endpoint::builder(presets::N0)
|
||||
.relay_mode(iroh::RelayMode::Disabled)
|
||||
.keylog(true)
|
||||
.bind()
|
||||
.await?;
|
||||
// ensure we have resolved the remote_id before connecting
|
||||
// so we get a more accurate connection timing
|
||||
let mut address_lookup_stream = endpoint.address_lookup()?.resolve(remote_id);
|
||||
let _item = address_lookup_stream
|
||||
.next()
|
||||
.await
|
||||
.context("failed to lookup remote")?;
|
||||
|
||||
let t0 = Instant::now();
|
||||
for i in 0..args.rounds {
|
||||
let t0 = Instant::now();
|
||||
let connecting = endpoint
|
||||
.connect_with_opts(remote_id, PINGPONG_ALPN, Default::default())
|
||||
.await?;
|
||||
let connection = if args.disable_0rtt {
|
||||
let connection = connecting.await.anyerr()?;
|
||||
trace!("connecting without 0-RTT");
|
||||
let (send, recv) = connection.open_bi().await.anyerr()?;
|
||||
pingpong(send, recv, i).await?;
|
||||
connection
|
||||
} else {
|
||||
match connecting.into_0rtt() {
|
||||
Ok(zrtt_connection) => {
|
||||
trace!("0-RTT possible from our side");
|
||||
let (send, recv) = zrtt_connection.open_bi().await.anyerr()?;
|
||||
// before we get the full handshake, attempt to send 0-RTT data
|
||||
let zrtt_task = tokio::spawn(ping(send, i));
|
||||
match zrtt_connection.handshake_completed().await? {
|
||||
ZeroRttStatus::Accepted(conn) => {
|
||||
let _ = zrtt_task.await.anyerr()?;
|
||||
pong(recv, i).await?;
|
||||
conn
|
||||
}
|
||||
ZeroRttStatus::Rejected(conn) => {
|
||||
zrtt_task.abort();
|
||||
let (send, recv) = conn.open_bi().await.anyerr()?;
|
||||
pingpong(send, recv, i).await?;
|
||||
conn
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(connecting) => {
|
||||
trace!("0-RTT not possible from our side");
|
||||
let conn = connecting.await.anyerr()?;
|
||||
let (send, recv) = conn.open_bi().await.anyerr()?;
|
||||
pingpong(send, recv, i).await?;
|
||||
conn
|
||||
}
|
||||
}
|
||||
};
|
||||
connection.close(0u8.into(), b"");
|
||||
let elapsed = t0.elapsed();
|
||||
println!("round {i}: {} us", elapsed.as_micros());
|
||||
}
|
||||
let elapsed = t0.elapsed();
|
||||
println!("total time: {} us", elapsed.as_micros());
|
||||
println!(
|
||||
"time per round: {} us",
|
||||
elapsed.as_micros() / (args.rounds as u128)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn accept(_args: Args) -> Result<()> {
|
||||
let secret_key = get_or_generate_secret_key()?;
|
||||
let endpoint = iroh::Endpoint::builder(presets::N0)
|
||||
.alpns(vec![PINGPONG_ALPN.to_vec()])
|
||||
.secret_key(secret_key)
|
||||
.relay_mode(iroh::RelayMode::Disabled)
|
||||
.bind()
|
||||
.await?;
|
||||
println!("endpoint id: {}", endpoint.id());
|
||||
|
||||
let accept = async move {
|
||||
while let Some(incoming) = endpoint.accept().await {
|
||||
tokio::spawn(async move {
|
||||
let accepting = incoming.accept().anyerr()?;
|
||||
let connection = accepting.into_0rtt();
|
||||
let (mut send, mut recv) = connection.accept_bi().await.anyerr()?;
|
||||
trace!("recv.is_0rtt: {}", recv.is_0rtt());
|
||||
let data = recv.read_to_end(8).await.anyerr()?;
|
||||
trace!("recv: {}", data.len());
|
||||
send.write_all(&data).await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
connection.closed().await;
|
||||
n0_error::Ok(())
|
||||
});
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
_ = accept => {
|
||||
info!("accept finished, shutting down");
|
||||
},
|
||||
_ = tokio::signal::ctrl_c()=> {
|
||||
info!("Ctrl-C received, shutting down");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
if args.endpoint_id.is_some() {
|
||||
connect(args).await?;
|
||||
} else {
|
||||
accept(args).await?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+355
@@ -0,0 +1,355 @@
|
||||
//! Implementation of authentication using iroh hooks
|
||||
//!
|
||||
//! This implements an auth protocol that works with iroh hooks.
|
||||
//! It allows to put authentication in front of iroh protocols. The protocols don't need any special support.
|
||||
//! Authentication is handled prior to establishing the connections, over a separate connection.
|
||||
|
||||
use iroh::{Endpoint, EndpointAddr, endpoint::presets, protocol::Router};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
|
||||
use crate::echo::Echo;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let server_router = accept_side(b"secret!!").await?;
|
||||
server_router.endpoint().online().await;
|
||||
let server_addr = server_router.endpoint().addr();
|
||||
|
||||
println!("-- no --");
|
||||
let res = connect_side_no_auth(server_addr.clone()).await;
|
||||
println!("echo without auth: {:#}", res.unwrap_err());
|
||||
|
||||
println!("-- wrong --");
|
||||
let res = connect_side(server_addr.clone(), b"dunno").await;
|
||||
println!("echo with wrong auth: {:#}", res.unwrap_err());
|
||||
|
||||
println!("-- correct --");
|
||||
let res = connect_side(server_addr.clone(), b"secret!!").await;
|
||||
println!("echo with correct auth: {res:?}");
|
||||
|
||||
server_router.shutdown().await.anyerr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_side(remote_addr: EndpointAddr, token: &[u8]) -> Result<()> {
|
||||
let (auth_hook, auth_task) = auth::outgoing(token.to_vec());
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
.hooks(auth_hook)
|
||||
.bind()
|
||||
.await?;
|
||||
let _guard = auth_task.spawn(endpoint.clone());
|
||||
Echo::connect(&endpoint, remote_addr, b"hello there!").await
|
||||
}
|
||||
|
||||
async fn connect_side_no_auth(remote_addr: EndpointAddr) -> Result<()> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
Echo::connect(&endpoint, remote_addr, b"hello there!").await
|
||||
}
|
||||
|
||||
async fn accept_side(token: &[u8]) -> Result<Router> {
|
||||
let (auth_hook, auth_protocol) = auth::incoming(token.to_vec());
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
.hooks(auth_hook)
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
let router = Router::builder(endpoint)
|
||||
.accept(auth::ALPN, auth_protocol)
|
||||
.accept(echo::ALPN, Echo)
|
||||
.spawn();
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
mod echo {
|
||||
//! A bare-bones protocol with no knowledge of auth whatsoever.
|
||||
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr,
|
||||
endpoint::Connection,
|
||||
protocol::{AcceptError, ProtocolHandler},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt, anyerr};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Echo;
|
||||
|
||||
pub const ALPN: &[u8] = b"iroh-example/echo/0";
|
||||
|
||||
impl Echo {
|
||||
pub async fn connect(
|
||||
endpoint: &Endpoint,
|
||||
remote: impl Into<EndpointAddr>,
|
||||
message: &[u8],
|
||||
) -> Result<()> {
|
||||
let conn = endpoint.connect(remote, ALPN).await?;
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
send.write_all(message).await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
let response = recv.read_to_end(1000).await.anyerr()?;
|
||||
conn.close(0u32.into(), b"bye!");
|
||||
if response == message {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyerr!("Received invalid response"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolHandler for Echo {
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
tokio::io::copy(&mut recv, &mut send).await?;
|
||||
send.finish()?;
|
||||
connection.closed().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod auth {
|
||||
//! Authentication hook
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, hash_map},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr, EndpointId,
|
||||
endpoint::{
|
||||
AfterHandshakeOutcome, BeforeConnectOutcome, Connection, ConnectionError, EndpointHooks,
|
||||
},
|
||||
protocol::{AcceptError, ProtocolHandler},
|
||||
};
|
||||
use n0_error::{AnyError, Result, StackResultExt, StdResultExt, anyerr};
|
||||
use n0_future::task::AbortOnDropHandle;
|
||||
use tokio::{
|
||||
sync::{mpsc, oneshot},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
pub const ALPN: &[u8] = b"iroh-example/auth/0";
|
||||
|
||||
const CLOSE_ACCEPTED: u32 = 1;
|
||||
const CLOSE_DENIED: u32 = 403;
|
||||
|
||||
/// Outgoing side: Use this if you want to pre-auth outgoing connections.
|
||||
pub fn outgoing(token: Vec<u8>) -> (OutgoingAuthHook, OutgoingAuthTask) {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let hook = OutgoingAuthHook { tx };
|
||||
let connector = OutgoingAuthTask {
|
||||
token,
|
||||
rx,
|
||||
allowed_remotes: Default::default(),
|
||||
pending_remotes: Default::default(),
|
||||
tasks: JoinSet::new(),
|
||||
};
|
||||
(hook, connector)
|
||||
}
|
||||
|
||||
type AuthResult = Result<(), Arc<AnyError>>;
|
||||
|
||||
/// Hook to mount on the endpoint builder.
|
||||
#[derive(Debug)]
|
||||
pub struct OutgoingAuthHook {
|
||||
tx: mpsc::Sender<(EndpointId, oneshot::Sender<AuthResult>)>,
|
||||
}
|
||||
|
||||
impl OutgoingAuthHook {
|
||||
async fn authenticate(&self, remote_id: EndpointId) -> Result<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send((remote_id, tx))
|
||||
.await
|
||||
.std_context("authenticator stopped")?;
|
||||
rx.await
|
||||
.std_context("authenticator stopped")?
|
||||
.context("failed to authenticate")
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointHooks for OutgoingAuthHook {
|
||||
async fn before_connect<'a>(
|
||||
&'a self,
|
||||
remote_addr: &'a EndpointAddr,
|
||||
alpn: &'a [u8],
|
||||
) -> BeforeConnectOutcome {
|
||||
// Don't intercept auth request themsevles
|
||||
if alpn == ALPN {
|
||||
BeforeConnectOutcome::Accept
|
||||
} else {
|
||||
match self.authenticate(remote_addr.id).await {
|
||||
Ok(()) => BeforeConnectOutcome::Accept,
|
||||
Err(err) => {
|
||||
debug!("authentication denied: {err:#}");
|
||||
BeforeConnectOutcome::Reject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connector task that initiates pre-auth request. Call [`Self::spawn`] once the endpoint is built.
|
||||
pub struct OutgoingAuthTask {
|
||||
token: Vec<u8>,
|
||||
rx: mpsc::Receiver<(EndpointId, oneshot::Sender<AuthResult>)>,
|
||||
allowed_remotes: HashSet<EndpointId>,
|
||||
pending_remotes: HashMap<EndpointId, Vec<oneshot::Sender<AuthResult>>>,
|
||||
tasks: JoinSet<(EndpointId, Result<()>)>,
|
||||
}
|
||||
|
||||
impl OutgoingAuthTask {
|
||||
pub fn spawn(self, endpoint: Endpoint) -> AbortOnDropHandle<()> {
|
||||
AbortOnDropHandle::new(tokio::spawn(self.run(endpoint)))
|
||||
}
|
||||
|
||||
async fn run(mut self, endpoint: Endpoint) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = self.rx.recv() => {
|
||||
let Some((remote_id, tx)) = msg else {
|
||||
break;
|
||||
};
|
||||
self.handle_msg(&endpoint, remote_id, tx);
|
||||
}
|
||||
Some(res) = self.tasks.join_next(), if !self.tasks.is_empty() => {
|
||||
let (remote_id, res) = res.expect("connect task panicked");
|
||||
let res = res.map_err(Arc::new);
|
||||
self.handle_task(remote_id, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_msg(
|
||||
&mut self,
|
||||
endpoint: &Endpoint,
|
||||
remote_id: EndpointId,
|
||||
tx: oneshot::Sender<Result<(), Arc<AnyError>>>,
|
||||
) {
|
||||
if self.allowed_remotes.contains(&remote_id) {
|
||||
tx.send(Ok(())).ok();
|
||||
} else {
|
||||
match self.pending_remotes.entry(remote_id) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().push(tx);
|
||||
}
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let endpoint = endpoint.clone();
|
||||
let token = self.token.clone();
|
||||
self.tasks.spawn(async move {
|
||||
let res = Self::connect(endpoint, remote_id, token).await;
|
||||
(remote_id, res)
|
||||
});
|
||||
entry.insert(vec![tx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_task(&mut self, remote_id: EndpointId, res: Result<(), Arc<AnyError>>) {
|
||||
if res.is_ok() {
|
||||
self.allowed_remotes.insert(remote_id);
|
||||
}
|
||||
let senders = self.pending_remotes.remove(&remote_id);
|
||||
for tx in senders.into_iter().flatten() {
|
||||
tx.send(res.clone()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(endpoint: Endpoint, remote_id: EndpointId, token: Vec<u8>) -> Result<()> {
|
||||
let conn = endpoint.connect(remote_id, ALPN).await?;
|
||||
let mut stream = conn.open_uni().await.anyerr()?;
|
||||
stream.write_all(&token).await.anyerr()?;
|
||||
stream.finish().anyerr()?;
|
||||
let reason = conn.closed().await;
|
||||
if let ConnectionError::ApplicationClosed(code) = &reason
|
||||
&& code.error_code.into_inner() as u32 == CLOSE_ACCEPTED
|
||||
{
|
||||
Ok(())
|
||||
} else if let ConnectionError::ApplicationClosed(code) = &reason
|
||||
&& code.error_code.into_inner() as u32 == CLOSE_DENIED
|
||||
{
|
||||
Err(anyerr!("authentication denied by remote"))
|
||||
} else {
|
||||
Err(AnyError::from_std(reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Incoming side: Use this if you want to only accept connections from peers with successful pre-auth requests.
|
||||
pub fn incoming(token: Vec<u8>) -> (IncomingAuthHook, AuthProtocol) {
|
||||
let allowed_remotes: Arc<Mutex<HashSet<EndpointId>>> = Default::default();
|
||||
let hook = IncomingAuthHook {
|
||||
allowed_remotes: allowed_remotes.clone(),
|
||||
};
|
||||
let protocol = AuthProtocol {
|
||||
allowed_remotes,
|
||||
token,
|
||||
};
|
||||
(hook, protocol)
|
||||
}
|
||||
|
||||
/// Accept-side auth hook: Mount this onto the endpoint.
|
||||
///
|
||||
/// This will reject incoming connections if the remote did not successfully authenticate before.
|
||||
#[derive(Debug)]
|
||||
pub struct IncomingAuthHook {
|
||||
allowed_remotes: Arc<Mutex<HashSet<EndpointId>>>,
|
||||
}
|
||||
|
||||
impl EndpointHooks for IncomingAuthHook {
|
||||
async fn after_handshake<'a>(
|
||||
&'a self,
|
||||
conn: &'a iroh::endpoint::Connection,
|
||||
) -> AfterHandshakeOutcome {
|
||||
if conn.alpn() == ALPN
|
||||
|| self
|
||||
.allowed_remotes
|
||||
.lock()
|
||||
.expect("poisoned")
|
||||
.contains(&conn.remote_id())
|
||||
{
|
||||
AfterHandshakeOutcome::Accept
|
||||
} else {
|
||||
AfterHandshakeOutcome::Reject {
|
||||
error_code: 403u32.into(),
|
||||
reason: b"not authenticated".to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept-side auth protocol. Mount this on the router to accept authentication requests.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthProtocol {
|
||||
token: Vec<u8>,
|
||||
allowed_remotes: Arc<Mutex<HashSet<EndpointId>>>,
|
||||
}
|
||||
|
||||
impl ProtocolHandler for AuthProtocol {
|
||||
/// The `accept` method is called for each incoming connection for our ALPN.
|
||||
///
|
||||
/// The returned future runs on a newly spawned tokio task, so it can run as long as
|
||||
/// the connection lasts.
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
let mut stream = connection.accept_uni().await?;
|
||||
let token = stream.read_to_end(256).await.anyerr()?;
|
||||
let remote_id = connection.remote_id();
|
||||
if token == self.token {
|
||||
self.allowed_remotes
|
||||
.lock()
|
||||
.expect("poisoned")
|
||||
.insert(remote_id);
|
||||
connection.close(CLOSE_ACCEPTED.into(), b"accepted");
|
||||
} else {
|
||||
connection.close(CLOSE_DENIED.into(), b"rejected");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect to a remote endpoint and pass bytes using unreliable datagrams.
|
||||
//!
|
||||
//! We use the endpoint ID (the PublicKey of the remote endpoint), the direct UDP addresses, and the relay url to achieve a connection.
|
||||
//!
|
||||
//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection.
|
||||
//!
|
||||
//! Run the `listen-unreliable` example first (`iroh/examples/listen-unreliable.rs`), which will give you instructions on how to run this example to watch two endpoints connect and exchange bytes.
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use clap::Parser;
|
||||
use iroh::{Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey, endpoint::presets};
|
||||
use iroh_base::TransportAddr;
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use tracing::info;
|
||||
|
||||
// An example ALPN that we are using to communicate over the `Endpoint`
|
||||
const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
/// The id of the remote endpoint.
|
||||
#[clap(long)]
|
||||
endpoint_id: iroh::EndpointId,
|
||||
/// The list of direct UDP addresses for the remote endpoint.
|
||||
#[clap(long, value_parser, num_args = 1.., value_delimiter = ' ')]
|
||||
addrs: Vec<SocketAddr>,
|
||||
/// The url of the relay server the remote endpoint can also be reached at.
|
||||
#[clap(long)]
|
||||
relay_url: RelayUrl,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
println!("\nconnect (unreliable) example!\n");
|
||||
let args = Cli::parse();
|
||||
let secret_key = SecretKey::from_bytes(&rand::random());
|
||||
println!("public key: {}", secret_key.public());
|
||||
|
||||
// Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers.
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
// The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase.
|
||||
.secret_key(secret_key)
|
||||
// Set the ALPN protocols this endpoint will accept on incoming connections
|
||||
.alpns(vec![EXAMPLE_ALPN.to_vec()])
|
||||
// `RelayMode::Default` means that we will use the default relay servers to holepunch and relay.
|
||||
// Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls.
|
||||
// Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS
|
||||
// If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code
|
||||
.relay_mode(RelayMode::Default)
|
||||
// You can choose an address to bind to, but passing in `None` will bind the socket to a random available port
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
// wait for the endpoint to be online
|
||||
endpoint.online().await;
|
||||
|
||||
let endpoint_addr = endpoint.addr();
|
||||
let me = endpoint_addr.id;
|
||||
println!("endpoint id: {me}");
|
||||
println!("endpoint listening addresses:");
|
||||
endpoint_addr
|
||||
.ip_addrs()
|
||||
.for_each(|addr| println!("\t{addr}"));
|
||||
let relay_url = endpoint_addr
|
||||
.relay_urls()
|
||||
.next()
|
||||
.expect("Should have a relay URL, assuming a default endpoint setup.");
|
||||
println!("endpoint relay server url: {relay_url}\n");
|
||||
// Build a `EndpointAddr` from the endpoint_id, relay url, and UDP addresses.
|
||||
let addrs = args
|
||||
.addrs
|
||||
.into_iter()
|
||||
.map(TransportAddr::Ip)
|
||||
.chain(std::iter::once(TransportAddr::Relay(args.relay_url)));
|
||||
|
||||
let addr = EndpointAddr::from_parts(args.endpoint_id, addrs);
|
||||
|
||||
// Attempt to connect, over the given ALPN.
|
||||
// Returns a QUIC connection.
|
||||
let conn = endpoint.connect(addr, EXAMPLE_ALPN).await?;
|
||||
info!("connected");
|
||||
|
||||
// Send a datagram over the connection.
|
||||
let message = format!("{me} is saying 'hello!'");
|
||||
conn.send_datagram(message.as_bytes().to_vec().into())
|
||||
.anyerr()?;
|
||||
|
||||
// Read a datagram over the connection.
|
||||
let message = conn.read_datagram().await.anyerr()?;
|
||||
let message = String::from_utf8(message.into()).anyerr()?;
|
||||
println!("received: {message}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect to a remote endpoint.
|
||||
//!
|
||||
//! We use the endpoint ID (the PublicKey of the remote endpoint), the direct UDP addresses, and the relay url to achieve a connection.
|
||||
//!
|
||||
//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection.
|
||||
//!
|
||||
//! Run the `listen` example first (`iroh/examples/listen.rs`), which will give you instructions on how to run this example to watch two endpoints connect and exchange bytes.
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use clap::Parser;
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey, TransportAddr, endpoint::presets,
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use tracing::info;
|
||||
|
||||
// An example ALPN that we are using to communicate over the `Endpoint`
|
||||
const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
/// The id of the remote endpoint.
|
||||
#[clap(long)]
|
||||
endpoint_id: iroh::EndpointId,
|
||||
/// The list of direct UDP addresses for the remote endpoint.
|
||||
#[clap(long, value_parser, num_args = 1.., value_delimiter = ' ')]
|
||||
addrs: Vec<SocketAddr>,
|
||||
/// The url of the relay server the remote endpoint can also be reached at.
|
||||
#[clap(long)]
|
||||
relay_url: RelayUrl,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
println!("\nconnect example!\n");
|
||||
let args = Cli::parse();
|
||||
let secret_key = SecretKey::generate();
|
||||
println!("public key: {}", secret_key.public());
|
||||
|
||||
// Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers.
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
// The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase.
|
||||
.secret_key(secret_key)
|
||||
// Set the ALPN protocols this endpoint will accept on incoming connections
|
||||
.alpns(vec![EXAMPLE_ALPN.to_vec()])
|
||||
// `RelayMode::Default` means that we will use the default relay servers to holepunch and relay.
|
||||
// Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls.
|
||||
// Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS
|
||||
// If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code
|
||||
.relay_mode(RelayMode::Default)
|
||||
// You can choose an address to bind to, but passing in `None` will bind the socket to a random available port
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
// wait for the endpoint to be online
|
||||
endpoint.online().await;
|
||||
|
||||
let endpoint_addr = endpoint.addr();
|
||||
let me = endpoint.id();
|
||||
println!("endpoint id: {me}");
|
||||
println!("endpoint listening addresses:");
|
||||
for addr in endpoint_addr.ip_addrs() {
|
||||
println!("\t{addr}")
|
||||
}
|
||||
|
||||
let relay_url = endpoint_addr
|
||||
.relay_urls()
|
||||
.next()
|
||||
.expect("should be connected to a relay server");
|
||||
println!("endpoint relay server url: {relay_url}\n");
|
||||
// Build a `EndpointAddr` from the endpoint_id, relay url, and UDP addresses.
|
||||
let addrs = args
|
||||
.addrs
|
||||
.into_iter()
|
||||
.map(TransportAddr::Ip)
|
||||
.chain(std::iter::once(TransportAddr::Relay(args.relay_url)));
|
||||
let addr = EndpointAddr::from_parts(args.endpoint_id, addrs);
|
||||
|
||||
// Attempt to connect, over the given ALPN.
|
||||
// Returns a Noq connection.
|
||||
let conn = endpoint.connect(addr, EXAMPLE_ALPN).await?;
|
||||
info!("connected");
|
||||
|
||||
// Use the Noq API to send and recv content.
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
|
||||
let message = format!("{me} is saying 'hello!'");
|
||||
send.write_all(message.as_bytes()).await.anyerr()?;
|
||||
|
||||
// Call `finish` to close the send side of the connection gracefully.
|
||||
send.finish().anyerr()?;
|
||||
let message = recv.read_to_end(100).await.anyerr()?;
|
||||
let message = String::from_utf8(message).anyerr()?;
|
||||
println!("received: {message}");
|
||||
|
||||
// We received the last message: close all connections and allow for the close
|
||||
// message to be sent.
|
||||
endpoint.close().await;
|
||||
Ok(())
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use iroh::{
|
||||
Endpoint, SecretKey, TransportAddr,
|
||||
endpoint::{
|
||||
Builder, Connection, presets,
|
||||
transports::{Addr, PathSelection, PathSelectionContext, PathSelector},
|
||||
},
|
||||
protocol::{AcceptError, ProtocolHandler, Router},
|
||||
test_utils::test_transport::{TEST_TRANSPORT_ID, TestNetwork, TestTransport},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
|
||||
/// Each protocol is identified by its ALPN string.
|
||||
///
|
||||
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
||||
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
||||
const ALPN: &[u8] = b"iroh-example/echo/0";
|
||||
|
||||
/// Example demonstrating custom transport usage.
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct Args {
|
||||
/// Keep IP transports enabled (in addition to custom transport)
|
||||
#[arg(long)]
|
||||
keep_ip: bool,
|
||||
|
||||
/// Keep relay transports enabled (in addition to custom transport)
|
||||
#[arg(long)]
|
||||
keep_relay: bool,
|
||||
|
||||
/// Delay in seconds to wait after connecting before re-checking the selected transport
|
||||
#[arg(long, default_value = "0")]
|
||||
delay: u64,
|
||||
}
|
||||
|
||||
/// A [`PathSelector`] that prefers the test custom transport whenever a candidate path on
|
||||
/// it exists, falling back to the lowest-RTT candidate otherwise.
|
||||
#[derive(Debug)]
|
||||
struct PreferTestTransport;
|
||||
|
||||
impl PathSelector for PreferTestTransport {
|
||||
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
|
||||
tracing::debug!("dumping path RTTs");
|
||||
for p in ctx.paths() {
|
||||
let network_path = p.network_path();
|
||||
let rtt = p.stats().map(|s| s.rtt);
|
||||
tracing::debug!(%network_path, ?rtt);
|
||||
}
|
||||
let mut selection = PathSelection::none();
|
||||
// First preference: any path on our test custom transport.
|
||||
if let Some(p) = ctx.paths().find(
|
||||
|p| matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID),
|
||||
) {
|
||||
selection.set(&p);
|
||||
return selection;
|
||||
}
|
||||
// Otherwise: lowest RTT wins. Paths whose stats can't be read (closed
|
||||
// concurrently with selection) are skipped entirely.
|
||||
if let Some(p) = ctx
|
||||
.paths()
|
||||
.filter_map(|p| p.stats().map(|s| (p, s.rtt)))
|
||||
.min_by_key(|(_, rtt)| *rtt)
|
||||
.map(|(p, _)| p)
|
||||
{
|
||||
selection.set(&p);
|
||||
}
|
||||
selection
|
||||
}
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Configure an endpoint builder with the custom transport and optional IP/relay transports.
|
||||
fn configure(&self, secret_key: SecretKey, transport: Arc<TestTransport>) -> Builder {
|
||||
let mut builder = Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_key)
|
||||
.preset(transport)
|
||||
// Always prefer the custom transport when it has a working path.
|
||||
.path_selector(Arc::new(PreferTestTransport));
|
||||
if !self.keep_ip {
|
||||
builder = builder.clear_ip_transports();
|
||||
}
|
||||
if !self.keep_relay {
|
||||
builder = builder.clear_relay_transports();
|
||||
}
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Echo;
|
||||
|
||||
impl ProtocolHandler for Echo {
|
||||
/// The `accept` method is called for each incoming connection for our ALPN.
|
||||
///
|
||||
/// The returned future runs on a newly spawned tokio task, so it can run as long as
|
||||
/// the connection lasts.
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
// We can get the remote's endpoint id from the connection.
|
||||
let endpoint_id = connection.remote_id();
|
||||
println!("accepted connection from {endpoint_id}");
|
||||
|
||||
// Our protocol is a simple request-response protocol, so we expect the
|
||||
// connecting peer to open a single bi-directional stream.
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
|
||||
// Echo any bytes received back directly.
|
||||
// This will keep copying until the sender signals the end of data on the stream.
|
||||
let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?;
|
||||
println!("Copied over {bytes_sent} byte(s)");
|
||||
|
||||
// By calling `finish` on the send stream we signal that we will not send anything
|
||||
// further, which makes the receive stream on the other end terminate.
|
||||
send.finish()?;
|
||||
|
||||
// Wait until the remote closes the connection, which it does once it
|
||||
// received the response.
|
||||
connection.closed().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
|
||||
println!(
|
||||
"Config: keep_ip={}, keep_relay={}, delay={}s",
|
||||
args.keep_ip, args.keep_relay, args.delay
|
||||
);
|
||||
|
||||
let network = TestNetwork::new();
|
||||
let s1 = SecretKey::from([0u8; 32]);
|
||||
let s2 = SecretKey::from([1u8; 32]);
|
||||
|
||||
// Create transports and configure builders with transport + address lookup
|
||||
let t1 = network.create_transport(s1.public())?;
|
||||
let ep1 = args.configure(s1.clone(), t1).bind().await?;
|
||||
|
||||
let t2 = network.create_transport(s2.public())?;
|
||||
let ep2 = args.configure(s2.clone(), t2).bind().await?;
|
||||
println!("ep2 addr: {:?}", ep2.addr());
|
||||
let server = Router::builder(ep2).accept(ALPN, Echo).spawn();
|
||||
|
||||
// Connect using just the endpoint ID - discovery will resolve addresses
|
||||
// Note: The test network's discovery is very fast (in-memory), so the custom
|
||||
// transport address is available immediately and wins before IP discovery runs.
|
||||
println!("Connecting to: {:?}", s2.public());
|
||||
let conn = ep1.connect(s2.public(), ALPN).await?;
|
||||
|
||||
// Helper to print paths and verify test transport is selected
|
||||
let verify_test_transport = |label: &str| {
|
||||
let paths = conn.paths();
|
||||
println!("Paths {}:", label);
|
||||
for path in paths.iter() {
|
||||
println!(
|
||||
" {} selected={} rtt={:?}",
|
||||
path.remote_addr(),
|
||||
path.is_selected(),
|
||||
path.rtt()
|
||||
);
|
||||
}
|
||||
let selected_path = paths.iter().find(|p| p.is_selected());
|
||||
let is_test_transport = selected_path.as_ref().is_some_and(|p| {
|
||||
matches!(p.remote_addr(), TransportAddr::Custom(addr) if addr.id() == TEST_TRANSPORT_ID)
|
||||
});
|
||||
assert!(
|
||||
is_test_transport,
|
||||
"Expected test transport (id={}) to be selected {}, got: {:?}",
|
||||
TEST_TRANSPORT_ID,
|
||||
label,
|
||||
selected_path.as_ref().map(|p| p.remote_addr())
|
||||
);
|
||||
println!(
|
||||
"Verified: test transport (id={}) is selected {}",
|
||||
TEST_TRANSPORT_ID, label
|
||||
);
|
||||
};
|
||||
|
||||
// Verify test transport is selected immediately after connecting
|
||||
verify_test_transport("immediately after connecting");
|
||||
|
||||
// If a delay is specified, wait and then re-check to see if the transport is still selected
|
||||
// after other discovery mechanisms have had time to run.
|
||||
if args.delay > 0 {
|
||||
println!(
|
||||
"Waiting {}s to let other discovery mechanisms run...",
|
||||
args.delay
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(args.delay)).await;
|
||||
verify_test_transport(&format!("after {}s delay", args.delay));
|
||||
}
|
||||
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
send.write_all(b"Hello custom transport!").await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
let response = recv.read_to_end(1000).await.anyerr()?;
|
||||
assert_eq!(&response, b"Hello custom transport!");
|
||||
conn.close(0u32.into(), b"bye!");
|
||||
server.shutdown().await.anyerr()?;
|
||||
drop(server);
|
||||
Ok(())
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
//! Very basic example showing how to implement a basic echo protocol,
|
||||
//! without using the `Router` API. (For the router version, check out the echo.rs example.)
|
||||
//!
|
||||
//! The echo protocol echos any data sent to it in the first stream.
|
||||
//!
|
||||
//! ## Running the Example
|
||||
//!
|
||||
//! cargo run --example echo-no-router
|
||||
|
||||
use iroh::{Endpoint, EndpointAddr, endpoint::presets};
|
||||
use n0_error::{AnyError as Error, Result, StdResultExt};
|
||||
|
||||
/// Each protocol is identified by its ALPN string.
|
||||
///
|
||||
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
||||
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
||||
const ALPN: &[u8] = b"iroh-example/echo/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let endpoint = start_accept_side().await?;
|
||||
|
||||
// wait for the endpoint to be online
|
||||
endpoint.online().await;
|
||||
|
||||
connect_side(endpoint.addr()).await?;
|
||||
|
||||
// This makes sure the endpoint is closed properly and connections close gracefully
|
||||
// and will indirectly close the tasks spawned by `start_accept_side`.
|
||||
endpoint.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_side(addr: EndpointAddr) -> Result<()> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
// Open a connection to the accepting endpoint
|
||||
let conn = endpoint.connect(addr, ALPN).await?;
|
||||
|
||||
// Open a bidirectional QUIC stream
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
|
||||
// Send some data to be echoed
|
||||
send.write_all(b"Hello, world!").await.anyerr()?;
|
||||
|
||||
// Signal the end of data for this particular stream
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// Receive the echo, but limit reading up to maximum 1000 bytes
|
||||
let response = recv.read_to_end(1000).await.anyerr()?;
|
||||
assert_eq!(&response, b"Hello, world!");
|
||||
|
||||
// Explicitly close the whole connection.
|
||||
conn.close(0u32.into(), b"bye!");
|
||||
|
||||
// The above call only queues a close message to be sent (see how it's not async!).
|
||||
// We need to actually call this to make sure this message is sent out.
|
||||
endpoint.close().await;
|
||||
// If we don't call this, but continue using the endpoint, then the queued
|
||||
// close call will eventually be picked up and sent.
|
||||
// But always try to wait for endpoint.close().await to go through before dropping
|
||||
// the endpoint to ensure any queued messages are sent through and connections are
|
||||
// closed gracefully.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_accept_side() -> Result<Endpoint> {
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
// The accept side needs to opt-in to the protocols it accepts,
|
||||
// as any connection attempts that can't be found with a matching ALPN
|
||||
// will be rejected.
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
// spawn a task so that `start_accept_side` returns immediately and we can continue in main().
|
||||
tokio::spawn({
|
||||
let endpoint = endpoint.clone();
|
||||
async move {
|
||||
// This task won't leak, because we call `endpoint.close()` in `main()`,
|
||||
// which causes `endpoint.accept().await` to return `None`.
|
||||
// In a more serious environment, we recommend avoiding `tokio::spawn` and use either a `TaskTracker` or
|
||||
// `JoinSet` instead to make sure you're not accidentally leaking tasks.
|
||||
while let Some(incoming) = endpoint.accept().await {
|
||||
// spawn a task for each incoming connection, so we can serve multiple connections asynchronously
|
||||
tokio::spawn(async move {
|
||||
let connection = incoming.await.anyerr()?;
|
||||
|
||||
// We can get the remote's endpoint id from the connection.
|
||||
let endpoint_id = connection.remote_id();
|
||||
println!("accepted connection from {endpoint_id}");
|
||||
|
||||
// Our protocol is a simple request-response protocol, so we expect the
|
||||
// connecting peer to open a single bi-directional stream.
|
||||
let (mut send, mut recv) = connection.accept_bi().await.anyerr()?;
|
||||
|
||||
// Echo any bytes received back directly.
|
||||
// This will keep copying until the sender signals the end of data on the stream.
|
||||
let bytes_sent = tokio::io::copy(&mut recv, &mut send).await.anyerr()?;
|
||||
println!("Copied over {bytes_sent} byte(s)");
|
||||
|
||||
// By calling `finish` on the send stream we signal that we will not send anything
|
||||
// further, which makes the receive stream on the other end terminate.
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// Wait until the remote closes the connection, which it does once it
|
||||
// received the response.
|
||||
connection.closed().await;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
});
|
||||
|
||||
Ok(endpoint)
|
||||
}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
//! Very basic example to showcase how to use iroh's APIs.
|
||||
//!
|
||||
//! This example implements a simple protocol that echos any data sent to it in the first stream.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! cargo run --example echo
|
||||
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr,
|
||||
endpoint::{Connection, presets},
|
||||
protocol::{AcceptError, ProtocolHandler, Router},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
|
||||
/// Each protocol is identified by its ALPN string.
|
||||
///
|
||||
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
||||
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
||||
const ALPN: &[u8] = b"iroh-example/echo/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let router = start_accept_side().await?;
|
||||
|
||||
// wait for the endpoint to be online
|
||||
router.endpoint().online().await;
|
||||
|
||||
connect_side(router.endpoint().addr()).await?;
|
||||
|
||||
// This makes sure the endpoint in the router is closed properly and connections close gracefully
|
||||
router.shutdown().await.anyerr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_side(addr: EndpointAddr) -> Result<()> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
// Open a connection to the accepting endpoint
|
||||
let conn = endpoint.connect(addr, ALPN).await?;
|
||||
|
||||
// Open a bidirectional QUIC stream
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
|
||||
// Send some data to be echoed
|
||||
send.write_all(b"Hello, world!").await.anyerr()?;
|
||||
|
||||
// Signal the end of data for this particular stream
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// Receive the echo, but limit reading up to maximum 1000 bytes
|
||||
let response = recv.read_to_end(1000).await.anyerr()?;
|
||||
assert_eq!(&response, b"Hello, world!");
|
||||
|
||||
// Explicitly close the whole connection.
|
||||
conn.close(0u32.into(), b"bye!");
|
||||
|
||||
// The above call only queues a close message to be sent (see how it's not async!).
|
||||
// We need to actually call this to make sure this message is sent out.
|
||||
endpoint.close().await;
|
||||
// If we don't call this, but continue using the endpoint, we then the queued
|
||||
// close call will eventually be picked up and sent.
|
||||
// But always try to wait for endpoint.close().await to go through before dropping
|
||||
// the endpoint to ensure any queued messages are sent through and connections are
|
||||
// closed gracefully.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_accept_side() -> Result<Router> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
// Build our protocol handler and add our protocol, identified by its ALPN, and spawn the endpoint.
|
||||
let router = Router::builder(endpoint).accept(ALPN, Echo).spawn();
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Echo;
|
||||
|
||||
impl ProtocolHandler for Echo {
|
||||
/// The `accept` method is called for each incoming connection for our ALPN.
|
||||
///
|
||||
/// The returned future runs on a newly spawned tokio task, so it can run as long as
|
||||
/// the connection lasts.
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
// We can get the remote's endpoint id from the connection.
|
||||
let endpoint_id = connection.remote_id();
|
||||
println!("accepted connection from {endpoint_id}");
|
||||
|
||||
// Our protocol is a simple request-response protocol, so we expect the
|
||||
// connecting peer to open a single bi-directional stream.
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
|
||||
// Echo any bytes received back directly.
|
||||
// This will keep copying until the sender signals the end of data on the stream.
|
||||
let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?;
|
||||
println!("Copied over {bytes_sent} byte(s)");
|
||||
|
||||
// By calling `finish` on the send stream we signal that we will not send anything
|
||||
// further, which makes the receive stream on the other end terminate.
|
||||
send.finish()?;
|
||||
|
||||
// Wait until the remote closes the connection, which it does once it
|
||||
// received the response.
|
||||
connection.closed().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
//! Tests connection to a home relay server and reports the status.
|
||||
//!
|
||||
//! Also shows how to specifically test if a connection fails due to authentication.
|
||||
|
||||
use clap::Parser;
|
||||
use iroh::{Endpoint, RelayMap, RelayUrl, Watcher, endpoint::presets};
|
||||
use n0_future::StreamExt;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
struct Args {
|
||||
/// Pass one or more relay URLs.
|
||||
///
|
||||
/// If unset will use the public default relays.
|
||||
relays: Vec<RelayUrl>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> n0_error::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
let relay_map = if args.relays.is_empty() {
|
||||
iroh::defaults::prod::default_relay_map()
|
||||
} else {
|
||||
RelayMap::from_iter(args.relays)
|
||||
};
|
||||
|
||||
let endpoint = Endpoint::builder(presets::Minimal)
|
||||
.relay_mode(iroh::RelayMode::Custom(relay_map))
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
println!("endpoint bound");
|
||||
|
||||
// Spawn a task to report the home relay status.
|
||||
//
|
||||
// You could pass a channel here if you wanted to abort something in case the
|
||||
// home relay connection fails.
|
||||
tokio::spawn(report_home_relay_status(endpoint.clone()));
|
||||
|
||||
// Wait for the endpoint to be online, i.e. connected to a home relay.
|
||||
//
|
||||
// Internally, this works like simplified version of the loop in `report_home_relay_status`,
|
||||
// only reporting about whether a connection was established and not exposing any
|
||||
// details in case of failure.
|
||||
endpoint.online().await;
|
||||
println!(
|
||||
"Endpoint is online. Home relay: {}",
|
||||
endpoint.addr().relay_urls().next().expect("has relay")
|
||||
);
|
||||
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
|
||||
endpoint.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn report_home_relay_status(endpoint: Endpoint) {
|
||||
let mut home_relay_status = endpoint.home_relay_status().stream();
|
||||
// The stream moves to the next item whenever the home relay status changes in any way.
|
||||
while let Some(status_list) = home_relay_status.next().await {
|
||||
// The stream's item is a list of status entries, one per home relay. Today the list
|
||||
// never holds more than one entry, because iroh only connects to a single home
|
||||
// relay. It is a list so that iroh could support multiple home relays without an API
|
||||
// change, so we may as well iterate it.
|
||||
for status in status_list {
|
||||
let url = status.url();
|
||||
if status.is_connected() {
|
||||
// We are connected!
|
||||
// When we reach this line, `Endpoint::online` also resolves.
|
||||
println!("Relay {url}: Connected");
|
||||
} else if let Some(reason) = status.auth_denied_reason() {
|
||||
// The relay server denied our authentication. Retrying will not help: we
|
||||
// would present the same credentials again. Report this prominently in
|
||||
// our app instead of waiting to come online.
|
||||
println!("Relay {url}: Authentication denied ({reason})");
|
||||
} else if let Some(error) = status.last_error() {
|
||||
// The connection failed for another reason, for example a network issue.
|
||||
// iroh keeps retrying with a backoff, so let's just print it.
|
||||
println!("Relay {url}: Connection failed ({error:#})");
|
||||
} else {
|
||||
// We are not connected, and there is no error to report: either no
|
||||
// connection has been attempted yet, or one is in progress. You should
|
||||
// usually just loop over this and wait for the next update.
|
||||
println!("Relay {url}: Disconnected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
//! Example demonstrating the [`IncomingFilter`] hook.
|
||||
//!
|
||||
//! This example requires all direct (UDP) connections to pass QUIC address
|
||||
//! validation via a retry token before being accepted.
|
||||
//!
|
||||
//! QUIC address validation ensures that a client truly owns its claimed IP
|
||||
//! address before proceeding with the more expensive part of the handshake,
|
||||
//! preventing denial-of-service attacks via spoofed source IPs.
|
||||
//!
|
||||
//! Relay connections are
|
||||
//! accepted without validation since the relay already vouches for the source.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run --example incoming-filter
|
||||
//! ```
|
||||
//!
|
||||
//! To test, connect from another process using:
|
||||
//! ```sh
|
||||
//! cargo run --example connect -- <NODE_ID>
|
||||
//! ```
|
||||
use std::sync::Arc;
|
||||
|
||||
use iroh::{
|
||||
Endpoint,
|
||||
endpoint::{Connection, Incoming, IncomingAddr, presets},
|
||||
protocol::{AcceptError, IncomingFilterOutcome, ProtocolHandler, Router},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
|
||||
const ALPN: &[u8] = b"iroh-example/incoming-filter/0";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Echo;
|
||||
|
||||
impl ProtocolHandler for Echo {
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
tokio::io::copy(&mut recv, &mut send).await?;
|
||||
send.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Require address validation for direct connections.
|
||||
///
|
||||
/// If the address is not yet validated, return `Retry` so the client has to
|
||||
/// prove it owns the source address before we do any further work. Validated
|
||||
/// connections and relay connections are accepted.
|
||||
fn filter(incoming: &Incoming) -> IncomingFilterOutcome {
|
||||
match incoming.remote_addr() {
|
||||
IncomingAddr::Ip(_) if !incoming.remote_addr_validated() => IncomingFilterOutcome::Retry,
|
||||
_ => IncomingFilterOutcome::Accept,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
endpoint.online().await;
|
||||
let addr = endpoint.addr();
|
||||
println!("Node ID: {}", endpoint.id());
|
||||
println!("Listening on: {addr:?}");
|
||||
println!("All direct connections require address validation via retry.\n");
|
||||
|
||||
let router = Router::builder(endpoint)
|
||||
.incoming_filter(Arc::new(filter))
|
||||
.accept(ALPN, Echo)
|
||||
.spawn();
|
||||
|
||||
tokio::signal::ctrl_c().await.anyerr()?;
|
||||
router.shutdown().await.anyerr()?;
|
||||
Ok(())
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect two devices and pass bytes using unreliable datagrams.
|
||||
//!
|
||||
//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection.
|
||||
//! run this example from the project root:
|
||||
//! $ cargo run --example listen-unreliable
|
||||
use iroh::{Endpoint, RelayMode, SecretKey, endpoint::presets};
|
||||
use n0_error::{AnyError as Error, Result, StdResultExt};
|
||||
use tracing::{info, warn};
|
||||
|
||||
// An example ALPN that we are using to communicate over the `Endpoint`
|
||||
const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
println!("\nlisten (unreliable) example!\n");
|
||||
let secret_key = SecretKey::generate();
|
||||
println!("public key: {}", secret_key.public());
|
||||
|
||||
// Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers.
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
// The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase.
|
||||
.secret_key(secret_key)
|
||||
// set the ALPN protocols this endpoint will accept on incoming connections
|
||||
.alpns(vec![EXAMPLE_ALPN.to_vec()])
|
||||
// `RelayMode::Default` means that we will use the default relay servers to holepunch and relay.
|
||||
// Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls.
|
||||
// Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS
|
||||
// If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code
|
||||
.relay_mode(RelayMode::Default)
|
||||
// you can choose a port to bind to, but passing in `0` will bind the socket to a random available port
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
let me = endpoint.id();
|
||||
println!("endpoint id: {me}");
|
||||
println!("endpoint listening addresses:");
|
||||
|
||||
// wait for the endpoint to be online
|
||||
endpoint.online().await;
|
||||
|
||||
let endpoint_addr = endpoint.addr();
|
||||
let local_addrs = endpoint_addr
|
||||
.ip_addrs()
|
||||
.map(|addr| {
|
||||
let addr = addr.to_string();
|
||||
println!("\t{addr}");
|
||||
addr
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let relay_url = endpoint_addr
|
||||
.relay_urls()
|
||||
.next()
|
||||
.expect("Should have a relay URL, assuming a default endpoint setup.");
|
||||
println!("endpoint relay server url: {relay_url}");
|
||||
println!("\nin a separate terminal run:");
|
||||
|
||||
println!(
|
||||
"\tcargo run --example connect-unreliable -- --endpoint-id {me} --addrs \"{local_addrs}\" --relay-url {relay_url}\n"
|
||||
);
|
||||
// accept incoming connections, returns a normal QUIC connection
|
||||
|
||||
while let Some(incoming) = endpoint.accept().await {
|
||||
let mut accepting = match incoming.accept() {
|
||||
Ok(accepting) => accepting,
|
||||
Err(err) => {
|
||||
warn!("incoming connection failed: {err:#}");
|
||||
// we can carry on in these cases:
|
||||
// this can be caused by retransmitted datagrams
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let alpn = accepting.alpn().await?;
|
||||
let conn = accepting.await?;
|
||||
let endpoint_id = conn.remote_id();
|
||||
info!(
|
||||
"new (unreliable) connection from {endpoint_id} with ALPN {}",
|
||||
String::from_utf8_lossy(&alpn),
|
||||
);
|
||||
// spawn a task to handle reading and writing off of the connection
|
||||
tokio::spawn(async move {
|
||||
// use the `noq` API to read a datagram off the connection, and send a datagra, in return
|
||||
while let Ok(message) = conn.read_datagram().await {
|
||||
let message = String::from_utf8(message.into()).anyerr()?;
|
||||
println!("received: {message}");
|
||||
|
||||
let message = format!("hi! you connected to {me}. bye bye");
|
||||
conn.send_datagram(message.as_bytes().to_vec().into())
|
||||
.anyerr()?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
});
|
||||
}
|
||||
// stop with SIGINT (ctrl-c)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+116
@@ -0,0 +1,116 @@
|
||||
//! The smallest example showing how to use iroh and [`iroh::Endpoint`] to connect two devices.
|
||||
//!
|
||||
//! This example uses the default relay servers to attempt to holepunch, and will use that relay server to relay packets if the two devices cannot establish a direct UDP connection.
|
||||
//! run this example from the project root:
|
||||
//! $ cargo run --example listen
|
||||
use std::time::Duration;
|
||||
|
||||
use iroh::{
|
||||
Endpoint, RelayMode, SecretKey,
|
||||
endpoint::{ConnectionError, presets},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
// An example ALPN that we are using to communicate over the `Endpoint`
|
||||
const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
println!("\nlisten example!\n");
|
||||
let secret_key = SecretKey::generate();
|
||||
println!("public key: {}", secret_key.public());
|
||||
|
||||
// Build a `Endpoint`, which uses PublicKeys as endpoint identifiers, uses QUIC for directly connecting to other endpoints, and uses the relay protocol and relay servers to holepunch direct connections between endpoints when there are NATs or firewalls preventing direct connections. If no direct connection can be made, packets are relayed over the relay servers.
|
||||
let endpoint = Endpoint::builder(presets::N0)
|
||||
// The secret key is used to authenticate with other endpoints. The PublicKey portion of this secret key is how we identify endpoints, often referred to as the `endpoint_id` in our codebase.
|
||||
.secret_key(secret_key)
|
||||
// set the ALPN protocols this endpoint will accept on incoming connections
|
||||
.alpns(vec![EXAMPLE_ALPN.to_vec()])
|
||||
// `RelayMode::Default` means that we will use the default relay servers to holepunch and relay.
|
||||
// Use `RelayMode::Custom` to pass in a `RelayMap` with custom relay urls.
|
||||
// Use `RelayMode::Disable` to disable holepunching and relaying over HTTPS
|
||||
// If you want to experiment with relaying using your own relay server, you must pass in the same custom relay url to both the `listen` code AND the `connect` code
|
||||
.relay_mode(RelayMode::Default)
|
||||
// you can choose a port to bind to, but passing in `0` will bind the socket to a random available port
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
let me = endpoint.id();
|
||||
println!("endpoint id: {me}");
|
||||
println!("endpoint listening addresses:");
|
||||
|
||||
// wait for the endpoint to be online
|
||||
endpoint.online().await;
|
||||
let endpoint_addr = endpoint.addr();
|
||||
|
||||
let local_addrs = endpoint_addr
|
||||
.ip_addrs()
|
||||
.map(|addr| {
|
||||
let addr = addr.to_string();
|
||||
println!("\t{addr}");
|
||||
addr
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let relay_url = endpoint_addr.relay_urls().next().expect("missing relay");
|
||||
println!("endpoint relay server url: {relay_url}");
|
||||
println!("\nin a separate terminal run:");
|
||||
|
||||
println!(
|
||||
"\tcargo run --example connect -- --endpoint-id {me} --addrs \"{local_addrs}\" --relay-url {relay_url}\n"
|
||||
);
|
||||
// accept incoming connections, returns a normal QUIC connection
|
||||
while let Some(incoming) = endpoint.accept().await {
|
||||
let mut accepting = match incoming.accept() {
|
||||
Ok(accepting) => accepting,
|
||||
Err(err) => {
|
||||
warn!("incoming connection failed: {err:#}");
|
||||
// we can carry on in these cases:
|
||||
// this can be caused by retransmitted datagrams
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let alpn = accepting.alpn().await?;
|
||||
let conn = accepting.await?;
|
||||
let endpoint_id = conn.remote_id();
|
||||
info!(
|
||||
"new connection from {endpoint_id} with ALPN {}",
|
||||
String::from_utf8_lossy(&alpn),
|
||||
);
|
||||
|
||||
// spawn a task to handle reading and writing off of the connection
|
||||
tokio::spawn(async move {
|
||||
// accept a bi-directional QUIC connection
|
||||
// use the `noq` APIs to send and recv content
|
||||
let (mut send, mut recv) = conn.accept_bi().await.anyerr()?;
|
||||
debug!("accepted bi stream, waiting for data...");
|
||||
let message = recv.read_to_end(100).await.anyerr()?;
|
||||
let message = String::from_utf8(message).anyerr()?;
|
||||
println!("received: {message}");
|
||||
|
||||
let message = format!("hi! you connected to {me}. bye bye");
|
||||
send.write_all(message.as_bytes()).await.anyerr()?;
|
||||
// call `finish` to close the connection gracefully
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// We sent the last message, so wait for the client to close the connection once
|
||||
// it received this message.
|
||||
let res = tokio::time::timeout(Duration::from_secs(3), async move {
|
||||
let closed = conn.closed().await;
|
||||
if !matches!(closed, ConnectionError::ApplicationClosed(_)) {
|
||||
println!("endpoint {endpoint_id} disconnected with an error: {closed:#}");
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if res.is_err() {
|
||||
println!("endpoint {endpoint_id} did not disconnect within 3 seconds");
|
||||
}
|
||||
n0_error::Ok(())
|
||||
});
|
||||
}
|
||||
// stop with SIGINT (ctrl-c)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use iroh::{
|
||||
Endpoint,
|
||||
endpoint::{
|
||||
AfterHandshakeOutcome, Closed, Connection, EndpointHooks, WeakConnectionHandle, presets,
|
||||
},
|
||||
};
|
||||
use n0_error::{Result, StackResultExt, StdResultExt, ensure_any};
|
||||
use n0_future::task::AbortOnDropHandle;
|
||||
use tokio::{
|
||||
sync::mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tracing::{Instrument, info, info_span};
|
||||
|
||||
const ALPN: &[u8] = b"iroh/test";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let monitor = Monitor::new();
|
||||
let server = Endpoint::builder(presets::Minimal)
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.hooks(monitor.clone())
|
||||
.bind()
|
||||
.instrument(info_span!("server"))
|
||||
.await?;
|
||||
let server_addr = server.addr();
|
||||
|
||||
let count = 2;
|
||||
|
||||
let client_task = tokio::spawn(
|
||||
async move {
|
||||
let client = Endpoint::builder(presets::Minimal)
|
||||
.bind()
|
||||
.instrument(info_span!("client"))
|
||||
.await?;
|
||||
for _i in 0..count {
|
||||
let conn = client.connect(server_addr.clone(), ALPN).await?;
|
||||
let mut s = conn.accept_uni().await.anyerr()?;
|
||||
let data = s.read_to_end(2).await.anyerr()?;
|
||||
ensure_any!(data == b"hi", "unexpected data");
|
||||
conn.close(23u32.into(), b"bye");
|
||||
}
|
||||
client.close().await;
|
||||
n0_error::Ok(client)
|
||||
}
|
||||
.instrument(info_span!("client")),
|
||||
);
|
||||
|
||||
let server_task = tokio::spawn(
|
||||
async move {
|
||||
for _i in 0..count {
|
||||
let conn = server
|
||||
.accept()
|
||||
.await
|
||||
.context("server endpoint closed")?
|
||||
.await?;
|
||||
let mut s = conn.open_uni().await.anyerr()?;
|
||||
s.write_all(b"hi").await.anyerr()?;
|
||||
s.finish().anyerr()?;
|
||||
conn.closed().await;
|
||||
}
|
||||
server.close().await;
|
||||
n0_error::Ok(())
|
||||
}
|
||||
.instrument(info_span!("server")),
|
||||
);
|
||||
client_task.await.std_context("client")?.context("client")?;
|
||||
server_task.await.std_context("server")?.context("server")?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
drop(monitor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Our connection monitor impl.
|
||||
///
|
||||
/// This here only logs connection open and close events via tracing.
|
||||
/// It could also maintain a datastructure of all connections, or send the stats to some metrics service.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Monitor {
|
||||
tx: UnboundedSender<MonitoredConnection>,
|
||||
_task: Arc<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
/// Static info captured at handshake time, paired with a weak handle to the connection.
|
||||
///
|
||||
/// We capture `alpn` and `remote_id` at hook time because [`WeakConnectionHandle`] only
|
||||
/// exposes [`upgrade`] and [`closed`], so reading these fields after the connection has
|
||||
/// been dropped would otherwise be impossible.
|
||||
///
|
||||
/// [`upgrade`]: WeakConnectionHandle::upgrade
|
||||
/// [`closed`]: WeakConnectionHandle::closed
|
||||
#[derive(Debug)]
|
||||
struct MonitoredConnection {
|
||||
alpn: Vec<u8>,
|
||||
remote_id: iroh::EndpointId,
|
||||
handle: WeakConnectionHandle,
|
||||
}
|
||||
|
||||
impl EndpointHooks for Monitor {
|
||||
async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
|
||||
let info = MonitoredConnection {
|
||||
alpn: conn.alpn().to_vec(),
|
||||
remote_id: conn.remote_id(),
|
||||
handle: conn.weak_handle(),
|
||||
};
|
||||
self.tx.send(info).ok();
|
||||
AfterHandshakeOutcome::Accept
|
||||
}
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let task = tokio::spawn(Self::run(rx).instrument(info_span!("watcher")));
|
||||
Self {
|
||||
tx,
|
||||
_task: Arc::new(AbortOnDropHandle::new(task)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut rx: UnboundedReceiver<MonitoredConnection>) {
|
||||
let mut tasks = JoinSet::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(MonitoredConnection { alpn, remote_id, handle }) = rx.recv() => {
|
||||
let alpn = String::from_utf8_lossy(&alpn).to_string();
|
||||
let remote = remote_id.fmt_short();
|
||||
let rtt = handle.upgrade().and_then(|c| c.paths().iter().map(|p| p.rtt()).min());
|
||||
info!(%remote, %alpn, ?rtt, "new connection");
|
||||
tasks.spawn(async move {
|
||||
match handle.closed().await {
|
||||
Some(Closed { reason, stats, .. }) => {
|
||||
// We have access to the final stats of the connection!
|
||||
info!(%remote, %alpn, ?reason, udp_rx=stats.udp_rx.bytes, udp_tx=stats.udp_tx.bytes, "connection closed");
|
||||
}
|
||||
None => {
|
||||
// The connection was closed before we could register our stats-on-close listener.
|
||||
info!(%remote, %alpn, "connection closed before tracking started");
|
||||
}
|
||||
}
|
||||
}.instrument(tracing::Span::current()));
|
||||
}
|
||||
Some(res) = tasks.join_next(), if !tasks.is_empty() => res.expect("conn close task panicked"),
|
||||
else => break,
|
||||
}
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
res.expect("conn close task panicked");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
//! Force iroh to negotiate ONLY a post-quantum key exchange (X25519MLKEM768).
|
||||
//!
|
||||
//! Requires `tls-aws-lc-rs` (the only rustls backend with ML-KEM today).
|
||||
//! Stripping `kx_groups` to the PQ group makes PQ *required* rather than
|
||||
//! *preferred*: peers without it fail the TLS handshake.
|
||||
//!
|
||||
//! Note: iroh's `crypto_provider` is shared with relay/discovery TLS, and
|
||||
//! n0's infra does not support PQ key exchange yet — so a PQ-only endpoint
|
||||
//! can't use n0 public relays or discovery servers.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! `tls-aws-lc-rs` is required (the example explicitly constructs an
|
||||
//! aws-lc-rs `CryptoProvider`, so the backend must be linked):
|
||||
//!
|
||||
//! cargo run --example pq-key-exchange --features=tls-aws-lc-rs
|
||||
//!
|
||||
//! With iroh's default features still on, both `ring` and `aws-lc-rs` get
|
||||
//! linked. That's harmless — we wire the aws-lc-rs provider in directly via
|
||||
//! `Builder::crypto_provider`.
|
||||
use std::sync::Arc;
|
||||
|
||||
use iroh::{
|
||||
RelayMode,
|
||||
endpoint::{Endpoint, presets::Empty},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
|
||||
const ALPN: &[u8] = b"iroh-example/pq-key-exchange/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let pq = pq_only_provider();
|
||||
|
||||
let server = Endpoint::builder(Empty)
|
||||
.crypto_provider(pq.clone())
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.relay_mode(RelayMode::Disabled)
|
||||
.bind()
|
||||
.await?;
|
||||
let server_addr = server.addr();
|
||||
|
||||
let server_task = tokio::spawn({
|
||||
let server = server.clone();
|
||||
async move {
|
||||
let conn = server
|
||||
.accept()
|
||||
.await
|
||||
.expect("incoming")
|
||||
.accept()
|
||||
.anyerr()?
|
||||
.await
|
||||
.anyerr()?;
|
||||
let mut recv = conn.accept_uni().await.anyerr()?;
|
||||
let msg = recv.read_to_end(1024).await.anyerr()?;
|
||||
println!("server received: {:?}", String::from_utf8_lossy(&msg));
|
||||
let mut send = conn.open_uni().await.anyerr()?;
|
||||
send.write_all(b"pong over PQ").await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
conn.closed().await;
|
||||
n0_error::Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let client = Endpoint::builder(Empty)
|
||||
.crypto_provider(pq)
|
||||
.relay_mode(RelayMode::Disabled)
|
||||
.bind()
|
||||
.await?;
|
||||
let conn = client.connect(server_addr, ALPN).await.anyerr()?;
|
||||
println!("client handshake done (X25519MLKEM768 was the only kx offered)");
|
||||
|
||||
let mut send = conn.open_uni().await.anyerr()?;
|
||||
send.write_all(b"ping over PQ").await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
let mut recv = conn.accept_uni().await.anyerr()?;
|
||||
let msg = recv.read_to_end(1024).await.anyerr()?;
|
||||
println!("client received: {:?}", String::from_utf8_lossy(&msg));
|
||||
|
||||
conn.close(0u32.into(), b"done");
|
||||
server_task.await.anyerr()?.anyerr()?;
|
||||
client.close().await;
|
||||
server.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pq_only_provider() -> Arc<rustls::crypto::CryptoProvider> {
|
||||
let mut p = aws_lc_rs::default_provider();
|
||||
p.kx_groups = vec![aws_lc_rs::kx_group::X25519MLKEM768];
|
||||
Arc::new(p)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
//! Prefer post-quantum key exchange when available, fall back to classical.
|
||||
//!
|
||||
//! Unlike `pq-only-key-exchange`, this example keeps classical kx groups in
|
||||
//! the list so the endpoint can still talk to peers that don't support
|
||||
//! ML-KEM, *and* so n0's relay/discovery TLS (classical kx) keeps working.
|
||||
//! Putting `X25519MLKEM768` first in `kx_groups` means rustls negotiates PQ
|
||||
//! whenever both peers support it, classical otherwise.
|
||||
//!
|
||||
//! Note: rustls' `aws_lc_rs::default_provider()` only puts `X25519MLKEM768`
|
||||
//! first when rustls is built with its `prefer-post-quantum` feature; without
|
||||
//! it, PQ is offered last. We override `kx_groups` here so the policy is
|
||||
//! independent of how rustls was compiled, and print the list at startup.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! `tls-aws-lc-rs` is required:
|
||||
//!
|
||||
//! cargo run --example prefer-pq-key-exchange --features=tls-aws-lc-rs
|
||||
use std::sync::Arc;
|
||||
|
||||
use iroh::endpoint::{Endpoint, presets::N0};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use rustls::crypto::aws_lc_rs::{self, kx_group};
|
||||
|
||||
const ALPN: &[u8] = b"iroh-example/prefer-pq-key-exchange/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let mut provider = aws_lc_rs::default_provider();
|
||||
provider.kx_groups = vec![
|
||||
kx_group::X25519MLKEM768,
|
||||
kx_group::X25519,
|
||||
kx_group::SECP256R1,
|
||||
kx_group::SECP384R1,
|
||||
];
|
||||
let kx_names: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
|
||||
println!("kx_groups (in offer order): {kx_names:?}");
|
||||
let pq = Arc::new(provider);
|
||||
|
||||
let server = Endpoint::builder(N0)
|
||||
.crypto_provider(pq.clone())
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.bind()
|
||||
.await?;
|
||||
server.online().await;
|
||||
let server_addr = server.addr();
|
||||
println!("server addr: {server_addr:?}");
|
||||
|
||||
let server_task = tokio::spawn({
|
||||
let server = server.clone();
|
||||
async move {
|
||||
let conn = server
|
||||
.accept()
|
||||
.await
|
||||
.expect("incoming")
|
||||
.accept()
|
||||
.anyerr()?
|
||||
.await
|
||||
.anyerr()?;
|
||||
let mut recv = conn.accept_uni().await.anyerr()?;
|
||||
let msg = recv.read_to_end(1024).await.anyerr()?;
|
||||
println!("server received: {:?}", String::from_utf8_lossy(&msg));
|
||||
let mut send = conn.open_uni().await.anyerr()?;
|
||||
send.write_all(b"pong over PQ-preferred").await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
conn.closed().await;
|
||||
n0_error::Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let client = Endpoint::builder(N0).crypto_provider(pq).bind().await?;
|
||||
client.online().await;
|
||||
let conn = client.connect(server_addr, ALPN).await.anyerr()?;
|
||||
println!("client handshake done (X25519MLKEM768 preferred, classical kx kept as fallback)");
|
||||
|
||||
let mut send = conn.open_uni().await.anyerr()?;
|
||||
send.write_all(b"ping over PQ-preferred").await.anyerr()?;
|
||||
send.finish().anyerr()?;
|
||||
let mut recv = conn.accept_uni().await.anyerr()?;
|
||||
let msg = recv.read_to_end(1024).await.anyerr()?;
|
||||
println!("client received: {:?}", String::from_utf8_lossy(&msg));
|
||||
|
||||
conn.close(0u32.into(), b"done");
|
||||
server_task.await.anyerr()?.anyerr()?;
|
||||
client.close().await;
|
||||
server.close().await;
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+447
@@ -0,0 +1,447 @@
|
||||
//! Example for using an iroh hook to collect information about remote endpoints.
|
||||
//!
|
||||
//! This implements a [`RemoteMap`] which collects information about all connections and paths from an iroh endpoint.
|
||||
//! The remote map can be cloned and inspected from other tasks at any time. It contains both data about all
|
||||
//! currently active connections, and an aggregate status for each remote that remains available even after
|
||||
//! all connections to the endpoint have been closed.
|
||||
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use iroh::{Endpoint, EndpointAddr, endpoint::presets};
|
||||
use n0_error::{Result, StackResultExt, StdResultExt, ensure_any};
|
||||
use n0_future::IterExt;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
|
||||
use crate::remote_map::RemoteMap;
|
||||
|
||||
const ALPN: &[u8] = b"iroh/test";
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Create the remote map and hook.
|
||||
let (hook, remote_map) = RemoteMap::new();
|
||||
|
||||
// Bind our endpoint and install the remote map hook.
|
||||
let server = Endpoint::builder(presets::N0)
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.hooks(hook)
|
||||
.bind()
|
||||
.instrument(info_span!("server"))
|
||||
.await?;
|
||||
// Wait for our endpoint to be fully online.
|
||||
server.online().await;
|
||||
let server_addr = server.addr();
|
||||
|
||||
// Spawn a task that creates `count` client endpoints that each connect to our server.
|
||||
let count = 3;
|
||||
let client_task = tokio::spawn(run_clients(server_addr, count));
|
||||
|
||||
// Spawn a task that prints info from the remote map while some connections are active.
|
||||
// You can use this info to make decisions about remotes.
|
||||
let _inspect_task = tokio::task::spawn({
|
||||
let remote_map = remote_map.clone();
|
||||
async move {
|
||||
// Wait a bit.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
println!("== while connections are active == ");
|
||||
log_active(&remote_map);
|
||||
log_aggregate(&remote_map);
|
||||
println!();
|
||||
}
|
||||
});
|
||||
|
||||
// Let the server accept `count` connections in parallel.
|
||||
// The server keeps all connections open for at least 500 milliseconds.
|
||||
std::iter::repeat_with(async || {
|
||||
let conn = server
|
||||
.accept()
|
||||
.await
|
||||
.context("server endpoint closed")?
|
||||
.await?;
|
||||
info!("accepted");
|
||||
let mut s = conn.open_uni().await.anyerr()?;
|
||||
// wait a bit.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
s.write_all(b"hi").await.anyerr()?;
|
||||
s.finish().anyerr()?;
|
||||
conn.closed().await;
|
||||
info!("closed");
|
||||
n0_error::Ok(())
|
||||
})
|
||||
.take(count)
|
||||
.enumerate()
|
||||
.map(|(i, fut)| fut.instrument(info_span!("server-conn", %i)))
|
||||
.try_join_all()
|
||||
.await?;
|
||||
|
||||
// Print the remote map again.
|
||||
println!("== all connections closed ==");
|
||||
log_active(&remote_map);
|
||||
log_aggregate(&remote_map);
|
||||
|
||||
server.close().await;
|
||||
client_task.await.std_context("client")?.context("client")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uses the current connection info to print info about a remote.
|
||||
///
|
||||
/// Uses the info about *currently active* connections, which return `None` if no connections are active.
|
||||
fn log_active(remote_map: &RemoteMap) {
|
||||
println!("current remote state:");
|
||||
for (id, info) in remote_map.read().iter() {
|
||||
println!(
|
||||
"[{}] is_active {}, connections {}, ip_path {:?}, relay_path {:?}, current_min_rtt {:?}",
|
||||
id.fmt_short(),
|
||||
info.is_active(),
|
||||
info.active_connections(),
|
||||
info.has_ip_path(),
|
||||
info.has_relay_path(),
|
||||
info.current_min_rtt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the aggregated info to print info about a remote.
|
||||
///
|
||||
/// The aggregated info is updated for all connection and path changes, and stays at the latest values
|
||||
/// even if all connections are closed.
|
||||
fn log_aggregate(remote_map: &RemoteMap) {
|
||||
println!("aggregate remote state:");
|
||||
for (id, info) in remote_map.read().iter() {
|
||||
let aggregate = info.aggregate();
|
||||
println!(
|
||||
"[{}] min_rtt {:?}, max_rtt {:?}, ip_path {:?}, relay_path {}, last_update {:?} ago",
|
||||
id.fmt_short(),
|
||||
aggregate.rtt_min,
|
||||
aggregate.rtt_max,
|
||||
aggregate.ip_path,
|
||||
aggregate.relay_path,
|
||||
SystemTime::now()
|
||||
.duration_since(aggregate.last_update)
|
||||
.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_clients(server_addr: EndpointAddr, count: usize) -> Result {
|
||||
std::iter::repeat_with(async || {
|
||||
let client = Endpoint::builder(presets::N0)
|
||||
.bind()
|
||||
.instrument(info_span!("client"))
|
||||
.await?;
|
||||
let conn = client.connect(server_addr.clone(), ALPN).await?;
|
||||
info!("connected");
|
||||
let mut s = conn.accept_uni().await.anyerr()?;
|
||||
let data = s.read_to_end(2).await.anyerr()?;
|
||||
ensure_any!(data == b"hi", "unexpected data");
|
||||
conn.close(23u32.into(), b"bye");
|
||||
info!("closed");
|
||||
client.close().await;
|
||||
n0_error::Ok(())
|
||||
})
|
||||
.take(count)
|
||||
.enumerate()
|
||||
.map(|(i, fut)| fut.instrument(info_span!("client", %i)))
|
||||
.try_join_all()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod remote_map {
|
||||
//! Implementation of a remote map and hook to track information about all remote endpoints to which an iroh endpoint
|
||||
//! has connections with.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock, RwLockReadGuard},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use iroh::{
|
||||
EndpointId, TransportAddr,
|
||||
endpoint::{
|
||||
AfterHandshakeOutcome, Connection, EndpointHooks, PathEvent, WeakConnectionHandle,
|
||||
},
|
||||
};
|
||||
use n0_future::{StreamExt, task::AbortOnDropHandle};
|
||||
use tokio::{sync::mpsc, task::JoinSet};
|
||||
use tracing::{Instrument, debug, info, info_span};
|
||||
|
||||
/// Information about a remote info.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RemoteInfo {
|
||||
aggregate: Aggregate,
|
||||
connections: HashMap<u64, WeakConnectionHandle>,
|
||||
}
|
||||
|
||||
/// Aggregate information about a remote info.
|
||||
#[derive(Debug)]
|
||||
pub struct Aggregate {
|
||||
/// Minimal RTT observed over all paths to this remote.
|
||||
pub rtt_min: Duration,
|
||||
/// Maximal RTT observed over all paths to this remote.
|
||||
pub rtt_max: Duration,
|
||||
/// Whether we ever had an IP path to this remote.
|
||||
pub ip_path: bool,
|
||||
/// Whether we ever had a relay path to this remote.
|
||||
pub relay_path: bool,
|
||||
/// Time this aggregate was last updated.
|
||||
pub last_update: SystemTime,
|
||||
}
|
||||
|
||||
impl Default for Aggregate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rtt_min: Duration::MAX,
|
||||
rtt_max: Duration::ZERO,
|
||||
ip_path: false,
|
||||
relay_path: false,
|
||||
last_update: SystemTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Aggregate {
|
||||
fn update(&mut self, addr: TransportAddr, stats: iroh::endpoint::PathStats) {
|
||||
self.last_update = SystemTime::now();
|
||||
self.ip_path |= addr.is_ip();
|
||||
self.relay_path |= addr.is_relay();
|
||||
debug!("path update {addr} {stats:?}");
|
||||
self.rtt_min = self.rtt_min.min(stats.rtt);
|
||||
self.rtt_max = self.rtt_max.max(stats.rtt);
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteInfo {
|
||||
/// Returns an aggregate of stats for this remote.
|
||||
///
|
||||
/// This includes info from closed connections.
|
||||
pub fn aggregate(&self) -> &Aggregate {
|
||||
&self.aggregate
|
||||
}
|
||||
|
||||
/// Returns the minimal RTT of all currently active paths.
|
||||
///
|
||||
/// Returns `None` if there are no active connections.
|
||||
pub fn current_min_rtt(&self) -> Option<Duration> {
|
||||
self.upgraded()
|
||||
.filter_map(|c| c.paths().iter().map(|p| p.rtt()).min())
|
||||
.min()
|
||||
}
|
||||
|
||||
/// Returns whether any active connection to the remote has an active IP path.
|
||||
pub fn has_ip_path(&self) -> bool {
|
||||
self.upgraded().any(|c| c.paths().iter().any(|p| p.is_ip()))
|
||||
}
|
||||
|
||||
/// Returns whether any active connection to the remote has an active relay path.
|
||||
pub fn has_relay_path(&self) -> bool {
|
||||
self.upgraded()
|
||||
.any(|c| c.paths().iter().any(|p| p.is_relay()))
|
||||
}
|
||||
|
||||
/// Returns `true` if there are active connections to this remote.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active_connections() > 0
|
||||
}
|
||||
|
||||
/// Returns the number of active connections to this remote.
|
||||
pub fn active_connections(&self) -> usize {
|
||||
self.upgraded().count()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all active handles upgraded to a [`Connection`].
|
||||
fn upgraded(&self) -> impl Iterator<Item = Connection> {
|
||||
self.connections
|
||||
.values()
|
||||
.filter_map(WeakConnectionHandle::upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
type RemoteMapInner = Arc<RwLock<HashMap<EndpointId, RemoteInfo>>>;
|
||||
|
||||
/// Contains information about remote nodes our endpoint has or had connections with.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RemoteMap {
|
||||
map: RemoteMapInner,
|
||||
_task: Arc<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
/// Hook to collect information about remote endpoints from an endpoint.
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteMapHook {
|
||||
tx: mpsc::Sender<TrackedConnection>,
|
||||
}
|
||||
|
||||
/// Pairing of a connection's remote endpoint id, captured at handshake time, with a
|
||||
/// weak handle to the connection. The `remote_id` is captured eagerly because
|
||||
/// [`WeakConnectionHandle`] only exposes [`upgrade`] and [`closed`].
|
||||
///
|
||||
/// [`upgrade`]: WeakConnectionHandle::upgrade
|
||||
/// [`closed`]: WeakConnectionHandle::closed
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackedConnection {
|
||||
pub remote_id: EndpointId,
|
||||
pub handle: WeakConnectionHandle,
|
||||
}
|
||||
|
||||
impl EndpointHooks for RemoteMapHook {
|
||||
async fn after_handshake(&self, conn: &Connection) -> AfterHandshakeOutcome {
|
||||
info!(remote=%conn.remote_id().fmt_short(), "after_handshake");
|
||||
// We track the connection via a weak handle so this map does not keep the
|
||||
// connection alive past the application's last reference to it.
|
||||
let tracked = TrackedConnection {
|
||||
remote_id: conn.remote_id(),
|
||||
handle: conn.weak_handle(),
|
||||
};
|
||||
self.tx.send(tracked).await.ok();
|
||||
AfterHandshakeOutcome::Accept
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteMap {
|
||||
/// Creates a new [`RemoteMapHook`] and [`RemoteMap`].
|
||||
pub fn new() -> (RemoteMapHook, Self) {
|
||||
Self::with_max_retention(Duration::from_secs(60 * 5))
|
||||
}
|
||||
|
||||
/// Creates a new [`RemoteMapHook`] and [`RemoteMap`] and configure the retention time.
|
||||
///
|
||||
/// `retention_time` is the time entries for remote endpoints remain in the map after the last connection has closed.
|
||||
pub fn with_max_retention(retention_time: Duration) -> (RemoteMapHook, Self) {
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
let map = RemoteMapInner::default();
|
||||
let task = tokio::spawn(
|
||||
Self::run(rx, map.clone(), retention_time)
|
||||
.instrument(info_span!("remote-map-task")),
|
||||
);
|
||||
let map = Self {
|
||||
map,
|
||||
_task: Arc::new(AbortOnDropHandle::new(task)),
|
||||
};
|
||||
let hook = RemoteMapHook { tx };
|
||||
(hook, map)
|
||||
}
|
||||
|
||||
/// Read the current state of the remote map.
|
||||
///
|
||||
/// Returns a [`RwLockReadGuard`] with the actual remote map. Don't hold over await points!
|
||||
pub fn read(&self) -> RwLockReadGuard<'_, HashMap<EndpointId, RemoteInfo>> {
|
||||
self.map.read().expect("poisoned")
|
||||
}
|
||||
|
||||
async fn run(
|
||||
mut rx: mpsc::Receiver<TrackedConnection>,
|
||||
map: RemoteMapInner,
|
||||
retention_time: Duration,
|
||||
) {
|
||||
let mut tasks = JoinSet::new();
|
||||
let mut conn_id = 0;
|
||||
|
||||
// Spawn a task to clear expired entries.
|
||||
let expiry_task = tasks.spawn(Self::clear_expired(retention_time, map.clone()));
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(conn) = rx.recv() => {
|
||||
conn_id += 1;
|
||||
Self::on_connection(&mut tasks, map.clone(), conn_id, conn);
|
||||
}
|
||||
Some(res) = tasks.join_next(), if !tasks.is_empty() => {
|
||||
res.expect("conn close task panicked");
|
||||
}
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Abort expiry task and join remaining tasks.
|
||||
expiry_task.abort();
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(err) = &res
|
||||
&& !err.is_cancelled()
|
||||
{
|
||||
res.expect("conn close task panicked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_connection(
|
||||
tasks: &mut JoinSet<()>,
|
||||
map: RemoteMapInner,
|
||||
conn_id: u64,
|
||||
conn: TrackedConnection,
|
||||
) {
|
||||
// Store conn info for full introspection possibility.
|
||||
{
|
||||
let mut inner = map.write().expect("poisoned");
|
||||
inner
|
||||
.entry(conn.remote_id)
|
||||
.or_default()
|
||||
.connections
|
||||
.insert(conn_id, conn.handle.clone());
|
||||
}
|
||||
|
||||
// Track path changes to update stats aggregate.
|
||||
if let Some(mut path_events) = conn.handle.upgrade().map(|conn| conn.path_events()) {
|
||||
tasks.spawn(
|
||||
async move {
|
||||
while let Some(event) = path_events.next().await {
|
||||
let mut inner = map.write().expect("poisoned");
|
||||
let info = inner.entry(conn.remote_id).or_default();
|
||||
match event {
|
||||
PathEvent::Closed {
|
||||
remote_addr,
|
||||
last_stats,
|
||||
..
|
||||
} => {
|
||||
info.aggregate.update(remote_addr, *last_stats);
|
||||
}
|
||||
_ => {
|
||||
if let Some(conn) = conn.handle.upgrade() {
|
||||
for path in conn.paths().iter() {
|
||||
info.aggregate
|
||||
.update(path.remote_addr().clone(), path.stats());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut inner = map.write().expect("poisoned");
|
||||
let info = inner.entry(conn.remote_id).or_default();
|
||||
info.connections.remove(&conn_id);
|
||||
info.aggregate.last_update = SystemTime::now();
|
||||
}
|
||||
.instrument(tracing::Span::current()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_expired(
|
||||
retention_time: Duration,
|
||||
map: Arc<RwLock<HashMap<iroh::PublicKey, RemoteInfo>>>,
|
||||
) {
|
||||
let mut interval = tokio::time::interval(retention_time);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let now = SystemTime::now();
|
||||
let mut inner = map.write().expect("poisoned");
|
||||
inner.retain(|_remote, info| {
|
||||
info.is_active()
|
||||
|| now
|
||||
.duration_since(info.aggregate().last_update)
|
||||
.map(|age| age < retention_time)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
//! Very basic example to showcase how to write a protocol that rejects new
|
||||
//! connections based on internal state. Useful when you want an endpoint to
|
||||
//! stop accepting new connections for some reason only known to the endpoint. Maybe
|
||||
//! it's doing a migration, starting up, in a "maintenance mode", or serving
|
||||
//! too many connections.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! cargo run --example screening-connection
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr,
|
||||
endpoint::{Accepting, Connection, presets},
|
||||
protocol::{AcceptError, ProtocolHandler, Router},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt, e};
|
||||
|
||||
/// Each protocol is identified by its ALPN string.
|
||||
///
|
||||
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
||||
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
||||
const ALPN: &[u8] = b"iroh-example/screening-connection/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let router = start_accept_side().await?;
|
||||
// Wait for the endpoint to be reachable
|
||||
router.endpoint().online().await;
|
||||
let endpoint_addr = router.endpoint().addr();
|
||||
|
||||
// call connect three times. connection index 1 will be an odd number, and rejected.
|
||||
connect_side(&endpoint_addr).await?;
|
||||
if let Err(err) = connect_side(&endpoint_addr).await {
|
||||
println!("Error connecting: {}", err);
|
||||
}
|
||||
connect_side(&endpoint_addr).await?;
|
||||
|
||||
// This makes sure the endpoint in the router is closed properly and connections close gracefully
|
||||
router.shutdown().await.anyerr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_side(addr: &EndpointAddr) -> Result<()> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
// Open a connection to the accepting endpoint
|
||||
let conn = endpoint.connect(addr.clone(), ALPN).await?;
|
||||
|
||||
// Open a bidirectional QUIC stream
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
|
||||
// Send some data to be echoed
|
||||
send.write_all(b"Hello, world!").await.anyerr()?;
|
||||
|
||||
// Signal the end of data for this particular stream
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// Receive the echo, but limit reading up to maximum 1000 bytes
|
||||
let response = recv.read_to_end(1000).await.anyerr()?;
|
||||
assert_eq!(&response, b"Hello, world!");
|
||||
|
||||
// Explicitly close the whole connection.
|
||||
conn.close(0u32.into(), b"bye!");
|
||||
|
||||
// The above call only queues a close message to be sent (see how it's not async!).
|
||||
// We need to actually call this to make sure this message is sent out.
|
||||
endpoint.close().await;
|
||||
// If we don't call this, but continue using the endpoint, we then the queued
|
||||
// close call will eventually be picked up and sent.
|
||||
// But always try to wait for endpoint.close().await to go through before dropping
|
||||
// the endpoint to ensure any queued messages are sent through and connections are
|
||||
// closed gracefully.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_accept_side() -> Result<Router> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
let echo = ScreenedEcho {
|
||||
conn_attempt_count: Arc::new(AtomicU64::new(0)),
|
||||
};
|
||||
|
||||
// Build our protocol handler and add our protocol, identified by its ALPN, and spawn the endpoint.
|
||||
let router = Router::builder(endpoint).accept(ALPN, echo).spawn();
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// This is the same as the echo example, but keeps an internal count of the
|
||||
/// number of connections that have been attempted. This is to demonstrate how
|
||||
/// to plumb state into the protocol handler
|
||||
#[derive(Debug, Clone)]
|
||||
struct ScreenedEcho {
|
||||
conn_attempt_count: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ProtocolHandler for ScreenedEcho {
|
||||
/// `on_accepting` allows us to intercept a connection as it's being formed,
|
||||
/// which is the right place to cut off a connection as early as possible.
|
||||
/// This is an optional method on the ProtocolHandler trait.
|
||||
async fn on_accepting(&self, accepting: Accepting) -> Result<Connection, AcceptError> {
|
||||
self.conn_attempt_count.fetch_add(1, Ordering::Relaxed);
|
||||
let count = self.conn_attempt_count.load(Ordering::Relaxed);
|
||||
|
||||
// reject every other connection
|
||||
if count.is_multiple_of(2) {
|
||||
println!("rejecting connection");
|
||||
return Err(e!(AcceptError::NotAllowed));
|
||||
}
|
||||
|
||||
// To allow normal connection construction, await the accepting future & return
|
||||
let conn = accepting.await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// The `accept` method is called for each incoming connection for our ALPN.
|
||||
/// This is the primary place to kick off work in response to a new connection.
|
||||
///
|
||||
/// The returned future runs on a newly spawned tokio task, so it can run as long as
|
||||
/// the connection lasts.
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
// We can get the remote's endpoint id from the connection.
|
||||
let endpoint_id = connection.remote_id();
|
||||
println!("accepted connection from {endpoint_id}");
|
||||
|
||||
// Our protocol is a simple request-response protocol, so we expect the
|
||||
// connecting peer to open a single bi-directional stream.
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
|
||||
// Echo any bytes received back directly.
|
||||
// This will keep copying until the sender signals the end of data on the stream.
|
||||
let bytes_sent = tokio::io::copy(&mut recv, &mut send).await?;
|
||||
println!("Copied over {bytes_sent} byte(s)");
|
||||
|
||||
// By calling `finish` on the send stream we signal that we will not send anything
|
||||
// further, which makes the receive stream on the other end terminate.
|
||||
send.finish()?;
|
||||
|
||||
// Wait until the remote closes the connection, which it does once it
|
||||
// received the response.
|
||||
connection.closed().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+228
@@ -0,0 +1,228 @@
|
||||
//! Example protocol for running search on a remote endpoint.
|
||||
//!
|
||||
//! We are building a very simple protocol here.
|
||||
//!
|
||||
//! Our protocol allows querying the text stored on the other endpoint.
|
||||
//!
|
||||
//! The example is contrived - we only use memory endpoints, and our database is a hashmap in a mutex,
|
||||
//! and our queries just match if the query string appears as-is.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! In one terminal, run
|
||||
//!
|
||||
//! cargo run --example search -- listen "hello-world" "foo-bar" "hello-moon"
|
||||
//!
|
||||
//! This spawns an iroh endpoint with three blobs. It will print the endpoint's endpoint id.
|
||||
//!
|
||||
//! In another terminal, run
|
||||
//!
|
||||
//! cargo run --example search -- query <endpoint-id> hello
|
||||
//!
|
||||
//! Replace <endpoint-id> with the endpoint id from above. This will connect to the listening endpoint with our
|
||||
//! protocol and query for the string `hello`. The listening endpoint will return a number of how many
|
||||
//! strings match the query.
|
||||
//!
|
||||
//! For this example, this will print:
|
||||
//!
|
||||
//! Found 2 matches
|
||||
//!
|
||||
//! That's it! Follow along in the code below, we added a bunch of comments to explain things.
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use iroh::{
|
||||
Endpoint, EndpointId,
|
||||
endpoint::{Connection, presets},
|
||||
protocol::{AcceptError, ProtocolHandler, Router},
|
||||
};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing_subscriber::{EnvFilter, prelude::*};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum Command {
|
||||
/// Spawn an endpoint in listening mode.
|
||||
Listen {
|
||||
/// Each text string will be imported as a blob and inserted into the search database.
|
||||
text: Vec<String>,
|
||||
},
|
||||
/// Query a remote endpoint for data and print the results.
|
||||
Query {
|
||||
/// The endpoint id of the endpoint we want to query.
|
||||
endpoint_id: EndpointId,
|
||||
/// The text we want to match.
|
||||
query: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Each protocol is identified by its ALPN string.
|
||||
///
|
||||
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
||||
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
||||
const ALPN: &[u8] = b"iroh-example/text-search/0";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
setup_logging();
|
||||
let args = Cli::parse();
|
||||
|
||||
// Build an endpoint
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
|
||||
// Build our protocol handler. The `builder` exposes access to various subsystems in the
|
||||
// iroh endpoint. In our case, we need a blobs client and the endpoint.
|
||||
let proto = BlobSearch::new(endpoint.clone());
|
||||
|
||||
let builder = Router::builder(endpoint);
|
||||
|
||||
// Add our protocol, identified by our ALPN, to the endpoint, and spawn the endpoint.
|
||||
let router = builder.accept(ALPN, proto.clone()).spawn();
|
||||
|
||||
match args.command {
|
||||
Command::Listen { text } => {
|
||||
let endpoint_id = router.endpoint().id();
|
||||
println!("our endpoint id: {endpoint_id}");
|
||||
|
||||
// Insert the text strings as blobs and index them.
|
||||
for text in text.into_iter() {
|
||||
proto.insert(text).await?;
|
||||
}
|
||||
|
||||
// Wait for Ctrl-C to be pressed.
|
||||
tokio::signal::ctrl_c().await.anyerr()?;
|
||||
}
|
||||
Command::Query { endpoint_id, query } => {
|
||||
// Query the remote endpoint.
|
||||
// This will send the query over our protocol, read hashes on the reply stream,
|
||||
// and download each hash over iroh-blobs.
|
||||
let num_matches = proto.query_remote(endpoint_id, &query).await?;
|
||||
|
||||
// Print out our query results.
|
||||
println!("Found {num_matches} matches");
|
||||
}
|
||||
}
|
||||
|
||||
router.shutdown().await.anyerr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BlobSearch {
|
||||
endpoint: Endpoint,
|
||||
blobs: Arc<Mutex<BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl ProtocolHandler for BlobSearch {
|
||||
/// The `accept` method is called for each incoming connection for our ALPN.
|
||||
///
|
||||
/// The returned future runs on a newly spawned tokio task, so it can run as long as
|
||||
/// the connection lasts.
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
// We can get the remote's endpoint id from the connection.
|
||||
let endpoint_id = connection.remote_id();
|
||||
println!("accepted connection from {endpoint_id}");
|
||||
|
||||
// Our protocol is a simple request-response protocol, so we expect the
|
||||
// connecting peer to open a single bi-directional stream.
|
||||
let (mut send, mut recv) = connection.accept_bi().await?;
|
||||
|
||||
// We read the query from the receive stream, while enforcing a max query length.
|
||||
let query_bytes = recv.read_to_end(64).await.map_err(AcceptError::from_err)?;
|
||||
|
||||
// Now, we can perform the actual query on our local database.
|
||||
let query = String::from_utf8(query_bytes).map_err(AcceptError::from_err)?;
|
||||
let num_matches = self.query_local(&query).await;
|
||||
|
||||
// We want to return a list of hashes. We do the simplest thing possible, and just send
|
||||
// one hash after the other. Because the hashes have a fixed size of 32 bytes, this is
|
||||
// very easy to parse on the other end.
|
||||
send.write_all(&num_matches.to_le_bytes())
|
||||
.await
|
||||
.map_err(AcceptError::from_err)?;
|
||||
|
||||
// By calling `finish` on the send stream we signal that we will not send anything
|
||||
// further, which makes the receive stream on the other end terminate.
|
||||
send.finish()?;
|
||||
|
||||
// Wait until the remote closes the connection, which it does once it
|
||||
// received the response.
|
||||
connection.closed().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BlobSearch {
|
||||
/// Create a new protocol handler.
|
||||
pub fn new(endpoint: Endpoint) -> Self {
|
||||
Self {
|
||||
endpoint,
|
||||
blobs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Query a remote endpoint, download all matching blobs and print the results.
|
||||
pub async fn query_remote(&self, endpoint_id: EndpointId, query: &str) -> Result<u64> {
|
||||
// Establish a connection to our endpoint.
|
||||
// We use the default address lookup in iroh, so we can connect by endpoint id without
|
||||
// providing further information.
|
||||
let conn = self.endpoint.connect(endpoint_id, ALPN).await?;
|
||||
|
||||
// Open a bi-directional in our connection.
|
||||
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
||||
|
||||
// Send our query.
|
||||
send.write_all(query.as_bytes()).await.anyerr()?;
|
||||
|
||||
// Finish the send stream, signalling that no further data will be sent.
|
||||
// This makes the `read_to_end` call on the accepting side terminate.
|
||||
send.finish().anyerr()?;
|
||||
|
||||
// The response is a 64 bit integer
|
||||
// We simply read it into a byte buffer.
|
||||
let mut num_matches = [0u8; 8];
|
||||
|
||||
// Read 8 bytes from the stream.
|
||||
recv.read_exact(&mut num_matches).await.anyerr()?;
|
||||
|
||||
let num_matches = u64::from_le_bytes(num_matches);
|
||||
|
||||
// Dropping the connection here will close it.
|
||||
|
||||
Ok(num_matches)
|
||||
}
|
||||
|
||||
/// Query the local database.
|
||||
///
|
||||
/// Returns how many matches were found.
|
||||
pub async fn query_local(&self, query: &str) -> u64 {
|
||||
let guard = self.blobs.lock().await;
|
||||
let count: usize = guard.iter().filter(|text| text.contains(query)).count();
|
||||
count as u64
|
||||
}
|
||||
|
||||
/// Insert a text string into the database.
|
||||
pub async fn insert(&self, text: String) -> Result<()> {
|
||||
let mut guard = self.blobs.lock().await;
|
||||
guard.insert(text);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the RUST_LOG env var to one of {debug,info,warn} to see logging.
|
||||
fn setup_logging() {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
|
||||
.with(EnvFilter::from_default_env())
|
||||
.try_init()
|
||||
.ok();
|
||||
}
|
||||
Vendored
+1372
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user