File chunks travel over a per-transfer "f:<xid>" WebRTC DataChannel —
direct DTLS-encrypted P2P, the anchor never sees file data. Once the
initial handshake is done the anchor can disappear and transfers continue.
Key design choices:
- Receiver sends "ok" on the file DC before sender streams, eliminating
a race where tiny files could be fully sent/closed before the receiver's
OnMessage handler is registered (open-race §6 analogue for data DCs).
- Auto-accept: receiver accepts every incoming offer immediately.
- Download dir: per-network at <data-dir>/downloads-<netid>/.
- Backpressure: bufferedAmountLowThreshold to avoid overwhelming the DC.
- SHA-256 verified on receive; mismatches emit EvtError and discard temp file.
- IPC: send_file {peer_id, path} → offers the named file from share dir.
- EvtFileComplete {transfer_id, path} emitted on success.
IPC command: {"type":"send_file","peer_id":"<hex>","path":"<filename>"}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 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
# 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:
# 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:
$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
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:
{"type":"join_network","network_name":"friends"}
{"type":"leave_network"}
{"type":"send_message","room":"general","body":"hi"}
{"type":"send_message","room":"dm:<peer-hex>","body":"hey","to":"<peer-hex>"}
{"type":"get_state"}
{"type":"generate_invite"}
Events the daemon pushes:
// 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:<base64>"}
// 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.jsonfiles 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):
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:
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:
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:
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:
./test-network.sh
Data lands at /tmp/waste-test (wiped on each run). Inspect after a run:
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
- Crypto layer — hex peer IDs,
nacl/boxsignaling, Ed25519→X25519 key derivation - Proto additions —
middedup field, signaling types, anchor wire types,hellomessage - Anchor server (
cmd/anchor) — WebSocket signaling server replacing TCP relay - WebRTC peer connections — pion/webrtc DataChannels; ICE hole-punching via STUN
- Anchor client (
internal/anchor) — offer/answer/candidate lifecycle,nacl/boxsealing - IPC updates —
join_network/leave_network;session_readyevent; DMs viatofield - Message persistence — SQLite (
internal/store); messages and peer alias cache - TUI — Bubble Tea terminal UI (
cmd/tui); three-pane layout with room switching and DMs - File transfer — chunked binary DataChannel (
f:<xid>) - Native UI — web frontend with native packaging (Tauri-style)