Files
tsunagi/crates/tsunagi-core/src/agent/status.rs
T
Aleksandr Bogomiakov 23af1a2d9d Add shared TCP and UDP virtual services
Provide named VIPs with independent protocol/port listeners and node/port
backends so applications can use one DNS endpoint without a separate load
balancer. Every authenticated member may create, edit or delete any service.
Client agents select the eligible backend with the lowest measured encrypted
data-plane RTT, including relayed paths, and pin existing flows. UDP recovers
on another backend after failure; established TCP is never replayed or moved.
An unanswered empty TCP SYN may retry another backend before establishment.

Keep signed multi-writer field registers in state/vip.rs. Independent backend
edits merge, same-field conflicts use Lamport/author/canonical ordering, and
listener generations prevent old backend entries returning on port recreation.
Permanent service tombstones protect replay while allowing obsolete fields to
be compacted. Active-service and deletion-history limits are separate. Commit
complete snapshots and the author's durable counter atomically before local
routing changes or publication. Restore counters from newer genuine author
updates learned from replicas after an old backup is restored.

Implement bounded client-side address/port NAT at overlay ingress and reverse
translation only after authenticated source/destination admission. Reserve OS
source ports to avoid direct-flow collisions and quarantine retired ports for
late replies. Handle a backend on the client itself, scoped fragment assembly,
checksums, re-fragmentation and quoted ICMP/PMTU headers. Plugins and encrypted
transit retain their existing contracts. Data-path probes measure RTT; bounded
fair TCP connect checks are enabled by default and configurable per listener.
Arbitrary UDP application readiness remains unknown; path checks and explicit
port errors drive eligibility without protocol-specific probe payloads.

Expose the shared draft API through local IPC, simple vip/listener/backend CLI
commands, JSON, editor-based atomic updates and watch output. Add a VIP button
on each network tile and a separate tray editor. Edits and removals stay local
until Save; stale revisions retain the draft, and closing a dirty window offers
explicit discard. Report local routing choices, health reasons, RTT age, active
and draining bindings, IP counters, rates, drops and UDP failovers. Extend DNS
with service names; keep numeric addresses secondary and preserve existing
bindings while VIP addresses or listeners change.

This changes control wire ALPN/version to tsunagi/ctrl/8 and local IPC to 22;
all members and local clients must upgrade together. Data ALPN remains 5.
SQLite schema 10 migrates existing mandatory state without changing identity,
secrets, network ids, derivation labels, handshake encoding or node records.
Crate versions and release tags are unchanged. No installed agent or tray was
replaced. Commands, limits and operational semantics are in docs/vip.md.

Checks on Linux in nix develop:
- cargo fmt --all -- --check
- cargo clippy --locked --workspace --all-targets -- -D warnings
- cargo test --locked --workspace --all-targets: 559 passed, 3 ignored
- git diff --cached --check

The full suite includes real encrypted TCP/UDP VIP traffic, backend-stop UDP
failover, real loopback TCP check recovery, source admission, signed merges,
stale/deleted state, SQLite migration and rollback, restart/author recovery,
network isolation, actual CLI/DNS processes, and headless text-entry/Save-button
interaction. Discovery and memory TUNs are the existing test substitutions;
authentication, transport, encryption and SQLite remain real.

Initial checks outside Nix failed for missing GTK pkg-config metadata, and an
initial CLI test inherited TSUNAGI_CONTROL_SOCKET. Final checks use the Nix
environment with that variable unset so tests address their temporary sockets.
Root/internet-dependent tests remain ignored. No live host TUN, native desktop
session, macOS or Windows validation was performed.
2026-10-21 15:40:45 +01:00

310 lines
12 KiB
Rust

//! 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>,
}
/// How packets reach a member right now.
///
/// Being reachable is one thing and being reachable well is another, and
/// the second is never a reason to call a member away: a member that can only
/// be reached through somebody else is online, over a longer path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataPath {
/// Nothing carries packets to it yet. The member is connected for
/// control only, which is the state of a data path being set up.
None,
/// A direct link, of the named kind of transport.
Direct {
/// The kind of transport carrying it, e.g. `wg`.
transport: String,
},
/// Through other members.
Relayed {
/// Number of links on the way, at least two.
hops: u8,
/// The member the first of them goes to.
via: EndpointId,
},
}
/// A name that a member claimed and somebody else holds.
///
/// A name belongs to whoever claimed it first. The member that claimed it
/// later is still a member and still reachable by its address; it is just
/// not found by that name, on any member of the network.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostnameConflict {
/// The name that is taken.
pub name: String,
/// The member that holds it.
pub holder: EndpointId,
}
/// 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 {
/// Current signed claim version, used to bind cleanup confirmation.
pub record_version: u64,
/// 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>,
/// The name it claimed, when somebody else claimed it first.
pub hostname_conflict: Option<HostnameConflict>,
/// Whether this agent can reach it now, by any path: a live control
/// session, or a data path through other members. The one answer to "is
/// it online" for every client, so that none of them has to work it out
/// from a different part of the picture. This agent counts as online.
pub online: bool,
/// How packets reach it. [`DataPath::None`] while it is offline.
pub data_path: DataPath,
}
/// 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,
/// How packets reach this peer right now.
pub data_path: DataPath,
}
/// 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,
}
/// An active network preventing another network from starting locally.
#[derive(Debug, Clone)]
pub struct RangeConflict {
/// The blocking network's public identifier.
pub network_id: NetworkId,
/// Its configured name.
pub name: NetworkName,
/// Its overlapping overlay range.
pub range: crate::state::Ipv4Range,
}
/// Status of one network.
#[derive(Debug, Clone)]
pub struct NetworkStatus {
/// Shared service configuration with this client's routing observations.
pub vips: Vec<crate::vip::VipStatus>,
/// Enabled local data-plane protocols, in preference order.
pub protocols: Vec<String>,
/// Whether this network is requested to start or resume automatically.
pub auto_start: bool,
/// 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>,
/// This agent's own name, when another member claimed it first and so
/// holds it. This agent keeps working by address; nobody finds it by
/// this name until it is renamed or the other member lets it go.
pub hostname_conflict: Option<HostnameConflict>,
/// The overlay range this network uses, once it has one.
pub range: Option<crate::state::Ipv4Range>,
/// The first overlapping active range; `range_conflicts` identifies all blockers.
pub range_conflict: Option<crate::state::Ipv4Range>,
/// Active networks that would be suspended by switching to this network.
pub range_conflicts: Vec<RangeConflict>,
/// 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)
}
}