dtls-in-stun: Defer writable until Ice has become writable

This patch modifies the DtlsTransport::set_writable
to only sinal writable (true) if ice has been writable
at least once. This is the behaviour before dtls-in-stun
(since the handshake starts when ice becomes writable).

This change reduce the observable changes (from within webrtc) of
the dtls-in-stun feature, keeping the positive aspects (i.e.
faster/more-robust dtls handshake)

BUG=webrtc:367395350

Change-Id: I2a70c8ecb6fcd83734c211a6b40aef02f6c23f55
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/381940
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Jonas Oreland <jonaso@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#44171}
diff --git a/p2p/dtls/dtls_transport.cc b/p2p/dtls/dtls_transport.cc
index 5e55bdb..5d0740b 100644
--- a/p2p/dtls/dtls_transport.cc
+++ b/p2p/dtls/dtls_transport.cc
@@ -619,6 +619,8 @@
                       << ": ice_transport writable state changed to "
                       << ice_transport_->writable();
 
+  ice_has_been_writable_ = true;
+
   if (!dtls_active_) {
     // Not doing DTLS.
     // Note: SignalWritableState fired by set_writable.
@@ -910,6 +912,16 @@
   if (writable_ == writable) {
     return;
   }
+  if (writable && !ice_has_been_writable_) {
+    // Wait with reporting writable until ICE has become writable once,
+    // so as to not confuse other part of stack (such as sctp).
+    RTC_DCHECK(dtls_in_stun_);
+    RTC_LOG(LS_INFO)
+        << ToString()
+        << ": defer set_writable(true) until ICE has become writable once";
+    return;
+  }
+
   if (event_log_) {
     event_log_->Log(
         std::make_unique<webrtc::RtcEventDtlsWritableState>(writable));
diff --git a/p2p/dtls/dtls_transport.h b/p2p/dtls/dtls_transport.h
index 3f0a51e..3bd5b68 100644
--- a/p2p/dtls/dtls_transport.h
+++ b/p2p/dtls/dtls_transport.h
@@ -286,6 +286,12 @@
   bool receiving_ = false;
   bool writable_ = false;
 
+  // Keep track if ICE has ever been writable.
+  // This is used to prevent "spurious" Dtls::Writable with DTLS-in-STUN,
+  // where DTLS can become writable before ICE. This can confuse other parts
+  // of the stack.
+  bool ice_has_been_writable_ = false;
+
   webrtc::RtcEventLog* const event_log_;
 
   // Initialized in constructor based on WebRTC-IceHandshakeDtls,
diff --git a/p2p/dtls/dtls_transport_unittest.cc b/p2p/dtls/dtls_transport_unittest.cc
index ae69627..de8f3f5 100644
--- a/p2p/dtls/dtls_transport_unittest.cc
+++ b/p2p/dtls/dtls_transport_unittest.cc
@@ -1457,6 +1457,67 @@
   ClearPacketFilters();
 }
 
+TEST_P(DtlsTransportDtlsInStunTest,
+       DtlsDoesNotSignalWritableUnlessIceWritableOnce) {
+  Prepare(/* rtt_estimate= */ false);
+  AddPacketLogging();
+
+  RTC_LOG(LS_INFO) << "client1: " << std::get<0>(GetParam());
+  RTC_LOG(LS_INFO) << "client2: " << std::get<1>(GetParam());
+
+  ASSERT_TRUE(client1_.ConnectIceTransport(&client2_));
+
+  client1_.SendIcePing();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client2_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_REQUEST) == 1;
+  }));
+  client2_.SendIcePingConf();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client1_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_RESPONSE) == 1;
+  }));
+  client1_.SendIcePing();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client2_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_REQUEST) == 2;
+  }));
+  client2_.SendIcePingConf();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client1_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_RESPONSE) == 2;
+  }));
+
+  bool dtls_in_stun = std::get<0>(GetParam()).dtls_in_stun &&
+                      std::get<1>(GetParam()).dtls_in_stun;
+  if (dtls_in_stun) {
+    ASSERT_TRUE(client1_.dtls_transport()->writable());
+  }
+  // Ice has never been writable on client2.
+  ASSERT_FALSE(client2_.dtls_transport()->writable());
+
+  client2_.SendIcePing();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client1_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_REQUEST) == 1;
+  }));
+  client1_.SendIcePingConf();
+  ASSERT_TRUE(WaitUntil([&] {
+    return client2_.fake_ice_transport()->GetCountOfReceivedStunMessages(
+               STUN_BINDING_RESPONSE) == 1;
+  }));
+
+  EXPECT_TRUE(WaitUntil([&] {
+    return client1_.dtls_transport()->writable() &&
+           client2_.dtls_transport()->writable();
+  }));
+
+  EXPECT_TRUE(client1_.dtls_transport()->writable());
+  EXPECT_TRUE(client2_.dtls_transport()->writable());
+
+  ClearPacketFilters();
+}
+
 INSTANTIATE_TEST_SUITE_P(DtlsTransportDtlsInStunTest,
                          DtlsTransportDtlsInStunTest,
                          testing::ValuesIn(AllEndpointVariants()));