aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin/tpd.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 02:07:37 -0300
committermurilo ijanc2026-03-25 02:07:37 -0300
commit7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch)
treebc987ece7eb78bb8375de1b20123ecd0f90472ba /src/bin/tpd.rs
downloadtesseras-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.rs293
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);
+ }
+}