diff options
| author | murilo ijanc | 2026-03-25 02:07:37 -0300 |
|---|---|---|
| committer | murilo ijanc | 2026-03-25 02:07:37 -0300 |
| commit | 7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch) | |
| tree | bc987ece7eb78bb8375de1b20123ecd0f90472ba /src/bin/tpd.rs | |
| download | tesseras-paste-7aff2e1d279a4e442b32f49ca0a0eca065355787.tar.gz | |
Initial commit: tesseras-paste decentralized pastebin
DHT-backed encrypted pastebin with two binaries (tp/tpd),
XChaCha20-Poly1305 encryption, content-addressed storage,
and Unix socket + HTTP interfaces.
Diffstat (limited to 'src/bin/tpd.rs')
| -rw-r--r-- | src/bin/tpd.rs | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/src/bin/tpd.rs b/src/bin/tpd.rs new file mode 100644 index 0000000..15e7d9b --- /dev/null +++ b/src/bin/tpd.rs @@ -0,0 +1,293 @@ +//! 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<PathBuf> = None; + let mut http_port: Option<u16> = None; + let mut global = false; + let mut bootstrap: Vec<String> = Vec::new(); + + let args: Vec<String> = 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<u8> { + 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); + } +} |