From c19906e85e2d90fe7c6cda8fa8c20d4bfb2aacff Mon Sep 17 00:00:00 2001 From: VaisVaisov Date: Sun, 21 Jun 2026 15:14:33 +0500 Subject: [PATCH] fix(jitsi): keep xmpp connection alive and accept server broadcasts xmppKeepalive was using fire-and-forget Send(); on a half-open TCP connection pings were sent but the missing reply went undetected, leaving the session stuck. Switch to SendIQWait so a timeout triggers a reconnect. In peer-routing mode the server may send the initial welcome frame as a broadcast (receiverEpoch=0) before it has learned the client's localEpoch. The previous requireTargetedPeer guard dropped every broadcast unconditionally. Tighten the guard: only drop frames whose senderEpoch is unknown or belongs to a different peer; frames from the already-latched peer are always accepted. --- internal/engine/jitsi/jitsi.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/internal/engine/jitsi/jitsi.go b/internal/engine/jitsi/jitsi.go index a140b15..dde4b00 100644 --- a/internal/engine/jitsi/jitsi.go +++ b/internal/engine/jitsi/jitsi.go @@ -64,6 +64,10 @@ const ( // periodic XMPP ping IQ resets that idle timer end-to-end and works for // the WebSocket transport too. xmppKeepaliveInterval = 25 * time.Second + // xmppKeepaliveTimeout is the maximum time to wait for a pong reply. + // A half-open TCP connection lets Send() succeed while replies never + // arrive; waiting for the IQ result detects this and triggers reconnect. + xmppKeepaliveTimeout = 15 * time.Second reconnectJoinTimeout = 30 * time.Second ) @@ -842,11 +846,11 @@ func (s *Session) xmppKeepalive() { ``, conn.Host(), id, ) - if err := conn.Send(ping); err != nil { + if _, err := conn.SendIQWait(ping, id, xmppKeepaliveTimeout); err != nil { if s.closed.Load() { return } - logger.Debugf("jitsi: xmpp keepalive send: %v", err) + logger.Debugf("jitsi: xmpp keepalive: %v", err) // Avoid spamming the supervisor with identical // requests during the reconnect; once a request // is enqueued the channel is buffered to depth 1, @@ -1090,6 +1094,7 @@ func (s *Session) sendLoop() { if !ok { return } + logger.Debugf("jitsi: bridge send broadcast len=%d", len(data)) s.sendBridgeFrame("", data) case frame, ok := <-s.peerSendQueue: if !ok { @@ -1151,17 +1156,22 @@ func (s *Session) recvLoop() { jSess := s.jSess.Load() if jSess == nil || (s.onData == nil && s.onPeerData == nil) || !s.bridgeReady.Load() { + logger.Debugf("jitsi: recvLoop early exit jSess=%v onData=%v onPeerData=%v bridgeReady=%v", + jSess != nil, s.onData != nil, s.onPeerData != nil, s.bridgeReady.Load()) return } msgs := jSess.BridgeMessages() if msgs == nil { + logger.Debugf("jitsi: recvLoop: BridgeMessages() returned nil, exiting") return } + logger.Debugf("jitsi: recvLoop started") for { select { case <-s.done: return case msg, ok := <-msgs: + logger.Debugf("jitsi: recvLoop got msg ok=%v class=%q from=%q", ok, msg.Class, msg.From) if !s.deliverBridgeMessage(msg, ok) { return } @@ -1179,6 +1189,7 @@ func (s *Session) deliverBridgeMessage(msg j.BridgeMessage, ok bool) bool { } return false } + logger.Debugf("jitsi: bridge msg class=%q from=%q len=%d", msg.Class, msg.From, len(msg.RawJSON)) payload, valid := bridgePayload(msg) if !valid { return true @@ -1262,10 +1273,16 @@ func (s *Session) acceptEpochFrame(payload []byte) ([]byte, bool) { receiverEpoch, s.localEpoch.Load()) return nil, false } + // Drop untargeted (broadcast) frames from unknown or third-party senders. + // Broadcasts from our latched peer are always accepted — the server may + // send welcome frames as broadcasts before it learns our localEpoch. if s.requireTargetedPeer && s.onPeerData == nil && receiverEpoch != s.localEpoch.Load() { - logger.Debugf("jitsi: drop untargeted bridge frame senderEpoch=0x%08x localEpoch=0x%08x", - senderEpoch, s.localEpoch.Load()) - return nil, false + knownPeerEpoch := s.peerEpoch.Load() + if knownPeerEpoch != 0 && senderEpoch != knownPeerEpoch { + logger.Debugf("jitsi: drop untargeted bridge frame senderEpoch=0x%08x localEpoch=0x%08x", + senderEpoch, s.localEpoch.Load()) + return nil, false + } } // Update the peer-epoch latch and ALWAYS accept the frame. //