Fredrik Johansson 13fb7ba1fe Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.

internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
  same master + same network always produces the same Ed25519 keypair (stable peer ID)

internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events

internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short

internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing

cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts

test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
2026-06-21 16:14:07 +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 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

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

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/box signaling, Ed25519→X25519 key derivation
  • Proto additionsmid dedup field, signaling types, anchor wire types, hello message
  • 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/box sealing
  • IPC updatesjoin_network/leave_network; session_ready event; DMs via to field
  • 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)
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%