Compare commits
2 Commits
d09aa2b219
...
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f425e0bb8e | ||
|
|
bf4009558d |
@@ -68,7 +68,8 @@ jobs:
|
||||
- name: Build desktop app
|
||||
run: |
|
||||
cd cmd/app
|
||||
wails build -trimpath -ldflags="-s -w" -tags webkit2_41 -o ../../dist/waste-linux-amd64
|
||||
wails build -trimpath -ldflags="-s -w" -tags webkit2_41
|
||||
cp build/bin/waste ../../dist/waste-linux-amd64
|
||||
|
||||
# ── Publish release (tags only) ─────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -151,9 +151,13 @@ entries from all applicable share roots, with relative `path` fields
|
||||
**Status:** implemented (browser mode + daemon mode)
|
||||
**Affects:** ICE server configuration only, no wire changes
|
||||
|
||||
The browser adapter reads `WASTE_CONFIG.turnURL` and `WASTE_CONFIG.turnSecret`
|
||||
and adds a TURN server to the WebRTC `ICEServers` list. Credentials are
|
||||
generated using HMAC-SHA1 of the username (coturn `use-auth-secret` scheme).
|
||||
The browser adapter reads `WASTE_CONFIG.turnURL` and fetches short-lived
|
||||
credentials from the anchor's `GET /turn-credentials` endpoint (derived from
|
||||
`WASTE_CONFIG.signalURL`, or overridden via `WASTE_CONFIG.turnCredentialsURL`).
|
||||
The anchor computes the credential using HMAC-SHA1 of the username (coturn
|
||||
`use-auth-secret` scheme) — the shared secret itself is never sent to the
|
||||
browser. Daemon mode does the equivalent computation locally, since the
|
||||
daemon already holds `-turn-secret` server-side.
|
||||
|
||||
YAW/2 §0 explicitly declines TURN ("No relay (TURN)"). This extension is
|
||||
opt-in via server configuration and does not affect peers that omit it.
|
||||
|
||||
@@ -41,7 +41,9 @@ Solved by using WebRTC DataChannels via pion. ICE gathers host + server-reflexiv
|
||||
### TURN relay ✅ (shipped)
|
||||
Both browser and daemon modes support TURN relay.
|
||||
|
||||
**Browser mode:** `iceServers()` in `browser.ts` reads `WASTE_CONFIG.turnURL` and `WASTE_CONFIG.turnSecret`, generates time-limited HMAC-SHA1 credentials (compatible with coturn `use-auth-secret`), and adds the TURN server to the ICE candidate list. The peer dot turns yellow for relayed connections (`candidate_type: relay`).
|
||||
**Browser mode:** `iceServers()` in `browser.ts` reads `WASTE_CONFIG.turnURL` and fetches a time-limited credential from the anchor's `GET /turn-credentials` endpoint (HMAC-SHA1, compatible with coturn `use-auth-secret`) rather than holding the shared secret client-side. The peer dot turns yellow for relayed connections (`candidate_type: relay`).
|
||||
|
||||
> **Security fix:** earlier this previously embedded `turnSecret` directly in `WASTE_CONFIG`, which let anyone reading the PWA's JS mint unlimited long-lived TURN credentials. The secret now lives only on the anchor (`-turn-secret` flag); the anchor mints short-lived credentials per-request instead.
|
||||
|
||||
**Daemon mode:** `-turn-url` and `-turn-secret` flags on `cmd/daemon`. `turnICEServers()` in `internal/netmgr/manager.go` generates HMAC-SHA1 credentials and injects them into the ICE server list for every new peer connection.
|
||||
|
||||
|
||||
75
README.md
75
README.md
@@ -21,6 +21,51 @@ waste-go/
|
||||
|
||||
---
|
||||
|
||||
## Prebuilt binaries
|
||||
|
||||
Every tagged release publishes cross-compiled binaries — no Go toolchain required. Grab them from the repo's **Releases** page:
|
||||
|
||||
```
|
||||
https://repo.explewd.com/explewd/waste-go/releases
|
||||
```
|
||||
|
||||
| File | What it is | Run it on |
|
||||
|---|---|---|
|
||||
| `waste-daemon-<os>-<arch>` | The peer process — your identity, mesh connections, file shares | Each friend's own machine |
|
||||
| `waste-anchor-<os>-<arch>` | The signaling relay (no message content ever passes through it) | One server you control (VPS) |
|
||||
|
||||
`<os>` is `linux`, `darwin` (macOS), or `windows`; `<arch>` is `amd64` or `arm64` (Windows builds are amd64 only). Windows binaries have a `.exe` suffix.
|
||||
|
||||
**Linux / macOS:**
|
||||
|
||||
```bash
|
||||
curl -LO https://repo.explewd.com/explewd/waste-go/releases/download/<tag>/waste-daemon-linux-amd64
|
||||
chmod +x waste-daemon-linux-amd64
|
||||
./waste-daemon-linux-amd64 -alias yourname -data-dir ~/.waste --join 'waste:eyJ...'
|
||||
```
|
||||
|
||||
> **macOS Gatekeeper:** unsigned binaries downloaded from a browser get a quarantine flag and macOS will refuse to run them ("cannot be opened because the developer cannot be verified"). Clear it once after downloading: `xattr -d com.apple.quarantine waste-daemon-darwin-arm64` (or right-click → Open the first time, which prompts for an override).
|
||||
|
||||
**Windows:** download the `.exe`, then run it from PowerShell or `cmd.exe`:
|
||||
|
||||
```powershell
|
||||
.\waste-daemon-windows-amd64.exe -alias yourname -data-dir C:\waste --join "waste:eyJ..."
|
||||
```
|
||||
|
||||
SmartScreen may warn about an unrecognized publisher on first run — these binaries aren't code-signed. Click "More info" → "Run anyway".
|
||||
|
||||
Once the daemon is running, drive it with the [TUI](#terminal-ui) (`./cmd/tui` built locally, or the web UI in [daemon mode](#daemon-mode-for-users-running-the-daemon-locally)) — the daemon itself has no UI of its own, it just exposes the local IPC API on port 17337.
|
||||
|
||||
The `waste-anchor` binary is for whoever is hosting a network — see [Hosting on a VPS](#hosting-on-a-vps) below for the full anchor + web UI setup. For a quick local anchor (e.g. testing on a LAN), just run:
|
||||
|
||||
```bash
|
||||
./waste-anchor-linux-amd64 -bind 0.0.0.0:8080
|
||||
```
|
||||
|
||||
> **No desktop app build in the current release.** A native Wails desktop app (`cmd/app/`) exists in the source tree, but the CI step that packages it for releases was broken (wrong output path) until this fix — earlier tagged releases only have daemon/anchor binaries. The next tag will include `waste-linux-amd64`. Until then, build it yourself with `./build-app.sh` (see [Desktop app (Wails)](#desktop-app-wails) below).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -89,7 +134,7 @@ This tells the browser where to connect for signaling. Without it the join form
|
||||
|
||||
### 3. Nginx Proxy Manager setup
|
||||
|
||||
Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS enabled. You need two locations:
|
||||
Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS enabled. You need these locations:
|
||||
|
||||
**Location 1 — WebSocket signaling (`/ws`)**
|
||||
- Location: `/ws`
|
||||
@@ -97,6 +142,12 @@ Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS en
|
||||
- Forward port: `8080`
|
||||
- Enable: WebSockets Support
|
||||
|
||||
**Location 1b — TURN credentials (`/turn-credentials`, only if using TURN — see [step 4](#4-turn-relay-optional-fixes-mobile--cgnat))**
|
||||
- Location: `/turn-credentials`
|
||||
- Forward hostname/IP: `127.0.0.1`
|
||||
- Forward port: `8080`
|
||||
- Plain HTTP, no WebSockets toggle needed
|
||||
|
||||
**Location 2 — Web UI (catch-all)**
|
||||
- Location: `/`
|
||||
- Choose "Serve Static Files" (or point to a local HTTP server serving `/var/www/waste-web`)
|
||||
@@ -117,6 +168,7 @@ Or use `serve-web.sh` which handles PID tracking and restart:
|
||||
The key requirements:
|
||||
|
||||
- `/ws` → anchor process (WebSocket, keep-alive)
|
||||
- `/turn-credentials` → anchor process (plain HTTP; only needed if using TURN)
|
||||
- `/*` → static file server (SPA fallback: return `index.html` for unknown paths)
|
||||
|
||||
### 4. TURN relay (optional, fixes mobile / CGNAT)
|
||||
@@ -155,21 +207,32 @@ systemctl enable coturn
|
||||
systemctl start coturn
|
||||
```
|
||||
|
||||
**Update `config.js`** to tell browsers about the TURN server:
|
||||
**Start the anchor with the same secret**, so it can mint short-lived credentials on your behalf:
|
||||
|
||||
```bash
|
||||
./waste-anchor -bind 0.0.0.0:8080 -turn-secret YOUR_SECRET_HERE
|
||||
```
|
||||
|
||||
This enables `GET /turn-credentials` on the anchor, which returns a fresh `{username, credential}` pair (1-hour TTL) computed from the shared secret — the secret itself never leaves the server.
|
||||
|
||||
**Update `config.js`** to tell browsers about the TURN server (no secret here — only the public relay address):
|
||||
|
||||
```js
|
||||
window.WASTE_CONFIG = {
|
||||
signalURL: 'wss://your-domain.com/ws',
|
||||
turnURL: 'turn:your-domain.com:3478',
|
||||
turnSecret: 'YOUR_SECRET_HERE',
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
> **Security note:** earlier versions of this doc had you put `turnSecret` directly in `config.js`. Don't — anyone reading the PWA's JS bundle could read it and mint unlimited, long-lived TURN credentials, turning your relay into an open proxy for anyone. The browser now calls the anchor's `/turn-credentials` endpoint instead and only ever sees a credential that expires in an hour. If you have an old `config.js` with `turnSecret` set, remove it and rotate the coturn secret (`static-auth-secret` in `turnserver.conf` and the anchor's `-turn-secret` flag) since the old one was exposed.
|
||||
|
||||
> 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).
|
||||
The browser adapter calls `signalURL` with `/ws` swapped for `/turn-credentials` to find the anchor's endpoint by default; set `turnCredentialsURL` explicitly in `WASTE_CONFIG` if the anchor is reachable at a different path. If `turnURL` is set but the credentials endpoint is unreachable, the browser falls back to STUN-only.
|
||||
|
||||
**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.
|
||||
You'll also need nginx to route the new path to the anchor, alongside `/ws` (see [step 3](#3-nginx-proxy-manager-setup)):
|
||||
|
||||
- `/turn-credentials` → anchor process (plain HTTP, no WebSocket upgrade needed)
|
||||
|
||||
**Daemon mode TURN:** pass `-turn-url turn:your-domain.com:3478 -turn-secret YOUR_SECRET_HERE` when starting the daemon. This is unaffected by the above — the daemon computes credentials itself server-side and never exposes the secret, same as the anchor now does for browser mode.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,12 +6,17 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -23,16 +28,40 @@ import (
|
||||
|
||||
func main() {
|
||||
bind := flag.String("bind", "0.0.0.0:17339", "address to listen on")
|
||||
turnSecret := flag.String("turn-secret", "", "coturn use-auth-secret shared secret; enables GET /turn-credentials")
|
||||
flag.Parse()
|
||||
|
||||
a := newAnchor()
|
||||
http.HandleFunc("/ws", a.handleWS)
|
||||
if *turnSecret != "" {
|
||||
http.HandleFunc("/turn-credentials", turnCredentialsHandler(*turnSecret))
|
||||
log.Printf("anchor: /turn-credentials enabled")
|
||||
}
|
||||
log.Printf("anchor: listening on %s", *bind)
|
||||
if err := http.ListenAndServe(*bind, nil); err != nil {
|
||||
log.Fatalf("anchor: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// turnCredentialsHandler mints short-lived coturn use-auth-secret credentials
|
||||
// server-side, so the shared secret never reaches the browser. Mirrors the
|
||||
// scheme in internal/netmgr.Manager.turnICEServers (daemon mode).
|
||||
func turnCredentialsHandler(secret string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
expiry := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
mac.Write([]byte(expiry))
|
||||
credential := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Username string `json:"username"`
|
||||
Credential string `json:"credential"`
|
||||
TTL int `json:"ttl"`
|
||||
}{Username: expiry, Credential: credential, TTL: 3600})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Anchor ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type client struct {
|
||||
|
||||
@@ -16,23 +16,31 @@ const EKEY_PREFIX = 'yaw/2.1 ekey'
|
||||
const FS_TIMEOUT = 2000
|
||||
const STUN = 'stun:stun.l.google.com:19302'
|
||||
|
||||
async function turnCredential(secret: string, username: string): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', new TextEncoder().encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false, ['sign']
|
||||
)
|
||||
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(username))
|
||||
return btoa(String.fromCharCode(...new Uint8Array(sig)))
|
||||
// TURN credentials are short-lived and minted server-side by the anchor's
|
||||
// GET /turn-credentials endpoint (see cmd/anchor). The shared coturn secret
|
||||
// never reaches the browser — only a time-limited username/credential pair.
|
||||
async function fetchTurnCredentials(credentialsURL: string): Promise<{ username: string; credential: string } | null> {
|
||||
try {
|
||||
const res = await fetch(credentialsURL)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
if (!data.username || !data.credential) return null
|
||||
return { username: data.username, credential: data.credential }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function iceServers(): Promise<RTCIceServer[]> {
|
||||
const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnSecret?: string } }).WASTE_CONFIG
|
||||
const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnCredentialsURL?: string; signalURL?: string } }).WASTE_CONFIG
|
||||
const servers: RTCIceServer[] = [{ urls: STUN }]
|
||||
if (cfg?.turnURL && cfg?.turnSecret) {
|
||||
const user = Math.floor(Date.now() / 1000) + 3600 + ':waste'
|
||||
const credential = await turnCredential(cfg.turnSecret, user)
|
||||
servers.push({ urls: cfg.turnURL, username: user, credential })
|
||||
if (cfg?.turnURL) {
|
||||
const credentialsURL = cfg.turnCredentialsURL
|
||||
?? cfg.signalURL?.replace(/^ws/, 'http').replace(/\/ws\/?$/, '/turn-credentials')
|
||||
if (credentialsURL) {
|
||||
const creds = await fetchTurnCredentials(credentialsURL)
|
||||
if (creds) servers.push({ urls: cfg.turnURL, username: creds.username, credential: creds.credential })
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user