@@ -375,25 +375,56 @@ func mustEnv(k string) string {
375375func main () {
376376 flag .Parse ()
377377 cfg := loadConfig ()
378- self := cfg .peerByID (cfg .SelfID )
378+ m := newMesh (cfg )
379+ m .Run (context .Background ())
380+ }
381+
382+ // Mesh is one runtime instance — one mesh-conn process. Owns the
383+ // per-peer session registry, signalling URL, and shutdown plumbing.
384+ // Methods that previously closed over the package-level `sessions`
385+ // map now receive *Mesh; main() builds one and calls Run().
386+ //
387+ // Reason for instance-scoping: the in-process loopback test boots two
388+ // Mesh values side by side; a package-global session map collides
389+ // across instances.
390+ type Mesh struct {
391+ cfg * Config
392+ self * Peer
393+
394+ sessionsMu sync.Mutex
395+ sessions map [string ]* peerSession
396+
397+ // onLinkUp is an optional test hook fired right after each
398+ // per-peer "link up" log line. nil in production.
399+ onLinkUp func (peerID string )
400+ }
401+
402+ func newMesh (cfg * Config ) * Mesh {
403+ return & Mesh {
404+ cfg : cfg ,
405+ self : cfg .peerByID (cfg .SelfID ),
406+ sessions : map [string ]* peerSession {},
407+ }
408+ }
379409
380- others := make ([]Peer , 0 , len (cfg .Peers )- 1 )
381- for _ , p := range cfg .Peers {
382- if p .ID != cfg .SelfID {
410+ func (m * Mesh ) Run (ctx context.Context ) {
411+ others := make ([]Peer , 0 , len (m .cfg .Peers )- 1 )
412+ for _ , p := range m .cfg .Peers {
413+ if p .ID != m .cfg .SelfID {
383414 others = append (others , p )
384415 }
385416 }
386417 log .Printf ("mesh-conn: self=%s vip=%d allowlist=%v other=%d" ,
387- cfg .SelfID , self .Vip , allowlistPorts (cfg .Allowlist ), len (others ))
418+ m . cfg .SelfID , m . self .Vip , allowlistPorts (m . cfg .Allowlist ), len (others ))
388419
389- go pollLoop (cfg )
420+ go m . pollLoop (ctx )
390421
391422 var wg sync.WaitGroup
392423 for _ , p := range others {
393424 wg .Add (1 )
394425 go func (p Peer ) {
395426 defer wg .Done ()
396- runPeerLink (cfg , * self , p )
427+ m . runPeerLink (ctx , p )
397428 }(p )
398429 }
399430 wg .Wait ()
@@ -404,24 +435,43 @@ func main() {
404435// per-peer link: ICE conn + bound UDP socket on peer's identity port
405436// =============================================================================
406437
407- func runPeerLink ( cfg * Config , self , peer Peer ) {
438+ func ( m * Mesh ) runPeerLink ( ctx context. Context , peer Peer ) {
408439 // One peerSession per peer for the lifetime of mesh-conn. The
409440 // session holds authCh + candidateBuffer + abortAttempt — the
410441 // stable wiring pollLoop dispatches through. The ICE agent itself
411442 // is rebuilt per attempt inside dialICE (pion's Restart() doesn't
412443 // support re-Dial; see peerSession docstring).
413- sess := setupPeerSession (peer .ID )
444+ sess := m . setupPeerSession (peer .ID )
414445 for {
415- if err := dialAndPump (cfg , self , peer , sess ); err != nil {
446+ if ctx .Err () != nil {
447+ return
448+ }
449+ if err := m .dialAndPump (ctx , peer , sess ); err != nil {
416450 log .Printf ("[%s] link failed: %v — retrying in 5s" , peer .ID , err )
417- time .Sleep (5 * time .Second )
451+ if ! sleepCtx (ctx , 5 * time .Second ) {
452+ return
453+ }
418454 continue
419455 }
420456 // dialAndPump returns nil only when the conn closed cleanly.
421457 log .Printf ("[%s] link closed — reconnecting" , peer .ID )
422458 }
423459}
424460
461+ // sleepCtx sleeps for d or until ctx is cancelled. Returns true if
462+ // the full sleep completed (so the caller should continue), false if
463+ // the caller should bail.
464+ func sleepCtx (ctx context.Context , d time.Duration ) bool {
465+ t := time .NewTimer (d )
466+ defer t .Stop ()
467+ select {
468+ case <- t .C :
469+ return true
470+ case <- ctx .Done ():
471+ return false
472+ }
473+ }
474+
425475// Stream header layout: 3 bytes per stream open.
426476// byte 0 = tag (streamUDP or streamTCP)
427477// bytes 1-2 = receiver-side port (big-endian uint16) — the port number
@@ -449,12 +499,16 @@ func quicConfig() *quic.Config {
449499 }
450500}
451501
452- func dialAndPump (cfg * Config , self , peer Peer , sess * peerSession ) error {
502+ func (m * Mesh ) dialAndPump (parentCtx context.Context , peer Peer , sess * peerSession ) error {
503+ cfg := m .cfg
504+ self := * m .self
453505 // One attemptCtx for the entire attempt — covers dialICE, QUIC dial,
454506 // and the long-running pump goroutines. pollLoop and the ICE state
455507 // callback both wire abortAttempt to this ctx; cancelling it makes
456508 // the whole pile collapse so runPeerLink's 5s retry can re-enter.
457- attemptCtx , abort := context .WithCancel (context .Background ())
509+ // parentCtx (from Mesh.Run) is the outer shutdown signal; cancelling
510+ // it also collapses the attempt.
511+ attemptCtx , abort := context .WithCancel (parentCtx )
458512 sess .mu .Lock ()
459513 sess .consumedAuth = [2 ]string {}
460514 sess .abortAttempt = abort
@@ -471,7 +525,7 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
471525 }()
472526
473527 // 1. Establish ICE + wrap with a counting conn for byte-level telemetry.
474- rawConn , err := dialICE (cfg , peer .ID , sess , attemptCtx )
528+ rawConn , err := m . dialICE (peer .ID , sess , attemptCtx )
475529 if err != nil {
476530 return fmt .Errorf ("ice: %w" , err )
477531 }
@@ -596,6 +650,9 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
596650
597651 log .Printf ("[%s] link up — peer vip=127.50.0.%d, allowlist=%v (udp=%d, tcp=%d)" ,
598652 peer .ID , peer .Vip , allowlistPorts (cfg .Allowlist ), udpPortCount , len (tcpListeners ))
653+ if m .onLinkUp != nil {
654+ m .onLinkUp (peer .ID )
655+ }
599656
600657 // 5. Start pumps. UDP: one bidirectional pump pair per UDP port.
601658 // TCP: one accept-loop per port (each accepted conn opens an
@@ -959,35 +1016,31 @@ type peerSession struct {
9591016 candidateBuffer []ice.Candidate // remote candidates seen since last peer-auth; replayed into each new agent
9601017}
9611018
962- var (
963- sessionsMu sync.Mutex
964- sessions = map [string ]* peerSession {} // key = remote peer id
965- )
966-
9671019// currentSession returns the persistent session for remoteID, or nil
9681020// if setupPeerSession hasn't installed it yet. Used by pollLoop to
9691021// dispatch incoming messages to the right agent.
970- func currentSession (remoteID string ) * peerSession {
971- sessionsMu .Lock ()
972- defer sessionsMu .Unlock ()
973- return sessions [remoteID ]
1022+ func ( m * Mesh ) currentSession (remoteID string ) * peerSession {
1023+ m . sessionsMu .Lock ()
1024+ defer m . sessionsMu .Unlock ()
1025+ return m . sessions [remoteID ]
9741026}
9751027
9761028// setupPeerSession allocates the long-lived session shell (authCh,
9771029// candidateBuffer, mutex) for one peer. The ICE agent itself is built
9781030// per-attempt inside dialICE — see the peerSession docstring for why
9791031// per-attempt rather than persistent.
980- func setupPeerSession (remoteID string ) * peerSession {
1032+ func ( m * Mesh ) setupPeerSession (remoteID string ) * peerSession {
9811033 sess := & peerSession {
9821034 authCh : make (chan [2 ]string , 1 ),
9831035 }
984- sessionsMu .Lock ()
985- sessions [remoteID ] = sess
986- sessionsMu .Unlock ()
1036+ m . sessionsMu .Lock ()
1037+ m . sessions [remoteID ] = sess
1038+ m . sessionsMu .Unlock ()
9871039 return sess
9881040}
9891041
990- func dialICE (cfg * Config , remoteID string , sess * peerSession , attemptCtx context.Context ) (* ice.Conn , error ) {
1042+ func (m * Mesh ) dialICE (remoteID string , sess * peerSession , attemptCtx context.Context ) (* ice.Conn , error ) {
1043+ cfg := m .cfg
9911044 var urls []* stun.URI
9921045 if cfg .TurnHost != "" {
9931046 user , pass := turnCreds (cfg .TurnSecret , time .Hour )
@@ -1181,36 +1234,52 @@ func publish(cfg *Config, to, typ, data string) {
11811234 resp .Body .Close ()
11821235}
11831236
1184- func pollLoop (cfg * Config ) {
1237+ func (m * Mesh ) pollLoop (ctx context.Context ) {
1238+ cfg := m .cfg
1239+ client := & http.Client {}
11851240 for {
1186- resp , err := http .Get (cfg .SignalingURL + "/poll?peer=" + url .QueryEscape (cfg .SelfID ))
1241+ if ctx .Err () != nil {
1242+ return
1243+ }
1244+ req , _ := http .NewRequestWithContext (ctx ,
1245+ http .MethodGet ,
1246+ cfg .SignalingURL + "/poll?peer=" + url .QueryEscape (cfg .SelfID ),
1247+ nil )
1248+ resp , err := client .Do (req )
11871249 if err != nil {
1250+ if ctx .Err () != nil {
1251+ return
1252+ }
11881253 log .Printf ("poll err: %v" , err )
1189- time .Sleep (time .Second )
1254+ if ! sleepCtx (ctx , time .Second ) {
1255+ return
1256+ }
11901257 continue
11911258 }
11921259 var msgs []Message
11931260 if err := json .NewDecoder (resp .Body ).Decode (& msgs ); err != nil {
11941261 log .Printf ("poll decode: %v" , err )
11951262 resp .Body .Close ()
1196- time .Sleep (time .Second )
1263+ if ! sleepCtx (ctx , time .Second ) {
1264+ return
1265+ }
11971266 continue
11981267 }
11991268 resp .Body .Close ()
1200- for _ , m := range msgs {
1201- sess := currentSession (m .From )
1269+ for _ , msg := range msgs {
1270+ sess := m . currentSession (msg .From )
12021271 if sess == nil {
12031272 // setupPeerSession hasn't installed this remote yet;
12041273 // drop. runPeerLink installs all peer sessions at
12051274 // startup, so this only fires if we receive a message
12061275 // from a non-peer id (mis-configured cluster).
12071276 continue
12081277 }
1209- switch m .Type {
1278+ switch msg .Type {
12101279 case "auth" :
1211- parts := strings .SplitN (m .Data , ":" , 2 )
1280+ parts := strings .SplitN (msg .Data , ":" , 2 )
12121281 if len (parts ) != 2 {
1213- log .Printf ("[%s] bad auth %q" , m .From , m .Data )
1282+ log .Printf ("[%s] bad auth %q" , msg .From , msg .Data )
12141283 continue
12151284 }
12161285 newAuth := [2 ]string {parts [0 ], parts [1 ]}
@@ -1235,7 +1304,7 @@ func pollLoop(cfg *Config) {
12351304 sess .consumedAuth != zero &&
12361305 sess .consumedAuth != newAuth {
12371306 log .Printf ("[%s] fresh auth (ufrag=%s) supersedes consumed (ufrag=%s) — aborting attempt" ,
1238- m .From , newAuth [0 ], sess .consumedAuth [0 ])
1307+ msg .From , newAuth [0 ], sess .consumedAuth [0 ])
12391308 sess .abortAttempt ()
12401309 }
12411310 sess .mu .Unlock ()
@@ -1250,9 +1319,9 @@ func pollLoop(cfg *Config) {
12501319 }
12511320 sess .authCh <- newAuth
12521321 case "candidate" :
1253- cand , err := ice .UnmarshalCandidate (m .Data )
1322+ cand , err := ice .UnmarshalCandidate (msg .Data )
12541323 if err != nil {
1255- log .Printf ("[%s] bad candidate: %v" , m .From , err )
1324+ log .Printf ("[%s] bad candidate: %v" , msg .From , err )
12561325 continue
12571326 }
12581327 // Buffer first, then dispatch to the live agent if any.
@@ -1269,7 +1338,7 @@ func pollLoop(cfg *Config) {
12691338 sess .mu .Unlock ()
12701339 if agent != nil {
12711340 if err := agent .AddRemoteCandidate (cand ); err != nil {
1272- log .Printf ("[%s] AddRemoteCandidate: %v" , m .From , err )
1341+ log .Printf ("[%s] AddRemoteCandidate: %v" , msg .From , err )
12731342 }
12741343 }
12751344 }
0 commit comments