From 8294f416b8eea16659ce1edfebf2337b7fa21512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Wed, 5 Aug 2026 15:22:56 +0200 Subject: [PATCH] [client] Deliver relay-borne packets when the relay comes up after ICE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/iface/wgproxy/bind/proxy.go | 10 ++++++++++ client/iface/wgproxy/ebpf/wrapper.go | 10 ++++++++++ client/iface/wgproxy/proxy.go | 7 ++++--- client/iface/wgproxy/udp/proxy.go | 11 +++++++++++ client/internal/peer/conn.go | 17 +++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/client/iface/wgproxy/bind/proxy.go b/client/iface/wgproxy/bind/proxy.go index be690ed4f..a786957cb 100644 --- a/client/iface/wgproxy/bind/proxy.go +++ b/client/iface/wgproxy/bind/proxy.go @@ -114,6 +114,10 @@ func (p *ProxyBind) Pause() { } func (p *ProxyBind) RedirectAs(endpoint *net.UDPAddr) { + if p.remoteConn == nil { + return + } + ep, err := addrToEndpoint(endpoint) if err != nil { log.Errorf("failed to start package redirection: %v", err) @@ -125,6 +129,12 @@ func (p *ProxyBind) RedirectAs(endpoint *net.UDPAddr) { p.wgCurrentUsed = ep + // start here (not only in Work) so the first packet already carries the redirected address + if !p.isStarted { + p.isStarted = true + go p.proxyToLocal(p.ctx) + } + p.pausedCond.Signal() p.pausedCond.L.Unlock() } diff --git a/client/iface/wgproxy/ebpf/wrapper.go b/client/iface/wgproxy/ebpf/wrapper.go index a6156a661..56b92e6b9 100644 --- a/client/iface/wgproxy/ebpf/wrapper.go +++ b/client/iface/wgproxy/ebpf/wrapper.go @@ -188,6 +188,10 @@ func (p *ProxyWrapper) Pause() { } func (p *ProxyWrapper) RedirectAs(endpoint *net.UDPAddr) { + if p.remoteConn == nil { + return + } + if endpoint == nil || endpoint.IP == nil { log.Errorf("failed to start package redirection, endpoint is nil") return @@ -215,6 +219,12 @@ func (p *ProxyWrapper) RedirectAs(endpoint *net.UDPAddr) { p.headerCurrentUsed = header p.rawConn = p.selectRawConn(header) + // start here (not only in Work) so the first packet already carries the rewritten headers + if !p.isStarted { + p.isStarted = true + go p.proxyToLocal(p.ctx) + } + p.pausedCond.Signal() p.pausedCond.L.Unlock() } diff --git a/client/iface/wgproxy/proxy.go b/client/iface/wgproxy/proxy.go index 40346bc15..4fa6ce26f 100644 --- a/client/iface/wgproxy/proxy.go +++ b/client/iface/wgproxy/proxy.go @@ -12,9 +12,10 @@ type Proxy interface { Work() // Work start or resume the proxy Pause() // Pause to forward the packages from remote connection to WireGuard. The opposite way still works. - //RedirectAs resume the forwarding the packages from relayed connection to WireGuard interface if it was paused - //and rewrite the src address to the endpoint address. - //With this logic can avoid the package loss from relayed connections. + //RedirectAs forwards the packages from the relayed connection to the WireGuard interface + //with the src address rewritten to the endpoint address, starting the proxy if needed and + //resuming it if it was paused. Never delivers a packet with the relayed fake address — + //WireGuard would roam to it. RedirectAs(endpoint *net.UDPAddr) CloseConn() error SetDisconnectListener(disconnected func()) diff --git a/client/iface/wgproxy/udp/proxy.go b/client/iface/wgproxy/udp/proxy.go index 783843aba..d0aa6b92e 100644 --- a/client/iface/wgproxy/udp/proxy.go +++ b/client/iface/wgproxy/udp/proxy.go @@ -123,6 +123,10 @@ func (p *WGUDPProxy) Pause() { // RedirectAs start to use the fake sourced raw socket as package sender func (p *WGUDPProxy) RedirectAs(endpoint *net.UDPAddr) { + if p.remoteConn == nil { + return + } + p.pausedCond.L.Lock() defer func() { p.pausedCond.Signal() @@ -145,6 +149,13 @@ func (p *WGUDPProxy) RedirectAs(endpoint *net.UDPAddr) { } p.srcFakerConn = srcFakerConn p.sendPkg = p.srcFakerConn.SendPkg + + // start here (not only in Work) so the first packet already carries the faked source + if !p.isStarted { + p.isStarted = true + go p.proxyToRemote(p.ctx) + go p.proxyToLocal(p.ctx) + } } // InjectPacket writes b to the remote peer over the underlying transport. diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 09a4e8b02..70117c0fb 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -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