//! This device as `rex-iroh` keeps it: its key, and the directory holding its //! allowlist, control socket and the peers it has joined. use std::path::PathBuf; use iroh::{ Endpoint, EndpointId, SecretKey, endpoint::{BindError, presets}, }; use crate::{ control::{self, ClientError, Pairing, PeerList, Revoke}, joined::Joined, key::{self, KeyError}, pair::Policy, }; const SECRET_KEY: &str = "secret.key"; const ALLOWLIST: &str = "allowlist"; const CONTROL_SOCKET: &str = "control.sock"; const PEERS: &str = "peers"; const SOCKETS: &str = "sockets"; /// Set (to anything) to keep every endpoint on loopback, with no relays or /// address lookup, so the binary's tests can pair and tunnel in a sandbox /// without network. Not gated on debug builds: Nix runs the tests in release. const LOOPBACK_ONLY: &str = "REX_IROH_LOOPBACK_ONLY"; fn loopback_only() -> bool { std::env::var_os(LOOPBACK_ONLY).is_some() } #[derive(Debug, thiserror::Error)] #[error("no data directory for this user")] pub struct NoDataDir; /// Where this device's state lives unless told otherwise. pub fn default_dir() -> Result { dirs::data_dir() .map(|dir| dir.join("rex-iroh")) .ok_or(NoDataDir) } /// This device's identity and the directory its state lives in. #[derive(Debug)] pub struct Node { dir: PathBuf, secret: SecretKey, } impl Node { /// Open the state in `dir`, creating this device's key on first use. pub fn open(dir: PathBuf) -> Result { let secret = key::load_or_create(&dir.join(SECRET_KEY))?; Ok(Self { dir, secret }) } pub fn id(&self) -> EndpointId { self.secret.public() } /// An endpoint with this device's key, reachable through n0's relays and /// address lookup. pub async fn bind(&self) -> Result { if loopback_only() { return Endpoint::builder(presets::Minimal) .secret_key(self.secret.clone()) .clear_ip_transports() .bind_addr("127.0.0.1:0") .expect("a literal socket address") .bind() .await; } Endpoint::builder(presets::N0) .secret_key(self.secret.clone()) .bind() .await } /// Whether endpoints stay on loopback, with no relays or address lookup. pub fn loopback_only(&self) -> bool { loopback_only() } /// The allowlist `serve` uses unless told otherwise. pub fn allowlist(&self) -> PathBuf { self.dir.join(ALLOWLIST) } /// Where `serve` listens for the commands that reach it locally. pub fn control_socket(&self) -> PathBuf { self.dir.join(CONTROL_SOCKET) } /// Ask the `serve` running from this state for an invite, to see its /// pairing through. pub async fn pair(&self, policy: Policy) -> Result { control::request_invite(&self.dir.join(CONTROL_SOCKET), policy).await } /// Who is allowed to connect, through `serve` if it's running. pub async fn peers(&self) -> Result { control::list_peers(&self.dir.join(CONTROL_SOCKET), &self.dir.join(ALLOWLIST)).await } /// Take the peer `peer` names off the allowlist, through `serve` if it's /// running so its connections close too. pub async fn revoke(&self, peer: &str) -> Result { control::revoke_peer( &self.dir.join(CONTROL_SOCKET), &self.dir.join(ALLOWLIST), peer, ) .await } /// The issuers this device has joined. pub fn joined(&self) -> Joined { Joined::new(self.dir.join(PEERS), self.dir.join(SOCKETS), self.id()) } } #[cfg(test)] mod tests { use super::*; #[test] fn reopening_keeps_the_identity() { let dir = tempfile::tempdir().unwrap(); let first = Node::open(dir.path().join("state")).unwrap(); let second = Node::open(dir.path().join("state")).unwrap(); assert_eq!(first.id(), second.id()); assert!(dir.path().join("state/secret.key").exists()); } }