//! 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 = "../dns.rs"] mod dns; #[path = "../ops.rs"] mod ops; #[path = "../paste.rs"] mod paste; #[path = "../protocol.rs"] mod protocol; #[path = "../sandbox.rs"] mod sandbox; #[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] [-n] [-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!(" -n no auto-bootstrap (skip DNS SRV)"); eprintln!(" -b host:port bootstrap peer (repeatable)"); eprintln!(" -v verbose (debug logging)"); eprintln!(" -h show this help"); } fn main() { 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 no_auto_bootstrap = false; let mut verbose = 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, "-n" => no_auto_bootstrap = true, "-v" => verbose = 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 default_level = if verbose { "debug" } else { "info" }; env_logger::Builder::from_env( env_logger::Env::default().default_filter_or(default_level), ) .format(|buf, record| { use std::io::Write; writeln!(buf, "[{}]: {}", record.level(), record.args()) }) .init(); let sock_path = sock.unwrap_or_else(|| dir.join("daemon.sock")); // Ensure directories exist if let Err(e) = std::fs::create_dir_all(&dir) { eprintln!("error: cannot create {}: {e}", dir.display()); std::process::exit(1); } if let Some(parent) = sock_path.parent() && let Err(e) = std::fs::create_dir_all(parent) { eprintln!("error: cannot create {}: {e}", parent.display()); std::process::exit(1); } 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); // ── Sandbox ───────────────────────────────────── // Apply unveil(2) to restrict filesystem visibility, // then pledge(2) to restrict syscalls. sandbox::do_unveil(&dir, "rwc"); if sock_path.parent() != Some(dir.as_ref()) && let Some(parent) = sock_path.parent() { sandbox::do_unveil(parent, "rwc"); } if !no_auto_bootstrap || !bootstrap.is_empty() { sandbox::do_unveil(std::path::Path::new("/etc/resolv.conf"), "r"); } sandbox::unveil_lock(); sandbox::do_pledge("stdio rpath wpath cpath fattr inet unix dns"); // If no explicit peers given and auto-bootstrap is enabled, // discover peers via DNS SRV (_tesseras._udp.tesseras.net). if bootstrap.is_empty() && !no_auto_bootstrap { log::info!("bootstrap: resolving SRV records"); let srv = dns::lookup_bootstrap(); if srv.is_empty() { log::warn!("bootstrap: no SRV records found"); } for rec in &srv { bootstrap.push(format!("{}:{}", rec.host, rec.port)); } } for peer in &bootstrap { let parts: Vec<&str> = peer.rsplitn(2, ':').collect(); if parts.len() != 2 { eprintln!("warning: bad bootstrap address: {peer}"); continue; } let host = parts[1]; let p: u16 = match parts[0].parse() { Ok(p) => p, Err(_) => { eprintln!("warning: bad bootstrap port: {peer}"); continue; } }; if let Err(e) = node.join(host, p) { log::warn!("bootstrap: failed to join {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 — Arc::into_raw intentionally leaks the // refcount so the pointer remains valid for the process // lifetime. No matching Arc::from_raw needed. 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, &bootstrap); 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); match write_private_file(path, &seed) { Ok(()) => { log::info!("identity: generated new keypair at {}", path.display()); } Err(e) => { log::warn!("identity: failed to save to {}: {e}", path.display()); } } seed.to_vec() } /// Write data to a file with mode 0600 (owner read/write only). fn write_private_file( path: &std::path::Path, data: &[u8], ) -> std::io::Result<()> { use std::io::Write; use std::os::unix::fs::OpenOptionsExt; let mut f = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .mode(0o600) .open(path)?; f.write_all(data)?; f.sync_all() } 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); } }