diff --git a/AGENTS.md b/AGENTS.md index 7c5188b..2a4f229 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,18 +18,31 @@ Design accordingly: a majority is not a root of trust. Keep these separate. Crossing them is the main thing to review for. - **Control plane vs data plane.** The separation is **logical, not physical**. - The control protocol in `crates/tsunagi/src/proto/` knows nothing about packets, and - `crates/tsunagi/src/dataplane/` knows nothing about the control protocol; either can be - replaced on its own. Both may ride on iroh — refusing to would throw away - iroh's NAT traversal and force the data plane to reimplement it. They use - different ALPNs and different connections, so a busy or broken data plane - cannot disturb control traffic. + The control protocol in `crates/tsunagi/src/proto/` knows nothing about + packets, and a protocol crate knows nothing about the control protocol; + either can be replaced on its own. Both may ride on iroh — refusing to + would throw away iroh's NAT traversal and force the data plane to + reimplement it. They use different ALPNs and different connections, so a + busy or broken data plane cannot disturb control traffic. +- **One agent, one interface.** It belongs to the system level, along with + the addresses on it and the decision of whose packet is whose. Several + protocols may be carrying traffic at once and none of them owns the thing + they carry it for. A protocol is handed a routed packet and hands back a + decrypted one; it never creates an interface and never picks an address. +- **A protocol is a separate crate with its own version.** The version peers + compare is the *wire* version, never the software version: two peers on + different releases work together for as long as the bytes between them + have not changed. Nothing negotiated may be derived from anything that + moves with a release. +- **A protocol never sees the network secret.** It proves who is at the other + end of a tunnel; proving membership of a network stays in the core, which + is why the authenticated transport does too. - **Plugins never learn reachability.** An `IpPlugin` is handed a `PacketLink` per peer and moves datagrams over it. Addresses, hole punching and relays belong to `crates/tsunagi/src/dataplane/transport/`. A plugin announcement says *who*, never *where*. - **The core never parses a plugin payload.** See `crates/tsunagi/src/dataplane/mod.rs`. Only - `crates/tsunagi/src/dataplane/wireguard/announcement.rs` interprets WireGuard payloads, and + `crates/tsunagi-wg-quic/src/announcement.rs` interprets `wg-quic` payloads, and only after bounding every field. A data plane failure must never stop the control plane. - **Derived, not claimed.** A peer's overlay address is derived from its public @@ -106,7 +119,10 @@ Keep these separate. Crossing them is the main thing to review for. | `crates/tsunagi/src/net.rs` | iroh endpoint adapter and observability snapshots | | `crates/tsunagi/src/agent/` | agent lifecycle, per-network runtimes, sessions, events, status | | `crates/tsunagi/src/state/` | signed records that outlive a session, their merge rules and address allocation | -| `crates/tsunagi/src/dataplane/` | the plugin contract, the packet transport, and the WireGuard plugin | +| `crates/tsunagi/src/overlay/` | the one interface an agent owns: provisioning, the TUN, routing, source checks | +| `crates/tsunagi/src/dataplane/` | the protocol contract and the authenticated packet transport | +| `crates/tsunagi/src/dns/` | the DNS view of a network: zone, server, resolver publication | +| `crates/tsunagi-wg-quic/` | the `wg-quic` protocol: its keys, its announcement, its tunnels | | `crates/tsunagi-cli/` | the command line agent; the only place that owns a runtime, a logger and signals | | `tests/` | integration tests; `tests/common/` is the shared harness | diff --git a/Cargo.lock b/Cargo.lock index f569f60..65a5dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3821,7 +3821,6 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "tsunagi" version = "0.1.0" dependencies = [ - "boringtun", "bytes", "caps", "data-encoding", @@ -3848,6 +3847,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tsunagi", "tun", "zbus", "zeroize", @@ -3868,6 +3868,32 @@ dependencies = [ "tracing", "tracing-subscriber", "tsunagi", + "tsunagi-wg-quic", +] + +[[package]] +name = "tsunagi-wg-quic" +version = "0.1.0" +dependencies = [ + "boringtun", + "bytes", + "data-encoding", + "hex", + "hkdf", + "iroh", + "postcard", + "rand 0.10.3", + "rusqlite", + "serde", + "sha2", + "subtle", + "tempfile", + "thiserror 2.0.20", + "tokio", + "tracing", + "tracing-subscriber", + "tsunagi", + "zeroize", ] [[package]] diff --git a/README.md b/README.md index 1423d57..9066d86 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,13 @@ It prints its endpoint id and then waits. On the second machine, pass that id: Within a few seconds both print something like: ```text - + peer b47c958462 connected over Direct rtt=Some(4.5ms) - + data link to b47c958462 for wireguard: Direct via Ip(…), datagram 1382 + + peer b47c958462 connected over direct rtt=Some(4.5ms) + + data link to b47c958462 for wg-quic: direct via 192.0.2.7:41234, datagram 1382 --- status --- control: 1 peer(s), 0 dial failure(s), 0 handshake failure(s) -wireguard: tsunkkcp43lmdje on fd15:1d9e:fa21:f201:…/64 mtu 1280, 1/1 tunnel(s) established - 4jO4kx9Z fd15:1d9e:fa21:f201:… handshake 3s ago tx=0 rx=0 dropped=0 path=Direct via Ip(…) +wg-quic: tsun0 on 10.13.37.69/24 mtu 1280, 1/1 tunnel(s) established + 4jO4kx9Z 10.13.37.237 handshake 3s ago tx=0 rx=0 dropped=0 path=direct via 192.0.2.7:41234 ``` `1/1 tunnel(s) established` means a real WireGuard handshake completed. @@ -384,7 +384,6 @@ tests: ```bash cargo run --example two_agents # control plane only -cargo run --example wireguard_mesh # a WireGuard overlay carrying a real packet ``` Both run with no privileges and change nothing on the host. @@ -477,9 +476,9 @@ paths; tests always use temporary directories. | `state.sqlite` | device identity, network configuration, hostname | clear error, never reset | | `cache.sqlite` | address hints and other recoverable data | discarded and recreated | -The WireGuard plugin keeps its own keys in its own `wireguard.sqlite`, wherever -its configuration points, because plugin keys are neither the iroh identity nor -the network secret. +The `wg-quic` protocol keeps its own keys in its own store under `wg-quic/`, +because a protocol's keys are neither the iroh identity nor the network +secret. The command line agent puts everything under the platform's per-user directories by default; `--state-dir` and `--cache-dir` override them. diff --git a/crates/tsunagi-cli/Cargo.toml b/crates/tsunagi-cli/Cargo.toml index dcc4049..d4f5ede 100644 --- a/crates/tsunagi-cli/Cargo.toml +++ b/crates/tsunagi-cli/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] tsunagi = { path = "../tsunagi", version = "0.1.0", features = ["tun-device", "dns-publish"] } +tsunagi-wg-quic = { path = "../tsunagi-wg-quic", version = "0.1.0" } iroh.workspace = true tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "signal"] } tracing.workspace = true diff --git a/crates/tsunagi-cli/src/main.rs b/crates/tsunagi-cli/src/main.rs index 16c20ae..2ccb3a7 100644 --- a/crates/tsunagi-cli/src/main.rs +++ b/crates/tsunagi-cli/src/main.rs @@ -15,14 +15,13 @@ use clap::{Args, Parser, Subcommand, ValueEnum}; use tsunagi::agent::Event; use tsunagi::config::{AgentConfig, StoragePaths, TransportPolicy}; use tsunagi::dataplane::IpPlugin; -use tsunagi::dataplane::wireguard::{ - MemoryTunFactory, TunFactory, WireguardConfig, WireguardPlugin, -}; use tsunagi::discovery::{CompositeDiscovery, NetworkDiscovery, StaticBootstrap}; use tsunagi::identity::{NetworkName, NetworkSecret}; use tsunagi::iroh_types::EndpointAddr; +use tsunagi::overlay::{MemoryTunFactory, TunFactory}; use tsunagi::state::Ipv4Range; use tsunagi::{Agent, NetworkId}; +use tsunagi_wg_quic::{WireguardConfig, WireguardPlugin}; /// A small agent for private mesh networks. #[derive(Debug, Parser)] @@ -775,8 +774,8 @@ struct ProtocolSpec { /// Every protocol this build has. const PROTOCOLS: &[ProtocolSpec] = &[ProtocolSpec { - name: tsunagi::dataplane::wireguard::WIREGUARD_PROTOCOL, - version: tsunagi::dataplane::wireguard::ANNOUNCEMENT_VERSION, + name: tsunagi_wg_quic::WIREGUARD_PROTOCOL, + version: tsunagi_wg_quic::ANNOUNCEMENT_VERSION, summary: "WireGuard's cryptography carried in iroh's QUIC datagrams, so it \ crosses NAT and survives where plain WireGuard is blocked", options: WireguardPlugin::OPTIONS, @@ -1509,7 +1508,7 @@ fn host_section() -> report::Section { }); } - use tsunagi::dataplane::wireguard::{Privilege, probe_net_admin}; + use tsunagi::overlay::{Privilege, probe_net_admin}; match probe_net_admin() { Privilege::Available => { host.push(Row::new(Health::Good, "privileges", "CAP_NET_ADMIN held")); @@ -2064,16 +2063,14 @@ async fn up(args: UpArgs) -> Result<(), Box> { } else { system_tun_factory()? }; - let mtu = args - .mtu - .unwrap_or(tsunagi::dataplane::wireguard::DEFAULT_MTU); + let mtu = args.mtu.unwrap_or(tsunagi_wg_quic::DEFAULT_MTU); config = config.with_interface(tun_factory, args.interface.clone(), mtu); } for spec in &wanted { let options = settings_for(spec, &settings); match spec.name { - tsunagi::dataplane::wireguard::WIREGUARD_PROTOCOL => { + tsunagi_wg_quic::WIREGUARD_PROTOCOL => { let mut wg = WireguardConfig::new(paths.state_dir.join("wg-quic")); if let Some(mtu) = args.mtu { wg = wg.with_mtu(mtu); @@ -2381,7 +2378,7 @@ async fn stop_signal() -> &'static str { /// goes away. #[cfg(target_os = "linux")] fn system_tun_factory() -> Result, Box> { - use tsunagi::dataplane::wireguard::{ManagedTunFactory, NetlinkProvisioner}; + use tsunagi::overlay::{ManagedTunFactory, NetlinkProvisioner}; let provisioner = NetlinkProvisioner::new()?; Ok(Arc::new(ManagedTunFactory::new(Arc::new(provisioner)))) } diff --git a/crates/tsunagi-wg-quic/Cargo.toml b/crates/tsunagi-wg-quic/Cargo.toml new file mode 100644 index 0000000..51fa0e6 --- /dev/null +++ b/crates/tsunagi-wg-quic/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "tsunagi-wg-quic" +# Its own version, and deliberately separate from the wire version it +# negotiates: a release here does not stop it talking to a peer on an older +# build, because what peers compare is `ANNOUNCEMENT_VERSION`. +version = "0.1.0" +description = "The wg-quic protocol for tsunagi: WireGuard's cryptography carried in iroh's QUIC datagrams." +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +tsunagi = { path = "../tsunagi", version = "0.1.0" } +iroh.workspace = true +tokio.workspace = true +serde.workspace = true +postcard.workspace = true +bytes.workspace = true +thiserror.workspace = true +tracing.workspace = true +data-encoding.workspace = true +hex.workspace = true +rusqlite = { version = "0.40", features = ["bundled"] } +sha2 = "0.11" +hkdf = "0.13" +subtle = "2.6" +zeroize = { version = "1.9", features = ["derive"] } +rand = "0.10" +boringtun = { version = "0.7.1", default-features = false } + +[dev-dependencies] +tsunagi = { path = "../tsunagi", features = ["testing"] } +tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros"] } +tempfile.workspace = true +tracing-subscriber.workspace = true + +[lints] +workspace = true diff --git a/crates/tsunagi/src/dataplane/wireguard/announcement.rs b/crates/tsunagi-wg-quic/src/announcement.rs similarity index 94% rename from crates/tsunagi/src/dataplane/wireguard/announcement.rs rename to crates/tsunagi-wg-quic/src/announcement.rs index 904ff1a..b676840 100644 --- a/crates/tsunagi/src/dataplane/wireguard/announcement.rs +++ b/crates/tsunagi-wg-quic/src/announcement.rs @@ -1,26 +1,26 @@ //! What a WireGuard peer tells the network about itself. //! //! This is the opaque payload the control plane carries in a -//! [`crate::dataplane::PluginCapability`]. The agent core never parses it — +//! [`tsunagi::dataplane::PluginCapability`]. The agent core never parses it — //! only this module does, and only after bounding every field. //! //! The announcement is deliberately tiny: a participant says **who it is**, //! not **where it is**. Reachability is the data plane transport's job, and //! the transport already solves it — see -//! [`crate::dataplane::transport`]. A plugin that also tried to advertise +//! [`tsunagi::dataplane::transport`]. A plugin that also tried to advertise //! addresses would be reimplementing NAT traversal badly. use serde::{Deserialize, Serialize}; -use crate::dataplane::PluginError; -use crate::identity::NetworkId; +use tsunagi::dataplane::PluginError; +use tsunagi::identity::NetworkId; -use super::keys::WgPublicKey; +use crate::keys::WgPublicKey; /// Version of the announcement format. /// /// Version 3 dropped the IPv4 range again: overlay addressing moved to the -/// signed records in [`crate::state`], which carry the range and survive a +/// signed records in [`tsunagi::state`], which carry the range and survive a /// participant being away. postcard is not self-describing, so an older peer /// cannot read a newer announcement; the mismatch is reported, not misparsed. pub const ANNOUNCEMENT_VERSION: u16 = 4; @@ -122,7 +122,7 @@ mod tests { #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use super::*; - use crate::identity::{NetworkKeys, NetworkName, NetworkSecret}; + use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; use super::super::keys::WgSecretKey; @@ -237,7 +237,7 @@ mod tests { let peer = WgSecretKey::generate().public(); let payload = WgAnnouncement::new(id, &peer).encode().unwrap(); assert!( - payload.len() < crate::config::Limits::default().max_capability_data_len, + payload.len() < tsunagi::config::Limits::default().max_capability_data_len, "announcement is {} bytes", payload.len() ); diff --git a/crates/tsunagi/src/dataplane/wireguard/device.rs b/crates/tsunagi-wg-quic/src/device.rs similarity index 99% rename from crates/tsunagi/src/dataplane/wireguard/device.rs rename to crates/tsunagi-wg-quic/src/device.rs index 5703a7b..9537dde 100644 --- a/crates/tsunagi/src/dataplane/wireguard/device.rs +++ b/crates/tsunagi-wg-quic/src/device.rs @@ -39,11 +39,11 @@ use bytes::Bytes; use iroh::EndpointId; use tokio::task::JoinHandle; -use crate::dataplane::transport::{SharedLink, TransportError}; -use crate::dataplane::{PacketSink, PluginError}; -use crate::identity::NetworkId; +use tsunagi::dataplane::transport::{SharedLink, TransportError}; +use tsunagi::dataplane::{PacketSink, PluginError}; +use tsunagi::identity::NetworkId; -use super::keys::{WgPublicKey, WgSecretKey}; +use crate::keys::{WgPublicKey, WgSecretKey}; /// How often WireGuard's own timers are driven. /// diff --git a/crates/tsunagi/src/dataplane/wireguard/keys.rs b/crates/tsunagi-wg-quic/src/keys.rs similarity index 99% rename from crates/tsunagi/src/dataplane/wireguard/keys.rs rename to crates/tsunagi-wg-quic/src/keys.rs index 7f7b1de..c8da6cf 100644 --- a/crates/tsunagi/src/dataplane/wireguard/keys.rs +++ b/crates/tsunagi-wg-quic/src/keys.rs @@ -11,7 +11,7 @@ use boringtun::x25519; use data_encoding::BASE64; use zeroize::{Zeroize, Zeroizing}; -use crate::dataplane::PluginError; +use tsunagi::dataplane::PluginError; /// Length of a raw WireGuard key, in bytes. pub const KEY_LEN: usize = 32; diff --git a/crates/tsunagi-wg-quic/src/lib.rs b/crates/tsunagi-wg-quic/src/lib.rs new file mode 100644 index 0000000..af7869c --- /dev/null +++ b/crates/tsunagi-wg-quic/src/lib.rs @@ -0,0 +1,50 @@ +//! The `wg-quic` protocol: WireGuard's cryptography in iroh's QUIC datagrams. +//! +//! A crate of its own, so the line between a protocol and the system level +//! is drawn by the compiler rather than by discipline: nothing here can +//! reach into `tsunagi` beyond what it makes public. It also carries its own +//! version, which is **not** the version peers compare — that is +//! [`ANNOUNCEMENT_VERSION`], and it moves only when the bytes on the wire +//! do, so two peers on different releases keep working. +//! +//! That it uses iroh is a convenience, not a requirement of the design: the +//! system level already has an iroh endpoint that crosses NAT, and plain +//! WireGuard is blocked on some networks where this is not. +//! +//! Where the line falls: +//! +//! * **It knows nothing about reachability.** It is handed a +//! [`PacketLink`](tsunagi::dataplane::transport::PacketLink) per peer and +//! moves datagrams over it. Hole punching and relaying are the transport's. +//! * **It knows nothing about addresses, and owns no interface.** One agent +//! has one interface at the system level, and every protocol carries +//! traffic for the same addresses on it. A packet arrives here already +//! routed and leaves here already decrypted, to be checked against the +//! signed claim by the level that holds those. +//! * **Its announcement says who, not where.** A public key and the network +//! it is for, so there is nothing about reachability to lie about. +//! * **The core never parses that announcement.** It moves a bounded opaque +//! blob; only [`announcement`] reads it. +//! * **Its keys are its own.** One per network, in its own store, unrelated +//! to the iroh device key and to the network secret — which it never sees. +//! * **WireGuard's own crypto is untouched.** The handshake and encryption +//! run end to end between the two ends of a tunnel, via [`boringtun`]'s +//! state machine in this process: no kernel module, no `wg` tool, the same +//! code on every platform. +//! +//! See `docs/wireguard.md` for the full picture. + +pub mod announcement; +pub mod device; +pub mod keys; +pub mod plugin; +pub mod store; + +pub use announcement::{ANNOUNCEMENT_VERSION, ValidatedAnnouncement, WgAnnouncement}; +pub use device::{PeerHealth, PeerStats, PeerSummary, WireguardDevice}; +pub use keys::{WgPublicKey, WgSecretKey}; +pub use plugin::{ + DEFAULT_MTU, MIN_MTU, NetworkOverview, PeerOverview, WIREGUARD_OVERHEAD, WIREGUARD_PROTOCOL, + WireguardConfig, WireguardPlugin, +}; +pub use store::WgKeyStore; diff --git a/crates/tsunagi/src/dataplane/wireguard/plugin.rs b/crates/tsunagi-wg-quic/src/plugin.rs similarity index 96% rename from crates/tsunagi/src/dataplane/wireguard/plugin.rs rename to crates/tsunagi-wg-quic/src/plugin.rs index 6db8e92..c023614 100644 --- a/crates/tsunagi/src/dataplane/wireguard/plugin.rs +++ b/crates/tsunagi-wg-quic/src/plugin.rs @@ -8,7 +8,7 @@ //! # What this plugin does and does not know //! //! * It does **not** know where a peer is. It is handed a -//! [`PacketLink`](crate::dataplane::transport::PacketLink) per peer and runs +//! [`PacketLink`](tsunagi::dataplane::transport::PacketLink) per peer and runs //! a WireGuard tunnel over it. Reachability, hole punching and relaying are //! the transport's problem. //! * It does **not** know which addresses anybody holds, and owns no @@ -34,17 +34,17 @@ use iroh::EndpointId; use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinHandle; -use crate::BoxFuture; -use crate::dataplane::PacketSink; -use crate::dataplane::transport::SharedLink; -use crate::dataplane::{IpPlugin, PluginCapability, PluginContext, PluginError}; -use crate::identity::NetworkId; +use tsunagi::BoxFuture; +use tsunagi::dataplane::PacketSink; +use tsunagi::dataplane::transport::SharedLink; +use tsunagi::dataplane::{IpPlugin, PluginCapability, PluginContext, PluginError}; +use tsunagi::identity::NetworkId; -use super::announcement::{ValidatedAnnouncement, WgAnnouncement}; -use super::device::{PeerSummary, WireguardDevice}; -use super::keys::{WgPublicKey, WgSecretKey}; -use super::store::WgKeyStore; -use crate::state::Ipv4Range; +use crate::announcement::{ValidatedAnnouncement, WgAnnouncement}; +use crate::device::{PeerSummary, WireguardDevice}; +use crate::keys::{WgPublicKey, WgSecretKey}; +use crate::store::WgKeyStore; +use tsunagi::state::Ipv4Range; /// The protocol identifier this plugin announces. /// The protocol id of this plugin. @@ -235,14 +235,14 @@ pub struct WireguardPlugin { impl WireguardPlugin { /// The settings this protocol accepts. - pub const OPTIONS: &'static [crate::dataplane::ProtocolOption] = &[ - crate::dataplane::ProtocolOption { + pub const OPTIONS: &'static [tsunagi::dataplane::ProtocolOption] = &[ + tsunagi::dataplane::ProtocolOption { key: "keepalive", value: "SECONDS", help: "keeps a tunnel and its link warm through a NAT; 0 turns it off", default: Some("25"), }, - crate::dataplane::ProtocolOption { + tsunagi::dataplane::ProtocolOption { key: "mtu", value: "BYTES", help: "largest packet a tunnel will carry, at least 576", @@ -470,7 +470,7 @@ impl Worker { Some(sink) => Arc::clone(sink), // Not attached to an agent: the protocol still runs, and its // packets have nowhere to go. - None => Arc::new(crate::dataplane::DiscardPackets) as Arc, + None => Arc::new(tsunagi::dataplane::DiscardPackets) as Arc, }; let device = Arc::new(WireguardDevice::start(network, key, sink)); @@ -650,10 +650,10 @@ impl IpPlugin for WireguardPlugin { } fn protocol_version(&self) -> u16 { - super::announcement::ANNOUNCEMENT_VERSION + crate::announcement::ANNOUNCEMENT_VERSION } - fn options(&self) -> &'static [crate::dataplane::ProtocolOption] { + fn options(&self) -> &'static [tsunagi::dataplane::ProtocolOption] { Self::OPTIONS } @@ -686,7 +686,7 @@ impl IpPlugin for WireguardPlugin { let announcement = WgAnnouncement::new(network, &state.key.public()); Ok(Some(PluginCapability { protocol: WIREGUARD_PROTOCOL.to_string(), - version: super::announcement::ANNOUNCEMENT_VERSION, + version: crate::announcement::ANNOUNCEMENT_VERSION, enabled: true, data: announcement.encode()?, })) diff --git a/crates/tsunagi/src/dataplane/wireguard/store.rs b/crates/tsunagi-wg-quic/src/store.rs similarity index 96% rename from crates/tsunagi/src/dataplane/wireguard/store.rs rename to crates/tsunagi-wg-quic/src/store.rs index f7a229c..d00f4dd 100644 --- a/crates/tsunagi/src/dataplane/wireguard/store.rs +++ b/crates/tsunagi-wg-quic/src/store.rs @@ -17,10 +17,10 @@ use std::sync::Mutex; use rusqlite::{Connection, OptionalExtension, params}; -use crate::dataplane::PluginError; -use crate::identity::NetworkId; +use tsunagi::dataplane::PluginError; +use tsunagi::identity::NetworkId; -use super::keys::{KEY_LEN, WgSecretKey}; +use crate::keys::{KEY_LEN, WgSecretKey}; /// Schema version written by this build. pub const SCHEMA_VERSION: i64 = 1; @@ -37,14 +37,14 @@ impl WgKeyStore { pub fn open(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); if let Some(parent) = path.parent() { - crate::storage::create_dir(parent) + tsunagi::storage::create_dir(parent) .map_err(|err| PluginError::Other(format!("cannot create {parent:?}: {err}")))?; } let existed = path.exists(); let conn = Connection::open(&path).map_err(|err| { PluginError::Other(format!("cannot open the WireGuard key store: {err}")) })?; - crate::storage::restrict_path_permissions(&path) + tsunagi::storage::restrict_path_permissions(&path) .map_err(|err| PluginError::Other(format!("cannot secure the key store: {err}")))?; conn.busy_timeout(std::time::Duration::from_secs(5)) @@ -169,7 +169,7 @@ mod tests { #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use super::*; - use crate::identity::{NetworkKeys, NetworkName, NetworkSecret}; + use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; fn network(name: &str) -> NetworkId { NetworkKeys::derive( diff --git a/crates/tsunagi/tests/interface_provisioning.rs b/crates/tsunagi-wg-quic/tests/interface_provisioning.rs similarity index 98% rename from crates/tsunagi/tests/interface_provisioning.rs rename to crates/tsunagi-wg-quic/tests/interface_provisioning.rs index 5c3d0fd..f24d2df 100644 --- a/crates/tsunagi/tests/interface_provisioning.rs +++ b/crates/tsunagi-wg-quic/tests/interface_provisioning.rs @@ -11,23 +11,21 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use std::time::Duration; -use common::{config_with, network, wait_until}; use tempfile::TempDir; use tsunagi::dataplane::IpPlugin; -use tsunagi::dataplane::wireguard::{ - Cidr, InterfaceState, LinkKind, ManagedTunFactory, MockHost, MockProvisioner, WireguardConfig, - WireguardPlugin, -}; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::NetworkId; +use tsunagi::overlay::{ + Cidr, InterfaceState, LinkKind, ManagedTunFactory, MockHost, MockProvisioner, +}; use tsunagi::state::DEFAULT_IPV4_RANGE; +use tsunagi::testing::{config_with, network, wait_until}; use tsunagi::{Agent, NetworkStatus}; +use tsunagi_wg_quic::{WireguardConfig, WireguardPlugin}; /// An agent whose overlay interface is applied to a pretend host. struct HostedAgent { diff --git a/crates/tsunagi/tests/local_control.rs b/crates/tsunagi-wg-quic/tests/local_control.rs similarity index 98% rename from crates/tsunagi/tests/local_control.rs rename to crates/tsunagi-wg-quic/tests/local_control.rs index 980aea3..e03619f 100644 --- a/crates/tsunagi/tests/local_control.rs +++ b/crates/tsunagi-wg-quic/tests/local_control.rs @@ -6,19 +6,18 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::sync::Arc; use std::time::Duration; -use common::{config_with, network, wait_for_peers, wait_until}; use tempfile::TempDir; use tsunagi::dataplane::IpPlugin; -use tsunagi::dataplane::wireguard::{MemoryTunFactory, WireguardConfig, WireguardPlugin}; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::ipc::unix::{ControlSocket, request_status}; use tsunagi::ipc::{StatusReport, control_socket_path}; +use tsunagi::overlay::MemoryTunFactory; +use tsunagi::testing::{config_with, network, wait_for_peers, wait_until}; use tsunagi::{Agent, BoxFuture}; +use tsunagi_wg_quic::{WireguardConfig, WireguardPlugin}; /// Builds the report the way the binary does, from the agent plus the plugin. fn source(agent: Agent, plugin: Arc) -> Arc { diff --git a/crates/tsunagi/tests/wireguard.rs b/crates/tsunagi-wg-quic/tests/wireguard.rs similarity index 96% rename from crates/tsunagi/tests/wireguard.rs rename to crates/tsunagi-wg-quic/tests/wireguard.rs index 50aea88..a6d3464 100644 --- a/crates/tsunagi/tests/wireguard.rs +++ b/crates/tsunagi-wg-quic/tests/wireguard.rs @@ -8,25 +8,24 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::net::Ipv4Addr; use std::sync::Arc; use std::time::Duration; use bytes::Bytes; -use common::{config_with, network, settle, wait_event, wait_for_peers, wait_until}; use iroh::EndpointId; use tempfile::TempDir; use tsunagi::agent::Event; use tsunagi::dataplane::IpPlugin; -use tsunagi::dataplane::wireguard::{ - Ipv4Range, MemoryTun, MemoryTunFactory, WIREGUARD_PROTOCOL, WgAnnouncement, WgSecretKey, - WireguardConfig, WireguardPlugin, -}; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::{NetworkId, NetworkName, NetworkSecret}; +use tsunagi::overlay::{MemoryTun, MemoryTunFactory}; +use tsunagi::state::Ipv4Range; +use tsunagi::testing::{config_with, network, settle, wait_event, wait_for_peers, wait_until}; use tsunagi::{Agent, NetworkStatus}; +use tsunagi_wg_quic::{ + WIREGUARD_PROTOCOL, WgAnnouncement, WgSecretKey, WireguardConfig, WireguardPlugin, +}; /// An agent with a WireGuard plugin backed by an in-memory packet interface. struct WgAgent { @@ -217,7 +216,7 @@ async fn two_agents_carry_real_ip_packets_through_a_wireguard_tunnel() { let payload = b"hello over the overlay"; tun_a.push_from_os(ipv4_packet(addr_a, addr_b, payload)); - let received = tokio::time::timeout(common::DEADLINE, tun_b.pop_to_os()) + let received = tokio::time::timeout(tsunagi::testing::DEADLINE, tun_b.pop_to_os()) .await .expect("the packet should arrive") .expect("the interface should still be open"); @@ -227,7 +226,7 @@ async fn two_agents_carry_real_ip_packets_through_a_wireguard_tunnel() { // And back the other way. tun_b.push_from_os(ipv4_packet(addr_b, addr_a, b"and back")); - let back = tokio::time::timeout(common::DEADLINE, tun_a.pop_to_os()) + let back = tokio::time::timeout(tsunagi::testing::DEADLINE, tun_a.pop_to_os()) .await .expect("the reply should arrive") .unwrap(); @@ -281,7 +280,7 @@ async fn a_peer_cannot_send_from_an_address_it_does_not_own() { // A legitimate packet still goes through, so the tunnel is not broken. tun_a.push_from_os(ipv4_packet(addr_a, addr_b, b"honest")); - let received = tokio::time::timeout(common::DEADLINE, tun_b.pop_to_os()) + let received = tokio::time::timeout(tsunagi::testing::DEADLINE, tun_b.pop_to_os()) .await .expect("the honest packet should arrive") .unwrap(); @@ -327,7 +326,7 @@ async fn the_overlay_uses_a_range_the_network_was_told_to_use() { // A real IPv4 packet through the same tunnel. tun_a.push_from_os(ipv4_packet(v4_a, v4_b, b"ipv4 over the overlay")); - let received = tokio::time::timeout(common::DEADLINE, tun_b.pop_to_os()) + let received = tokio::time::timeout(tsunagi::testing::DEADLINE, tun_b.pop_to_os()) .await .expect("the IPv4 packet should arrive") .unwrap(); @@ -379,7 +378,7 @@ async fn an_ipv4_source_a_peer_does_not_own_is_dropped() { // The honest one still gets through. tun_a.push_from_os(ipv4_packet(v4_a, v4_b, b"honest v4")); - let received = tokio::time::timeout(common::DEADLINE, tun_b.pop_to_os()) + let received = tokio::time::timeout(tsunagi::testing::DEADLINE, tun_b.pop_to_os()) .await .expect("the honest packet should arrive") .unwrap(); @@ -669,10 +668,13 @@ async fn a_mesh_of_three_establishes_every_tunnel() { a.tun(network_id) .await .push_from_os(ipv4_packet(addr_a, addr_c, b"a to c")); - let received = tokio::time::timeout(common::DEADLINE, c.tun(network_id).await.pop_to_os()) - .await - .expect("the packet should arrive") - .unwrap(); + let received = tokio::time::timeout( + tsunagi::testing::DEADLINE, + c.tun(network_id).await.pop_to_os(), + ) + .await + .expect("the packet should arrive") + .unwrap(); assert_eq!(&received[20..], b"a to c"); a.shutdown().await; @@ -773,10 +775,13 @@ async fn two_networks_share_one_interface_with_keys_and_ranges_of_their_own() { hub.tun(alpha) .await .push_from_os(ipv4_packet(hub_alpha, left_addr, b"alpha only")); - let seen = tokio::time::timeout(common::DEADLINE, left.tun(alpha).await.pop_to_os()) - .await - .expect("the packet should arrive") - .unwrap(); + let seen = tokio::time::timeout( + tsunagi::testing::DEADLINE, + left.tun(alpha).await.pop_to_os(), + ) + .await + .expect("the packet should arrive") + .unwrap(); assert_eq!(&seen[20..], b"alpha only"); assert!( tokio::time::timeout( @@ -1062,7 +1067,7 @@ impl IpPlugin for FromTheFuture { } fn protocol_version(&self) -> u16 { - tsunagi::dataplane::wireguard::ANNOUNCEMENT_VERSION + 1 + tsunagi_wg_quic::ANNOUNCEMENT_VERSION + 1 } fn local_capability( @@ -1105,7 +1110,7 @@ impl IpPlugin for ForgingPlugin { /// The same version, so the announcement is looked at rather than /// dismissed for the wrong reason. fn protocol_version(&self) -> u16 { - tsunagi::dataplane::wireguard::ANNOUNCEMENT_VERSION + tsunagi_wg_quic::ANNOUNCEMENT_VERSION } fn local_capability( @@ -1120,7 +1125,7 @@ impl IpPlugin for ForgingPlugin { protocol: WIREGUARD_PROTOCOL.to_string(), // The version it actually speaks, so the payload is examined // rather than set aside for the wrong reason. - version: tsunagi::dataplane::wireguard::ANNOUNCEMENT_VERSION, + version: tsunagi_wg_quic::ANNOUNCEMENT_VERSION, enabled: true, data, })) @@ -1141,7 +1146,7 @@ impl IpPlugin for ForgingPlugin { #[tokio::test] async fn an_mtu_below_what_ipv4_guarantees_is_refused() { - use tsunagi::dataplane::wireguard::{DEFAULT_MTU, MIN_MTU, WIREGUARD_OVERHEAD}; + use tsunagi_wg_quic::{DEFAULT_MTU, MIN_MTU, WIREGUARD_OVERHEAD}; // 576 bytes is what every IPv4 host must be able to reassemble, so // nothing below it is worth offering. The floor used to be 1280 for diff --git a/crates/tsunagi/Cargo.toml b/crates/tsunagi/Cargo.toml index 4eb40d7..21e3ad4 100644 --- a/crates/tsunagi/Cargo.toml +++ b/crates/tsunagi/Cargo.toml @@ -18,6 +18,9 @@ tun-device = ["dep:tun", "dep:rtnetlink", "dep:caps", "dep:futures-util"] # Telling the operating system where to send its DNS questions. Linux only # for now; the zone and the server work without it. dns-publish = ["dep:zbus"] +# The test harness in `testing`, for this crate's own tests and for a +# protocol crate's. Not on by default: it is only of use to a test. +testing = ["dep:tempfile", "dep:tracing-subscriber"] [dependencies] iroh.workspace = true @@ -50,8 +53,10 @@ directories = "6.0" # forbidden `unsafe` and will not make one. gethostname = "1.1" netwatch = "0.19.3" -boringtun = { version = "0.7.1", default-features = false } tun = { version = "0.8", features = ["async"], optional = true } +# Only for the `testing` harness, which a protocol crate's tests use too. +tempfile = { version = "3.24", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } # Linux-only interface provisioning. `rtnetlink` configures the interface in # process, so no `ip` invocation is ever needed; `caps` keeps CAP_NET_ADMIN @@ -65,6 +70,8 @@ caps = { version = "0.5", optional = true } futures-util = { version = "0.3", default-features = false, optional = true } [dev-dependencies] +# Its own tests use the harness it publishes. +tsunagi = { path = ".", features = ["testing"] } tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process"] } tempfile.workspace = true tracing-subscriber.workspace = true diff --git a/crates/tsunagi/src/dataplane/mod.rs b/crates/tsunagi/src/dataplane/mod.rs index eef06df..8d6a7fb 100644 --- a/crates/tsunagi/src/dataplane/mod.rs +++ b/crates/tsunagi/src/dataplane/mod.rs @@ -20,7 +20,6 @@ //! recorded and surfaced, the control plane keeps running. pub mod transport; -pub mod wireguard; use std::sync::Arc; @@ -42,7 +41,7 @@ pub const MAX_PROTOCOL_ID_LEN: usize = 32; /// chooses to do so must validate it itself. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct PluginCapability { - /// Protocol identifier, e.g. `wireguard`. Bounded by [`MAX_PROTOCOL_ID_LEN`]. + /// Protocol identifier, e.g. `wg-quic`. Bounded by [`MAX_PROTOCOL_ID_LEN`]. pub protocol: String, /// Version of the plugin's announcement format. pub version: u16, @@ -112,20 +111,6 @@ pub enum PluginError { Other(String), } -/// Scaffolding while the interface moves out of the plugin. -/// -/// The overlay interface belongs to the system level now, so a plugin has no -/// business failing because of it. This exists only for the callers that -/// have not been moved over yet and goes when the last of them does. -impl From for PluginError { - fn from(err: crate::overlay::OverlayError) -> Self { - match err { - crate::overlay::OverlayError::Unavailable(reason) => PluginError::Unavailable(reason), - crate::overlay::OverlayError::Other(reason) => PluginError::Other(reason), - } - } -} - /// A request a plugin makes of the agent that owns it. #[derive(Debug)] pub(crate) enum PluginRequest { @@ -249,7 +234,7 @@ impl std::fmt::Debug for PluginContext { /// Implementations must be cheap and non-blocking: the agent calls them from /// its runtime tasks. Anything slow belongs in the plugin's own tasks. pub trait IpPlugin: Send + Sync + std::fmt::Debug + 'static { - /// Stable protocol identifier, e.g. `wireguard`. + /// Stable protocol identifier, e.g. `wg-quic`. /// /// Must be non-empty and at most [`MAX_PROTOCOL_ID_LEN`] bytes. fn protocol_id(&self) -> &str; diff --git a/crates/tsunagi/src/dataplane/wireguard/mod.rs b/crates/tsunagi/src/dataplane/wireguard/mod.rs deleted file mode 100644 index 8253497..0000000 --- a/crates/tsunagi/src/dataplane/wireguard/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! The WireGuard protocol plugin. -//! -//! The first of them. It carries IP packets between participants; deciding -//! who is in the network, what addresses they hold and which interface those -//! sit on belongs to the system level, which configures this and then leaves -//! it to get on with it. -//! -//! Where the line falls: -//! -//! * **The plugin knows nothing about reachability.** It is handed a -//! [`PacketLink`](crate::dataplane::transport::PacketLink) per peer and -//! moves datagrams over it. Hole punching and relaying belong to the -//! transport. -//! * **The plugin knows nothing about addresses either.** They are allocated -//! and signed at the system level, the same ones whichever protocol is -//! moving the packets. Nothing here derives one. -//! * **The announcement says who, not where.** A public key and the network -//! it is for, so there is nothing about reachability for a peer to lie -//! about. -//! * **The core never parses these announcements.** It moves a bounded -//! opaque blob; only [`announcement`] interprets it. -//! * **Keys are separate.** One key per network, in its own store, unrelated -//! to the iroh device key and to the network secret. -//! * **WireGuard's own crypto is untouched.** The handshake and encryption -//! run end to end between the two ends of a tunnel. -//! -//! WireGuard itself is [`boringtun`]'s protocol state machine, running in -//! this process: no kernel module, no `wg` tool, the same code on every -//! platform. [`device::WireguardDevice`] drives one tunnel per peer. -//! -//! The interface underneath comes from [`crate::overlay`], and with its -//! in-memory implementation the whole data plane — handshake, encryption, -//! routing, address ownership — runs and is tested with no privileges at -//! all. -//! -//! See `docs/wireguard.md` for the full picture. - -pub mod announcement; -pub mod device; -pub mod keys; -pub mod plugin; -pub mod store; - -pub use crate::state::Ipv4Range; -pub use announcement::{ANNOUNCEMENT_VERSION, ValidatedAnnouncement, WgAnnouncement}; -// The interface, its addresses and how it is created belong to the system -// level now: one agent has one interface, and no protocol owns it. Re-exported -// here while callers are moved over. -pub use crate::overlay::provision::{ - Changes, InterfacePlan, InterfaceProvisioner, InterfaceState, LinkKind, ManagedTunFactory, - MockHost, MockProvisioner, Privilege, Provisioned, UnsupportedProvisioner, plan_changes, - probe_net_admin, -}; -pub use crate::overlay::{ - Cidr, DEFAULT_INTERFACE_PREFIX, IpHeader, MAX_INTERFACE_NAME_LEN, MemoryTun, MemoryTunFactory, - TunDevice, TunFactory, TunRequest, address_is_local, interface_name, -}; -pub use device::{PeerHealth, PeerStats, PeerSummary, WireguardDevice}; -pub use keys::{WgPublicKey, WgSecretKey}; -pub use plugin::{ - DEFAULT_MTU, MIN_MTU, NetworkOverview, PeerOverview, WIREGUARD_OVERHEAD, WIREGUARD_PROTOCOL, - WireguardConfig, WireguardPlugin, -}; -pub use store::WgKeyStore; - -#[cfg(all(feature = "tun-device", target_os = "linux"))] -pub use crate::overlay::NetlinkProvisioner; diff --git a/crates/tsunagi/src/lib.rs b/crates/tsunagi/src/lib.rs index 1b96c6b..1619602 100644 --- a/crates/tsunagi/src/lib.rs +++ b/crates/tsunagi/src/lib.rs @@ -48,6 +48,8 @@ pub mod overlay; pub mod proto; pub mod state; pub mod storage; +#[cfg(feature = "testing")] +pub mod testing; pub use agent::{Agent, AgentStatus, Event, NetworkStatus, PeerStatus}; pub use config::{AgentConfig, Limits, ReconnectPolicy, StoragePaths, TransportPolicy}; diff --git a/crates/tsunagi/src/storage/mod.rs b/crates/tsunagi/src/storage/mod.rs index 5ca1a78..a7fda18 100644 --- a/crates/tsunagi/src/storage/mod.rs +++ b/crates/tsunagi/src/storage/mod.rs @@ -56,8 +56,13 @@ fn restrict_permissions(_file: &std::fs::File, _path: &Path) -> Result<()> { Ok(()) } +/// Restricts a file to the user that owns it. +/// +/// Part of what the system level offers a protocol: a plugin keeping keys of +/// its own on disk has the same obligation as the agent, and should not have +/// to work out the platform details again to meet it. #[cfg(unix)] -pub(crate) fn restrict_path_permissions(path: &Path) -> Result<()> { +pub fn restrict_path_permissions(path: &Path) -> Result<()> { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|source| { Error::Io { @@ -67,13 +72,15 @@ pub(crate) fn restrict_path_permissions(path: &Path) -> Result<()> { }) } +/// Restricts a file to the user that owns it. A no-op off Unix. #[cfg(not(unix))] -pub(crate) fn restrict_path_permissions(_path: &Path) -> Result<()> { +pub fn restrict_path_permissions(_path: &Path) -> Result<()> { Ok(()) } +/// Restricts a directory to the user that owns it. #[cfg(unix)] -pub(crate) fn restrict_dir_permissions(path: &Path) -> Result<()> { +pub fn restrict_dir_permissions(path: &Path) -> Result<()> { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(|source| { Error::Io { @@ -83,12 +90,14 @@ pub(crate) fn restrict_dir_permissions(path: &Path) -> Result<()> { }) } +/// Restricts a directory to the user that owns it. A no-op off Unix. #[cfg(not(unix))] -pub(crate) fn restrict_dir_permissions(_path: &Path) -> Result<()> { +pub fn restrict_dir_permissions(_path: &Path) -> Result<()> { Ok(()) } -pub(crate) fn create_dir(path: &Path) -> Result<()> { +/// Creates a directory and every parent, restricted to this user. +pub fn create_dir(path: &Path) -> Result<()> { std::fs::create_dir_all(path).map_err(|source| Error::Io { path: path.to_path_buf(), source, diff --git a/crates/tsunagi/tests/common/mod.rs b/crates/tsunagi/src/testing.rs similarity index 82% rename from crates/tsunagi/tests/common/mod.rs rename to crates/tsunagi/src/testing.rs index 7d3f903..86c0b8b 100644 --- a/crates/tsunagi/tests/common/mod.rs +++ b/crates/tsunagi/src/testing.rs @@ -1,26 +1,31 @@ -//! Shared helpers for the integration tests. +//! A harness for testing against a real agent. //! -//! Every test uses real iroh endpoints on loopback, its own temporary SQLite -//! files and independent agent instances. Only discovery is substituted; iroh, -//! the handshake, message passing and persistent storage are not. +//! Behind the `testing` feature, because it is only of use to a test and +//! pulls in `tempfile` and a tracing subscriber. It lives here rather than +//! being copied into each crate's `tests/` directory: a protocol plugin in +//! its own crate needs exactly the same harness, and two copies of it would +//! drift. //! -//! Synchronisation is always "wait for a specific event or condition, under one -//! overall deadline", never a fixed multi-second sleep. +//! Everything here builds agents that touch nothing outside a temporary +//! directory: loopback only, no relay, no address publication. -#![allow(dead_code, clippy::unwrap_used, clippy::expect_used, clippy::panic)] +// A harness fails a test loudly and at once; that is the whole job. The +// crate forbids these elsewhere for the opposite reason — nothing off the +// network may panic — and this module never sees anything off the network. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use std::future::Future; use std::path::Path; use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::agent::Event; +use crate::config::{AgentConfig, StoragePaths, TransportPolicy}; +use crate::discovery::SharedMemoryDiscovery; +use crate::identity::{NetworkName, NetworkSecret}; +use crate::{Agent, Result}; use tempfile::TempDir; use tokio::sync::broadcast::error::RecvError; -use tsunagi::agent::Event; -use tsunagi::config::{AgentConfig, StoragePaths, TransportPolicy}; -use tsunagi::discovery::SharedMemoryDiscovery; -use tsunagi::identity::{NetworkName, NetworkSecret}; -use tsunagi::{Agent, Result}; /// Installs a tracing subscriber when `TSUNAGI_TEST_LOG` is set. /// @@ -50,7 +55,7 @@ const POLL_INTERVAL: Duration = Duration::from_millis(25); /// port mapping, so the suite needs neither the internet nor privileges. /// Timeouts are shortened so that failure paths finish quickly. pub fn local_config(dir: &Path) -> AgentConfig { - let limits = tsunagi::Limits { + let limits = crate::Limits { dial_timeout: Duration::from_millis(1500), handshake_timeout: Duration::from_secs(5), ..Default::default() @@ -69,7 +74,9 @@ pub fn config_with(dir: &Path, discovery: &SharedMemoryDiscovery) -> AgentConfig /// A temporary directory plus the agent running on it. pub struct TestAgent { + /// The directory it keeps its state in, removed when this is dropped. pub dir: TempDir, + /// The agent itself. pub agent: Agent, } @@ -170,7 +177,7 @@ pub async fn settle() { /// Waits until `agent` has `count` authenticated peers in `network`. pub async fn wait_for_peers( agent: &Agent, - network: tsunagi::NetworkId, + network: crate::NetworkId, count: usize, ) -> Vec { wait_until(&format!("{count} peers in {network}"), || async move { diff --git a/crates/tsunagi/tests/authentication.rs b/crates/tsunagi/tests/authentication.rs index 5d00454..2d23e74 100644 --- a/crates/tsunagi/tests/authentication.rs +++ b/crates/tsunagi/tests/authentication.rs @@ -8,9 +8,6 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{TestAgent, network, settle, wait_event}; use iroh::endpoint::{Connection, PortmapperConfig, RecvStream, SendStream, presets}; use iroh::{Endpoint, EndpointAddr, RelayMode}; use tsunagi::agent::Event; @@ -22,6 +19,7 @@ use tsunagi::proto::message::{ }; use tsunagi::proto::{read_frame, write_frame}; use tsunagi::test_support; +use tsunagi::testing::{TestAgent, network, settle, wait_event}; /// A bare iroh endpoint with no tsunagi agent behind it. async fn raw_endpoint() -> Endpoint { diff --git a/crates/tsunagi/tests/cache_and_state.rs b/crates/tsunagi/tests/cache_and_state.rs index f9bf8b2..5b58464 100644 --- a/crates/tsunagi/tests/cache_and_state.rs +++ b/crates/tsunagi/tests/cache_and_state.rs @@ -2,14 +2,12 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::io::Write; -use common::{TestAgent, config_with, local_config, network, wait_for_peers}; use tsunagi::config::StoragePaths; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::storage::CacheOutcome; +use tsunagi::testing::{TestAgent, config_with, local_config, network, wait_for_peers}; use tsunagi::{Agent, Error}; /// Overwrites a file with bytes that are definitely not a SQLite database. diff --git a/crates/tsunagi/tests/device_identity.rs b/crates/tsunagi/tests/device_identity.rs index 42a7354..4cd761d 100644 --- a/crates/tsunagi/tests/device_identity.rs +++ b/crates/tsunagi/tests/device_identity.rs @@ -8,9 +8,6 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{config_with, network, wait_until}; use tempfile::TempDir; use tsunagi::Agent; use tsunagi::config::StoragePaths; @@ -18,6 +15,7 @@ use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; use tsunagi::state::{RecordBody, StateSet}; use tsunagi::storage::StateStore; +use tsunagi::testing::{config_with, network, wait_until}; #[tokio::test] async fn a_changed_name_reaches_peers_and_replaces_the_old_claim() { diff --git a/crates/tsunagi/tests/discovery.rs b/crates/tsunagi/tests/discovery.rs index c39cdaf..a3c6b5a 100644 --- a/crates/tsunagi/tests/discovery.rs +++ b/crates/tsunagi/tests/discovery.rs @@ -3,16 +3,14 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::sync::Arc; -use common::{TestAgent, local_config, network, settle, wait_for_peers}; use tsunagi::Agent; use tsunagi::discovery::{ CandidateSource, CompositeDiscovery, NetworkDiscovery, SharedMemoryDiscovery, StaticBootstrap, }; use tsunagi::identity::NetworkKeys; +use tsunagi::testing::{TestAgent, local_config, network, settle, wait_for_peers}; #[tokio::test] async fn a_static_bootstrap_candidate_is_enough_to_join() { diff --git a/crates/tsunagi/tests/end_to_end.rs b/crates/tsunagi/tests/end_to_end.rs index a93c044..a63945f 100644 --- a/crates/tsunagi/tests/end_to_end.rs +++ b/crates/tsunagi/tests/end_to_end.rs @@ -3,12 +3,10 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{TestAgent, network, wait_event, wait_for_peers}; use tsunagi::agent::Event; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::proto::ControlMessage; +use tsunagi::testing::{TestAgent, network, wait_event, wait_for_peers}; #[tokio::test] async fn two_agents_authenticate_and_exchange_messages() { diff --git a/crates/tsunagi/tests/identity.rs b/crates/tsunagi/tests/identity.rs index a72dec7..d7dba37 100644 --- a/crates/tsunagi/tests/identity.rs +++ b/crates/tsunagi/tests/identity.rs @@ -5,12 +5,10 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{TestAgent, config_with, network}; use tsunagi::Agent; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::{NetworkKeys, NetworkName, NetworkSecret}; +use tsunagi::testing::{TestAgent, config_with, network}; #[test] fn derivation_is_a_pure_function_of_name_and_secret() { diff --git a/crates/tsunagi/tests/multi_peer.rs b/crates/tsunagi/tests/multi_peer.rs index b42011e..f7f5231 100644 --- a/crates/tsunagi/tests/multi_peer.rs +++ b/crates/tsunagi/tests/multi_peer.rs @@ -3,16 +3,14 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::collections::HashSet; use std::sync::Arc; -use common::{TestAgent, network, wait_event, wait_for_peers, wait_until}; use tsunagi::agent::Event; use tsunagi::dataplane::TestCapabilityPlugin; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::proto::ControlMessage; +use tsunagi::testing::{TestAgent, network, wait_event, wait_for_peers, wait_until}; #[tokio::test] async fn four_agents_form_a_mesh_and_exchange_distinguishable_messages() { diff --git a/crates/tsunagi/tests/network_isolation.rs b/crates/tsunagi/tests/network_isolation.rs index c679b35..8a75cec 100644 --- a/crates/tsunagi/tests/network_isolation.rs +++ b/crates/tsunagi/tests/network_isolation.rs @@ -2,9 +2,6 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{TestAgent, network, settle, wait_event, wait_for_peers}; use iroh::endpoint::{PortmapperConfig, presets}; use iroh::{Endpoint, RelayMode}; use tsunagi::agent::Event; @@ -16,6 +13,7 @@ use tsunagi::proto::message::{ }; use tsunagi::proto::{read_frame, write_frame}; use tsunagi::test_support; +use tsunagi::testing::{TestAgent, network, settle, wait_event, wait_for_peers}; const LIMIT: usize = 64 * 1024; diff --git a/crates/tsunagi/tests/resilience.rs b/crates/tsunagi/tests/resilience.rs index c6ae634..60fa93c 100644 --- a/crates/tsunagi/tests/resilience.rs +++ b/crates/tsunagi/tests/resilience.rs @@ -3,18 +3,18 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - use std::net::SocketAddr; use std::time::Duration; -use common::{TestAgent, config_with, local_config, network, settle, wait_event, wait_for_peers}; use iroh::{EndpointAddr, SecretKey}; use tsunagi::agent::Event; use tsunagi::config::ReconnectPolicy; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::NetworkKeys; use tsunagi::proto::ControlMessage; +use tsunagi::testing::{ + TestAgent, config_with, local_config, network, settle, wait_event, wait_for_peers, +}; use tsunagi::{Agent, Error}; /// An endpoint id nobody is listening for, at an address nothing answers on. diff --git a/crates/tsunagi/tests/restart.rs b/crates/tsunagi/tests/restart.rs index 841aa2f..8370504 100644 --- a/crates/tsunagi/tests/restart.rs +++ b/crates/tsunagi/tests/restart.rs @@ -2,13 +2,11 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] -mod common; - -use common::{TestAgent, config_with, network, settle, wait_event, wait_for_peers}; use tsunagi::agent::Event; use tsunagi::discovery::SharedMemoryDiscovery; use tsunagi::identity::{NetworkName, NetworkSecret}; use tsunagi::proto::ControlMessage; +use tsunagi::testing::{TestAgent, config_with, network, settle, wait_event, wait_for_peers}; use tsunagi::{Agent, Error}; #[tokio::test]