# waste-go A modern reimagining of [WASTE](https://en.wikipedia.org/wiki/WASTE) — decentralized, friend-to-friend encrypted mesh networking with chat and file sharing. Written in Go. ## Project layout ``` waste-go/ ├── cmd/ │ ├── daemon/ The peer process — run one on each friend's machine │ ├── anchor/ WebSocket signaling server — run this on your Hetzner VPS │ └── tui/ Bubble Tea terminal UI (connects to a running daemon) └── internal/ ├── proto/ All wire types (shared by daemon and anchor) ├── crypto/ Ed25519 identity, nacl/box signaling, ChaCha20-Poly1305 ├── mesh/ Connected peer state + DataChannel helpers ├── anchor/ Anchor client — WebRTC signaling via the anchor server └── ipc/ Local JSON API (UI talks to daemon here, port 17337) ``` ## Prerequisites - Go 1.24+ → https://go.dev/dl/ - VS Code with the Go extension (`golang.go`) On first open VS Code will prompt you to install `gopls`, `dlv`, and `goimports` — accept all of them. ## Getting started ```bash # Fetch dependencies go mod tidy # Build everything (confirms it compiles) go build ./... # Terminal 1 — anchor (required for peers to find each other) go run ./cmd/anchor -bind 127.0.0.1:17339 # Terminal 2 — peer A go run ./cmd/daemon -alias alice -data-dir /tmp/waste-alice -ipc-port 17337 -anchor ws://127.0.0.1:17339/ws # Terminal 3 — peer B (or use --join with an invite from peer A) go run ./cmd/daemon -alias bob -data-dir /tmp/waste-bob -ipc-port 17341 -anchor ws://127.0.0.1:17339/ws ``` Both peers join the same named network via IPC: ```bash # Join peer A to a network called "friends" echo '{"type":"join_network","network_name":"friends"}' | nc 127.0.0.1 17337 # Join peer B to the same network echo '{"type":"join_network","network_name":"friends"}' | nc 127.0.0.1 17341 # Subscribe to peer A's events (in a separate terminal) nc 127.0.0.1 17337 & # Send a message from B echo '{"type":"send_message","room":"general","body":"hello from bob"}' | nc 127.0.0.1 17341 ``` **On Windows** — use PowerShell's built-in TCP client instead of `nc`: ```powershell $c = [System.Net.Sockets.TcpClient]::new('127.0.0.1', 17341) $w = [System.IO.StreamWriter]::new($c.GetStream()); $w.AutoFlush = $true $w.WriteLine('{"type":"join_network","network_name":"friends"}') $w.WriteLine('{"type":"send_message","room":"general","body":"hello from bob"}') # In a separate terminal — subscribe to peer A's events $r = [System.Net.Sockets.TcpClient]::new('127.0.0.1', 17337) $reader = [System.IO.StreamReader]::new($r.GetStream()) while ($true) { $reader.ReadLine() } ``` ## Deploying the anchor on your Hetzner VPS ```bash GOOS=linux GOARCH=amd64 go build -o bin/waste-anchor ./cmd/anchor scp bin/waste-anchor user@your-vps:~/ # On the VPS (also run coturn in STUN-only mode on port 3478) ./waste-anchor -bind 0.0.0.0:17339 ``` Then start daemons with `-anchor ws://your-vps-ip:17339/ws` and they'll connect via WebRTC with ICE (STUN-assisted hole punching) through the anchor for signaling. ## IPC protocol (plain JSON over TCP) Everything is newline-delimited JSON. You can test with `nc 127.0.0.1 17337`. **Commands you send:** ```jsonc {"type":"join_network","network_name":"friends"} {"type":"leave_network"} {"type":"send_message","room":"general","body":"hi"} {"type":"send_message","room":"dm:","body":"hey","to":""} {"type":"get_state"} {"type":"generate_invite"} ``` **Events the daemon pushes:** ```jsonc // Sent immediately on connect and in response to get_state {"type":"state_snapshot","local_peer":{"id":"<64-hex>","alias":"alice","public_key":"<64-hex>","created_at":"..."},"connected_peers":[...],"rooms":["general"]} // Peer lifecycle {"type":"peer_connected","peer":{"id":"<64-hex>","alias":"bob",...}} {"type":"session_ready","peer_id":"<64-hex>","nick":"bob"} {"type":"peer_disconnected","peer_id":"<64-hex>"} // Incoming message — mid is a 32-hex dedup token, to is set for DMs {"type":"message_received","message":{"mid":"<32-hex>","from":"<64-hex>","to":"<64-hex>","room":"general","body":"hi","sent_at":"..."}} // Invite generation response {"type":"invite_generated","invite":"waste:"} // Error {"type":"error","error_message":"..."} ``` ## Crypto choices | Purpose | Algorithm | Notes | |---|---|---| | Identity | Ed25519 | Fast, small keys, standard | | Peer ID | Hex-encoded Ed25519 pubkey | 64 lowercase hex chars (YAW/2 §2) | | Signaling encryption | XSalsa20-Poly1305 (`nacl/box`) | X25519 keys derived from Ed25519 identity (YAW/2 §3) | | Transport | WebRTC DataChannels (DTLS+SCTP) | pion/webrtc — ICE, hole punching included | | Hashing | SHA-256 | File integrity, network name hashing | Replaces WASTE's original Blowfish/PCBC (broken cipher mode) + RSA. > Peer IDs are 64-char lowercase hex (Ed25519 public key). Existing `identity.json` files > on disk are unaffected — only the over-the-wire representation changed from base64url. ## Onboarding a new peer Alice is already on the network and wants to add Bob. **Alice generates an invite** (from the TUI with `Ctrl+I`, or via IPC directly): ```bash echo '{"type":"generate_invite"}' | nc 127.0.0.1 17337 # → {"type":"invite_generated","invite":"waste:eyJhbmNob3IiOiJ3czovL..."} ``` **Bob starts his daemon using the invite** — the `--join` flag sets the anchor URL and auto-joins the network: ```bash go run ./cmd/daemon -alias bob -data-dir ~/.waste-bob --join 'waste:eyJhbmNob3IiOiJ3czovL...' ``` **Bob opens the TUI** — `--join` also accepts the invite to skip the `-network` flag: ```bash go run ./cmd/tui --join 'waste:eyJhbmNob3IiOiJ3czovL...' ``` The invite encodes the anchor URL and network name as a `waste:` URI. Share it over Signal, email, or any side channel — the anchor never sees plaintext messages, so the invite leaking to a third party only lets them join the same network (which is by design: same network = mutual trust). ## Terminal UI Start the daemon first (see Getting started above), then: ```bash go run ./cmd/tui -network friends ``` Options: | Flag | Default | Description | |---|---|---| | `-network` | *(required unless -join)* | Network name to join on startup | | `-join` | — | `waste:` invite string — sets the network name automatically | | `-ipc` | `17337` | Daemon IPC port | **Layout:** ``` ╭─ Rooms ──────╮╭─── #general ────────────────╮╭─ Peers ──────╮ │ ▶ #general ││ 15:04 alice hey everyone ││ ◉ alice (me) │ │ @ bob ││ 15:04 bob hi alice! ││ ● bob │ │ ││ 15:05 charlie the mesh works ││ ● charlie │ ╰──────────────╯╰─────────────────────────────╯╰──────────────╯ ╭─────────────────────────────────────────────────────────────╮ │ Type a message… │ ╰─────────────────────────────────────────────────────────────╯ ``` **Key bindings:** `Tab` / `Shift+Tab` — switch rooms · `PgUp` / `PgDn` — scroll · `Enter` — send · `Ctrl+I` — generate invite · `Esc` — close invite overlay · `Ctrl+C` — quit ## Testing A self-contained test script boots anchor + three peers, joins them to a named network, exchanges group messages and DMs, and verifies SQLite persistence: ```bash ./test-network.sh ``` Data lands at `/tmp/waste-test` (wiped on each run). Inspect after a run: ```bash sqlite3 /tmp/waste-test/alice/messages.db .headers on SELECT room, from_peer, body, sent_at FROM messages; SELECT peer_id, alias, last_seen FROM peers; ``` ## Roadmap - [x] **Crypto layer** — hex peer IDs, `nacl/box` signaling, Ed25519→X25519 key derivation - [x] **Proto additions** — `mid` dedup field, signaling types, anchor wire types, `hello` message - [x] **Anchor server** (`cmd/anchor`) — WebSocket signaling server replacing TCP relay - [x] **WebRTC peer connections** — pion/webrtc DataChannels; ICE hole-punching via STUN - [x] **Anchor client** (`internal/anchor`) — offer/answer/candidate lifecycle, `nacl/box` sealing - [x] **IPC updates** — `join_network`/`leave_network`; `session_ready` event; DMs via `to` field - [x] **Message persistence** — SQLite (`internal/store`); messages and peer alias cache - [x] **TUI** — Bubble Tea terminal UI (`cmd/tui`); three-pane layout with room switching and DMs - [ ] **File transfer** — chunked binary DataChannel (`f:`) - [ ] **Native UI** — web frontend with native packaging (Tauri-style)