package socket
import (
"context"
"crypto/rand"
"errors"
mrand "math/rand/v2"
"sync"
"sync/atomic"
"time"
"github.com/tmc/go-iroh/internal/relayclient"
"github.com/tmc/go-iroh/internal/relayproto"
"github.com/tmc/go-iroh/key"
"github.com/tmc/go-iroh/netaddr"
"github.com/tmc/go-iroh/relay"
"github.com/tmc/go-iroh/watch"
)
const (
relayInactiveCleanupTime = 60 * time .Second
pingInterval = 15 * time .Second
sendDatagramBatchSize = 20
connectTimeout = 10 * time .Second
undeliverableDatagramTimeout = 3 * time .Second
)
const (
backoffMinDelay = 10 * time .Millisecond
backoffMaxDelay = 16 * time .Second
)
const (
relayPingTimeoutMin = 500 * time .Millisecond
relayPingTimeoutMax = 5 * time .Second
)
const (
relayRecvQueueDepth = 512
relaySendChannelDepth = 256
perRelaySendDepth = 64
)
type RelayConnState int
const (
RelayConnecting RelayConnState = iota
RelayConnected
RelayDisconnected
)
func (s RelayConnState ) String () string {
switch s {
case RelayConnecting :
return "connecting"
case RelayConnected :
return "connected"
case RelayDisconnected :
return "disconnected"
default :
return "unknown"
}
}
type RelayStatus struct {
URL netaddr .RelayURL
State RelayConnState
LastError error
}
func (s RelayStatus ) IsConnected () bool { return s .State == RelayConnected }
type RelaySendItem struct {
RemoteEndpoint key .EndpointID
URL netaddr .RelayURL
Datagrams relayproto .Datagrams
}
type RelayRecvDatagram struct {
URL netaddr .RelayURL
Src key .EndpointID
Datagrams relayproto .Datagrams
}
type relayDialer func (ctx context .Context , url netaddr .RelayURL , opts relayclient .Options ) (relayClient , error )
type relayClient interface {
Send(ctx context .Context , msg relayproto .ClientToRelayMsg ) error
Recv(ctx context .Context ) (relayproto .RelayToClientMsg , error )
Close() error
}
func defaultRelayDialer(ctx context .Context , url netaddr .RelayURL , opts relayclient .Options ) (relayClient , error ) {
return relayclient .Connect (ctx , url , opts )
}
type RelayActorConfig struct {
SecretKey key .SecretKey
Map *relay .Map
dialer relayDialer
}
type RelayActor struct {
cfg RelayActorConfig
dialer relayDialer
recvCh chan RelayRecvDatagram
sendCh chan RelaySendItem
homeURL *watch .Value [*RelayStatus ]
metrics atomic .Pointer [Metrics ]
mu sync .Mutex
active map [string ]*activeRelay
home netaddr .RelayURL
closed bool
wg sync .WaitGroup
}
func (a *RelayActor ) setMetrics (m *Metrics ) {
if a != nil {
a .metrics .Store (m )
}
}
func NewRelayActor (cfg RelayActorConfig ) *RelayActor {
dialer := cfg .dialer
if dialer == nil {
dialer = defaultRelayDialer
}
if cfg .Map == nil {
cfg .Map = relay .NewMap ()
} else {
cfg .Map = cfg .Map .Clone ()
}
return &RelayActor {
cfg : cfg ,
dialer : dialer ,
recvCh : make (chan RelayRecvDatagram , relayRecvQueueDepth ),
sendCh : make (chan RelaySendItem , relaySendChannelDepth ),
homeURL : watch .NewValueFunc [*RelayStatus ](nil , statusEqual ),
active : make (map [string ]*activeRelay ),
}
}
func (a *RelayActor ) Recv () <-chan RelayRecvDatagram { return a .recvCh }
func (a *RelayActor ) HomeRelayStatus () watch .Observer [*RelayStatus ] {
return a .homeURL .Watch ()
}
func (a *RelayActor ) InsertRelay (url netaddr .RelayURL , cfg relay .Config ) (relay .Config , bool ) {
a .mu .Lock ()
defer a .mu .Unlock ()
if a .closed {
return relay .Config {}, false
}
cfg .URL = url
prev , ok := a .cfg .Map .Insert (cfg )
if a .home .IsZero () {
a .home = url
a .homeURL .Set (&RelayStatus {URL : url , State : RelayConnecting })
a .ensureActiveLocked (url , true )
}
return prev , ok
}
func (a *RelayActor ) RemoveRelay (url netaddr .RelayURL ) (relay .Config , bool ) {
a .mu .Lock ()
defer a .mu .Unlock ()
if a .closed {
return relay .Config {}, false
}
prev , ok := a .cfg .Map .Remove (url )
if !ok {
return relay .Config {}, false
}
if ar := a .active [url .String ()]; ar != nil {
ar .stop ()
}
if !a .home .Equal (url ) {
return prev , true
}
a .home = netaddr .RelayURL {}
a .homeURL .Set (nil )
if urls := a .cfg .Map .URLs (); len (urls ) > 0 {
next := urls [0 ]
a .home = next
a .homeURL .Set (&RelayStatus {URL : next , State : RelayConnecting })
for key , ar := range a .active {
ar .setHome (key == next .String ())
}
a .ensureActiveLocked (next , true )
}
return prev , true
}
func (a *RelayActor ) Send (item RelaySendItem ) bool {
a .mu .Lock ()
closed := a .closed
a .mu .Unlock ()
if closed {
return false
}
select {
case a .sendCh <- item :
return true
default :
return false
}
}
func (a *RelayActor ) SetHomeRelay (url netaddr .RelayURL ) {
a .mu .Lock ()
defer a .mu .Unlock ()
if a .closed {
return
}
if url .IsZero () {
a .home = netaddr .RelayURL {}
a .homeURL .Set (nil )
for _ , ar := range a .active {
ar .setHome (false )
}
return
}
if a .home .Equal (url ) {
return
}
a .home = url
a .homeURL .Set (&RelayStatus {URL : url , State : RelayConnecting })
for key , ar := range a .active {
ar .setHome (key == url .String ())
}
a .ensureActiveLocked (url , true )
}
func (a *RelayActor ) Run (ctx context .Context ) {
defer close (a .recvCh )
for {
select {
case <- ctx .Done ():
a .shutdown ()
return
case item := <- a .sendCh :
a .dispatch (ctx , item )
}
}
}
func (a *RelayActor ) dispatch (ctx context .Context , item RelaySendItem ) {
a .mu .Lock ()
if a .closed {
a .mu .Unlock ()
return
}
ar , ok := a .active [item .URL .String ()]
if !ok {
if alt := a .routeForEndpointLocked (item .RemoteEndpoint ); alt != nil {
ar = alt
} else {
ar = a .ensureActiveLocked (item .URL , a .home .Equal (item .URL ))
}
}
a .mu .Unlock ()
ar .enqueue (item )
}
func (a *RelayActor ) routeForEndpointLocked (eid key .EndpointID ) *activeRelay {
for _ , ar := range a .active {
if ar .hasRoute (eid ) {
return ar
}
}
return nil
}
func (a *RelayActor ) ensureActiveLocked (url netaddr .RelayURL , home bool ) *activeRelay {
key := url .String ()
if ar , ok := a .active [key ]; ok {
if home {
ar .setHome (true )
}
return ar
}
ar := newActiveRelay (a , url , home )
a .active [key ] = ar
a .wg .Add (1 )
go func () {
defer a .wg .Done ()
ar .run ()
a .mu .Lock ()
if a .active [key ] == ar {
delete (a .active , key )
}
a .mu .Unlock ()
}()
return ar
}
func (a *RelayActor ) shutdown () {
a .mu .Lock ()
if a .closed {
a .mu .Unlock ()
return
}
a .closed = true
actors := make ([]*activeRelay , 0 , len (a .active ))
for _ , ar := range a .active {
actors = append (actors , ar )
}
a .mu .Unlock ()
for _ , ar := range actors {
ar .stop ()
}
a .wg .Wait ()
}
func (a *RelayActor ) publishStatus (url netaddr .RelayURL , state RelayConnState , lastErr error ) {
a .mu .Lock ()
isHome := a .home .Equal (url )
a .mu .Unlock ()
if !isHome {
return
}
a .homeURL .Set (&RelayStatus {URL : url , State : state , LastError : lastErr })
}
func (a *RelayActor ) authTokenFor (url netaddr .RelayURL ) string {
a .mu .Lock ()
defer a .mu .Unlock ()
if a .cfg .Map == nil {
return ""
}
if c , ok := a .cfg .Map .Get (url ); ok {
return c .AuthToken
}
return ""
}
func statusEqual(a , b *RelayStatus ) bool {
if a == nil || b == nil {
return a == b
}
return a .URL .Equal (b .URL ) && a .State == b .State && a .LastError == b .LastError
}
type activeRelay struct {
parent *RelayActor
url netaddr .RelayURL
sendCh chan RelaySendItem
stopCh chan struct {}
stopOnce sync .Once
mu sync .Mutex
isHome bool
routes map [key .EndpointID ]struct {}
lastSrc key .EndpointID
haveSrc bool
}
func newActiveRelay(parent *RelayActor , url netaddr .RelayURL , home bool ) *activeRelay {
return &activeRelay {
parent : parent ,
url : url ,
sendCh : make (chan RelaySendItem , perRelaySendDepth ),
stopCh : make (chan struct {}),
isHome : home ,
routes : make (map [key .EndpointID ]struct {}),
}
}
func (r *activeRelay ) enqueue (item RelaySendItem ) {
select {
case r .sendCh <- item :
default :
}
}
func (r *activeRelay ) stop () {
r .stopOnce .Do (func () { close (r .stopCh ) })
}
func (r *activeRelay ) setHome (home bool ) {
r .mu .Lock ()
r .isHome = home
r .mu .Unlock ()
}
func (r *activeRelay ) home () bool {
r .mu .Lock ()
defer r .mu .Unlock ()
return r .isHome
}
func (r *activeRelay ) hasRoute (eid key .EndpointID ) bool {
r .mu .Lock ()
defer r .mu .Unlock ()
_ , ok := r .routes [eid ]
return ok
}
func (r *activeRelay ) noteRoute (eid key .EndpointID ) {
r .mu .Lock ()
defer r .mu .Unlock ()
if r .haveSrc && r .lastSrc .Equal (eid ) {
return
}
r .lastSrc = eid
r .haveSrc = true
r .routes [eid ] = struct {}{}
}
func (r *activeRelay ) dropRoute (eid key .EndpointID ) {
r .mu .Lock ()
delete (r .routes , eid )
r .mu .Unlock ()
}
func (r *activeRelay ) run () {
delay := backoffMinDelay
for {
select {
case <- r .stopCh :
return
default :
}
established , err := r .runOnce ()
if err == nil {
return
}
r .parent .publishStatus (r .url , RelayDisconnected , err )
if established {
delay = backoffMinDelay
continue
}
select {
case <- r .stopCh :
return
case <- time .After (jitter (delay )):
}
delay *= 2
if delay > backoffMaxDelay {
delay = backoffMaxDelay
}
}
}
func (r *activeRelay ) runOnce () (established bool , err error ) {
r .parent .publishStatus (r .url , RelayConnecting , nil )
client , ok , derr := r .dial ()
if !ok {
return false , nil
}
if derr != nil {
return false , derr
}
defer client .Close ()
r .parent .publishStatus (r .url , RelayConnected , nil )
return r .runConnected (client )
}
func (r *activeRelay ) dial () (relayClient , bool , error ) {
ctx , cancel := context .WithTimeout (context .Background (), connectTimeout )
defer cancel ()
type result struct {
c relayClient
err error
}
done := make (chan result , 1 )
go func () {
c , err := r .parent .dialer (ctx , r .url , relayclient .Options {
SecretKey : r .parent .cfg .SecretKey ,
AuthToken : r .parent .authTokenFor (r .url ),
})
done <- result {c : c , err : err }
}()
flush := time .NewTicker (undeliverableDatagramTimeout )
defer flush .Stop ()
idle := time .NewTimer (relayInactiveCleanupTime )
defer idle .Stop ()
for {
select {
case <- r .stopCh :
cancel ()
<-done
return nil , false , nil
case res := <- done :
if res .err != nil {
return nil , true , res .err
}
return res .c , true , nil
case <- flush .C :
drain (r .sendCh )
case <- idle .C :
if !r .home () {
cancel ()
<-done
return nil , false , nil
}
idle .Reset (relayInactiveCleanupTime )
}
}
}
type connectedState struct {
established bool
pendingPong [8 ]byte
havePong bool
pingSent [8 ]byte
pingSentAt time .Time
awaitingPng bool
lastRTT time .Duration
}
func (r *activeRelay ) runConnected (client relayClient ) (bool , error ) {
recvCtx , recvCancel := context .WithCancel (context .Background ())
defer recvCancel ()
frames := make (chan relayproto .RelayToClientMsg , 16 )
recvErr := make (chan error , 1 )
go func () {
for {
msg , err := client .Recv (recvCtx )
if err != nil {
recvErr <- err
return
}
select {
case frames <- msg :
case <- recvCtx .Done ():
return
}
}
}()
st := &connectedState {}
pingTick := time .NewTicker (pingInterval )
defer pingTick .Stop ()
pingTimeout := time .NewTimer (pingInterval )
pingTimeout .Stop ()
defer pingTimeout .Stop ()
idle := time .NewTimer (relayInactiveCleanupTime )
defer idle .Stop ()
if err := r .sendPing (client , st , pingTimeout ); err != nil {
return st .established , err
}
sendBuf := make ([]RelaySendItem , 0 , sendDatagramBatchSize )
for {
if st .havePong {
data := st .pendingPong
st .havePong = false
if err := r .send (client , relayproto .ClientToRelayMsg {
Type : relayproto .FramePong , Ping : data ,
}); err != nil {
return st .established , err
}
}
select {
case <- r .stopCh :
return st .established , nil
case err := <- recvErr :
if errors .Is (err , context .Canceled ) {
return st .established , nil
}
return st .established , err
case msg := <- frames :
wasAwaiting := st .awaitingPng
r .handleFrame (msg , st )
pingTick .Reset (pingInterval )
if wasAwaiting && !st .awaitingPng {
stopTimer (pingTimeout )
}
case <- pingTick .C :
if err := r .sendPing (client , st , pingTimeout ); err != nil {
return st .established , err
}
case <- pingTimeout .C :
return st .established , errPingTimeout
case <- idle .C :
if !r .home () {
return st .established , nil
}
idle .Reset (relayInactiveCleanupTime )
case first := <- r .sendCh :
idle .Reset (relayInactiveCleanupTime )
sendBuf = append (sendBuf [:0 ], first )
for len (sendBuf ) < sendDatagramBatchSize {
select {
case it := <- r .sendCh :
sendBuf = append (sendBuf , it )
default :
goto flush
}
}
flush :
if err := r .sendDatagrams (client , sendBuf ); err != nil {
return st .established , err
}
}
}
}
func (r *activeRelay ) send (client relayClient , msg relayproto .ClientToRelayMsg ) error {
ctx , cancel := context .WithTimeout (context .Background (), pingInterval )
defer cancel ()
return client .Send (ctx , msg )
}
func (r *activeRelay ) sendPing (client relayClient , st *connectedState , timeout *time .Timer ) error {
var data [8 ]byte
rand .Read (data [:])
st .pingSent = data
st .pingSentAt = time .Now ()
st .awaitingPng = true
stopTimer (timeout )
timeout .Reset (pingTimeoutDuration (st ))
return r .send (client , relayproto .ClientToRelayMsg {Type : relayproto .FramePing , Ping : data })
}
func stopTimer(t *time .Timer ) {
if !t .Stop () {
select {
case <- t .C :
default :
}
}
}
func pingTimeoutDuration(st *connectedState ) time .Duration {
if st != nil && st .lastRTT > 0 {
return min (max (st .lastRTT *3 , relayPingTimeoutMin ), relayPingTimeoutMax )
}
return relayPingTimeoutMax
}
func (r *activeRelay ) sendDatagrams (client relayClient , items []RelaySendItem ) error {
for _ , it := range items {
err := r .send (client , relayproto .ClientToRelayMsg {
Type : relayproto .FrameClientToRelayDatagram ,
DstEndpointID : it .RemoteEndpoint ,
Datagrams : it .Datagrams ,
})
if err != nil {
return err
}
}
return nil
}
func (r *activeRelay ) handleFrame (msg relayproto .RelayToClientMsg , st *connectedState ) {
r .handleFrameAt (msg , st , time .Now ())
}
func (r *activeRelay ) handleFrameAt (msg relayproto .RelayToClientMsg , st *connectedState , now time .Time ) {
switch msg .Type {
case relayproto .FrameRelayToClientDatagram , relayproto .FrameRelayToClientDatagramBat :
r .noteRoute (msg .RemoteEndpointID )
select {
case r .parent .recvCh <- RelayRecvDatagram {
URL : r .url , Src : msg .RemoteEndpointID , Datagrams : msg .Datagrams ,
}:
default :
}
case relayproto .FrameEndpointGone :
r .dropRoute (msg .EndpointGone )
case relayproto .FramePing :
st .pendingPong = msg .Ping
st .havePong = true
case relayproto .FramePong :
if st .awaitingPng && st .pingSent == msg .Ping {
st .awaitingPng = false
if !st .pingSentAt .IsZero () {
st .lastRTT = now .Sub (st .pingSentAt )
}
}
st .established = true
case relayproto .FrameStatus :
if msg .Status == relayproto .StatusRateLimited {
if mm := r .parent .metrics .Load (); mm != nil {
mm .relayRateLimited .Add (1 )
}
}
case relayproto .FrameHealth , relayproto .FrameRestarting :
}
}
var errPingTimeout = errors .New ("relay: ping timeout" )
func jitter(d time .Duration ) time .Duration {
return time .Duration (float64 (d ) * (0.5 + mrand .Float64 ()))
}
func drain[T any ](ch chan T ) {
for {
select {
case <- ch :
default :
return
}
}
}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .