tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>