tor-browser

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

RTCIceTransport.html (22666B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <meta name="timeout" content="long">
      4 <title>RTCIceTransport</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="RTCPeerConnection-helper.js"></script>
      8 <script src='RTCConfiguration-helper.js'></script>
      9 <script>
     10  'use strict';
     11 
     12  // Test is based on the following editor draft:
     13  // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
     14 
     15  // The following helper functions are called from RTCPeerConnection-helper.js:
     16  //  createDataChannelPair
     17  //  awaitMessage
     18 
     19  /*
     20    5.6.  RTCIceTransport Interface
     21      interface RTCIceTransport {
     22        readonly attribute RTCIceRole           role;
     23        readonly attribute RTCIceComponent      component;
     24        readonly attribute RTCIceTransportState state;
     25        readonly attribute RTCIceGathererState  gatheringState;
     26        sequence<RTCIceCandidate> getLocalCandidates();
     27        sequence<RTCIceCandidate> getRemoteCandidates();
     28        RTCIceCandidatePair?      getSelectedCandidatePair();
     29        RTCIceParameters?         getLocalParameters();
     30        RTCIceParameters?         getRemoteParameters();
     31        ...
     32      };
     33 
     34      getLocalCandidates
     35        Returns a sequence describing the local ICE candidates gathered for this
     36        RTCIceTransport and sent in onicecandidate
     37 
     38      getRemoteCandidates
     39        Returns a sequence describing the remote ICE candidates received by this
     40        RTCIceTransport via addIceCandidate()
     41 
     42      getSelectedCandidatePair
     43        Returns the selected candidate pair on which packets are sent, or null if
     44        there is no such pair.
     45 
     46      getLocalParameters
     47        Returns the local ICE parameters received by this RTCIceTransport via
     48        setLocalDescription , or null if the parameters have not yet been received.
     49 
     50      getRemoteParameters
     51        Returns the remote ICE parameters received by this RTCIceTransport via
     52        setRemoteDescription or null if the parameters have not yet been received.
     53   */
     54  function getIceTransportFromSctp(pc) {
     55    const sctpTransport = pc.sctp;
     56    assert_true(sctpTransport instanceof RTCSctpTransport,
     57      'Expect pc.sctp to be instantiated from RTCSctpTransport');
     58 
     59    const dtlsTransport = sctpTransport.transport;
     60    assert_true(dtlsTransport instanceof RTCDtlsTransport,
     61      'Expect sctp.transport to be an RTCDtlsTransport');
     62 
     63    const {iceTransport} = dtlsTransport;
     64    assert_true(iceTransport instanceof RTCIceTransport,
     65      'Expect dtlsTransport.transport to be an RTCIceTransport');
     66 
     67    return iceTransport;
     68  }
     69 
     70  function validateCandidates(candidates) {
     71    assert_greater_than(candidates.length, 0,
     72      'Expect at least one ICE candidate returned from get*Candidates()');
     73 
     74    for(const candidate of candidates) {
     75      assert_true(candidate instanceof RTCIceCandidate,
     76        'Expect candidate elements to be instance of RTCIceCandidate');
     77    }
     78  }
     79 
     80  function validateCandidateParameter(param) {
     81    assert_not_equals(param, null,
     82      'Expect candidate parameter to be non-null after data channels are connected');
     83 
     84    assert_equals(typeof param.usernameFragment, 'string',
     85      'Expect param.usernameFragment to be set with string value');
     86    assert_equals(typeof param.password, 'string',
     87      'Expect param.password to be set with string value');
     88  }
     89 
     90  function validateConnectedIceTransport(iceTransport) {
     91    const { state, gatheringState, role, component } = iceTransport;
     92 
     93    assert_true(role === 'controlling' || role === 'controlled',
     94      'Expect RTCIceRole to be either controlling or controlled, found ' + role);
     95 
     96    assert_true(component === 'rtp' || component === 'rtcp',
     97      'Expect RTCIceComponent to be either rtp or rtcp');
     98 
     99    assert_true(state === 'connected' || state === 'completed',
    100      'Expect ICE transport to be in connected or completed state after data channels are connected');
    101 
    102    assert_true(gatheringState === 'gathering' || gatheringState === 'completed',
    103      'Expect ICE transport to be in gathering or completed gatheringState after data channels are connected');
    104 
    105    validateCandidates(iceTransport.getLocalCandidates());
    106    validateCandidates(iceTransport.getRemoteCandidates());
    107 
    108    const candidatePair = iceTransport.getSelectedCandidatePair();
    109    assert_not_equals(candidatePair, null,
    110      'Expect selected candidate pair to be non-null after ICE transport is connected');
    111 
    112    assert_true(candidatePair.local instanceof RTCIceCandidate,
    113      'Expect candidatePair.local to be instance of RTCIceCandidate');
    114 
    115    assert_true(candidatePair.remote instanceof RTCIceCandidate,
    116      'Expect candidatePair.remote to be instance of RTCIceCandidate');
    117 
    118    validateCandidateParameter(iceTransport.getLocalParameters());
    119    validateCandidateParameter(iceTransport.getRemoteParameters());
    120  }
    121 
    122  promise_test(t => {
    123    const pc1 = new RTCPeerConnection();
    124    t.add_cleanup(() => pc1.close());
    125    const pc2 = new RTCPeerConnection();
    126    t.add_cleanup(() => pc2.close());
    127 
    128    const pc1Candidates = new Set();
    129    const pc2Candidates = new Set();
    130    pc1.addEventListener('icecandidate', e => { if (e.candidate) pc1Candidates.add(e.candidate.candidate); });
    131    pc2.addEventListener('icecandidate', e => { if (e.candidate) pc2Candidates.add(e.candidate.candidate); });
    132 
    133    return createDataChannelPair(t, {}, pc1, pc2)
    134    .then(([channel1, channel2]) => {
    135      // Send a ping message and wait for it just to make sure
    136      // that the connection is fully working before testing
    137      channel1.send('ping');
    138      return awaitMessage(channel2);
    139    })
    140    .then(() => {
    141      const iceTransport1 = getIceTransportFromSctp(pc1);
    142      const iceTransport2 = getIceTransportFromSctp(pc2);
    143 
    144      test(() => {
    145          const candidatePair1 = iceTransport1.getSelectedCandidatePair();
    146          const candidatePair2 = iceTransport2.getSelectedCandidatePair();
    147 
    148          assert_equals(candidatePair1.local.candidate, candidatePair2.remote.candidate,
    149              'Expect selected local candidate of one pc is the selected remote candidate or another');
    150 
    151          assert_equals(candidatePair1.remote.candidate, candidatePair2.local.candidate,
    152              'Expect selected local candidate of one pc is the selected remote candidate or another');
    153 
    154          assert_true(pc1Candidates.has(candidatePair1.local.candidate), "pc1 selected local candidate in pc1 candidates");
    155          assert_true(pc1Candidates.has(candidatePair2.remote.candidate), "pc2 selected remote candidate in pc1 candidates");
    156          assert_true(pc2Candidates.has(candidatePair2.local.candidate), "pc2 selected local candidate in pc2 candidates");
    157          assert_true(pc2Candidates.has(candidatePair1.remote.candidate), "pc1 selected remote candidate in pc2 candidates");
    158      }, "Validate selected candidate pair");
    159 
    160      validateConnectedIceTransport(iceTransport1);
    161      validateConnectedIceTransport(iceTransport2);
    162 
    163      assert_equals(
    164        iceTransport1.getLocalCandidates().length,
    165        iceTransport2.getRemoteCandidates().length,
    166        `Expect iceTransport1 to have same number of local candidate as iceTransport2's remote candidates`);
    167 
    168      assert_equals(
    169        iceTransport1.getRemoteCandidates().length,
    170        iceTransport2.getLocalCandidates().length,
    171        `Expect iceTransport1 to have same number of remote candidate as iceTransport2's local candidates`);
    172 
    173      assert_equals(iceTransport1.role, 'controlling',
    174        `Expect offerer's iceTransport to take the controlling role`);
    175 
    176      assert_equals(iceTransport2.role, 'controlled',
    177        `Expect answerer's iceTransport to take the controlled role`);
    178    });
    179  }, 'Two connected iceTransports should have matching local/remote candidates returned');
    180 
    181  promise_test(t => {
    182    const pc1 = new RTCPeerConnection();
    183    t.add_cleanup(() => pc1.close());
    184    const pc2 = new RTCPeerConnection();
    185    t.add_cleanup(() => pc2.close());
    186    pc1.createDataChannel('');
    187 
    188    // setRemoteDescription(answer) without the other peer
    189    // setting answer it's localDescription
    190    return pc1.createOffer()
    191    .then(offer =>
    192      pc1.setLocalDescription(offer)
    193      .then(() => pc2.setRemoteDescription(offer))
    194      .then(() => pc2.createAnswer()))
    195    .then(answer => pc1.setRemoteDescription(answer))
    196    .then(() => {
    197      const iceTransport = getIceTransportFromSctp(pc1);
    198 
    199      assert_array_equals(iceTransport.getRemoteCandidates(), [],
    200        'Expect iceTransport to not have any remote candidate');
    201 
    202      assert_equals(iceTransport.getSelectedCandidatePair(), null,
    203        'Expect selectedCandidatePair to be null');
    204    });
    205  }, 'Unconnected iceTransport should have empty remote candidates and selected pair');
    206 
    207  promise_test(async t => {
    208    const pc1 = new RTCPeerConnection();
    209    t.add_cleanup(() => pc1.close());
    210    const transceiver = pc1.addTransceiver('audio');
    211    await pc1.setLocalDescription();
    212    const {iceTransport} = transceiver.sender.transport;
    213    assert_equals(iceTransport.state, 'new');
    214    assert_equals(iceTransport.gatheringState, 'new');
    215  }, 'RTCIceTransport should be in state "new" initially');
    216 
    217  promise_test(async t => {
    218    const pc1 = new RTCPeerConnection();
    219    t.add_cleanup(() => pc1.close());
    220    const transceiver = pc1.addTransceiver('audio');
    221    await pc1.setLocalDescription();
    222    const {iceTransport} = transceiver.sender.transport;
    223    assert_equals(await nextGatheringState(iceTransport), 'gathering');
    224    assert_equals(await nextGatheringState(iceTransport), 'complete');
    225  }, 'RTCIceTransport should transition to "gathering" then "complete", after sLD');
    226 
    227  promise_test(async t => {
    228    const pc1 = new RTCPeerConnection();
    229    t.add_cleanup(() => pc1.close());
    230    const transceiver = pc1.addTransceiver('audio');
    231    await pc1.setLocalDescription();
    232    const {iceTransport} = transceiver.sender.transport;
    233    assert_equals(await nextGatheringState(iceTransport), 'gathering');
    234    pc1.close();
    235    assert_equals(iceTransport.gatheringState, 'gathering');
    236    const result = await Promise.race([
    237      gatheringStateReached(iceTransport, 'complete'),
    238      new Promise(r => t.step_timeout(r, 1000))]);
    239    assert_equals(result, undefined, `Did not expect a statechange after PC.close(), but got one. state is "${result}"`);
    240  }, 'PC.close() should not cause the RTCIceTransport gathering state to transition to "complete"');
    241 
    242  promise_test(async t => {
    243    const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
    244    t.add_cleanup(() => pc1.close());
    245    const pc2 = new RTCPeerConnection();
    246    t.add_cleanup(() => pc2.close());
    247    pc1.createDataChannel('test');
    248    // TODO: If the spec settles on exposing the sctp transport in
    249    // have-local-offer, we won't need this audio transceiver hack.
    250    // See https://github.com/w3c/webrtc-pc/issues/2898 and
    251    // https://github.com/w3c/webrtc-pc/issues/2899
    252    const transceiver = pc1.addTransceiver('audio');
    253    await pc1.setLocalDescription();
    254    const {iceTransport} = transceiver.sender.transport;
    255    assert_equals(await nextGatheringState(iceTransport), 'gathering');
    256    assert_equals(await nextGatheringState(iceTransport), 'complete');
    257    // TODO: Maybe, maybe not.
    258    assert_not_equals(pc1.sctp, null, 'pc1.sctp should be set after sLD');
    259    await pc2.setRemoteDescription(pc1.localDescription);
    260    await pc2.setLocalDescription();
    261    await pc1.setRemoteDescription(pc2.localDescription);
    262    assert_equals(pc1.sctp.transport.iceTransport, transceiver.sender.transport.iceTransport);
    263  }, 'RTCIceTransport should transition to "gathering", then "complete" after sLD (DataChannel case)');
    264 
    265  promise_test(async t => {
    266    const pc1 = new RTCPeerConnection();
    267    t.add_cleanup(() => pc1.close());
    268    const pc2 = new RTCPeerConnection();
    269    t.add_cleanup(() => pc2.close());
    270 
    271    const {sender} = pc1.addTransceiver('audio');
    272    await pc1.setLocalDescription();
    273    // Copy the SDP before it has candidate attrs
    274    const offer = pc1.localDescription;
    275    const checkingReached = connectionStateReached(sender.transport.iceTransport, 'checking');
    276 
    277    let result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
    278    assert_equals(result, undefined, `Did not expect a statechange right after sLD(offer), but got one. state is "${result}"`);
    279 
    280    await pc2.setRemoteDescription(offer);
    281 
    282    const firstPc2CandidatePromise =
    283      new Promise(r => pc2.onicecandidate = e => r(e.candidate));
    284 
    285    await pc2.setLocalDescription();
    286    await pc1.setRemoteDescription(pc2.localDescription);
    287 
    288    result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
    289    assert_equals(result, undefined, `Did not expect a statechange callback after sRD(answer), but got one. state is "${result}"`);
    290 
    291    const candidate = await firstPc2CandidatePromise;
    292    pc1.addIceCandidate(candidate);
    293 
    294    await checkingReached;
    295  }, 'RTCIceTransport should not transition to "checking" until after the answer is set _and_ the first remote candidate is received');
    296 
    297 
    298  promise_test(async t => {
    299    const pc1 = new RTCPeerConnection();
    300    t.add_cleanup(() => pc1.close());
    301    const pc2 = new RTCPeerConnection();
    302    t.add_cleanup(() => pc2.close());
    303    const {sender} = pc1.addTransceiver('audio');
    304    exchangeIceCandidates(pc1, pc2);
    305    const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
    306    await pc1.setLocalDescription();
    307    await pc2.setRemoteDescription(pc1.localDescription);
    308    await pc2.setLocalDescription();
    309    await pc1.setRemoteDescription(pc2.localDescription);
    310    assert_equals(await nextConnectionState(sender.transport.iceTransport), 'checking');
    311    assert_equals(await nextConnectionState(sender.transport.iceTransport), 'connected');
    312    await gatheringDone;
    313    pc2.close();
    314    await connectionStateReached(sender.transport.iceTransport, 'disconnected');
    315  }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing');
    316 
    317  promise_test(async t => {
    318    const pc1 = new RTCPeerConnection();
    319    t.add_cleanup(() => pc1.close());
    320    const pc2 = new RTCPeerConnection();
    321    t.add_cleanup(() => pc2.close());
    322    pc1.createDataChannel('test');
    323    exchangeIceCandidates(pc1, pc2);
    324    const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
    325    await pc1.setLocalDescription();
    326    await pc2.setRemoteDescription(pc1.localDescription);
    327    await pc2.setLocalDescription();
    328    await pc1.setRemoteDescription(pc2.localDescription);
    329    const {sctp} = pc1;
    330    assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'checking');
    331    assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'connected');
    332    await gatheringDone;
    333    pc2.close();
    334    await connectionStateReached(sctp.transport.iceTransport, 'disconnected');
    335  }, 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)');
    336 
    337  promise_test(async t => {
    338    const pc1 = new RTCPeerConnection();
    339    t.add_cleanup(() => pc1.close());
    340    const pc2 = new RTCPeerConnection();
    341    t.add_cleanup(() => pc2.close());
    342    const {sender} = pc1.addTransceiver('audio');
    343    await pc1.setLocalDescription();
    344    const {iceTransport} = sender.transport;
    345    await pc2.setRemoteDescription(pc1.localDescription);
    346    await pc2.setLocalDescription();
    347    await pc1.setRemoteDescription(pc2.localDescription);
    348 
    349    pc1.restartIce();
    350 
    351    await pc1.setLocalDescription();
    352    await pc2.setRemoteDescription(pc1.localDescription);
    353    await pc2.setLocalDescription();
    354    await pc1.setRemoteDescription(pc2.localDescription);
    355 
    356    assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
    357  }, 'Local ICE restart should not result in a different ICE transport');
    358 
    359  promise_test(async t => {
    360    const pc1 = new RTCPeerConnection();
    361    t.add_cleanup(() => pc1.close());
    362    const pc2 = new RTCPeerConnection();
    363    t.add_cleanup(() => pc2.close());
    364    pc1.createDataChannel('test');
    365    await pc1.setLocalDescription();
    366    await pc2.setRemoteDescription(pc1.localDescription);
    367    await pc2.setLocalDescription();
    368    await pc1.setRemoteDescription(pc2.localDescription);
    369    const {iceTransport} = pc1.sctp.transport;
    370 
    371    pc1.restartIce();
    372 
    373    await pc1.setLocalDescription();
    374    await pc2.setRemoteDescription(pc1.localDescription);
    375    await pc2.setLocalDescription();
    376    await pc1.setRemoteDescription(pc2.localDescription);
    377 
    378    assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
    379  }, 'Local ICE restart should not result in a different ICE transport (DataChannel case)');
    380 
    381  promise_test(async t => {
    382    const pc1 = new RTCPeerConnection();
    383    t.add_cleanup(() => pc1.close());
    384    const pc2 = new RTCPeerConnection();
    385    t.add_cleanup(() => pc2.close());
    386    const {sender} = pc1.addTransceiver('audio');
    387 
    388    await pc1.setLocalDescription();
    389    const {iceTransport} = sender.transport;
    390    await pc2.setRemoteDescription(pc1.localDescription);
    391    await pc2.setLocalDescription();
    392    await pc1.setRemoteDescription(pc2.localDescription);
    393 
    394    pc2.restartIce();
    395 
    396    await pc2.setLocalDescription();
    397    await pc1.setRemoteDescription(pc2.localDescription);
    398    await pc2.setLocalDescription();
    399    await pc1.setRemoteDescription(pc1.localDescription);
    400 
    401    assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
    402  }, 'Remote ICE restart should not result in a different ICE transport');
    403 
    404  promise_test(async t => {
    405    const pc1 = new RTCPeerConnection();
    406    t.add_cleanup(() => pc1.close());
    407    const pc2 = new RTCPeerConnection();
    408    t.add_cleanup(() => pc2.close());
    409    pc1.createDataChannel('test');
    410 
    411    await pc1.setLocalDescription();
    412    await pc2.setRemoteDescription(pc1.localDescription);
    413    await pc2.setLocalDescription();
    414    await pc1.setRemoteDescription(pc2.localDescription);
    415    const {iceTransport} = pc1.sctp.transport;
    416 
    417    pc2.restartIce();
    418 
    419    await pc2.setLocalDescription();
    420    await pc1.setRemoteDescription(pc2.localDescription);
    421    await pc2.setLocalDescription();
    422    await pc1.setRemoteDescription(pc1.localDescription);
    423 
    424    assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
    425  }, 'Remote ICE restart should not result in a different ICE transport (DataChannel case)');
    426 
    427  promise_test(async t => {
    428    const pc1 = new RTCPeerConnection();
    429    t.add_cleanup(() => pc1.close());
    430    const pc2 = new RTCPeerConnection();
    431    t.add_cleanup(() => pc2.close());
    432    // Add two transceivers, one audio and one video. The default bundlePolicy
    433    // ("balanced") will result in each being offered with its own transport,
    434    // but allowing the answerer to bundle the second transceiver on the
    435    // transport of the first, which the answerer will do by default.
    436    const audioTransceiver = pc1.addTransceiver('audio');
    437    const videoTransceiver = pc1.addTransceiver('video');
    438    pc1.createDataChannel('test');
    439 
    440    await pc1.setLocalDescription();
    441    const audioIceTransport = audioTransceiver.sender.transport.iceTransport;
    442    const videoIceTransport = videoTransceiver.sender.transport.iceTransport;
    443 
    444    assert_not_equals(audioIceTransport, videoIceTransport, 'audio and video should start out with different transports');
    445 
    446    await pc2.setRemoteDescription(pc1.localDescription);
    447    await pc2.setLocalDescription();
    448    await pc1.setRemoteDescription(pc2.localDescription);
    449    const sctpIceTransport = pc1.sctp.transport.iceTransport;
    450 
    451    assert_equals(videoTransceiver.sender.transport.iceTransport, audioIceTransport, 'After negotiation, the video sender should use the bundle ICE transport from the audio sender');
    452    assert_equals(pc1.sctp.transport.iceTransport, audioIceTransport, 'After negotiation, the datachannel should use the bundle ICE transport from the audio sender');
    453    assert_not_equals(videoIceTransport.state, 'closed', 'Completion of offer/answer should not close the unused ICE transport immediately');
    454 
    455    await connectionStateReached(videoIceTransport, 'closed');
    456  }, 'RTCIceTransport should transition to "closed" if the underlying transport is closed because the answer used bundle');
    457 
    458  promise_test(async t => {
    459    const pc1 = new RTCPeerConnection();
    460    t.add_cleanup(() => pc1.close());
    461    const pc2 = new RTCPeerConnection();
    462    t.add_cleanup(() => pc2.close());
    463    const {sender} = pc1.addTransceiver('audio');
    464    exchangeIceCandidates(pc1, pc2);
    465    const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
    466    await pc1.setLocalDescription();
    467    const {iceTransport} = sender.transport;
    468    await pc2.setRemoteDescription(pc1.localDescription);
    469    await pc2.setLocalDescription();
    470    await pc1.setRemoteDescription(pc2.localDescription);
    471    assert_equals(await nextConnectionState(iceTransport), 'checking');
    472    assert_equals(await nextConnectionState(iceTransport), 'connected');
    473    await gatheringDone;
    474 
    475    const closedEvent = connectionStateReached(iceTransport, 'closed');
    476    pc1.close();
    477    assert_equals(sender.transport.iceTransport, iceTransport, 'PC.close() should not unset the sender transport');
    478    assert_equals(iceTransport.state, 'closed', 'pc.close() should close the sender transport synchronously');
    479    const result = await Promise.race([closedEvent, new Promise(r => t.step_timeout(r, 1000))]);
    480    assert_equals(result, undefined, 'statechange event should not fire when transitioning to closed due to PC.close()');
    481  }, 'RTCIceTransport should synchronously transition to "closed" with no event if the underlying transport is closed due to PC.close()');
    482 
    483  promise_test(async t => {
    484    const pc1 = new RTCPeerConnection();
    485    t.add_cleanup(() => pc1.close());
    486    const pc2 = new RTCPeerConnection();
    487    t.add_cleanup(() => pc2.close());
    488    const {sender} = pc1.addTransceiver('audio');
    489 
    490    // Candidates only go in one direction, like in webrtc-stats/getStats-remote-candidate-address.html
    491    // This means the remote candidates are peer-reflexive candidates.
    492    pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
    493    exchangeOfferAnswer(pc1, pc2);
    494    await listenToConnected(pc1);
    495 
    496    assert_equals(sender.transport.iceTransport.getRemoteCandidates().length, 0);
    497  }, 'RTCIceTransport does not expose remote peer-reflexive candidates.');
    498 
    499 </script>