[client] Deliver relay-borne packets when the relay comes up after ICE

When the relay connection becomes ready while ICE is already the active
path, the proxy was only stored for later: neither Work() nor RedirectAs()
ran, so nothing consumed the relayed connection. The remote peer's paths
can come up in the opposite order, in which case it is already sending
WireGuard traffic over the relay — those packets were never delivered. A
responder that had configured a nil endpoint then waited for a handshake
that could not arrive, until the 5s delayed-update fallback fired; on an
Android e2e run this stretched connected-to-first-ping to ~12s.

Redirect the standby proxy to the active ICE endpoint. RedirectAs now also
starts the proxy when it has not been started yet, with the attribution
set before the reader runs: starting through Work() first would hand
packets to WireGuard labelled with the relayed fake address, and WireGuard
would roam to it and send its replies there. The udp proxy starts its
writer worker together with the reader — Work() skips the isStarted block
on a later switch to relay, so the writer must already exist by then.

RedirectAs also gained the same nil-remoteConn guard Work() has: now that
it can start workers, calling it before AddTurnConn must stay a no-op.
This commit is contained in:
Zoltán Papp
2026-08-05 15:22:56 +02:00
parent 906fdf4bb5
commit 8294f416b8
5 changed files with 52 additions and 3 deletions
+17
View File
@@ -134,6 +134,9 @@ type Conn struct {
wgProxyRelay wgproxy.Proxy
handshaker *Handshaker
// endpoint of the active ICE path, for redirecting a later relay conn. Guarded by mu.
iceWgEndpoint *net.UDPAddr
guard *guard.Guard
wg sync.WaitGroup
@@ -470,6 +473,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
conn.handleConfigurationFailure(err, wgProxy)
return
}
conn.iceWgEndpoint = ep
wgConfigWorkaround()
if conn.wgProxyRelay != nil {
@@ -495,6 +499,8 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
conn.Log.Tracef("ICE connection state changed to disconnected")
conn.iceWgEndpoint = nil
if conn.wgProxyICE != nil {
if err := conn.wgProxyICE.CloseConn(); err != nil {
conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err)
@@ -577,6 +583,17 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
if conn.isICEActive() {
conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String())
conn.setRelayedProxy(wgProxy)
// The remote may already be sending WireGuard traffic over the relay;
// keep consuming it, attributed to the ICE endpoint. Work() would
// attribute it to the relayed fake address and WireGuard would roam there.
if ep := conn.iceWgEndpoint; ep != nil {
conn.Log.Debugf("redirect packets from relayed conn to WireGuard as %s", ep)
wgProxy.RedirectAs(ep)
} else {
conn.Log.Warnf("ICE is active but its wg endpoint is unknown, leaving relayed proxy parked")
}
conn.statusRelay.SetConnected()
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, time.Now())
return