From 231e3204862f928ddc89973e917fb22cd5ad5617 Mon Sep 17 00:00:00 2001 From: saeednourian82 Date: Tue, 19 May 2026 02:50:26 +0330 Subject: [PATCH] webrtc: drop stale mux candidates after network change When webrtcLocalUDPAddress is set, mediamtx creates a single UDPMuxDefault at startup. Pion populates its localAddrsForUnspecified once at construction time. After a network change (e.g. WiFi switch), the mux still advertises startup-time IPs as host candidates in SDP answers, while the new network IP is never generated. Fix by querying net.Interfaces() fresh in removeUnwantedCandidates for each new session when ICEUDPMux is set and no explicit AdditionalHosts or IPsFromInterfaces filter is configured, dropping any host UDP candidate whose IP no longer appears on a live interface. Fixes #5097 --- internal/protocols/webrtc/peer_connection.go | 36 +++++++++++++++---- .../protocols/webrtc/peer_connection_test.go | 30 ++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index 4df76f9de48..7e40820682f 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -469,8 +469,20 @@ func (co *PeerConnection) removeUnwantedCandidates(firstMedia *sdp.MediaDescript } } - var newAttributes []sdp.Attribute //nolint:prealloc + // When using the shared UDP mux with no explicit interface filter and no + // additional hosts, fetch current live IPs to detect stale candidates + // left over from a prior network configuration (e.g. after a WiFi change). + var liveIPs []string + if co.ICEUDPMux != nil && !co.IPsFromInterfaces && len(co.AdditionalHosts) == 0 { + var err error + liveIPs, err = interfaceIPs(nil) + if err != nil { + co.Log.Log(logger.Warn, + "failed to query live interface IPs, skipping stale candidate filter: %v", err) + } + } + var newAttributes []sdp.Attribute //nolint:prealloc for _, attr := range firstMedia.Attributes { if attr.Key == "candidate" { parts := strings.Split(attr.Value, " ") @@ -480,17 +492,27 @@ func (co *PeerConnection) removeUnwantedCandidates(firstMedia *sdp.MediaDescript continue } - // hide disallowed IPs - if parts[7] == "host" && !slices.Contains(allowedIPs, parts[4]) { - continue + if parts[7] == "host" { + if len(liveIPs) > 0 && parts[2] == "udp" { + // drop stale mux candidates whose IP no longer exists + // on any live interface (network-change fix) + if !slices.Contains(liveIPs, parts[4]) { + co.Log.Log(logger.Debug, + "dropping stale mux candidate %s: IP %s not found on current interfaces", + attr.Value, parts[4]) + continue + } + } else { + // hide IPs not in the allowed interface list + if !slices.Contains(allowedIPs, parts[4]) { + continue + } + } } } - newAttributes = append(newAttributes, attr) } - firstMedia.Attributes = newAttributes - return nil } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 006d4e2293f..f2e6fd95ee8 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -18,6 +18,13 @@ import ( "github.com/stretchr/testify/require" ) +type fakeUDPMux struct{} + +func (f *fakeUDPMux) Close() error { return nil } +func (f *fakeUDPMux) GetConn(string, net.Addr) (net.PacketConn, error) { return nil, nil } +func (f *fakeUDPMux) RemoveConnByUfrag(string) {} +func (f *fakeUDPMux) GetListenAddresses() []net.Addr { return nil } + type nilWriter struct{} func (nilWriter) Write(p []byte) (int, error) { @@ -886,3 +893,26 @@ func TestPeerConnectionPublishDataChannel(t *testing.T) { <-dataReceived } + +func TestRemoveUnwantedCandidatesStaleIP(t *testing.T) { + // Simulates a candidate whose IP is no longer present on any interface + // (e.g. after a WiFi network change). It should be dropped. + co := &PeerConnection{ + ICEUDPMux: &fakeUDPMux{}, // non-nil triggers the liveIPs check + Log: test.NilLogger, + } + + staleIP := "10.99.99.99" // guaranteed not on any real interface + media := &sdp.MediaDescription{ + Attributes: []sdp.Attribute{ + {Key: "candidate", Value: "123 1 udp 2130706431 " + staleIP + " 8189 typ host generation 0"}, + {Key: "candidate", Value: "456 1 udp 2130706431 127.0.0.1 8189 typ host generation 0"}, + }, + } + + err := co.removeUnwantedCandidates(media) + require.NoError(t, err) + + require.Len(t, media.Attributes, 1) + require.Contains(t, media.Attributes[0].Value, "127.0.0.1") +}