2026-06-21 16:14:07 +02:00
# 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/
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
│ ├── daemon/ The peer process — run one on each friend's machine
2026-06-23 09:57:44 +02:00
│ ├── anchor/ WebSocket signaling server — run this on your VPS
2026-06-21 18:30:56 +02:00
│ └── tui/ Bubble Tea terminal UI (connects to a running daemon)
2026-06-21 16:14:07 +02:00
└── internal/
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
├── proto/ All wire types (shared by daemon and anchor)
├── crypto/ Ed25519 identity, nacl/box signaling, ChaCha20-Poly1305
2026-06-21 18:30:56 +02:00
├── mesh/ Connected peer state + DataChannel helpers
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
├── anchor/ Anchor client — WebRTC signaling via the anchor server
└── ipc/ Local JSON API (UI talks to daemon here, port 17337)
2026-06-21 16:14:07 +02:00
```
2026-06-23 09:57:44 +02:00
---
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
## Hosting on a VPS
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
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.
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
### 1. Build and run the anchor
2026-06-21 16:14:07 +02:00
```bash
2026-06-23 09:57:44 +02:00
# On your local machine — cross-compile for Linux
GOOS=linux GOARCH=amd64 go build -o bin/waste-anchor ./cmd/anchor
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
# Copy to VPS
scp bin/waste-anchor user@your -vps:~/waste-anchor
```
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
On the VPS, run the anchor and keep it alive (systemd, screen, whatever you use):
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
```bash
./waste-anchor -bind 127.0.0.1:8080
2026-06-21 16:14:07 +02:00
```
2026-06-26 20:16:18 +02:00
Or use the helper script which handles background execution and logging:
```bash
./setup-anchor.sh --bg # start in background, logs to waste-anchor.log
./setup-anchor.sh --stop # stop it
```
To cross-compile and redeploy the anchor binary from your local machine:
```bash
./deploy-daemon.sh
```
This kills the existing anchor, uploads the new binary, and restarts it.
2026-06-23 09:57:44 +02:00
The anchor listens locally on port 8080 — Nginx Proxy Manager will expose it over TLS.
### 2. Build and upload the web UI
2026-06-21 16:14:07 +02:00
```bash
2026-06-23 09:57:44 +02:00
# On your local machine
cd web
npm install
npm run build
# Produces web/dist/
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
# Copy to VPS
2026-06-25 20:31:54 +02:00
rsync -az web/dist/ user@your -vps:~/waste-www/
2026-06-23 09:57:44 +02:00
```
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
2026-06-26 20:16:18 +02:00
Or use the deploy script (builds + rsyncs in one step):
```bash
./deploy-web.sh
```
2026-06-23 09:57:44 +02:00
Create a `/var/www/waste-web/config.js` on the VPS (not in git — this is host-specific):
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
2026-06-23 09:57:44 +02:00
```js
window.WASTE_CONFIG = {
signalURL: 'wss://your-domain.com/ws',
}
2026-06-21 16:14:07 +02:00
```
2026-06-23 09:57:44 +02:00
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.
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
### 3. Nginx Proxy Manager setup
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
Create one **Proxy Host ** for your domain (e.g. `waste.example.com` ) with TLS enabled. You need two locations:
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
**Location 1 — WebSocket signaling (`/ws` )**
- Location: `/ws`
- Forward hostname/IP: `127.0.0.1`
- Forward port: `8080`
- Enable: WebSockets Support
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
**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
2026-06-21 16:14:07 +02:00
2026-06-26 20:16:18 +02:00
If NPM doesn't support static file serving directly, run a small static server on a spare port and proxy `/` to it:
```bash
nohup npx serve -s ~/waste-www -l 1337 &
```
Or use `serve-web.sh` which handles PID tracking and restart:
```bash
./serve-web.sh # kills existing instance, starts fresh, logs to waste-www.log
```
The key requirements:
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
- `/ws` → anchor process (WebSocket, keep-alive)
- `/*` → static file server (SPA fallback: return `index.html` for unknown paths)
2026-06-21 16:14:07 +02:00
2026-06-25 21:12:03 +02:00
### 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
2026-06-25 21:52:50 +02:00
static-auth-secret=YOUR_SECRET_HERE
2026-06-25 21:12:03 +02:00
realm=your-domain.com
no-tcp-relay
```
2026-06-25 21:16:48 +02:00
Generate your own secret (do not reuse the example above):
```bash
openssl rand -hex 32
```
Enable and start:
2026-06-25 21:12:03 +02:00
```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',
2026-06-25 21:52:50 +02:00
turnSecret: 'YOUR_SECRET_HERE',
2026-06-25 21:12:03 +02:00
}
```
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).
2026-06-26 22:36:13 +02:00
**Daemon mode TURN:** pass `-turn-url turn:your-domain.com:3478 -turn-secret YOUR_SECRET_HERE` when starting the daemon. The same coturn `use-auth-secret` HMAC-SHA1 scheme is used — no extra config required beyond what you set up for browser mode.
2026-06-23 09:57:44 +02:00
---
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
## How it works: daemon vs browser mode
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
There are two ways to use the web UI.
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
### Browser mode (for anyone with just a URL)
2026-06-21 16:14:07 +02:00
2026-06-25 20:31:54 +02:00
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.
2026-06-21 18:57:36 +02:00
2026-06-23 09:57:44 +02:00
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.
2026-06-21 18:57:36 +02:00
2026-06-23 09:57:44 +02:00
**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.
2026-06-22 14:45:15 +02:00
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
**Mobile:** the web UI ships a PWA manifest (`manifest.json` ) so iOS and Android users can pin it to their home screen via "Add to Home Screen" in Safari or Chrome. It runs as a standalone app with no browser chrome. No app store, no native install required.
2026-06-26 20:16:18 +02:00
**Session persistence:** the last-used network name, alias, and anchor URL are saved to `localStorage` . On reload, the browser automatically rejoins the saved network — no login screen. To leave a network or switch identity, click the * * ⏻** button in the top-left of the sidebar. You'll be asked whether to also clear the identity keypair (export a backup first if you want to keep it).
2026-06-23 09:57:44 +02:00
### Daemon mode (for users running the daemon locally)
2026-06-21 18:57:36 +02:00
2026-06-23 09:57:44 +02:00
`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.
2026-06-21 18:57:36 +02:00
2026-06-25 20:31:54 +02:00
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
2026-06-26 21:18:56 +02:00
In the sidebar under **Sharing ** , click * * + Share folder** to pick a local directory. The selected files become available for peers to browse and download. A checkbox lets you control whether subfolders are included (default: yes).
Multiple folders can be shared — each appears in the list with a ↺ re-pick button (to restore after a page reload) and a ✕ remove button. The share list is saved in `localStorage` so it survives reloads; you'll be prompted to re-pick any folder whose files were lost on reload.
2026-06-25 20:31:54 +02:00
2026-06-26 20:18:34 +02:00
> Your browser will show a warning along the lines of "really upload X files?" when you pick a folder. This is a built-in browser security prompt — **no files are uploaded anywhere.** Files are transferred directly to a peer only when they explicitly request one via the file browser.
2026-06-25 20:31:54 +02:00
### Browsing a peer's files
2026-06-26 21:18:56 +02:00
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. Folders appear first and are clickable — navigate into them with a breadcrumb trail at the top. Sort by name or size; search to filter across all files in the current directory. Click * * ↓** next to any file to download it directly from that peer.
2026-06-25 20:31:54 +02:00
### 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.
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
### Transfer resume (daemon mode)
When a file transfer is interrupted mid-stream — peer disconnects, network drops — the partially-received data is kept on disk. A `.meta` sidecar is written alongside each in-progress `.tmp` file recording the file name, size, and SHA-256.
When the peer reconnects and re-offers the same file, the daemon finds the matching partial by SHA-256, sends back a `file-accept` with a non-zero `resume_offset` , and the sender seeks to that byte position before streaming. The receiver appends to the existing file and verifies the full SHA-256 at the end.
Corrupted transfers (all bytes received but hash mismatch) are removed. Interrupted transfers are kept indefinitely until either successfully completed or the `.tmp` /`.meta` files are manually deleted.
2026-06-26 21:18:56 +02:00
> In daemon mode, use `add_share` / `remove_share` via IPC to manage share roots. Share configuration is stored in `shares.json` next to `identity.json` in the data directory and survives restarts. `networks: ["*"]` makes a share visible on all networks; omit to scope it to specific network IDs. The legacy `set_share_dir` single-dir command still works alongside it.
2026-06-23 09:57:44 +02:00
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
### Daemon download directory
Received files are saved into a per-network subdirectory so networks stay isolated. By default this is inside the data directory:
```
~/.waste/downloads-<network-id>/
```
Pass `-download-dir` at startup to use a different base:
```bash
go run ./cmd/daemon -download-dir ~/Downloads ...
```
Files then land in `~/Downloads/downloads-<network-id>/` . The path can also be changed at runtime via the `set_download_dir` IPC command (takes effect for the next incoming transfer on that network). The current path is reported in every `state_snapshot` event under `networks[].download_dir` .
---
## Desktop app (Wails)
`cmd/app/` is a [Wails v2 ](https://wails.io ) shell that packages the React frontend and the daemon logic into a single native binary for Windows, macOS, and Linux. No Rust, no Electron — just Go + the OS webview.
The daemon runs embedded in the same process. The webview connects to `ws://127.0.0.1:17338` exactly as it does in browser-daemon mode, so the frontend code is unchanged. Identity and stores use the OS config directory (`~/Library/Application Support/waste` on macOS, `~/.config/waste` on Linux, `%APPDATA%\waste` on Windows).
### Prerequisites
- Go 1.24+
- Node.js 20+
- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
- Linux platform deps: `sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev`
### Build
```bash
# Production build → dist/waste-linux-amd64 (or waste.exe / waste.app)
./build-app.sh
# Dev mode (hot-reload; start Vite in another terminal: cd web && npm run dev)
./build-app.sh dev
# Build without system tray (no libayatana-appindicator3-dev needed)
CGO_ENABLED=1 WAILS_TAGS=notray ./build-app.sh
```
`build-app.sh` builds the React frontend, copies `web/dist/` into `cmd/app/frontend/dist/` for embedding, then runs `wails build` . The result is a single self-contained binary.
### What the desktop app provides
- **Single binary** — daemon logic is embedded, no subprocess to manage
- **System tray** (Linux/Windows) — hide to tray, reopen from tray menu, Quit
- **macOS** — hides to Dock on window close; full menu-bar tray is future work
- **OS notifications** — native popup on new message or completed file transfer (uses the browser `Notification` API via Wails events; the app will request permission on first notification)
- **Data directory** — OS-appropriate config dir (`~/.config/waste` , `~/Library/Application Support/waste` , `%APPDATA%\waste` )
### CI / automated builds
`.gitea/workflows/build.yml` runs on every `v*` tag push:
- **Server binaries** (daemon + anchor): cross-compiled for Linux amd64/arm64, macOS amd64/arm64, Windows amd64 — no CGo, no special runner needed
- **Desktop app**: Linux amd64 on the default runner (installs GTK + WebKit + appindicator deps automatically)
- **Releases**: all artifacts attached to the Gitea release
To add macOS or Windows desktop builds, add self-hosted Gitea runners on those platforms and mirror the `desktop-linux` job.
2026-06-23 09:57:44 +02:00
---
## 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
2026-06-21 16:14:07 +02:00
```
2026-06-23 09:57:44 +02:00
Join both to a network:
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
```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
```
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
### Web UI (daemon mode)
2026-06-21 16:14:07 +02:00
2026-06-23 09:57:44 +02:00
```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
2026-06-22 14:45:15 +02:00
2026-06-23 09:57:44 +02:00
```bash
./test-network.sh
```
2026-06-22 14:45:15 +02:00
2026-06-23 09:57:44 +02:00
Boots anchor + three peers, joins them to a network, sends group messages and DMs, verifies SQLite persistence.
2026-06-22 14:45:15 +02:00
2026-06-23 09:57:44 +02:00
---
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
2026-06-21 18:57:36 +02:00
## Onboarding a new peer
2026-06-23 09:57:44 +02:00
Alice generates an invite (TUI: `Ctrl+I` , or via IPC):
2026-06-21 18:57:36 +02:00
```bash
echo '{"type":"generate_invite"}' | nc 127.0.0.1 17337
2026-06-23 09:57:44 +02:00
# → {"type":"invite_generated","invite":"waste:eyJ..."}
2026-06-21 18:57:36 +02:00
```
2026-06-23 09:57:44 +02:00
Bob joins using the invite:
2026-06-21 18:57:36 +02:00
```bash
2026-06-23 09:57:44 +02:00
go run ./cmd/daemon -alias bob -data-dir ~/.waste-bob --join 'waste:eyJ...'
go run ./cmd/tui --join 'waste:eyJ...'
2026-06-21 18:57:36 +02:00
```
2026-06-23 09:57:44 +02:00
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.
2026-06-21 18:57:36 +02:00
2026-06-23 09:57:44 +02:00
Invite links also work in the web UI. Share `https://your-domain.com/?invite=waste:eyJ...` and the join form is pre-filled.
2026-06-26 22:05:56 +02:00
### Signed invites and invite-only networks (waste-go extension)
Invites generated by `generate_invite` are **cryptographically signed ** by the generating peer. The `waste:` payload carries an `inviter` field (Ed25519 public key) and a `sig` field (signature over anchor + network + inviter). When Bob joins, the invite is forwarded in the `hello` message so Alice can verify it.
To enable invite-only enforcement on a network, pass `require_invite: true` in the `join_network` command. Peers presenting no invite, an unsigned invite, or an invite signed by an unknown peer are rejected.
### "Come hang" hang links
The 🔗 button in the web UI copies a **hash-based hang link ** :
```
https://your-domain.com/#waste:eyJ ...
```
The fragment (`#...` ) is never sent to the server, so the network name stays server-opaque. Anyone who opens the link gets the join form pre-filled — but they still need a proper signed invite to be accepted on networks with `require_invite` enabled. Suitable for public announcements of open or semi-open networks.
See [EXTENSIONS.md ](EXTENSIONS.md ) for the full protocol addendum.
2026-06-23 09:57:44 +02:00
---
2026-06-21 18:30:56 +02:00
2026-06-23 09:57:44 +02:00
## Terminal UI
2026-06-21 18:30:56 +02:00
```bash
go run ./cmd/tui -network friends
```
| Flag | Default | Description |
|---|---|---|
2026-06-21 18:57:36 +02:00
| `-network` | * (required unless -join) * | Network name to join on startup |
2026-06-23 09:57:44 +02:00
| `-join` | — | `waste:` invite string |
2026-06-21 18:30:56 +02:00
| `-ipc` | `17337` | Daemon IPC port |
2026-06-23 09:57:44 +02:00
**Key bindings:** `Tab` /`Shift+Tab` — switch rooms · `PgUp` /`PgDn` — scroll · `Enter` — send · `Ctrl+I` — generate invite · `Esc` — close overlay · `Ctrl+C` — quit
2026-06-21 18:30:56 +02:00
2026-06-26 22:36:13 +02:00
**Slash commands:** `/room <name>` — create a new room (persisted in SQLite, restored on reconnect). Rooms with unread messages show a `*` prefix in the sidebar.
2026-06-23 09:57:44 +02:00
---
2026-06-21 18:30:56 +02:00
2026-06-23 09:57:44 +02:00
## IPC protocol
2026-06-21 18:30:56 +02:00
2026-06-23 09:57:44 +02:00
Newline-delimited JSON on TCP port 17337 (or WebSocket on 17338).
2026-06-21 18:16:45 +02:00
2026-06-23 09:57:44 +02:00
**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"}
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
{"type":"add_share","path":"/home/alice/Music"} // global share
{"type":"add_share","path":"/home/alice/Docs","network_ids":["abc123"]} // network-scoped
2026-06-26 21:18:56 +02:00
{"type":"remove_share","path":"/home/alice/Music"}
{"type":"list_shares"}
2026-06-26 22:36:13 +02:00
{"type":"create_room","room":"dev"}
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
{"type":"set_download_dir","path":"/home/alice/Downloads"} // change download base for current network
{"type":"set_download_dir","network_id":"<16-hex>","path":"/home/alice/Downloads/friends"}
2026-06-23 09:57:44 +02:00
{"type":"export_identity","passphrase":"..."}
{"type":"import_identity","passphrase":"...","backup":"..."}
```
2026-06-21 18:16:45 +02:00
2026-06-23 09:57:44 +02:00
**Events:**
```jsonc
feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:38:28 +02:00
{"type":"state_snapshot","local_peer":{...},"connected_peers":[...],"master_alias":"alice","master_id":"<64-hex>","networks":[{"network_id":"...","network_name":"friends","share_dir":"...","download_dir":"..."}]}
2026-06-23 09:57:44 +02:00
{"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"}
2026-06-26 22:36:13 +02:00
{"type":"room_created","network_id":"...","room":"dev"}
2026-06-23 09:57:44 +02:00
{"type":"identity_exported","backup":"..."}
{"type":"error","error_message":"..."}
2026-06-21 18:16:45 +02:00
```
2026-06-23 09:57:44 +02:00
---
2026-06-21 18:16:45 +02:00
2026-06-23 09:57:44 +02:00
## Crypto
2026-06-21 18:16:45 +02:00
2026-06-23 09:57:44 +02:00
| 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 |
2026-06-22 14:45:15 +02:00
2026-06-23 09:57:44 +02:00
### Forward-secret signaling (YAW/2.1)
2026-06-22 14:45:15 +02:00
2026-06-23 09:57:44 +02:00
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.