Add TURN relay support for daemon mode

-turn-url and -turn-secret flags on the daemon; credentials generated
using coturn use-auth-secret HMAC-SHA1 scheme (same as browser mode).
ICEServers field on mesh.Mesh threads extra ICE servers through to
every PeerConnection created by the anchor client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-26 22:16:18 +02:00
parent 31e13fd509
commit 1308082c7b
4 changed files with 50 additions and 8 deletions

View File

@@ -6,13 +6,20 @@ package netmgr
import (
"context"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/pion/webrtc/v3"
"github.com/waste-go/internal/anchor"
"github.com/waste-go/internal/crypto"
@@ -28,6 +35,8 @@ type Config struct {
StoreDir string // base directory for per-network SQLite files
AnchorURL string // WebSocket anchor URL used for all networks
ShareDir string // default share directory; overridden per network via Join or SetShareDir
TurnURL string // optional TURN server URL, e.g. "turn:your-vps:3478"
TurnSecret string // shared secret for coturn use-auth-secret HMAC credential
}
// Network is a single joined network context.
@@ -113,6 +122,9 @@ func (mgr *Manager) Join(name, shareDir string) (string, error) {
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID_full)
capturedNetID := netID
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID) }
if ice := mgr.turnICEServers(); ice != nil {
m.ICEServers = ice
}
// Forward all mesh events to the Manager's fan-out, tagging with network_id.
meshEvents := m.Subscribe()
@@ -198,6 +210,9 @@ func (mgr *Manager) JoinByHash(netHash64, shareDir string) (string, error) {
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID)
capturedNetID2 := netID
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID2) }
if ice := mgr.turnICEServers(); ice != nil {
m.ICEServers = ice
}
meshEvents := m.Subscribe()
go func() {
@@ -411,6 +426,26 @@ func (mgr *Manager) emit(msg proto.IpcMessage) {
}
}
// turnICEServers returns TURN ICE servers if TurnURL and TurnSecret are set,
// using coturn's use-auth-secret HMAC-SHA1 time-limited credential scheme.
// Returns nil if TURN is not configured.
func (mgr *Manager) turnICEServers() []webrtc.ICEServer {
if mgr.cfg.TurnURL == "" || mgr.cfg.TurnSecret == "" {
return nil
}
// Username = Unix timestamp 1 hour from now.
expiry := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)
mac := hmac.New(sha1.New, []byte(mgr.cfg.TurnSecret))
mac.Write([]byte(expiry))
credential := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return []webrtc.ICEServer{{
URLs: []string{mgr.cfg.TurnURL},
Username: expiry,
Credential: credential,
CredentialType: webrtc.ICECredentialTypePassword,
}}
}
// ── helpers ───────────────────────────────────────────────────────────────────
func hashNetName(name string) string {