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:
@@ -22,6 +22,8 @@ func main() {
|
|||||||
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
||||||
shareDir := flag.String("share-dir", "", "directory to share with peers on the network")
|
shareDir := flag.String("share-dir", "", "directory to share with peers on the network")
|
||||||
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
||||||
|
turnURL := flag.String("turn-url", "", "TURN server URL, e.g. turn:your-vps:3478")
|
||||||
|
turnSecret := flag.String("turn-secret", "", "shared secret for coturn use-auth-secret HMAC credential")
|
||||||
importBackup := flag.String("import-identity", "", "path to a yaw-key-backup-1 JSON file to import")
|
importBackup := flag.String("import-identity", "", "path to a yaw-key-backup-1 JSON file to import")
|
||||||
importPassword := flag.String("import-passphrase", "", "passphrase for --import-identity")
|
importPassword := flag.String("import-passphrase", "", "passphrase for --import-identity")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -74,6 +76,8 @@ func main() {
|
|||||||
StoreDir: dir,
|
StoreDir: dir,
|
||||||
AnchorURL: *anchorURL,
|
AnchorURL: *anchorURL,
|
||||||
ShareDir: expandHome(*shareDir),
|
ShareDir: expandHome(*shareDir),
|
||||||
|
TurnURL: *turnURL,
|
||||||
|
TurnSecret: *turnSecret,
|
||||||
})
|
})
|
||||||
|
|
||||||
if autoJoinNetwork != "" {
|
if autoJoinNetwork != "" {
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ func runOnce(ctx context.Context, anchorURL, netHash string, id *crypto.Identity
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
if sess == nil {
|
if sess == nil {
|
||||||
// Answerer: we haven't created a session yet, do it now.
|
// Answerer: we haven't created a session yet, do it now.
|
||||||
pc, err := newPC()
|
pc, err := newPC(m.ICEServers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
log.Printf("anchor: new PC for answerer: %v", err)
|
log.Printf("anchor: new PC for answerer: %v", err)
|
||||||
@@ -412,7 +412,7 @@ func dispatchSignaling(
|
|||||||
// startOffer creates a session, sends our ekey, waits up to ekeyTimeout for
|
// startOffer creates a session, sends our ekey, waits up to ekeyTimeout for
|
||||||
// the peer's ekey, then sends the offer (ephemeral or static).
|
// the peer's ekey, then sends the offer (ephemeral or static).
|
||||||
func startOffer(ctx context.Context, peerID proto.PeerID, id *crypto.Identity, m *mesh.Mesh, s *sender) (*peerSession, error) {
|
func startOffer(ctx context.Context, peerID proto.PeerID, id *crypto.Identity, m *mesh.Mesh, s *sender) (*peerSession, error) {
|
||||||
pc, err := newPC()
|
pc, err := newPC(m.ICEServers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -473,7 +473,7 @@ func startOffer(ctx context.Context, peerID proto.PeerID, id *crypto.Identity, m
|
|||||||
|
|
||||||
// answerOffer processes an incoming offer and returns the PeerConnection.
|
// answerOffer processes an incoming offer and returns the PeerConnection.
|
||||||
func answerOffer(ctx context.Context, payload proto.SignalingPayload, fromID proto.PeerID, id *crypto.Identity, m *mesh.Mesh, s *sender, sess *peerSession) (*webrtc.PeerConnection, error) {
|
func answerOffer(ctx context.Context, payload proto.SignalingPayload, fromID proto.PeerID, id *crypto.Identity, m *mesh.Mesh, s *sender, sess *peerSession) (*webrtc.PeerConnection, error) {
|
||||||
pc, err := newPC()
|
pc, err := newPC(m.ICEServers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -618,10 +618,9 @@ func hashNetName(name string) string {
|
|||||||
return hex.EncodeToString(h[:])
|
return hex.EncodeToString(h[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPC() (*webrtc.PeerConnection, error) {
|
func newPC(extra []webrtc.ICEServer) (*webrtc.PeerConnection, error) {
|
||||||
return webrtc.NewPeerConnection(webrtc.Configuration{
|
servers := append([]webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}, extra...)
|
||||||
ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}},
|
return webrtc.NewPeerConnection(webrtc.Configuration{ICEServers: servers})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolPtr(b bool) *bool { return &b }
|
func boolPtr(b bool) *bool { return &b }
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import (
|
|||||||
"github.com/waste-go/internal/store"
|
"github.com/waste-go/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ICEServer mirrors webrtc.ICEServer so callers don't import pion directly.
|
||||||
|
type ICEServer = webrtc.ICEServer
|
||||||
|
|
||||||
// PeerConn is a live connection to one peer.
|
// PeerConn is a live connection to one peer.
|
||||||
type PeerConn struct {
|
type PeerConn struct {
|
||||||
Info proto.PeerInfo
|
Info proto.PeerInfo
|
||||||
@@ -33,7 +36,8 @@ type Mesh struct {
|
|||||||
InviteString string // the invite this peer used to join (sent in hello to other peers)
|
InviteString string // the invite this peer used to join (sent in hello to other peers)
|
||||||
// ScanFiles overrides ScanShareDir when set — allows the manager to inject
|
// ScanFiles overrides ScanShareDir when set — allows the manager to inject
|
||||||
// multi-share scanning without the mesh needing to know about shares.json.
|
// multi-share scanning without the mesh needing to know about shares.json.
|
||||||
ScanFiles func() []proto.FileEntry
|
ScanFiles func() []proto.FileEntry
|
||||||
|
ICEServers []ICEServer // extra ICE servers (e.g. TURN); appended to the default STUN entry
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
peers map[proto.PeerID]*PeerConn
|
peers map[proto.PeerID]*PeerConn
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ package netmgr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/webrtc/v3"
|
||||||
|
|
||||||
"github.com/waste-go/internal/anchor"
|
"github.com/waste-go/internal/anchor"
|
||||||
"github.com/waste-go/internal/crypto"
|
"github.com/waste-go/internal/crypto"
|
||||||
@@ -28,6 +35,8 @@ type Config struct {
|
|||||||
StoreDir string // base directory for per-network SQLite files
|
StoreDir string // base directory for per-network SQLite files
|
||||||
AnchorURL string // WebSocket anchor URL used for all networks
|
AnchorURL string // WebSocket anchor URL used for all networks
|
||||||
ShareDir string // default share directory; overridden per network via Join or SetShareDir
|
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.
|
// 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)
|
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID_full)
|
||||||
capturedNetID := netID
|
capturedNetID := netID
|
||||||
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID) }
|
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.
|
// Forward all mesh events to the Manager's fan-out, tagging with network_id.
|
||||||
meshEvents := m.Subscribe()
|
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)
|
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID)
|
||||||
capturedNetID2 := netID
|
capturedNetID2 := netID
|
||||||
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID2) }
|
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID2) }
|
||||||
|
if ice := mgr.turnICEServers(); ice != nil {
|
||||||
|
m.ICEServers = ice
|
||||||
|
}
|
||||||
|
|
||||||
meshEvents := m.Subscribe()
|
meshEvents := m.Subscribe()
|
||||||
go func() {
|
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 ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func hashNetName(name string) string {
|
func hashNetName(name string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user