Reland "Surface ICE candidates that match an updated candidate filter."

This is a reland of cd8d1cf68e4eeed71fba51c97006a91bfd41813d

Original change's description:
> Surface ICE candidates that match an updated candidate filter.
> 
> After this change an ICE agent can surface candidates that do not match
> the previous filter but are allowed by the updated one. The candidate
> filter, as part of the internal implementation in the ICE transport,
> manifests the RTCIceTransportPolicy field in RTCConfiguration.
> 
> This new feature would allow an ICE agent to gather new candidates when
> the transport policy changes from e.g. 'relay' to 'all' without an ICE
> restart.
> 
> A caveat in the current implementation remains, and a candidate can
> surface multiple times if the transport policy, or the candidate filter
> directly, performs multiple transitions from a value that disallows to
> one that allows the underlying candidate type. For example, if the
> transport policy is updated by 'all' -> 'relay' -> 'all', the same host
> candidate can surface after the second update.
> 
> 
> Bug: webrtc:8939
> Change-Id: I92c2e07dafab225c702c5de28f47958a0d3270cc
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/132282
> Commit-Queue: Qingsi Wang <qingsi@webrtc.org>
> Reviewed-by: Jeroen de Borst <jeroendb@webrtc.org>
> Reviewed-by: Seth Hampson <shampson@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#27674}

Bug: webrtc:8939
Change-Id: I9c32b1ea05028ecd937ab4912779dd958faf734f
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/133582
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Reviewed-by: Jeroen de Borst <jeroendb@webrtc.org>
Commit-Queue: Qingsi Wang <qingsi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#27694}
diff --git a/p2p/client/basic_port_allocator.cc b/p2p/client/basic_port_allocator.cc
index 83c8bf2..4f418ee 100644
--- a/p2p/client/basic_port_allocator.cc
+++ b/p2p/client/basic_port_allocator.cc
@@ -31,6 +31,7 @@
 
 using rtc::CreateRandomId;
 
+namespace cricket {
 namespace {
 
 enum {
@@ -112,9 +113,37 @@
   networks->erase(start_to_remove, networks->end());
 }
 
+bool IsAllowedByCandidateFilter(const Candidate& c, uint32_t filter) {
+  // When binding to any address, before sending packets out, the getsockname
+  // returns all 0s, but after sending packets, it'll be the NIC used to
+  // send. All 0s is not a valid ICE candidate address and should be filtered
+  // out.
+  if (c.address().IsAnyIP()) {
+    return false;
+  }
+
+  if (c.type() == RELAY_PORT_TYPE) {
+    return ((filter & CF_RELAY) != 0);
+  } else if (c.type() == STUN_PORT_TYPE) {
+    return ((filter & CF_REFLEXIVE) != 0);
+  } else if (c.type() == LOCAL_PORT_TYPE) {
+    if ((filter & CF_REFLEXIVE) && !c.address().IsPrivateIP()) {
+      // We allow host candidates if the filter allows server-reflexive
+      // candidates and the candidate is a public IP. Because we don't generate
+      // server-reflexive candidates if they have the same IP as the host
+      // candidate (i.e. when the host candidate is a public IP), filtering to
+      // only server-reflexive candidates won't work right when the host
+      // candidates have public IPs.
+      return true;
+    }
+
+    return ((filter & CF_HOST) != 0);
+  }
+  return false;
+}
+
 }  // namespace
 
-namespace cricket {
 const uint32_t DISABLE_ALL_PHASES =
     PORTALLOCATOR_DISABLE_UDP | PORTALLOCATOR_DISABLE_TCP |
     PORTALLOCATOR_DISABLE_STUN | PORTALLOCATOR_DISABLE_RELAY;
@@ -307,20 +336,57 @@
   if (filter == candidate_filter_) {
     return;
   }
-  // We assume the filter will only change from "ALL" to something else.
-  RTC_DCHECK(candidate_filter_ == CF_ALL);
+  uint32_t prev_filter = candidate_filter_;
   candidate_filter_ = filter;
-  for (PortData& port : ports_) {
-    if (!port.has_pairable_candidate()) {
+  for (PortData& port_data : ports_) {
+    if (port_data.error() || port_data.pruned()) {
       continue;
     }
+    PortData::State cur_state = port_data.state();
+    bool found_signalable_candidate = false;
+    bool found_pairable_candidate = false;
+    cricket::Port* port = port_data.port();
+    for (const auto& c : port->Candidates()) {
+      if (!IsStopped() && !IsAllowedByCandidateFilter(c, prev_filter) &&
+          IsAllowedByCandidateFilter(c, filter)) {
+        // This candidate was not signaled because of not matching the previous
+        // filter (see OnCandidateReady below). Let the Port to fire the signal
+        // again.
+        //
+        // Note that
+        //  1) we would need the Port to enter the state of in-progress of
+        //     gathering to have candidates signaled;
+        //
+        //  2) firing the signal would also let the session set the port ready
+        //     if needed, so that we could form candidate pairs with candidates
+        //     from this port;
+        //
+        //  *  See again OnCandidateReady below for 1) and 2).
+        //
+        //  3) we only try to resurface candidates if we have not stopped
+        //     getting ports, which is always true for the continual gathering.
+        if (!found_signalable_candidate) {
+          found_signalable_candidate = true;
+          port_data.set_state(PortData::STATE_INPROGRESS);
+        }
+        port->SignalCandidateReady(port, c);
+      }
+
+      if (CandidatePairable(c, port)) {
+        found_pairable_candidate = true;
+      }
+    }
+    // Restore the previous state.
+    port_data.set_state(cur_state);
     // Setting a filter may cause a ready port to become non-ready
     // if it no longer has any pairable candidates.
-    if (absl::c_none_of(port.port()->Candidates(),
-                        [this, &port](const Candidate& candidate) {
-                          return CandidatePairable(candidate, port.port());
-                        })) {
-      port.set_has_pairable_candidate(false);
+    //
+    // Note that we only set for the negative case here, since a port would be
+    // set to have pairable candidates when it signals a ready candidate, which
+    // requires the port is still in the progress of gathering/surfacing
+    // candidates, and would be done in the firing of the signal above.
+    if (!found_pairable_candidate) {
+      port_data.set_has_pairable_candidate(false);
     }
   }
 }
@@ -597,6 +663,7 @@
 
 void BasicPortAllocatorSession::GetPortConfigurations() {
   RTC_DCHECK_RUN_ON(network_thread_);
+
   PortConfiguration* config =
       new PortConfiguration(allocator_->stun_servers(), username(), password());
 
@@ -633,7 +700,7 @@
     if (it->inprogress()) {
       // Updating port state to error, which didn't finish allocating candidates
       // yet.
-      it->set_error();
+      it->set_state(PortData::STATE_ERROR);
       send_signal = true;
     }
   }
@@ -1025,7 +1092,7 @@
   }
 
   // Moving to COMPLETE state.
-  data->set_complete();
+  data->set_state(PortData::STATE_COMPLETE);
   // Send candidate allocation complete signal if this was the last port.
   MaybeSignalCandidatesAllocationDone();
 }
@@ -1043,7 +1110,7 @@
 
   // SignalAddressError is currently sent from StunPort/TurnPort.
   // But this signal itself is generic.
-  data->set_error();
+  data->set_state(PortData::STATE_ERROR);
   // Send candidate allocation complete signal if this was the last port.
   MaybeSignalCandidatesAllocationDone();
 }
@@ -1051,34 +1118,7 @@
 bool BasicPortAllocatorSession::CheckCandidateFilter(const Candidate& c) const {
   RTC_DCHECK_RUN_ON(network_thread_);
 
-  uint32_t filter = candidate_filter_;
-
-  // When binding to any address, before sending packets out, the getsockname
-  // returns all 0s, but after sending packets, it'll be the NIC used to
-  // send. All 0s is not a valid ICE candidate address and should be filtered
-  // out.
-  if (c.address().IsAnyIP()) {
-    return false;
-  }
-
-  if (c.type() == RELAY_PORT_TYPE) {
-    return ((filter & CF_RELAY) != 0);
-  } else if (c.type() == STUN_PORT_TYPE) {
-    return ((filter & CF_REFLEXIVE) != 0);
-  } else if (c.type() == LOCAL_PORT_TYPE) {
-    if ((filter & CF_REFLEXIVE) && !c.address().IsPrivateIP()) {
-      // We allow host candidates if the filter allows server-reflexive
-      // candidates and the candidate is a public IP. Because we don't generate
-      // server-reflexive candidates if they have the same IP as the host
-      // candidate (i.e. when the host candidate is a public IP), filtering to
-      // only server-reflexive candidates won't work right when the host
-      // candidates have public IPs.
-      return true;
-    }
-
-    return ((filter & CF_HOST) != 0);
-  }
-  return false;
+  return IsAllowedByCandidateFilter(c, candidate_filter_);
 }
 
 bool BasicPortAllocatorSession::CandidatePairable(const Candidate& c,
@@ -1259,25 +1299,39 @@
   // This can happen if, say, there's a network change event right before an
   // application-triggered ICE restart. Hopefully this problem will just go
   // away if we get rid of the gathering "phases" though, which is planned.
+  //
+  //
+  // PORTALLOCATOR_DISABLE_UDP is used to disable a Port from gathering the host
+  // candidate (and srflx candidate if Port::SharedSocket()), and we do not want
+  // to disable the gathering of these candidates just becaue of an existing
+  // Port over PROTO_UDP, namely a TurnPort over UDP.
   if (absl::c_any_of(session_->ports_,
                      [this](const BasicPortAllocatorSession::PortData& p) {
-                       return p.port()->Network() == network_ &&
+                       return !p.pruned() && p.port()->Network() == network_ &&
                               p.port()->GetProtocol() == PROTO_UDP &&
-                              !p.error();
+                              p.port()->Type() == LOCAL_PORT_TYPE && !p.error();
                      })) {
     *flags |= PORTALLOCATOR_DISABLE_UDP;
   }
+  // Similarly we need to check both the protocol used by an existing Port and
+  // its type.
   if (absl::c_any_of(session_->ports_,
                      [this](const BasicPortAllocatorSession::PortData& p) {
-                       return p.port()->Network() == network_ &&
+                       return !p.pruned() && p.port()->Network() == network_ &&
                               p.port()->GetProtocol() == PROTO_TCP &&
-                              !p.error();
+                              p.port()->Type() == LOCAL_PORT_TYPE && !p.error();
                      })) {
     *flags |= PORTALLOCATOR_DISABLE_TCP;
   }
 
   if (config_ && config) {
-    if (config_->StunServers() == config->StunServers()) {
+    // We need to regather srflx candidates if either of the following
+    // conditions occurs:
+    //  1. The STUN servers are different from the previous gathering.
+    //  2. We will regather host candidates, hence possibly inducing new NAT
+    //     bindings.
+    if (config_->StunServers() == config->StunServers() &&
+        (*flags & PORTALLOCATOR_DISABLE_UDP)) {
       // Already got this STUN servers covered.
       *flags |= PORTALLOCATOR_DISABLE_STUN;
     }
diff --git a/p2p/client/basic_port_allocator.h b/p2p/client/basic_port_allocator.h
index edc6b87..26eea1e 100644
--- a/p2p/client/basic_port_allocator.h
+++ b/p2p/client/basic_port_allocator.h
@@ -125,6 +125,15 @@
   rtc::Thread* network_thread() { return network_thread_; }
   rtc::PacketSocketFactory* socket_factory() { return socket_factory_; }
 
+  // If the new filter allows new types of candidates compared to the previous
+  // filter, gathered candidates that were discarded because of not matching the
+  // previous filter will be signaled if they match the new one.
+  //
+  // We do not perform any regathering since the port allocator flags decide
+  // the type of candidates to gather and the candidate filter only controls the
+  // signaling of candidates. As a result, with the candidate filter changed
+  // alone, all newly allowed candidates for signaling should already be
+  // gathered by the respective cricket::Port.
   void SetCandidateFilter(uint32_t filter) override;
   void StartGettingPorts() override;
   void StopGettingPorts() override;
@@ -158,6 +167,14 @@
  private:
   class PortData {
    public:
+    enum State {
+      STATE_INPROGRESS,  // Still gathering candidates.
+      STATE_COMPLETE,    // All candidates allocated and ready for process.
+      STATE_ERROR,       // Error in gathering candidates.
+      STATE_PRUNED       // Pruned by higher priority ports on the same network
+                         // interface. Only TURN ports may be pruned.
+    };
+
     PortData() {}
     PortData(Port* port, AllocationSequence* seq)
         : port_(port), sequence_(seq) {}
@@ -165,6 +182,7 @@
     Port* port() const { return port_; }
     AllocationSequence* sequence() const { return sequence_; }
     bool has_pairable_candidate() const { return has_pairable_candidate_; }
+    State state() const { return state_; }
     bool complete() const { return state_ == STATE_COMPLETE; }
     bool error() const { return state_ == STATE_ERROR; }
     bool pruned() const { return state_ == STATE_PRUNED; }
@@ -187,20 +205,12 @@
       }
       has_pairable_candidate_ = has_pairable_candidate;
     }
-    void set_complete() { state_ = STATE_COMPLETE; }
-    void set_error() {
-      RTC_DCHECK(state_ == STATE_INPROGRESS);
-      state_ = STATE_ERROR;
+    void set_state(State state) {
+      RTC_DCHECK(state != STATE_ERROR || state_ == STATE_INPROGRESS);
+      state_ = state;
     }
 
    private:
-    enum State {
-      STATE_INPROGRESS,  // Still gathering candidates.
-      STATE_COMPLETE,    // All candidates allocated and ready for process.
-      STATE_ERROR,       // Error in gathering candidates.
-      STATE_PRUNED       // Pruned by higher priority ports on the same network
-                         // interface. Only TURN ports may be pruned.
-    };
     Port* port_ = nullptr;
     AllocationSequence* sequence_ = nullptr;
     bool has_pairable_candidate_ = false;
diff --git a/p2p/client/basic_port_allocator_unittest.cc b/p2p/client/basic_port_allocator_unittest.cc
index 1682e9d..067e757 100644
--- a/p2p/client/basic_port_allocator_unittest.cc
+++ b/p2p/client/basic_port_allocator_unittest.cc
@@ -1266,7 +1266,7 @@
 TEST_F(BasicPortAllocatorTest,
        TestDisableAdapterEnumerationWithoutNatRelayTransportOnly) {
   ResetWithStunServerNoNat(kStunAddr);
-  allocator().set_candidate_filter(CF_RELAY);
+  allocator().SetCandidateFilter(CF_RELAY);
   // Expect to see no ports and no candidates.
   CheckDisableAdapterEnumeration(0U, rtc::IPAddress(), rtc::IPAddress(),
                                  rtc::IPAddress(), rtc::IPAddress());
@@ -1527,7 +1527,7 @@
   AddInterface(kClientAddr);
   ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
   // Set candidate filter *after* creating the session. Should have no effect.
-  allocator().set_candidate_filter(CF_RELAY);
+  allocator().SetCandidateFilter(CF_RELAY);
   session_->StartGettingPorts();
   // 7 candidates and 4 ports is what we would normally get (see the
   // TestGetAllPorts* tests).
@@ -1546,7 +1546,7 @@
   AddInterface(kClientAddr);
   // GTURN is not configured here.
   ResetWithTurnServersNoNat(kTurnUdpIntAddr, rtc::SocketAddress());
-  allocator().set_candidate_filter(CF_RELAY);
+  allocator().SetCandidateFilter(CF_RELAY);
   ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
   session_->StartGettingPorts();
   EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
@@ -1565,7 +1565,7 @@
 TEST_F(BasicPortAllocatorTest, TestCandidateFilterWithHostOnly) {
   AddInterface(kClientAddr);
   allocator().set_flags(PORTALLOCATOR_ENABLE_SHARED_SOCKET);
-  allocator().set_candidate_filter(CF_HOST);
+  allocator().SetCandidateFilter(CF_HOST);
   ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
   session_->StartGettingPorts();
   EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
@@ -1583,7 +1583,7 @@
   ResetWithStunServerAndNat(kStunAddr);
 
   allocator().set_flags(PORTALLOCATOR_ENABLE_SHARED_SOCKET);
-  allocator().set_candidate_filter(CF_REFLEXIVE);
+  allocator().SetCandidateFilter(CF_REFLEXIVE);
   ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
   session_->StartGettingPorts();
   EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
@@ -1602,7 +1602,7 @@
 TEST_F(BasicPortAllocatorTest, TestCandidateFilterWithReflexiveOnlyAndNoNAT) {
   AddInterface(kClientAddr);
   allocator().set_flags(PORTALLOCATOR_ENABLE_SHARED_SOCKET);
-  allocator().set_candidate_filter(CF_REFLEXIVE);
+  allocator().SetCandidateFilter(CF_REFLEXIVE);
   ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
   session_->StartGettingPorts();
   EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
@@ -2133,7 +2133,7 @@
                            kDefaultAllocationTimeout, fake_clock);
   size_t initial_candidates_size = peeked_session->ReadyCandidates().size();
   size_t initial_ports_size = peeked_session->ReadyPorts().size();
-  allocator_->set_candidate_filter(CF_RELAY);
+  allocator_->SetCandidateFilter(CF_RELAY);
   // Assume that when TakePooledSession is called, the candidate filter will be
   // applied to the pooled session. This is tested by PortAllocatorTest.
   session_ =
@@ -2157,6 +2157,145 @@
   }
 }
 
+// Test that candidates that do not match a previous candidate filter can be
+// surfaced if they match the new one after setting the filter value.
+TEST_F(BasicPortAllocatorTest,
+       SurfaceNewCandidatesAfterSetCandidateFilterToAddCandidateTypes) {
+  // We would still surface a host candidate if the IP is public, even though it
+  // is disabled by the candidate filter. See
+  // BasicPortAllocatorSession::CheckCandidateFilter. Use the private address so
+  // that the srflx candidate is not equivalent to the host candidate.
+  AddInterface(kPrivateAddr);
+  ResetWithStunServerAndNat(kStunAddr);
+
+  AddTurnServers(kTurnUdpIntAddr, rtc::SocketAddress());
+
+  allocator_->set_flags(allocator().flags() |
+                        PORTALLOCATOR_ENABLE_SHARED_SOCKET |
+                        PORTALLOCATOR_DISABLE_TCP);
+
+  allocator_->SetCandidateFilter(CF_NONE);
+  ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
+  session_->StartGettingPorts();
+  EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
+                             kDefaultAllocationTimeout, fake_clock);
+  EXPECT_TRUE(candidates_.empty());
+  EXPECT_TRUE(ports_.empty());
+
+  // Surface the relay candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_RELAY);
+  ASSERT_EQ_SIMULATED_WAIT(1u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(RELAY_PORT_TYPE, candidates_.back().type());
+  EXPECT_EQ(1u, ports_.size());
+
+  // Surface the srflx candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_RELAY | CF_REFLEXIVE);
+  ASSERT_EQ_SIMULATED_WAIT(2u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(STUN_PORT_TYPE, candidates_.back().type());
+  EXPECT_EQ(2u, ports_.size());
+
+  // Surface the srflx candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_ALL);
+  ASSERT_EQ_SIMULATED_WAIT(3u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(LOCAL_PORT_TYPE, candidates_.back().type());
+  EXPECT_EQ(2u, ports_.size());
+}
+
+// This is a similar test as
+// SurfaceNewCandidatesAfterSetCandidateFilterToAddCandidateTypes, and we
+// test the transitions for which the new filter value is not a super set of the
+// previous value.
+TEST_F(
+    BasicPortAllocatorTest,
+    SurfaceNewCandidatesAfterSetCandidateFilterToAllowDifferentCandidateTypes) {
+  // We would still surface a host candidate if the IP is public, even though it
+  // is disabled by the candidate filter. See
+  // BasicPortAllocatorSession::CheckCandidateFilter. Use the private address so
+  // that the srflx candidate is not equivalent to the host candidate.
+  AddInterface(kPrivateAddr);
+  ResetWithStunServerAndNat(kStunAddr);
+
+  AddTurnServers(kTurnUdpIntAddr, rtc::SocketAddress());
+
+  allocator_->set_flags(allocator().flags() |
+                        PORTALLOCATOR_ENABLE_SHARED_SOCKET |
+                        PORTALLOCATOR_DISABLE_TCP);
+
+  allocator_->SetCandidateFilter(CF_NONE);
+  ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
+  session_->StartGettingPorts();
+  EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
+                             kDefaultAllocationTimeout, fake_clock);
+  EXPECT_TRUE(candidates_.empty());
+  EXPECT_TRUE(ports_.empty());
+
+  // Surface the relay candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_RELAY);
+  EXPECT_EQ_SIMULATED_WAIT(1u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(RELAY_PORT_TYPE, candidates_.back().type());
+  EXPECT_EQ(1u, ports_.size());
+
+  // Surface the srflx candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_REFLEXIVE);
+  EXPECT_EQ_SIMULATED_WAIT(2u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(STUN_PORT_TYPE, candidates_.back().type());
+  EXPECT_EQ(2u, ports_.size());
+
+  // Surface the host candidate previously gathered but not signaled.
+  session_->SetCandidateFilter(CF_HOST);
+  EXPECT_EQ_SIMULATED_WAIT(3u, candidates_.size(), kDefaultAllocationTimeout,
+                           fake_clock);
+  EXPECT_EQ(LOCAL_PORT_TYPE, candidates_.back().type());
+  // We use a shared socket and cricket::UDPPort handles the srflx candidate.
+  EXPECT_EQ(2u, ports_.size());
+}
+
+// Test that after an allocation session has stopped getting ports, changing the
+// candidate filter to allow new types of gathered candidates does not surface
+// any candidate.
+TEST_F(BasicPortAllocatorTest,
+       NoCandidateSurfacedWhenUpdatingCandidateFilterIfSessionStopped) {
+  AddInterface(kPrivateAddr);
+  ResetWithStunServerAndNat(kStunAddr);
+
+  AddTurnServers(kTurnUdpIntAddr, rtc::SocketAddress());
+
+  allocator_->set_flags(allocator().flags() |
+                        PORTALLOCATOR_ENABLE_SHARED_SOCKET |
+                        PORTALLOCATOR_DISABLE_TCP);
+
+  allocator_->SetCandidateFilter(CF_NONE);
+  ASSERT_TRUE(CreateSession(ICE_CANDIDATE_COMPONENT_RTP));
+  session_->StartGettingPorts();
+  EXPECT_TRUE_SIMULATED_WAIT(candidate_allocation_done_,
+                             kDefaultAllocationTimeout, fake_clock);
+  auto test_invariants = [this]() {
+    EXPECT_TRUE(candidates_.empty());
+    EXPECT_TRUE(ports_.empty());
+  };
+
+  test_invariants();
+
+  session_->StopGettingPorts();
+
+  session_->SetCandidateFilter(CF_RELAY);
+  SIMULATED_WAIT(false, kDefaultAllocationTimeout, fake_clock);
+  test_invariants();
+
+  session_->SetCandidateFilter(CF_RELAY | CF_REFLEXIVE);
+  SIMULATED_WAIT(false, kDefaultAllocationTimeout, fake_clock);
+  test_invariants();
+
+  session_->SetCandidateFilter(CF_ALL);
+  SIMULATED_WAIT(false, kDefaultAllocationTimeout, fake_clock);
+  test_invariants();
+}
+
 TEST_F(BasicPortAllocatorTest, SetStunKeepaliveIntervalForPorts) {
   const int pool_size = 1;
   const int expected_stun_keepalive_interval = 123;