test_peerConnection_iceCandidateSelection.html (11871B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <script type="application/javascript" src="pc.js"></script> 5 <script type="application/javascript" src="iceTestUtils.js"></script> 6 <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script></head> 7 <body> 8 <pre id="test"> 9 <script type="application/javascript"> 10 createHTML({ 11 bug: "1898696", 12 title: "Corner cases for ICE candidate pair selection" 13 }); 14 15 const tests = [ 16 async function checkRelayPriorityWithLateTrickle() { 17 // Test that relay-based candidate pairs don't get prflx priority when 18 // trickle is late. 19 20 // Block host candidates; if we mess up and interpret relay as 21 // prflx, we won't have host candidates with a higher priority 22 // masking the problem. 23 await pushPrefs( 24 ['media.peerconnection.ice.obfuscate_host_addresses', false], 25 ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], 26 ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], 27 ['media.peerconnection.nat_simulator.network_delay_ms', 50], 28 ['media.peerconnection.nat_simulator.block_udp', false], 29 ['media.peerconnection.nat_simulator.block_tcp', false], 30 ['media.peerconnection.nat_simulator.block_tls', false], 31 ['media.peerconnection.ice.loopback', true], 32 // The above triggers warning about 5 ICE servers 33 ['media.peerconnection.treat_warnings_as_errors', false], 34 ['media.getusermedia.insecure.enabled', true]); 35 36 let turnServer = structuredClone( 37 iceServersArray.find(server => "username" in server)); 38 // Disable TCP-based TURN; this goes through the NAT simulator much more 39 // quickly than UDP, and can result in TURN TCP establishment happening 40 // before srflx is even attempted. 41 turnServer.urls = turnServer.urls.filter( 42 u => u.indexOf("turns:") == -1 && u.indexOf("transport=t") == -1); 43 let stunServer = structuredClone( 44 iceServersArray.find(server => !("username" in server))); 45 46 // This is a somewhat contrived situation. What we're trying to do is 47 // cause the non-controlling side to learn about the controller's relay 48 // candidate from a STUN check, but learn about the srflx through 49 // trickle. 50 const pc1 = new RTCPeerConnection({iceServers: [turnServer]}); 51 const pc2 = new RTCPeerConnection({iceServers: [stunServer]}); 52 53 // Ensure that no host or relay candidates are trickled. Also, record all 54 // interfaces which are able to gather a srflx (ie; are able to reach the 55 // TURN server). Anything that cannot reach the TURN server and gather a 56 // srflx must be filtered out in both directions, otherwise pc2 will 57 // learn about those as prflx, and we want pc2 to only have prflx for 58 // pc1's relay candidates. 59 const ipAddrsWithSrflx = new Set(); 60 pc1.onicecandidate = e => { 61 if (e.candidate && e.candidate.type == "srflx") { 62 ipAddrsWithSrflx.add(e.candidate.address); 63 } 64 // Add only srflx or the end-of-candidates signal 65 if (!e.candidate || e.candidate.type == "srflx") { 66 pc2.addIceCandidate(e.candidate); 67 } 68 }; 69 const transceiver = pc1.addTransceiver('audio'); 70 await pc1.setLocalDescription(); 71 // Wait for gathering to complete. 72 await new Promise(r => pc1.onicegatheringstatechange = () => { 73 if (pc1.iceGatheringState == "complete") { 74 r(); 75 } 76 }); 77 78 // Remove any candidates in the offer. 79 let mungedOffer = { 80 type: "offer", 81 sdp: pc1.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:") 82 }; 83 await pc2.setRemoteDescription(mungedOffer); 84 85 ok(ipAddrsWithSrflx.size != 0, "PC1 was able to reach the TURN server with at least one address"); 86 87 pc2.onicecandidate = e => { 88 if (!e.candidate || ipAddrsWithSrflx.has(e.candidate.address)) { 89 pc1.addIceCandidate(e.candidate); 90 } 91 }; 92 93 await pc2.setLocalDescription(); 94 let mungedAnswer = { 95 type: "answer", 96 sdp: pc2.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:") 97 }; 98 await pc1.setRemoteDescription(mungedAnswer); 99 100 await Promise.all([iceConnected(pc1), iceConnected(pc2)]); 101 info("ICE connected"); 102 const stats = await pc2.getStats(); 103 info("Have all stats"); 104 stats.forEach((value, key) => { 105 info(`${key} => ${JSON.stringify(value)}`); 106 }); 107 108 function getRemoteCandidate(pair, stats) { 109 info(`Getting ${pair.remoteCandidateId} => ${JSON.stringify(stats.get(pair.remoteCandidateId))}`); 110 return stats.get(pair.remoteCandidateId); 111 } 112 113 // Convert the iterable to an array so we can use it more than once 114 const pairs = [...stats.values().filter(s => s.type == "candidate-pair")]; 115 116 const srflxPriorities = pairs.filter(p => getRemoteCandidate(p, stats).candidateType == "srflx").map(p => p.priority); 117 // We obfuscate remote prflx candidates, so cannot match on port. The 118 // above code is intended to only allow prflx for the relay candidates. 119 const prflxPriorities = pairs.filter(p => getRemoteCandidate(p, stats).candidateType == "prflx").map(p => p.priority); 120 121 const minSrflxPriority = Math.min(...srflxPriorities); 122 const maxRelayPriority = Math.max(...prflxPriorities); 123 ok(maxRelayPriority < minSrflxPriority, `relay priorities should be less than srflx priorities (${maxRelayPriority} vs ${minSrflxPriority})`); 124 await SpecialPowers.popPrefEnv(); 125 }, 126 127 async function checkTurnTcpPriority() { 128 await pushPrefs( 129 ['media.peerconnection.ice.obfuscate_host_addresses', false], 130 ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'], 131 ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'], 132 ['media.peerconnection.nat_simulator.network_delay_ms', 150], 133 ['media.peerconnection.nat_simulator.block_udp', false], 134 ['media.peerconnection.nat_simulator.block_tcp', false], 135 ['media.peerconnection.nat_simulator.block_tls', false], 136 ['media.peerconnection.ice.loopback', true], 137 // The above triggers warning about 5 ICE servers 138 ['media.peerconnection.treat_warnings_as_errors', false], 139 ['media.getusermedia.insecure.enabled', true]); 140 141 let turnServer = structuredClone( 142 iceServersArray.find(server => "username" in server)); 143 turnServer.urls = turnServer.urls.filter(u => u.indexOf("turns:") == -1); 144 let stunServer = structuredClone( 145 iceServersArray.find(server => !("username" in server))); 146 147 const pc1 = new RTCPeerConnection( 148 {iceServers: [turnServer], iceTransportPolicy: "relay"}); 149 const pc2 = new RTCPeerConnection({iceServers: [stunServer]}); 150 151 // We will not allow the relay-only side (pc1) to trickle candidates. pc2 152 // will learn about those relay candidates as prflx, as long as it 153 // trickles its srflx. 154 pc2.onicecandidate = e => { 155 if (e.candidate && e.candidate.type == "srflx") { 156 pc1.addIceCandidate(e.candidate); 157 } 158 }; 159 160 const stream = await navigator.mediaDevices.getUserMedia({ video: true }); 161 const sender = pc1.addTrack(stream.getTracks()[0]); 162 163 await pc1.setLocalDescription(); 164 165 // Ensure that the relay candidates are gathered and ready to go. 166 await new Promise(r => pc1.onicegatheringstatechange = () => { 167 if (pc1.iceGatheringState == "complete") { 168 r(); 169 } 170 }); 171 172 // Finish negotiation while removing any candidates in SDP 173 let mungedOffer = { 174 type: "offer", 175 sdp: pc1.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:") 176 }; 177 await pc2.setRemoteDescription(mungedOffer); 178 179 await pc2.setLocalDescription(); 180 let mungedAnswer = { 181 type: "answer", 182 sdp: pc2.localDescription.sdp.replaceAll("a=candidate:", "a=candid8:") 183 }; 184 185 await pc1.setRemoteDescription(mungedAnswer); 186 187 await Promise.all([iceConnected(pc1), iceConnected(pc2)]); 188 info("ICE connected"); 189 const offererStats = await pc1.getStats(); 190 const answererStats = await pc2.getStats(); 191 info("Have all stats"); 192 offererStats.forEach((value, key) => { 193 info(`${key} => ${JSON.stringify(value)}`); 194 }); 195 answererStats.forEach((value, key) => { 196 info(`${key} => ${JSON.stringify(value)}`); 197 }); 198 199 const turnUdpLocalCandidates = [...offererStats.values().filter(s => { 200 return s.type == "local-candidate" && 201 s.candidateType == "relay" && s.relayProtocol == "udp"; 202 })]; 203 const turnTcpLocalCandidates = [...offererStats.values().filter(s => { 204 return s.type == "local-candidate" && 205 s.candidateType == "relay" && s.relayProtocol == "tcp"; 206 })]; 207 208 // Remote candidates don't have relay protocol, but we can find them by 209 // matching ports. 210 const turnUdpPorts = [...turnUdpLocalCandidates.map(c => c.port)]; 211 const turnTcpPorts = [...turnTcpLocalCandidates.map(c => c.port)]; 212 213 // The relay candidates will be prflx, and we'll be able to tell which is 214 // which by port number. 215 const turnUdpRemoteCandidates = [...answererStats.values().filter(s => { 216 return s.type == "remote-candidate" && 217 s.candidateType == "prflx" && turnUdpPorts.includes(s.port); 218 })]; 219 const turnTcpRemoteCandidates = [...answererStats.values().filter(s => { 220 return s.type == "remote-candidate" && 221 s.candidateType == "prflx" && turnTcpPorts.includes(s.port); 222 })]; 223 224 ok(turnTcpLocalCandidates.length, 225 "There are local TURN TCP candidates"); 226 ok(turnUdpLocalCandidates.length, 227 "There are local TURN UDP candidates"); 228 ok(turnTcpRemoteCandidates.length, 229 "There are remote TURN TCP candidates"); 230 ok(turnUdpRemoteCandidates.length, 231 "There are remote TURN UDP candidates"); 232 const maxLocalTurnTcpPriority = 233 Math.max(...turnTcpLocalCandidates.map(c => c.priority)); 234 const maxRemoteTurnTcpPriority = 235 Math.max(...turnTcpRemoteCandidates.map(c => c.priority)); 236 const minLocalTurnUdpPriority = 237 Math.min(...turnUdpLocalCandidates.map(c => c.priority)); 238 const minRemoteTurnUdpPriority = 239 Math.min(...turnUdpRemoteCandidates.map(c => c.priority)); 240 241 ok(minLocalTurnUdpPriority > 2 * maxLocalTurnTcpPriority, 242 `Local TURN UDP candidates all have much higher priority than` + 243 ` local TURN TCP candidates` + 244 ` (${minLocalTurnUdpPriority} vs ${maxLocalTurnTcpPriority})`); 245 ok(minRemoteTurnUdpPriority > 2 * maxRemoteTurnTcpPriority, 246 `Remote TURN UDP candidates all have much higher priority than` + 247 ` remote TURN TCP candidates` + 248 ` (${minRemoteTurnUdpPriority} vs ${maxRemoteTurnTcpPriority})`); 249 250 await SpecialPowers.popPrefEnv(); 251 }, 252 ]; 253 254 if (!("mediaDevices" in navigator)) { 255 SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]}, 256 () => location.reload()); 257 } else { 258 runNetworkTest(async () => { 259 for (const test of tests) { 260 info(`Running test: ${test.name}`); 261 try { 262 await test(); 263 } catch (e) { 264 ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`); 265 } 266 info(`Done running test: ${test.name}`); 267 // Make sure we don't build up a pile of GC work, and also get PCImpl to 268 // print their timecards. 269 await new Promise(r => SpecialPowers.exactGC(r)); 270 } 271 272 await SpecialPowers.popPrefEnv(); 273 }, { useIceServer: true }); 274 } 275 276 </script> 277 </pre> 278 </body> 279 </html>