Rename the CLI package to tsunagi and its executable to tsng
The command line package is now `tsunagi` and builds the `tsng` executable, which the packages, the unit, the installers and the workflows install and run. The core library package becomes `tsunagi-core`, keeping the library name `tsunagi`, so no code changes `use tsunagi::...`. Directories follow (crates/tsunagi-core, crates/tsunagi). Every command shown to a person is now `tsng ...`. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
bb38562b0a
commit
9966b50407
120 files changed
+631
-635
No files matched your search
@@ -0,0 +1,141 @@
|
||||
//! Events published by a running agent.
|
||||
//!
|
||||
//! Events are delivered through a bounded [`tokio::sync::broadcast`] channel.
|
||||
//! A slow subscriber is lagged, never allowed to stall the runtime.
|
||||
//!
|
||||
//! Nothing here ever carries a secret, a derived key or a handshake proof.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use iroh::EndpointId;
|
||||
|
||||
use crate::identity::NetworkId;
|
||||
use crate::net::TransportKind;
|
||||
use crate::proto::ControlMessage;
|
||||
use crate::proto::handshake::Role;
|
||||
|
||||
/// Something that happened inside the agent.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Event {
|
||||
/// A network was activated locally.
|
||||
NetworkActivated {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
},
|
||||
/// A network was deactivated locally.
|
||||
///
|
||||
/// This is a local deactivation only. It is not a signed revocation of
|
||||
/// membership, and it says nothing about the network's other participants.
|
||||
NetworkDeactivated {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
},
|
||||
/// A peer completed the handshake and has an authenticated session.
|
||||
PeerConnected {
|
||||
/// The network the session belongs to.
|
||||
network: NetworkId,
|
||||
/// Authenticated peer endpoint id.
|
||||
peer: EndpointId,
|
||||
/// Which side this agent played.
|
||||
role: Role,
|
||||
/// How the connection currently reaches the peer.
|
||||
transport: TransportKind,
|
||||
/// RTT of the selected path, if iroh reported one.
|
||||
rtt: Option<Duration>,
|
||||
},
|
||||
/// A peer's session ended.
|
||||
PeerDisconnected {
|
||||
/// The network the session belonged to.
|
||||
network: NetworkId,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// Why the session ended. Free of secrets.
|
||||
reason: String,
|
||||
},
|
||||
/// A control message arrived on an authenticated session.
|
||||
MessageReceived {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
/// The peer that sent it.
|
||||
peer: EndpointId,
|
||||
/// The message.
|
||||
message: ControlMessage,
|
||||
},
|
||||
/// An outbound dial failed.
|
||||
///
|
||||
/// A dead candidate produces these and nothing else; other peers keep
|
||||
/// connecting normally.
|
||||
DialFailed {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
/// The candidate that could not be reached.
|
||||
peer: EndpointId,
|
||||
/// Why it failed.
|
||||
reason: String,
|
||||
},
|
||||
/// A handshake was rejected.
|
||||
///
|
||||
/// The network is `None` when the failure happened before the peer's
|
||||
/// requested network could be resolved.
|
||||
HandshakeRejected {
|
||||
/// The network, when known.
|
||||
network: Option<NetworkId>,
|
||||
/// The peer, when known.
|
||||
peer: Option<EndpointId>,
|
||||
/// Why it was rejected.
|
||||
reason: String,
|
||||
},
|
||||
/// A message or session was rejected for violating the protocol.
|
||||
ProtocolViolation {
|
||||
/// The network, when known.
|
||||
network: Option<NetworkId>,
|
||||
/// The peer, when known.
|
||||
peer: Option<EndpointId>,
|
||||
/// What was wrong.
|
||||
reason: String,
|
||||
},
|
||||
/// The disposable cache was discarded and recreated at startup.
|
||||
CacheReset {
|
||||
/// Why it was discarded. Free of secrets.
|
||||
reason: String,
|
||||
},
|
||||
/// A data plane link to a peer is up.
|
||||
///
|
||||
/// The data plane is a separate connection from the control plane; this
|
||||
/// says nothing about the control session, and vice versa.
|
||||
DataLinkUp {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// Plugin protocol the link carries.
|
||||
protocol: String,
|
||||
/// What the transport reports about the path in use.
|
||||
path: String,
|
||||
/// Largest datagram the link can carry.
|
||||
max_datagram: usize,
|
||||
},
|
||||
/// A data plane link went away or could not be opened.
|
||||
///
|
||||
/// Never fatal: the control plane keeps running and the link is retried.
|
||||
DataLinkDown {
|
||||
/// The network.
|
||||
network: NetworkId,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// Plugin protocol the link would have carried.
|
||||
protocol: String,
|
||||
/// Why it is not up.
|
||||
reason: String,
|
||||
},
|
||||
/// An IP plugin reported an error. Never fatal.
|
||||
PluginError {
|
||||
/// The network the call was scoped to.
|
||||
network: NetworkId,
|
||||
/// Plugin protocol id.
|
||||
protocol: String,
|
||||
/// The reported error.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,395 @@
|
||||
//! One authenticated session with one peer, in one network.
|
||||
//!
|
||||
//! A session owns a reader task and a writer task over a single QUIC
|
||||
//! bidirectional stream. Splitting them keeps both halves simple and avoids
|
||||
//! cancelling a partially consumed frame, which stream reads do not tolerate.
|
||||
//!
|
||||
//! Every inbound message is re-checked against the session's network id, so an
|
||||
//! authenticated session for one network can never deliver into another.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use iroh::EndpointId;
|
||||
use iroh::endpoint::{Connection, RecvStream, SendStream};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::config::Limits;
|
||||
use crate::dataplane::PluginCapability;
|
||||
use crate::error::ProtocolError;
|
||||
use crate::identity::NetworkId;
|
||||
use crate::proto::handshake::Role;
|
||||
use crate::proto::message::{ControlMessage, Envelope, decode, validate};
|
||||
use crate::proto::{read_frame, write_frame};
|
||||
|
||||
use super::shutdown::Shutdown;
|
||||
|
||||
/// What a session task reports back to its network runtime.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SessionEvent {
|
||||
/// A valid control message arrived.
|
||||
Message {
|
||||
/// Which session instance produced this.
|
||||
session_id: u64,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// The decoded message.
|
||||
message: ControlMessage,
|
||||
/// Payload bytes read off the wire.
|
||||
bytes: usize,
|
||||
},
|
||||
/// The peer sent something the protocol does not allow.
|
||||
Violation {
|
||||
/// Which session instance produced this.
|
||||
session_id: u64,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// What was wrong.
|
||||
error: ProtocolError,
|
||||
},
|
||||
/// The session ended.
|
||||
Closed {
|
||||
/// Which session instance ended.
|
||||
session_id: u64,
|
||||
/// The peer.
|
||||
peer: EndpointId,
|
||||
/// Why it ended.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A live session, as held by the network runtime.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Session {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) peer: EndpointId,
|
||||
pub(crate) role: Role,
|
||||
pub(crate) established: Instant,
|
||||
pub(crate) conn: Connection,
|
||||
/// Already encoded frame payloads, so the runtime can account for the exact
|
||||
/// number of control bytes it queues.
|
||||
pub(crate) outbound: mpsc::Sender<Vec<u8>>,
|
||||
pub(crate) hostname: Option<String>,
|
||||
pub(crate) capabilities: Vec<PluginCapability>,
|
||||
pub(crate) broadcast: bool,
|
||||
/// The peer offers itself as an exit node in this network.
|
||||
pub(crate) exit_node: bool,
|
||||
pub(crate) messages_sent: u64,
|
||||
pub(crate) messages_received: u64,
|
||||
pub(crate) bytes_sent: u64,
|
||||
pub(crate) bytes_received: u64,
|
||||
reader: JoinHandle<()>,
|
||||
writer: JoinHandle<()>,
|
||||
path_monitor: JoinHandle<()>,
|
||||
shutdown: Shutdown,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Signals all tasks to stop and waits for them, with a bounded grace
|
||||
/// period.
|
||||
///
|
||||
/// A peer that stops reading must not be able to hold up shutdown, so the
|
||||
/// tasks are aborted if they do not wind down in time.
|
||||
pub(crate) async fn stop(self) {
|
||||
let Session {
|
||||
conn,
|
||||
reader,
|
||||
writer,
|
||||
path_monitor,
|
||||
shutdown,
|
||||
..
|
||||
} = self;
|
||||
shutdown.trigger();
|
||||
// Closing the connection unblocks a reader parked on the stream.
|
||||
conn.close(0u32.into(), b"session stopped by local agent");
|
||||
|
||||
let reader_abort = reader.abort_handle();
|
||||
let writer_abort = writer.abort_handle();
|
||||
let path_abort = path_monitor.abort_handle();
|
||||
let joined = tokio::time::timeout(STOP_GRACE, async move {
|
||||
let _ = reader.await;
|
||||
let _ = writer.await;
|
||||
let _ = path_monitor.await;
|
||||
})
|
||||
.await;
|
||||
if joined.is_err() {
|
||||
tracing::debug!("session tasks did not wind down in time; aborting them");
|
||||
reader_abort.abort();
|
||||
writer_abort.abort();
|
||||
path_abort.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Aborts all tasks without waiting. Used on replacement.
|
||||
pub(crate) fn abort(&self) {
|
||||
self.shutdown.trigger();
|
||||
self.reader.abort();
|
||||
self.writer.abort();
|
||||
self.path_monitor.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a stopping session may take to wind down before its tasks are
|
||||
/// aborted. Shutdown must be bounded even if a peer stops reading.
|
||||
const STOP_GRACE: std::time::Duration = std::time::Duration::from_millis(500);
|
||||
|
||||
/// Source of monotonically increasing session instance ids.
|
||||
static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
/// Starts the reader and writer tasks for an authenticated stream.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn spawn(
|
||||
network_id: NetworkId,
|
||||
peer: EndpointId,
|
||||
role: Role,
|
||||
conn: Connection,
|
||||
send: SendStream,
|
||||
recv: RecvStream,
|
||||
limits: Arc<Limits>,
|
||||
events: mpsc::Sender<SessionEvent>,
|
||||
parent_shutdown: Shutdown,
|
||||
) -> Session {
|
||||
let id = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let shutdown = Shutdown::new();
|
||||
let (outbound_tx, outbound_rx) = mpsc::channel(limits.session_send_queue);
|
||||
|
||||
let writer = tokio::spawn(writer_task(
|
||||
send,
|
||||
outbound_rx,
|
||||
Arc::clone(&limits),
|
||||
shutdown.clone(),
|
||||
parent_shutdown.clone(),
|
||||
));
|
||||
|
||||
let path_conn = conn.clone();
|
||||
let path_shutdown = shutdown.clone();
|
||||
let path_parent = parent_shutdown.clone();
|
||||
let path_monitor = tokio::spawn(async move {
|
||||
use tokio_stream::StreamExt;
|
||||
let mut events = path_conn.path_events();
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = path_shutdown.wait() => break,
|
||||
_ = path_parent.wait() => break,
|
||||
ev = events.next() => {
|
||||
match ev {
|
||||
Some(iroh::endpoint::PathEvent::Selected { remote_addr, local_addr, .. }) => {
|
||||
tracing::debug!(
|
||||
peer = %peer.fmt_short(),
|
||||
remote = %remote_addr,
|
||||
local = ?local_addr,
|
||||
"Connection path selected (direct / relay / hole punch switch)"
|
||||
);
|
||||
}
|
||||
Some(iroh::endpoint::PathEvent::Opened { remote_addr, .. }) => {
|
||||
tracing::debug!(
|
||||
peer = %peer.fmt_short(),
|
||||
remote = %remote_addr,
|
||||
"Network path opened to peer"
|
||||
);
|
||||
}
|
||||
Some(iroh::endpoint::PathEvent::Closed { remote_addr, .. }) => {
|
||||
tracing::debug!(
|
||||
peer = %peer.fmt_short(),
|
||||
remote = %remote_addr,
|
||||
"Network path closed for peer"
|
||||
);
|
||||
}
|
||||
Some(iroh::endpoint::PathEvent::Lagged { missed, .. }) => {
|
||||
tracing::trace!(peer = %peer.fmt_short(), missed, "Path events lagged");
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let reader = tokio::spawn(reader_task(
|
||||
id,
|
||||
network_id,
|
||||
peer,
|
||||
recv,
|
||||
limits,
|
||||
events,
|
||||
shutdown.clone(),
|
||||
parent_shutdown,
|
||||
));
|
||||
|
||||
Session {
|
||||
id,
|
||||
peer,
|
||||
role,
|
||||
established: Instant::now(),
|
||||
conn,
|
||||
outbound: outbound_tx,
|
||||
hostname: None,
|
||||
capabilities: Vec::new(),
|
||||
broadcast: false,
|
||||
exit_node: false,
|
||||
messages_sent: 0,
|
||||
messages_received: 0,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
reader,
|
||||
writer,
|
||||
path_monitor,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
async fn writer_task(
|
||||
mut send: SendStream,
|
||||
mut outbound: mpsc::Receiver<Vec<u8>>,
|
||||
limits: Arc<Limits>,
|
||||
shutdown: Shutdown,
|
||||
parent: Shutdown,
|
||||
) {
|
||||
loop {
|
||||
let encoded = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait() => break,
|
||||
_ = parent.wait() => break,
|
||||
encoded = outbound.recv() => match encoded {
|
||||
Some(encoded) => encoded,
|
||||
None => break,
|
||||
},
|
||||
};
|
||||
|
||||
// The write must stay cancellable: shutting the session down cannot
|
||||
// wait for a peer that has stopped reading. Abandoning a half written
|
||||
// frame is fine, because the session is going away with it.
|
||||
let write = tokio::time::timeout(
|
||||
limits.write_timeout,
|
||||
write_frame(&mut send, &encoded, limits.max_frame_len),
|
||||
);
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait() => break,
|
||||
_ = parent.wait() => break,
|
||||
result = write => result,
|
||||
};
|
||||
match result {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
tracing::debug!(%err, "control stream write failed");
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("control stream write timed out");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = send.finish();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn reader_task(
|
||||
session_id: u64,
|
||||
network_id: NetworkId,
|
||||
peer: EndpointId,
|
||||
mut recv: RecvStream,
|
||||
limits: Arc<Limits>,
|
||||
events: mpsc::Sender<SessionEvent>,
|
||||
shutdown: Shutdown,
|
||||
parent: Shutdown,
|
||||
) {
|
||||
let reason = loop {
|
||||
let frame = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.wait() => break "stopped locally".to_string(),
|
||||
_ = parent.wait() => break "network deactivated".to_string(),
|
||||
frame = read_frame(&mut recv, limits.max_frame_len) => frame,
|
||||
};
|
||||
|
||||
let payload = match frame {
|
||||
Ok(payload) => payload,
|
||||
Err(ProtocolError::StreamClosed) => break "peer closed the control stream".to_string(),
|
||||
Err(ProtocolError::Stream(err)) => {
|
||||
// The transport went away. That is a disconnect, not a peer
|
||||
// misbehaving, so it ends the session without being counted as
|
||||
// a protocol violation.
|
||||
break format!("control stream error: {err}");
|
||||
}
|
||||
Err(err) => {
|
||||
// A framing violation ends this session. Framing errors are not
|
||||
// recoverable mid-stream: the next bytes have no known meaning.
|
||||
let text = err.to_string();
|
||||
let _ = events
|
||||
.send(SessionEvent::Violation {
|
||||
session_id,
|
||||
peer,
|
||||
error: err,
|
||||
})
|
||||
.await;
|
||||
break text;
|
||||
}
|
||||
};
|
||||
|
||||
let bytes = payload.len();
|
||||
let envelope: Envelope = match decode(&payload) {
|
||||
Ok(envelope) => envelope,
|
||||
Err(err) => {
|
||||
let _ = events
|
||||
.send(SessionEvent::Violation {
|
||||
session_id,
|
||||
peer,
|
||||
error: err,
|
||||
})
|
||||
.await;
|
||||
break "malformed control frame".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
// Network isolation: a session authenticated for one network must never
|
||||
// deliver a message belonging to another.
|
||||
if envelope.network_id != *network_id.as_bytes() {
|
||||
let _ = events
|
||||
.send(SessionEvent::Violation {
|
||||
session_id,
|
||||
peer,
|
||||
error: ProtocolError::NetworkMismatch,
|
||||
})
|
||||
.await;
|
||||
break "network id mismatch on an authenticated session".to_string();
|
||||
}
|
||||
|
||||
if let Err(err) = validate(&envelope.message, &limits) {
|
||||
let _ = events
|
||||
.send(SessionEvent::Violation {
|
||||
session_id,
|
||||
peer,
|
||||
error: err,
|
||||
})
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
|
||||
if events
|
||||
.send(SessionEvent::Message {
|
||||
session_id,
|
||||
peer,
|
||||
message: envelope.message,
|
||||
bytes,
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break "network runtime stopped".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
// Best effort: the runtime may already have stopped draining this channel
|
||||
// while it tears the network down, and a closing session must not block on
|
||||
// that.
|
||||
let _ = events.try_send(SessionEvent::Closed {
|
||||
session_id,
|
||||
peer,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! A minimal cancellation primitive.
|
||||
//!
|
||||
//! Kept local so the crate does not pull in a utility dependency for one type,
|
||||
//! and so that no global state is involved: every agent and every network
|
||||
//! runtime owns its own token.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::watch;
|
||||
|
||||
/// A clonable cancellation token.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Shutdown {
|
||||
tx: Arc<watch::Sender<bool>>,
|
||||
rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
impl Shutdown {
|
||||
/// Creates an untriggered token.
|
||||
pub(crate) fn new() -> Self {
|
||||
let (tx, rx) = watch::channel(false);
|
||||
Self {
|
||||
tx: Arc::new(tx),
|
||||
rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers cancellation. Idempotent.
|
||||
pub(crate) fn trigger(&self) {
|
||||
let _ = self.tx.send(true);
|
||||
}
|
||||
|
||||
/// Whether cancellation has been triggered.
|
||||
pub(crate) fn is_triggered(&self) -> bool {
|
||||
*self.rx.borrow()
|
||||
}
|
||||
|
||||
/// Resolves once cancellation has been triggered.
|
||||
pub(crate) async fn wait(&self) {
|
||||
let mut rx = self.rx.clone();
|
||||
{
|
||||
if *rx.borrow() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
while rx.changed().await.is_ok() {
|
||||
if *rx.borrow() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The sender is gone, which for our purposes means "stop".
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn waiting_resolves_once_triggered_and_stays_resolved() {
|
||||
let shutdown = Shutdown::new();
|
||||
assert!(!shutdown.is_triggered());
|
||||
let waiter = shutdown.clone();
|
||||
let waiting = tokio::spawn(async move { waiter.wait().await });
|
||||
shutdown.trigger();
|
||||
waiting.await.unwrap();
|
||||
assert!(shutdown.is_triggered());
|
||||
// Triggering twice is not an error, and a token cloned afterwards
|
||||
// sees the trigger that already happened.
|
||||
shutdown.trigger();
|
||||
shutdown.clone().wait().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Status snapshots.
|
||||
//!
|
||||
//! Metrics are reported at the level they actually belong to. Values that
|
||||
//! genuinely cannot be attributed to a single network — everything the iroh
|
||||
//! endpoint aggregates, for instance — stay at the endpoint level rather than
|
||||
//! being split between networks with invented precision.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use iroh::{EndpointAddr, EndpointId};
|
||||
|
||||
use crate::dataplane::PluginCapability;
|
||||
use crate::discovery::CandidateSource;
|
||||
use crate::identity::{NetworkDescriptor, NetworkId, NetworkName};
|
||||
use crate::net::{ConnectionCounters, PathAddr, PathInfo, TransportKind};
|
||||
use crate::proto::handshake::Role;
|
||||
use crate::storage::CacheOutcome;
|
||||
|
||||
/// Whether a configured network is running locally.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NetworkState {
|
||||
/// Configured and running.
|
||||
Active,
|
||||
/// Configured but not running.
|
||||
Inactive,
|
||||
}
|
||||
|
||||
/// An unverified candidate as seen by a network runtime.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CandidateStatus {
|
||||
/// Candidate endpoint id.
|
||||
pub endpoint_id: EndpointId,
|
||||
/// Where it came from.
|
||||
pub source: CandidateSource,
|
||||
/// Consecutive failed dial attempts since the last success.
|
||||
pub consecutive_failures: u32,
|
||||
}
|
||||
|
||||
/// The overlay interface, as the agent sees it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OverlayStatus {
|
||||
/// The interface name the operating system gave.
|
||||
pub interface: String,
|
||||
/// Whether that interface exists on the host.
|
||||
///
|
||||
/// `false` with an in-memory device: the name is real to the agent and
|
||||
/// to nothing else. Reported, because everything that would configure
|
||||
/// the operating system for this interface must not when it is this.
|
||||
pub on_host: bool,
|
||||
/// Its MTU.
|
||||
pub mtu: u32,
|
||||
/// The addresses it should be carrying.
|
||||
pub addresses: Vec<crate::overlay::Cidr>,
|
||||
/// What has happened on it.
|
||||
pub counters: crate::overlay::Counters,
|
||||
/// The route and firewall allowance installed for LAN broadcast, when
|
||||
/// some network has broadcast on and the interface is on the host.
|
||||
/// `None` is "none installed", not "unknown".
|
||||
pub broadcast_rules: Option<crate::overlay::BroadcastRulesReport>,
|
||||
}
|
||||
|
||||
/// A member the signed state knows about, connected or not.
|
||||
///
|
||||
/// This is the durable roster: it comes from signed records, so a member that
|
||||
/// went away last month is still here. That is what makes it possible to say
|
||||
/// "this peer is offline" rather than only "nobody is connected".
|
||||
///
|
||||
/// It is not a complete membership list, and cannot be. A member is in signed
|
||||
/// state once it has claimed something — an address, a name, or both — so a
|
||||
/// member that has joined but never published is visible only while it is
|
||||
/// connected.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MemberStatus {
|
||||
/// The member's device identity.
|
||||
pub endpoint_id: EndpointId,
|
||||
/// The IPv4 overlay address it claimed and signed for.
|
||||
pub overlay_address_v4: Option<std::net::Ipv4Addr>,
|
||||
/// The name it claimed, when it holds that name uncontested.
|
||||
///
|
||||
/// Signed, so it is still known while the member is away — which is what
|
||||
/// lets an absent member be named rather than shown as a bare id.
|
||||
pub hostname: Option<String>,
|
||||
/// The last name this agent saw it announce, whether or not it is here.
|
||||
///
|
||||
/// Not signed, so it is a convenience for naming a member that is away
|
||||
/// and never a claim: the zone is served from `hostname`.
|
||||
pub last_hostname: Option<String>,
|
||||
}
|
||||
|
||||
/// Status of one authenticated session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerStatus {
|
||||
/// This authenticated peer accepts network broadcasts.
|
||||
pub broadcast: bool,
|
||||
/// This authenticated peer offers itself as an exit node.
|
||||
pub exit_node: bool,
|
||||
/// Authenticated endpoint id.
|
||||
pub endpoint_id: EndpointId,
|
||||
/// Which side this agent played in the handshake.
|
||||
pub role: Role,
|
||||
/// Hostname the peer announced, if it has announced one yet.
|
||||
///
|
||||
/// A mutable binding, not an identity.
|
||||
pub hostname: Option<String>,
|
||||
/// Protocols agreed with this peer: both sides have them at the same
|
||||
/// version.
|
||||
///
|
||||
/// Empty means no data plane with that peer, which leaves the control
|
||||
/// plane working — messages and state still flow.
|
||||
pub protocols: Vec<String>,
|
||||
/// Capabilities the peer announced. Payloads stay opaque.
|
||||
pub capabilities: Vec<PluginCapability>,
|
||||
/// How long the session has been up.
|
||||
pub connected_for: Duration,
|
||||
/// Verified paths of the underlying connection.
|
||||
pub paths: Vec<PathInfo>,
|
||||
/// How the connection currently reaches the peer.
|
||||
pub transport: TransportKind,
|
||||
/// RTT of the selected path, when iroh reported one.
|
||||
pub rtt: Option<Duration>,
|
||||
/// Per-connection counters.
|
||||
pub connection: ConnectionCounters,
|
||||
/// Control messages sent on this session.
|
||||
pub control_messages_sent: u64,
|
||||
/// Control messages received on this session.
|
||||
pub control_messages_received: u64,
|
||||
/// Control payload bytes queued for this session.
|
||||
pub control_bytes_sent: u64,
|
||||
/// Control payload bytes read from this session.
|
||||
pub control_bytes_received: u64,
|
||||
}
|
||||
|
||||
/// Counters scoped to one logical network.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkMetrics {
|
||||
/// Outbound dial attempts started.
|
||||
pub dial_attempts: u64,
|
||||
/// Outbound dials that failed before or during the handshake.
|
||||
pub dial_failures: u64,
|
||||
/// Handshakes rejected, in either direction.
|
||||
pub handshake_failures: u64,
|
||||
/// Sessions that reached the authenticated state.
|
||||
pub sessions_established: u64,
|
||||
/// Sessions that ended.
|
||||
pub disconnects: u64,
|
||||
/// Control messages sent in this network.
|
||||
pub control_messages_sent: u64,
|
||||
/// Control messages received in this network.
|
||||
pub control_messages_received: u64,
|
||||
/// Control bytes sent in this network, payload only.
|
||||
pub control_bytes_sent: u64,
|
||||
/// Control bytes received in this network, payload only.
|
||||
pub control_bytes_received: u64,
|
||||
/// Messages or sessions rejected for protocol violations.
|
||||
pub protocol_violations: u64,
|
||||
/// Errors reported by IP plugins. Never fatal.
|
||||
pub plugin_errors: u64,
|
||||
/// Data plane links that were established.
|
||||
pub data_links_established: u64,
|
||||
/// Attempts to open a data plane link that failed.
|
||||
pub data_link_failures: u64,
|
||||
}
|
||||
|
||||
/// Status of one network.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkStatus {
|
||||
/// Local broadcast participation in this network.
|
||||
pub broadcast: bool,
|
||||
/// This agent offers itself as an exit node in this network.
|
||||
pub exit_offer: bool,
|
||||
/// The member this device sends all its internet traffic through.
|
||||
pub exit_via: Option<EndpointId>,
|
||||
/// Whether that member is connected and still offering to be one.
|
||||
pub exit_via_online: bool,
|
||||
/// What the host rules for the exit settings are doing.
|
||||
pub exit_rules: crate::overlay::ExitRulesReport,
|
||||
/// Immutable deterministic description of the network space.
|
||||
pub descriptor: NetworkDescriptor,
|
||||
/// Network name, for convenience.
|
||||
pub name: NetworkName,
|
||||
/// Public network identifier.
|
||||
pub network_id: NetworkId,
|
||||
/// Whether the network is running locally.
|
||||
pub state: NetworkState,
|
||||
/// Authenticated sessions.
|
||||
pub peers: Vec<PeerStatus>,
|
||||
/// Unverified candidates currently known. Not peers.
|
||||
pub candidates: Vec<CandidateStatus>,
|
||||
/// Members the signed state knows about, whether connected or not.
|
||||
pub members: Vec<MemberStatus>,
|
||||
/// The overlay range this network uses, once it has one.
|
||||
pub range: Option<crate::state::Ipv4Range>,
|
||||
/// The range it could not have, because another network on this agent
|
||||
/// already holds it.
|
||||
///
|
||||
/// One agent has one interface, so an address belongs to one network.
|
||||
/// This network waits to adopt whatever its members settle on instead of
|
||||
/// proposing something it could not route.
|
||||
pub range_conflict: Option<crate::state::Ipv4Range>,
|
||||
/// Per-network counters.
|
||||
pub metrics: NetworkMetrics,
|
||||
/// What has gone through a peer in the middle, in both directions.
|
||||
pub relay: crate::dataplane::relay::RelayCounters,
|
||||
}
|
||||
|
||||
impl NetworkStatus {
|
||||
/// Endpoint ids of peers with an authenticated session.
|
||||
pub fn connected_peers(&self) -> Vec<EndpointId> {
|
||||
self.peers.iter().map(|peer| peer.endpoint_id).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of the whole agent.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentStatus {
|
||||
/// This device's persistent endpoint id.
|
||||
pub endpoint_id: EndpointId,
|
||||
/// Hostname announced to peers.
|
||||
pub hostname: String,
|
||||
/// Sockets actually bound.
|
||||
pub bound_sockets: Vec<std::net::SocketAddr>,
|
||||
/// Addresses iroh believes this endpoint has. Observed, not verified.
|
||||
pub observed_addrs: Vec<PathAddr>,
|
||||
/// The dialable address of this endpoint, as iroh currently reports it.
|
||||
pub endpoint_addr: EndpointAddr,
|
||||
/// What happened to the disposable cache at startup.
|
||||
pub cache_outcome: CacheOutcome,
|
||||
/// Whether the cache is currently usable.
|
||||
pub cache_healthy: bool,
|
||||
/// Per-network status, including configured but inactive networks.
|
||||
pub networks: Vec<NetworkStatus>,
|
||||
}
|
||||
|
||||
impl AgentStatus {
|
||||
/// Looks up one network's status.
|
||||
pub fn network(&self, network_id: &NetworkId) -> Option<&NetworkStatus> {
|
||||
self.networks
|
||||
.iter()
|
||||
.find(|status| &status.network_id == network_id)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user