# 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 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) ``` --- ## Hosting on a VPS You need two things on the server: the **anchor** (signaling process) and the **web UI** (static files). Both are served through the same domain via Nginx Proxy Manager. ### 1. Build and run the anchor ```bash # On your local machine — cross-compile for Linux GOOS=linux GOARCH=amd64 go build -o bin/waste-anchor ./cmd/anchor # Copy to VPS scp bin/waste-anchor user@your-vps:~/waste-anchor ``` On the VPS, run the anchor and keep it alive (systemd, screen, whatever you use): ```bash ./waste-anchor -bind 127.0.0.1:8080 ``` The anchor listens locally on port 8080 — Nginx Proxy Manager will expose it over TLS. ### 2. Build and upload the web UI ```bash # On your local machine cd web npm install npm run build # Produces web/dist/ # Copy to VPS rsync -az web/dist/ user@your-vps:~/waste-www/ ``` Create a `/var/www/waste-web/config.js` on the VPS (not in git — this is host-specific): ```js window.WASTE_CONFIG = { signalURL: 'wss://your-domain.com/ws', } ``` This tells the browser where to connect for signaling. Without it the join form shows a blank signal server field and the user must fill it in manually. ### 3. Nginx Proxy Manager setup Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS enabled. You need two locations: **Location 1 — WebSocket signaling (`/ws`)** - Location: `/ws` - Forward hostname/IP: `127.0.0.1` - Forward port: `8080` - Enable: WebSockets Support **Location 2 — Web UI (catch-all)** - Location: `/` - Choose "Serve Static Files" (or point to a local HTTP server serving `/var/www/waste-web`) - Enable the SPA fallback so unknown paths return `index.html` — this is required for invite links to work If NPM doesn't support static file serving directly, run a small static server on a spare port (e.g. `npx serve -s /var/www/waste-web -l 3000`) and proxy `/` to `127.0.0.1:3000`. The key requirements: - `/ws` → anchor process (WebSocket, keep-alive) - `/*` → static file server (SPA fallback: return `index.html` for unknown paths) ### 4. TURN relay (optional, fixes mobile / CGNAT) WebRTC hole-punching fails when both peers are behind symmetric NAT — common on mobile data and some ISPs. A TURN relay fixes this. It runs directly on the VPS, not through Nginx Proxy Manager. **Firewall:** open UDP 3478 (and optionally TCP 3478) on the Hetzner firewall. No NPM config needed — coturn speaks its own protocol. **Install coturn:** ```bash apt install coturn ``` **`/etc/turnserver.conf`:** ``` listening-port=3478 fingerprint use-auth-secret static-auth-secret=YOUR_SECRET_HERE realm=your-domain.com no-tcp-relay ``` Generate your own secret (do not reuse the example above): ```bash openssl rand -hex 32 ``` Enable and start: ```bash systemctl enable coturn systemctl start coturn ``` **Update `config.js`** to tell browsers about the TURN server: ```js window.WASTE_CONFIG = { signalURL: 'wss://your-domain.com/ws', turnURL: 'turn:your-domain.com:3478', turnSecret: 'YOUR_SECRET_HERE', } ``` The `use-auth-secret` mode generates short-lived TURN credentials from the shared secret — no user database required. The relay only sees opaque DTLS-encrypted blobs. > The browser adapter reads `turnURL` and `turnSecret` from `WASTE_CONFIG` and adds the TURN server to the WebRTC `ICEServers` list automatically. If not configured, STUN-only is used (works for most desktop/home NAT situations). --- ## How it works: daemon vs browser mode There are two ways to use the web UI. ### Browser mode (for anyone with just a URL) When the web UI is served from a non-localhost origin — or locally with `config.js` setting `signalURL` — it runs entirely in the browser. No daemon, no install. Crypto (Ed25519/X25519) runs via libsodium compiled to WebAssembly. The identity seed is stored in `localStorage` and persists across sessions. A user visits your domain, enters their name and a network name, and joins. Invite links (`waste:…` or `?n=name&a=wss://…`) pre-fill the join form. **Identity note:** browser mode uses the master identity directly (same keypair on all networks, compatible with yaw2). The daemon derives a separate keypair per network via HKDF. A browser user and a daemon user on the same network will see each other and can chat — they just appear as different peers even if they're the same person. ### Daemon mode (for users running the daemon locally) `launch-web.sh` starts the Go daemon and the Vite dev server. The web UI connects to the local daemon over WebSocket IPC (`ws://127.0.0.1:17338`). The daemon handles all crypto and connects to the anchor. When the web UI is loaded from `localhost` without a `config.js`, it defaults to daemon mode. A "Switch to browser mode" button is available in the join screen if the daemon is not running. --- ## File sharing (browser mode) File transfer runs peer-to-peer over WebRTC DataChannels — files never touch the anchor or any server. ### Sharing a folder In the sidebar under **Sharing**, click **+ Share folder** to pick a local directory. The selected files become available for peers to browse and download. You can re-click to change the folder at any time; the new set is immediately reflected for any peer that browses again. ### Browsing a peer's files Hover over a peer in the sidebar to reveal action buttons. Click **⊞** to request their file list. A panel opens on the right showing their shared files with names and sizes. Click **↓** next to any file to download it directly from that peer. ### Sending a file directly Hover over a peer and click **📎** to open a file picker. The selected file is pushed immediately to that peer — they don't need to be sharing anything. The recipient's browser auto-downloads the file on arrival. > File transfer is currently browser mode only. In daemon mode, use `set_share_dir` via IPC and `get_file_list`/`send_file` commands (see IPC protocol below). --- ## Local development ### Prerequisites - Go 1.24+ — https://go.dev/dl/ - Node.js 20+ ### Quick start (three peers in one terminal session) ```bash # Terminal 1 — local anchor 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 go run ./cmd/daemon -alias bob -data-dir /tmp/waste-bob -ipc-port 17341 -anchor ws://127.0.0.1:17339/ws ``` Join both to a network: ```bash echo '{"type":"join_network","network_name":"friends"}' | nc 127.0.0.1 17337 echo '{"type":"join_network","network_name":"friends"}' | nc 127.0.0.1 17341 ``` ### Web UI (daemon mode) ```bash # Requires a running daemon on port 17337 ./launch-web.sh # Or with a custom alias and network: ALIAS=alice NETWORK=friends ./launch-web.sh ``` ### Automated test ```bash ./test-network.sh ``` Boots anchor + three peers, joins them to a network, sends group messages and DMs, verifies SQLite persistence. --- ## Onboarding a new peer Alice generates an invite (TUI: `Ctrl+I`, or via IPC): ```bash echo '{"type":"generate_invite"}' | nc 127.0.0.1 17337 # → {"type":"invite_generated","invite":"waste:eyJ..."} ``` Bob joins using the invite: ```bash go run ./cmd/daemon -alias bob -data-dir ~/.waste-bob --join 'waste:eyJ...' go run ./cmd/tui --join 'waste:eyJ...' ``` The invite encodes the anchor URL and network name. Sharing it only lets the recipient join the same network — the anchor never sees plaintext messages. Invite links also work in the web UI. Share `https://your-domain.com/?invite=waste:eyJ...` and the join form is pre-filled. --- ## Terminal UI ```bash go run ./cmd/tui -network friends ``` | Flag | Default | Description | |---|---|---| | `-network` | *(required unless -join)* | Network name to join on startup | | `-join` | — | `waste:` invite string | | `-ipc` | `17337` | Daemon IPC port | **Key bindings:** `Tab`/`Shift+Tab` — switch rooms · `PgUp`/`PgDn` — scroll · `Enter` — send · `Ctrl+I` — generate invite · `Esc` — close overlay · `Ctrl+C` — quit --- ## IPC protocol Newline-delimited JSON on TCP port 17337 (or WebSocket on 17338). **Commands:** ```jsonc {"type":"join_network","network_name":"friends"} {"type":"send_message","room":"general","body":"hi"} {"type":"send_message","to":"<64-hex>","body":"hey"} // DM {"type":"generate_invite"} {"type":"get_state"} {"type":"get_file_list"} {"type":"get_file_list","peer_id":"<64-hex>"} {"type":"send_file","peer_id":"<64-hex>","path":"notes.txt"} {"type":"export_identity","passphrase":"..."} {"type":"import_identity","passphrase":"...","backup":"..."} ``` **Events:** ```jsonc {"type":"state_snapshot","local_peer":{...},"connected_peers":[...],"master_alias":"alice","master_id":"<64-hex>"} {"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>"} {"type":"message_received","message":{"mid":"<32-hex>","from":"<64-hex>","room":"general","text":"hi","ts":1700000000000}} {"type":"network_joined","network_id":"...","network_name":"friends"} {"type":"invite_generated","invite":"waste:"} {"type":"incoming_file","peer_id":"<64-hex>","offer":{"xid":"...","name":"notes.txt","size":1024,"sha256":"..."}} {"type":"file_complete","transfer_id":"...","path":"/downloads/notes.txt"} {"type":"identity_exported","backup":"..."} {"type":"error","error_message":"..."} ``` --- ## Crypto | Purpose | Algorithm | |---|---| | Identity | Ed25519 | | Signaling (2.0) | XSalsa20-Poly1305, X25519 keys derived from Ed25519 | | Signaling (2.1) | XSalsa20-Poly1305, ephemeral X25519 per session (forward secrecy) | | Transport | WebRTC DataChannels (DTLS+SCTP via pion/webrtc) | | File integrity | SHA-256 | ### Forward-secret signaling (YAW/2.1) Each peer generates a fresh X25519 keypair per session and broadcasts the public half in a signed `ekey` message before sending an offer. The `esk` is zeroed when the session ends. A 2.0 peer ignores `ekey` and the offerer falls back to static-key sealing after 2 s — so 2.1↔2.0 sessions work, just without forward secrecy.