GOOG_PING: improve handshake

This patch improves handshake wrt GOOG_PING support so that
- if goog_ping_enable: sender send it's goog-ping version until it gets
STUN_BINDING_RESPONSE
- receiver only sends it's goog-ping-version if getting a
goog-ping-version in the request

This means that the overhead of STUN_ATTR_GOOG_MISC_INFO is only
- added on STUN_BINDING_REQUEST until a response is received.
- added on STUN_BINDING_RESPONSE if remote peer request it.

This is wire compatible with older versions so that
- new sender will enable GOOG_PING with new/old receiver.
- old sender will enable GOOG_PING with old receiver.
- old version will not enable GOOG_PING with new receiver
  (receiver expecting sender to announce first).

BUG: webrtc:11100
Change-Id: Ib3434c593988188150f4c7506918139aaf138d0c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/165787
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Sebastian Jansson <srte@webrtc.org>
Commit-Queue: Jonas Oreland <jonaso@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#30269}
diff --git a/api/transport/stun.h b/api/transport/stun.h
index 7860da2..41f76a1 100644
--- a/api/transport/stun.h
+++ b/api/transport/stun.h
@@ -678,7 +678,9 @@
 // consistent with those used in ConnectionRequest::Prepare when forming a STUN
 // message for the ICE connectivity check, and they are used when parsing a
 // received STUN message.
-enum class IceGoogMiscInfoBindingRequestAttributeIndex {};
+enum class IceGoogMiscInfoBindingRequestAttributeIndex {
+  SUPPORT_GOOG_PING_VERSION = 0,
+};
 
 enum class IceGoogMiscInfoBindingResponseAttributeIndex {
   SUPPORT_GOOG_PING_VERSION = 0,
diff --git a/p2p/base/connection.cc b/p2p/base/connection.cc
index cd5d290..f3692c5 100644
--- a/p2p/base/connection.cc
+++ b/p2p/base/connection.cc
@@ -143,7 +143,11 @@
 // Default field trials.
 const cricket::IceFieldTrials kDefaultFieldTrials;
 
-constexpr int kSupportGoogPingVersionIndex =
+constexpr int kSupportGoogPingVersionRequestIndex =
+    static_cast<int>(cricket::IceGoogMiscInfoBindingRequestAttributeIndex::
+                         SUPPORT_GOOG_PING_VERSION);
+
+constexpr int kSupportGoogPingVersionResponseIndex =
     static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
                          SUPPORT_GOOG_PING_VERSION);
 
@@ -224,6 +228,17 @@
   request->AddAttribute(std::make_unique<StunUInt32Attribute>(
       STUN_ATTR_PRIORITY, prflx_priority));
 
+  if (connection_->field_trials_->enable_goog_ping &&
+      !connection_->remote_support_goog_ping_.has_value()) {
+    // Check if remote supports GOOG PING by announcing which version we
+    // support. This is sent on all STUN_BINDING_REQUEST until we get a
+    // STUN_BINDING_RESPONSE.
+    auto list =
+        StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO);
+    list->AddTypeAtIndex(kSupportGoogPingVersionRequestIndex, kGoogPingVersion);
+    request->AddAttribute(std::move(list));
+  }
+
   if (connection_->ShouldSendGoogPing(request)) {
     request->SetType(GOOG_PING_REQUEST);
     request->ClearAttributes();
@@ -647,10 +662,18 @@
       STUN_ATTR_XOR_MAPPED_ADDRESS, remote_candidate_.address()));
 
   if (field_trials_->announce_goog_ping) {
-    auto list =
-        StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO);
-    list->AddTypeAtIndex(kSupportGoogPingVersionIndex, kGoogPingVersion);
-    response.AddAttribute(std::move(list));
+    // Check if request contains a announce-request.
+    auto goog_misc = request->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO);
+    if (goog_misc != nullptr &&
+        goog_misc->Size() >= kSupportGoogPingVersionRequestIndex &&
+        // Which version can we handle...currently any >= 1
+        goog_misc->GetType(kSupportGoogPingVersionRequestIndex) >= 1) {
+      auto list =
+          StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO);
+      list->AddTypeAtIndex(kSupportGoogPingVersionResponseIndex,
+                           kGoogPingVersion);
+      response.AddAttribute(std::move(list));
+    }
   }
 
   response.AddMessageIntegrity(local_candidate().password());
@@ -1053,12 +1076,18 @@
       response->reduced_transaction_id());
 
   if (request->msg()->type() == STUN_BINDING_REQUEST) {
-    auto goog_misc = response->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO);
-    if (goog_misc != nullptr &&
-        goog_misc->Size() >= kSupportGoogPingVersionIndex &&
-        goog_misc->GetType(kSupportGoogPingVersionIndex) >= kGoogPingVersion) {
-      // The remote peer has indicated that it supports GOOG_PING.
-      remote_support_goog_ping_ = true;
+    if (!remote_support_goog_ping_.has_value()) {
+      auto goog_misc = response->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO);
+      if (goog_misc != nullptr &&
+          goog_misc->Size() >= kSupportGoogPingVersionResponseIndex) {
+        // The remote peer has indicated that it {does/does not} supports
+        // GOOG_PING.
+        remote_support_goog_ping_ =
+            goog_misc->GetType(kSupportGoogPingVersionResponseIndex) >=
+            kGoogPingVersion;
+      } else {
+        remote_support_goog_ping_ = false;
+      }
     }
 
     MaybeUpdateLocalCandidate(request, response);
@@ -1289,12 +1318,15 @@
 }
 
 bool Connection::ShouldSendGoogPing(const StunMessage* message) {
-  if (remote_support_goog_ping_ && cached_stun_binding_ &&
+  if (remote_support_goog_ping_ == true && cached_stun_binding_ &&
       cached_stun_binding_->EqualAttributes(message, [](int type) {
         // Ignore these attributes.
+        // NOTE: Consider what to do if adding more content to
+        // STUN_ATTR_GOOG_MISC_INFO
         return type != STUN_ATTR_FINGERPRINT &&
                type != STUN_ATTR_MESSAGE_INTEGRITY &&
-               type != STUN_ATTR_RETRANSMIT_COUNT;
+               type != STUN_ATTR_RETRANSMIT_COUNT &&
+               type != STUN_ATTR_GOOG_MISC_INFO;
       })) {
     return true;
   }
diff --git a/p2p/base/p2p_transport_channel_ice_field_trials.h b/p2p/base/p2p_transport_channel_ice_field_trials.h
index 95f5bb5..e55f7ce 100644
--- a/p2p/base/p2p_transport_channel_ice_field_trials.h
+++ b/p2p/base/p2p_transport_channel_ice_field_trials.h
@@ -32,8 +32,9 @@
   // give us chance to find a better connection before starting.
   absl::optional<int> initial_select_dampening_ping_received;
 
-  // Announce GOOG_PING support in STUN_BINDING_RESPONSE.
-  bool announce_goog_ping = false;
+  // Announce GOOG_PING support in STUN_BINDING_RESPONSE if requested
+  // by peer.
+  bool announce_goog_ping = true;
 
   // Enable sending GOOG_PING if remote announce it.
   bool enable_goog_ping = false;
diff --git a/p2p/base/port_unittest.cc b/p2p/base/port_unittest.cc
index f203d48..8d6d99e 100644
--- a/p2p/base/port_unittest.cc
+++ b/p2p/base/port_unittest.cc
@@ -2648,21 +2648,36 @@
 namespace {
 
 // Utility function for testing goog ping.
-absl::optional<int> GetSupportedGoogPingVersion(const StunMessage* response) {
-  auto goog_misc = response->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO);
+absl::optional<int> GetSupportedGoogPingVersion(const StunMessage* msg) {
+  auto goog_misc = msg->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO);
   if (goog_misc == nullptr) {
     return absl::nullopt;
   }
 
-  if (goog_misc->Size() <
-      static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
-                           SUPPORT_GOOG_PING_VERSION)) {
-    return absl::nullopt;
+  if (msg->type() == STUN_BINDING_REQUEST) {
+    if (goog_misc->Size() <
+        static_cast<int>(cricket::IceGoogMiscInfoBindingRequestAttributeIndex::
+                             SUPPORT_GOOG_PING_VERSION)) {
+      return absl::nullopt;
+    }
+
+    return goog_misc->GetType(
+        static_cast<int>(cricket::IceGoogMiscInfoBindingRequestAttributeIndex::
+                             SUPPORT_GOOG_PING_VERSION));
   }
 
-  return goog_misc->GetType(
-      static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
-                           SUPPORT_GOOG_PING_VERSION));
+  if (msg->type() == STUN_BINDING_RESPONSE) {
+    if (goog_misc->Size() <
+        static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
+                             SUPPORT_GOOG_PING_VERSION)) {
+      return absl::nullopt;
+    }
+
+    return goog_misc->GetType(
+        static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
+                             SUPPORT_GOOG_PING_VERSION));
+  }
+  return absl::nullopt;
 }
 
 }  // namespace
@@ -2710,6 +2725,11 @@
 
   ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout);
   const IceMessage* request1 = port1->last_stun_msg();
+
+  ASSERT_EQ(trials.enable_goog_ping,
+            GetSupportedGoogPingVersion(request1) &&
+                GetSupportedGoogPingVersion(request1) >= kGoogPingVersion);
+
   auto* con = port2->CreateConnection(port1->Candidates()[0],
                                       cricket::Port::ORIGIN_MESSAGE);
   con->SetIceFieldTrials(&trials);
@@ -2718,8 +2738,8 @@
 
   // Then check the response matches the settings.
   const auto* response = port2->last_stun_msg();
-  ASSERT_EQ(response->type(), STUN_BINDING_RESPONSE);
-  ASSERT_EQ(trials.announce_goog_ping,
+  EXPECT_EQ(response->type(), STUN_BINDING_RESPONSE);
+  EXPECT_EQ(trials.enable_goog_ping && trials.announce_goog_ping,
             GetSupportedGoogPingVersion(response) &&
                 GetSupportedGoogPingVersion(response) >= kGoogPingVersion);
 
@@ -2741,6 +2761,10 @@
     con->SendGoogPingResponse(request2);
   } else {
     ASSERT_EQ(request2->type(), STUN_BINDING_REQUEST);
+    // If we sent a BINDING with enable, and we got a reply that
+    // didn't contain announce, the next ping should not contain
+    // the enable again.
+    ASSERT_FALSE(GetSupportedGoogPingVersion(request2).has_value());
     con->SendStunBindingResponse(request2);
   }
 
@@ -2757,6 +2781,170 @@
   ch1.Stop();
 }
 
+// This test if a someone send a STUN_BINDING with unsupported version
+// (kGoogPingVersion == 0)
+TEST_F(PortTest, TestGoogPingUnsupportedVersionInStunBinding) {
+  IceFieldTrials trials;
+  trials.announce_goog_ping = true;
+  trials.enable_goog_ping = true;
+
+  auto port1_unique =
+      CreateTestPort(kLocalAddr1, "lfrag", "lpass",
+                     cricket::ICEROLE_CONTROLLING, kTiebreaker1);
+  auto* port1 = port1_unique.get();
+  auto port2 = CreateTestPort(kLocalAddr2, "rfrag", "rpass",
+                              cricket::ICEROLE_CONTROLLED, kTiebreaker2);
+
+  TestChannel ch1(std::move(port1_unique));
+  // Block usage of STUN_ATTR_USE_CANDIDATE so that
+  // ch1.conn() will sent GOOG_PING_REQUEST directly.
+  // This only makes test a bit shorter...
+  ch1.SetIceMode(ICEMODE_LITE);
+  // Start gathering candidates.
+  ch1.Start();
+  port2->PrepareAddress();
+
+  ASSERT_EQ_WAIT(1, ch1.complete_count(), kDefaultTimeout);
+  ASSERT_FALSE(port2->Candidates().empty());
+
+  ch1.CreateConnection(GetCandidate(port2.get()));
+  ASSERT_TRUE(ch1.conn() != NULL);
+  EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state());
+  ch1.conn()->SetIceFieldTrials(&trials);
+
+  // Send ping.
+  ch1.Ping();
+
+  ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout);
+  const IceMessage* request1 = port1->last_stun_msg();
+
+  ASSERT_TRUE(GetSupportedGoogPingVersion(request1) &&
+              GetSupportedGoogPingVersion(request1) >= kGoogPingVersion);
+
+  // Modify the STUN message request1 to send GetSupportedGoogPingVersion == 0
+  auto modified_request1 = request1->Clone();
+  ASSERT_TRUE(modified_request1->RemoveAttribute(STUN_ATTR_GOOG_MISC_INFO) !=
+              nullptr);
+  ASSERT_TRUE(modified_request1->RemoveAttribute(STUN_ATTR_MESSAGE_INTEGRITY) !=
+              nullptr);
+  {
+    auto list =
+        StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO);
+    list->AddTypeAtIndex(
+        static_cast<uint16_t>(
+            cricket::IceGoogMiscInfoBindingRequestAttributeIndex::
+                SUPPORT_GOOG_PING_VERSION),
+        /* version */ 0);
+    modified_request1->AddAttribute(std::move(list));
+    modified_request1->AddMessageIntegrity("rpass");
+  }
+  auto* con = port2->CreateConnection(port1->Candidates()[0],
+                                      cricket::Port::ORIGIN_MESSAGE);
+  con->SetIceFieldTrials(&trials);
+
+  con->SendStunBindingResponse(modified_request1.get());
+
+  // Then check the response matches the settings.
+  const auto* response = port2->last_stun_msg();
+  EXPECT_EQ(response->type(), STUN_BINDING_RESPONSE);
+  EXPECT_FALSE(GetSupportedGoogPingVersion(response));
+
+  ch1.Stop();
+}
+
+// This test if a someone send a STUN_BINDING_RESPONSE with unsupported version
+// (kGoogPingVersion == 0)
+TEST_F(PortTest, TestGoogPingUnsupportedVersionInStunBindingResponse) {
+  IceFieldTrials trials;
+  trials.announce_goog_ping = true;
+  trials.enable_goog_ping = true;
+
+  auto port1_unique =
+      CreateTestPort(kLocalAddr1, "lfrag", "lpass",
+                     cricket::ICEROLE_CONTROLLING, kTiebreaker1);
+  auto* port1 = port1_unique.get();
+  auto port2 = CreateTestPort(kLocalAddr2, "rfrag", "rpass",
+                              cricket::ICEROLE_CONTROLLED, kTiebreaker2);
+
+  TestChannel ch1(std::move(port1_unique));
+  // Block usage of STUN_ATTR_USE_CANDIDATE so that
+  // ch1.conn() will sent GOOG_PING_REQUEST directly.
+  // This only makes test a bit shorter...
+  ch1.SetIceMode(ICEMODE_LITE);
+  // Start gathering candidates.
+  ch1.Start();
+  port2->PrepareAddress();
+
+  ASSERT_EQ_WAIT(1, ch1.complete_count(), kDefaultTimeout);
+  ASSERT_FALSE(port2->Candidates().empty());
+
+  ch1.CreateConnection(GetCandidate(port2.get()));
+  ASSERT_TRUE(ch1.conn() != NULL);
+  EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state());
+  ch1.conn()->SetIceFieldTrials(&trials);
+
+  // Send ping.
+  ch1.Ping();
+
+  ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout);
+  const IceMessage* request1 = port1->last_stun_msg();
+
+  ASSERT_TRUE(GetSupportedGoogPingVersion(request1) &&
+              GetSupportedGoogPingVersion(request1) >= kGoogPingVersion);
+
+  auto* con = port2->CreateConnection(port1->Candidates()[0],
+                                      cricket::Port::ORIGIN_MESSAGE);
+  con->SetIceFieldTrials(&trials);
+
+  con->SendStunBindingResponse(request1);
+
+  // Then check the response matches the settings.
+  const auto* response = port2->last_stun_msg();
+  EXPECT_EQ(response->type(), STUN_BINDING_RESPONSE);
+  EXPECT_TRUE(GetSupportedGoogPingVersion(response));
+
+  // Modify the STUN message response to contain GetSupportedGoogPingVersion ==
+  // 0
+  auto modified_response = response->Clone();
+  ASSERT_TRUE(modified_response->RemoveAttribute(STUN_ATTR_GOOG_MISC_INFO) !=
+              nullptr);
+  ASSERT_TRUE(modified_response->RemoveAttribute(STUN_ATTR_MESSAGE_INTEGRITY) !=
+              nullptr);
+  ASSERT_TRUE(modified_response->RemoveAttribute(STUN_ATTR_FINGERPRINT) !=
+              nullptr);
+  {
+    auto list =
+        StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO);
+    list->AddTypeAtIndex(
+        static_cast<uint16_t>(
+            cricket::IceGoogMiscInfoBindingResponseAttributeIndex::
+                SUPPORT_GOOG_PING_VERSION),
+        /* version */ 0);
+    modified_response->AddAttribute(std::move(list));
+    modified_response->AddMessageIntegrity("rpass");
+    modified_response->AddFingerprint();
+  }
+
+  rtc::ByteBufferWriter buf;
+  modified_response->Write(&buf);
+
+  // Feeding the modified respone message back.
+  ch1.conn()->OnReadPacket(buf.Data(), buf.Length(), /* packet_time_us */ -1);
+
+  port1->Reset();
+  port2->Reset();
+
+  ch1.Ping();
+  ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout);
+
+  // This should now be a STUN_BINDING...without a kGoogPingVersion
+  const IceMessage* request2 = port1->last_stun_msg();
+  EXPECT_EQ(request2->type(), STUN_BINDING_REQUEST);
+  EXPECT_FALSE(GetSupportedGoogPingVersion(request2));
+
+  ch1.Stop();
+}
+
 INSTANTIATE_TEST_SUITE_P(GoogPingTest,
                          GoogPingTest,
                          // test all combinations of <announce, enable> pairs.