[client] stop offering to every peer when the relay transport drops

The relay transport is shared: one connection per relay server carries the
streams of every peer using it. When it drops, each of those peers gets a
Disconnected verdict from evalConnStatus even when ICE is still carrying its
traffic, because peerUsesRelay comes from HasRelayAddress(), which only reports
that management offered relay servers, not that we are connected to one. The
guard answers Disconnected with the aggressive retry, so every peer starts
sending offers over signal for a transport that no offer can restore: the relay
client's own guard is what reconnects it.

Feed relayManager.Ready() into the status inputs and return PartiallyConnected
when ICE is up and the missing side is the shared transport. That is the
existing "one path works, the other does not" branch, which retries three times
and then hourly instead of walking the exponential ladder forever.

Peers are not left waiting for the hourly tick: when the transport comes back,
Manager.onServerConnected notifies srWatcher, the guard resets the ticker to
800ms and iceState.reset() clears the hourly mode.

The verdict is unchanged when the transport is up but this peer is unreachable
over relay - it may have moved to another server, and only an offer carries its
new relay address - and in force-relay mode, where relay is the only transport.
This commit is contained in:
riccardom
2026-08-06 17:14:24 +02:00
parent 6526fc2bec
commit 08b8c19dcc
4 changed files with 50 additions and 18 deletions

View File

@@ -823,13 +823,14 @@ func (conn *Conn) isConnectedOnAllWay() (status guard.ConnStatus) {
}
return evalConnStatus(connStatusInputs{
forceRelay: IsForceRelayed(),
peerUsesRelay: conn.workerRelay.IsRelayConnectionSupportedWithPeer(),
relayConnected: conn.statusRelay.Get() == worker.StatusConnected,
remoteSupportsICE: conn.handshaker.RemoteICESupported(),
iceWorkerCreated: iceWorkerCreated,
iceStatusConnecting: conn.statusICE.Get() != worker.StatusDisconnected,
iceInProgress: iceInProgress,
forceRelay: IsForceRelayed(),
peerUsesRelay: conn.workerRelay.IsRelayConnectionSupportedWithPeer(),
relayConnected: conn.statusRelay.Get() == worker.StatusConnected,
relayTransportConnected: conn.workerRelay.IsTransportConnected(),
remoteSupportsICE: conn.handshaker.RemoteICESupported(),
iceWorkerCreated: iceWorkerCreated,
iceStatusConnecting: conn.statusICE.Get() != worker.StatusDisconnected,
iceInProgress: iceInProgress,
})
}
@@ -1054,6 +1055,9 @@ func evalConnStatus(in connStatusInputs) guard.ConnStatus {
case relayUsedAndUp:
// Relay is up but ICE is down — partially connected.
return guard.ConnStatusPartiallyConnected
case iceUp && !in.relayTransportConnected:
// ICE is up and the shared relay transport is down — offers cannot restore it.
return guard.ConnStatusPartiallyConnected
default:
return guard.ConnStatusDisconnected
}

View File

@@ -17,13 +17,14 @@ const (
// tri-state connection classification. Extracted so the decision logic can be unit-tested
// without constructing full Worker/Handshaker objects.
type connStatusInputs struct {
forceRelay bool // NB_FORCE_RELAY or JS/WASM
peerUsesRelay bool // remote peer advertises relay support AND local has relay
relayConnected bool // statusRelay reports Connected (independent of whether peer uses relay)
remoteSupportsICE bool // remote peer sent ICE credentials
iceWorkerCreated bool // local WorkerICE exists (false in force-relay mode)
iceStatusConnecting bool // statusICE is anything other than Disconnected
iceInProgress bool // a negotiation is currently in flight
forceRelay bool // NB_FORCE_RELAY or JS/WASM
peerUsesRelay bool // remote peer advertises relay support AND local has relay
relayConnected bool // statusRelay reports Connected (independent of whether peer uses relay)
relayTransportConnected bool // the relay transport shared by all peers on that server is up
remoteSupportsICE bool // remote peer sent ICE credentials
iceWorkerCreated bool // local WorkerICE exists (false in force-relay mode)
iceStatusConnecting bool // statusICE is anything other than Disconnected
iceInProgress bool // a negotiation is currently in flight
}
// ConnStatus describe the status of a peer's connection

View File

@@ -166,15 +166,38 @@ func TestEvalConnStatus_FullyAvailable(t *testing.T) {
want: guard.ConnStatusDisconnected,
},
{
name: "ICE up, peer uses relay but relay down -> partial (relay required, ICE ignored)",
name: "ICE up, relay down for this peer but the shared transport is up -> disconnected",
mutator: func(in *connStatusInputs) {
in.peerUsesRelay = true
in.relayConnected = false
in.relayTransportConnected = true
in.iceStatusConnecting = true
},
// relayOK = false (peer uses relay but it's down), iceUp = true
// first switch arm fails (relayOK false), relayUsedAndUp = false (relay down),
// falls into default: Disconnected.
// The transport is fine, so the peer itself is unreachable over relay: it may have
// moved to another server, and only an offer carries its new relay address.
want: guard.ConnStatusDisconnected,
},
{
name: "ICE up, the shared relay transport is down -> partial",
mutator: func(in *connStatusInputs) {
in.peerUsesRelay = true
in.relayConnected = false
in.relayTransportConnected = false
in.iceStatusConnecting = true
},
// ICE carries the traffic and the relay transport is restored by the relay client's
// own guard, not by offers, so this must not trigger the aggressive retry.
want: guard.ConnStatusPartiallyConnected,
},
{
name: "ICE down and the shared relay transport is down -> disconnected",
mutator: func(in *connStatusInputs) {
in.peerUsesRelay = true
in.relayConnected = false
in.relayTransportConnected = false
in.iceStatusConnecting = false
in.iceInProgress = false
},
want: guard.ConnStatusDisconnected,
},
{

View File

@@ -107,6 +107,10 @@ func (w *WorkerRelay) RelayIsSupportedLocally() bool {
return w.relayManager.HasRelayAddress()
}
func (w *WorkerRelay) IsTransportConnected() bool {
return w.relayManager.Ready()
}
func (w *WorkerRelay) CloseConn() {
w.relayLock.Lock()
defer w.relayLock.Unlock()