Initial commit: waste-rs mesh daemon, relay, and proto crates
Full Rust implementation with Ed25519 identity, X25519 ECDH handshake, ChaCha20-Poly1305 encrypted peer connections, IPC server, and relay server. Builds on Windows (MSVC), Linux, and macOS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
relay/Cargo.toml
Normal file
19
relay/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "waste-relay"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "waste-relay"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
proto = { path = "../proto" }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
153
relay/src/main.rs
Normal file
153
relay/src/main.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
/// waste-relay: minimal relay server.
|
||||
///
|
||||
/// Run this on your Hetzner VPS. Peers register with their public key + address,
|
||||
/// and can ask the relay to forward encrypted envelopes to other peers.
|
||||
/// The relay NEVER sees plaintext — it only forwards opaque blobs.
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use proto::{GossipEntry, PeerId, RelayClientMessage, RelayServerMessage};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::{mpsc, RwLock},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "waste-relay", about = "WASTE-rs relay/bootstrap server")]
|
||||
struct Args {
|
||||
#[arg(long, default_value = "0.0.0.0:17339")]
|
||||
bind: String,
|
||||
}
|
||||
|
||||
// ── Peer registry ─────────────────────────────────────────────────────────────
|
||||
|
||||
struct RegisteredPeer {
|
||||
gossip: GossipEntry,
|
||||
/// Channel to forward envelopes to this peer's connection handler.
|
||||
tx: mpsc::Sender<RelayServerMessage>,
|
||||
}
|
||||
|
||||
type Registry = Arc<RwLock<HashMap<PeerId, RegisteredPeer>>>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("waste_relay=debug,info")
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
let registry: Registry = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
let listener = TcpListener::bind(&args.bind).await?;
|
||||
info!("waste-relay listening on {}", args.bind);
|
||||
|
||||
loop {
|
||||
let (stream, addr) = listener.accept().await?;
|
||||
info!("Client connected: {addr}");
|
||||
let registry = registry.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_client(stream, registry).await {
|
||||
warn!("Client error: {e:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(stream: TcpStream, registry: Registry) -> Result<()> {
|
||||
let peer_addr = stream.peer_addr()?.to_string();
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut reader = BufReader::new(reader);
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<RelayServerMessage>(64);
|
||||
|
||||
// Writer task
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
if let Ok(line) = serde_json::to_string(&msg) {
|
||||
if writer.write_all((line + "\n").as_bytes()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut my_peer_id: Option<PeerId> = None;
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = reader.read_line(&mut line).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let cmd: RelayClientMessage = match serde_json::from_str(line.trim()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Bad relay message from {peer_addr}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match cmd {
|
||||
RelayClientMessage::Register { peer, signature: _ } => {
|
||||
// TODO: verify signature against peer.public_key
|
||||
info!("Registered peer {} ({}) at {peer_addr}", peer.id, peer.alias);
|
||||
my_peer_id = Some(peer.id.clone());
|
||||
|
||||
let entry = GossipEntry {
|
||||
peer: peer.clone(),
|
||||
addr_hint: peer_addr.clone(),
|
||||
last_seen: Utc::now(),
|
||||
};
|
||||
registry.write().await.insert(
|
||||
peer.id,
|
||||
RegisteredPeer {
|
||||
gossip: entry,
|
||||
tx: tx.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
RelayClientMessage::ListPeers => {
|
||||
let peers: Vec<GossipEntry> = registry
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.map(|p| p.gossip.clone())
|
||||
.collect();
|
||||
let _ = tx.send(RelayServerMessage::PeerList { peers }).await;
|
||||
}
|
||||
|
||||
RelayClientMessage::Forward { to, envelope } => {
|
||||
let reg = registry.read().await;
|
||||
if let Some(dest) = reg.get(&to) {
|
||||
let from = my_peer_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| PeerId("unknown".into()));
|
||||
let _ = dest
|
||||
.tx
|
||||
.send(RelayServerMessage::Forwarded { from, envelope })
|
||||
.await;
|
||||
} else {
|
||||
let _ = tx
|
||||
.send(RelayServerMessage::Error {
|
||||
message: format!("Peer {to} not registered"),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up on disconnect
|
||||
if let Some(pid) = my_peer_id {
|
||||
registry.write().await.remove(&pid);
|
||||
info!("Peer {pid} ({peer_addr}) unregistered");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user