//! tpd — tesseras-paste daemon. //! //! Runs a DHT node that stores and serves encrypted pastes. //! Communicates with the CLI (`tp`) over a Unix socket and //! optionally serves pastes via HTTP. #[path = "../base58.rs"] mod base58; #[path = "../crypto.rs"] mod crypto; #[path = "../daemon.rs"] mod daemon; #[path = "../ops.rs"] mod ops; #[path = "../paste.rs"] mod paste; #[path = "../protocol.rs"] mod protocol; #[path = "../store.rs"] mod store; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, mpsc}; use tesseras_dht::nat::NatState; use tesseras_dht::node::NodeBuilder; use store::PasteStore; fn default_dir() -> PathBuf { PathBuf::from("/var/tesseras-paste") } fn usage() { eprintln!( "usage: tpd [-p port] [-d dir] [-s sock] \ [-w http_port] [-g] [-b host:port] [-h]" ); eprintln!(); eprintln!(" -p port UDP port (0 = random)"); eprintln!(" -d dir data directory"); eprintln!(" -s sock Unix socket path"); eprintln!(" -w port HTTP server port"); eprintln!(" -g global NAT (public server)"); eprintln!(" -b host:port bootstrap peer (repeatable)"); eprintln!(" -h show this help"); } fn main() { env_logger::Builder::from_env( env_logger::Env::default().default_filter_or("info"), ) .format(|buf, record| { use std::io::Write; writeln!(buf, "[{}]: {}", record.level(), record.args()) }) .init(); let mut port: u16 = 0; let mut dir = default_dir(); let mut sock: Option = None; let mut http_port: Option = None; let mut global = false; let mut bootstrap: Vec = Vec::new(); let args: Vec = std::env::args().collect(); let mut i = 1; while i < args.len() { match args[i].as_str() { "-p" => { i += 1; port = args.get(i).and_then(|s| s.parse().ok()).unwrap_or_else( || { eprintln!("error: -p requires a port"); std::process::exit(1); }, ); } "-d" => { i += 1; dir = args.get(i).map(PathBuf::from).unwrap_or_else(|| { eprintln!("error: -d requires a path"); std::process::exit(1); }); } "-s" => { i += 1; sock = args.get(i).map(PathBuf::from); if sock.is_none() { eprintln!("error: -s requires a path"); std::process::exit(1); } } "-w" => { i += 1; http_port = Some( args.get(i).and_then(|s| s.parse().ok()).unwrap_or_else( || { eprintln!("error: -w requires a port"); std::process::exit(1); }, ), ); } "-g" => global = true, "-b" => { i += 1; if let Some(addr) = args.get(i) { bootstrap.push(addr.clone()); } else { eprintln!("error: -b requires host:port"); std::process::exit(1); } } "-h" | "--help" => { usage(); return; } other => { eprintln!("unknown option: {other}"); usage(); std::process::exit(1); } } i += 1; } let sock_path = sock.unwrap_or_else(|| dir.join("daemon.sock")); // Ensure directories exist let _ = std::fs::create_dir_all(&dir); if let Some(parent) = sock_path.parent() { let _ = std::fs::create_dir_all(parent); } let store = match PasteStore::open(&dir) { Ok(s) => s, Err(e) => { eprintln!("error: {e}"); std::process::exit(1); } }; // Load or generate persistent identity let identity_path = dir.join("identity.key"); let identity_seed = load_or_create_identity(&identity_path); let mut builder = NodeBuilder::new().port(port).seed(&identity_seed); if global { builder = builder.nat(NatState::Global); } let cfg = tesseras_dht::config::Config { default_ttl: 65535, max_value_size: 128 * 1024, require_signatures: true, ..Default::default() }; builder = builder.config(cfg); let mut node = match builder.build() { Ok(n) => n, Err(e) => { eprintln!("error: {e}"); std::process::exit(1); } }; node.set_routing_persistence(Box::new(store.clone())); node.set_data_persistence(Box::new(store.clone())); node.load_persisted(); let addr = match node.local_addr() { Ok(a) => a, Err(e) => { eprintln!("error: could not determine local address: {e}"); std::process::exit(1); } }; let id = node.id_hex(); eprintln!("tpd {addr} id={:.8}", id); for peer in &bootstrap { let parts: Vec<&str> = peer.rsplitn(2, ':').collect(); if parts.len() != 2 { eprintln!("warning: bad bootstrap: {peer}"); continue; } let host = parts[1]; let p: u16 = match parts[0].parse() { Ok(p) => p, Err(_) => { eprintln!("warning: bad port: {peer}"); continue; } }; if let Err(e) = node.join(host, p) { eprintln!("warning: bootstrap {peer}: {e}"); } else { log::info!("bootstrap: connected to {peer}"); } } for _ in 0..10 { let _ = node.poll(); } eprintln!( "peers={} socket={}", node.routing_table_size(), sock_path.display() ); let shutdown = Arc::new(AtomicBool::new(false)); // Signal handler let sig = Arc::clone(&shutdown); unsafe { SHUTDOWN_PTR.store( Arc::into_raw(sig) as *mut AtomicBool as usize, Ordering::SeqCst, ); signal(SIGINT, sig_handler as *const () as usize); signal(SIGTERM, sig_handler as *const () as usize); } let (tx, rx) = mpsc::channel(); let listener_shutdown = Arc::clone(&shutdown); let listener_path = sock_path.clone(); let handle = std::thread::spawn(move || { daemon::run_unix_listener(&listener_path, tx, &listener_shutdown); }); // HTTP server thread (optional) let http_handle = http_port.map(|hp| { let http_store = store.clone(); let http_shutdown = Arc::clone(&shutdown); let http_sock = sock_path.clone(); eprintln!("http on 0.0.0.0:{hp}"); std::thread::spawn(move || { daemon::run_http(hp, &http_sock, &http_store, &http_shutdown); }) }); daemon::run_daemon(&mut node, &store, &rx, &shutdown); let _ = std::fs::remove_file(&sock_path); let _ = handle.join(); if let Some(h) = http_handle { let _ = h.join(); } eprintln!("shutdown complete"); } /// Load identity seed from file, or generate and save /// a new one. This ensures the node keeps the same /// Ed25519 keypair (and NodeId) across restarts. fn load_or_create_identity(path: &std::path::Path) -> Vec { if let Ok(data) = std::fs::read(path) && data.len() == 32 { log::info!("identity: loaded from {}", path.display()); return data; } let mut seed = [0u8; 32]; tesseras_dht::sys::random_bytes(&mut seed); if let Err(e) = std::fs::write(path, seed) { log::warn!("identity: failed to save to {}: {e}", path.display()); } else { log::info!("identity: generated new keypair at {}", path.display()); } seed.to_vec() } const SIGINT: i32 = 2; const SIGTERM: i32 = 15; unsafe extern "C" { fn signal(sig: i32, handler: usize) -> usize; } static SHUTDOWN_PTR: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); extern "C" fn sig_handler(_sig: i32) { let ptr = SHUTDOWN_PTR.load(Ordering::SeqCst); if ptr != 0 { let flag = unsafe { &*(ptr as *const AtomicBool) }; flag.store(true, Ordering::SeqCst); } }