Fredrik Johansson 02eb83b63a docs: use a real generated secret in coturn example
Replace placeholder YOUR_RANDOM_SECRET with an actual openssl rand -hex 32
output, with a note to generate a fresh one. Keeps the example copy-pasteable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:16:48 +02:00
2026-06-21 16:14:07 +02:00
2026-06-25 19:37:48 +02:00

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:~/waste-www/

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)

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:

apt install coturn

/etc/turnserver.conf:

listening-port=3478
fingerprint
use-auth-secret
static-auth-secret=b06c504fd091d23c1ae3699337259d7883a0af1c72fe782044de19f314a267aa
realm=your-domain.com
no-tcp-relay

Generate your own secret (do not reuse the example above):

openssl rand -hex 32

Enable and start:

systemctl enable coturn
systemctl start coturn

Update config.js to tell browsers about the TURN server:

window.WASTE_CONFIG = {
  signalURL: 'wss://your-domain.com/ws',
  turnURL: 'turn:your-domain.com:3478',
  turnSecret: 'b06c504fd091d23c1ae3699337259d7883a0af1c72fe782044de19f314a267aa',
}

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

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.

Description
No description provided
Readme 1.8 MiB
v0.1.6 Latest
2026-07-02 18:35:57 +02:00
Languages
Go 52.1%
TypeScript 30.2%
Shell 12%
CSS 5.2%
HTML 0.3%
Other 0.2%