#[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<PathBuf> = None;
let mut http_port: Option<u16> = None;
let mut global = false;
let mut no_auto_bootstrap = false;
let mut verbose = 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,
"-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"));
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);
}
};
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::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 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));
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);
});
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");
}
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);
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()
}
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);
}
}