Files
waste-go/README.md
2026-06-23 09:57:44 +02:00

8.3 KiB

waste-go

A modern reimagining of 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

# 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):

./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

# On your local machine
cd web
npm install
npm run build
# Produces web/dist/

# Copy to VPS
rsync -az web/dist/ user@your-vps:/var/www/waste-web/

Create a /var/www/waste-web/config.js on the VPS (not in git — this is host-specific):

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)

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 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, it defaults to daemon mode. A "Switch to browser mode" button is available in the join screen if the daemon is not running.


Local development

Prerequisites

Quick start (three peers in one terminal session)

# 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:

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)

# 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

./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):

echo '{"type":"generate_invite"}' | nc 127.0.0.1 17337
# → {"type":"invite_generated","invite":"waste:eyJ..."}

Bob joins using the invite:

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

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:

{"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:

{"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:<base64>"}
{"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.