diff --git a/src/ObservedPeerConnection.ts b/src/ObservedPeerConnection.ts index b999fc8..a262ab6 100644 --- a/src/ObservedPeerConnection.ts +++ b/src/ObservedPeerConnection.ts @@ -225,11 +225,15 @@ export class ObservedPeerConnection extends EventEmitter { } public get selectedIceCandiadtePairForTurn() { + // The ICE server `url` is only exposed on *local* candidates (it identifies the server + // the candidate was obtained from); remote candidates never carry it. `turn` also + // matches `turns:` (TURN over TLS). return this.selectedIceCandidatePairs - .filter((pair) => - pair.getLocalCandidate()?.candidateType === 'relay' && - pair.getRemoteCandidate()?.url?.startsWith('turn:') - ); + .filter((pair) => { + const localCandidate = pair.getLocalCandidate(); + + return localCandidate?.candidateType === 'relay' && localCandidate?.url?.startsWith('turn') === true; + }); } public close() { @@ -539,12 +543,20 @@ export class ObservedPeerConnection extends EventEmitter { this.usingTURN = false; for (const selectedCandidatePair of selectedIceCandidatePairs) { - if (selectedCandidatePair.getLocalCandidate()?.protocol === 'tcp') { + const localCandidate = selectedCandidatePair.getLocalCandidate(); + + if (localCandidate?.protocol === 'tcp') { this.usingTCP = true; } - if (selectedCandidatePair.getLocalCandidate()?.candidateType === 'relay' && selectedCandidatePair.getRemoteCandidate()?.url?.startsWith('turn:')) { - selectedCandidatePairForTurn.push(selectedCandidatePair); + // relay candidates are only obtained from TURN servers, so the local candidate's + // type alone establishes TURN usage. The server `url` is only exposed on *local* + // candidates (and not by every browser) — it is required only to attribute the + // traffic to a concrete TURN server. + if (localCandidate?.candidateType === 'relay') { this.usingTURN = true; + if (localCandidate.url?.startsWith('turn')) { + selectedCandidatePairForTurn.push(selectedCandidatePair); + } } this.deltaTransportReceivedBytes += selectedCandidatePair.deltaBytesReceived; this.deltaTransportSentBytes += selectedCandidatePair.deltaBytesSent; diff --git a/src/ObservedTURN.ts b/src/ObservedTURN.ts index a8b6bd4..ce7a004 100644 --- a/src/ObservedTURN.ts +++ b/src/ObservedTURN.ts @@ -53,27 +53,34 @@ export class ObservedTURN extends EventEmitter { } public addPeerConnection(peerConnection: ObservedPeerConnection) { - const turnPairs = peerConnection.selectedIceCandidatePairs.filter((pair) => pair.getLocalCandidate()?.candidateType === 'relay' && pair.getRemoteCandidate()?.url?.startsWith('turn:')); + // selected pairs whose *local* candidate is a relay candidate with a `turn(s):` url + // (the ICE server url is only exposed on local candidates). + const turnPairs = peerConnection.selectedIceCandiadtePairForTurn; if (turnPairs.length !== 1) { return (logger.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), undefined); } const candidatePair = turnPairs[0]; - const rawUrl = candidatePair.getRemoteCandidate()?.url; + const rawUrl = candidatePair.getLocalCandidate()?.url; if (!rawUrl) { - return (logger.warn(`No remote candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), undefined); + return (logger.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), undefined); } - const turnUrl = new URL(rawUrl); - const turnServerUrl = `${turnUrl.protocol}//${turnUrl.hostname}:${turnUrl.port}`; + // A TURN url looks like `turn(s):host[:port][?transport=udp|tcp]` (RFC 7065). The WHATWG + // `URL` class parses these non-special schemes as opaque paths (empty hostname/port), so + // derive the server key by stripping the query instead of using `new URL()`. + const turnServerUrl = rawUrl.split('?')[0]; let turnServer = this.servers.get(turnServerUrl); - + if (!turnServer) { turnServer = new ObservedTurnServer(turnServerUrl, this); + this.servers.set(turnServerUrl, turnServer); } + turnServer.observedPeerConnections.set(peerConnection.peerConnectionId, peerConnection); + return turnServer; }