Add StunDictionary - patch 2

This patch hooks up the StunDictionary to Connection
and P2PTransportChannel.

Bug: webrtc:15392
Change-Id: Ibeea4d8706ebd42f2353d9d300631c02bf0d484d
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/315100
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Jonas Oreland <jonaso@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40519}
diff --git a/p2p/base/connection.cc b/p2p/base/connection.cc
index c5e6993..1ef42cc 100644
--- a/p2p/base/connection.cc
+++ b/p2p/base/connection.cc
@@ -704,6 +704,28 @@
     }
   }
 
+  const StunByteStringAttribute* delta =
+      message->GetByteString(STUN_ATTR_GOOG_DELTA);
+  if (delta) {
+    if (field_trials_->answer_goog_delta && goog_delta_consumer_) {
+      auto ack = (*goog_delta_consumer_)(delta);
+      if (ack) {
+        RTC_LOG(LS_INFO) << "Sending GOOG_DELTA_ACK"
+                         << " delta len: " << delta->length();
+        response.AddAttribute(std::move(ack));
+      } else {
+        RTC_LOG(LS_ERROR) << "GOOG_DELTA consumer did not return ack!";
+      }
+    } else {
+      RTC_LOG(LS_WARNING) << "Ignore GOOG_DELTA"
+                          << " len: " << delta->length()
+                          << " answer_goog_delta = "
+                          << field_trials_->answer_goog_delta
+                          << " goog_delta_consumer_ = "
+                          << goog_delta_consumer_.has_value();
+    }
+  }
+
   response.AddMessageIntegrity(local_candidate().password());
   response.AddFingerprint();
 
@@ -933,7 +955,8 @@
   return last_ping_sent_;
 }
 
-void Connection::Ping(int64_t now) {
+void Connection::Ping(int64_t now,
+                      std::unique_ptr<StunByteStringAttribute> delta) {
   RTC_DCHECK_RUN_ON(network_thread_);
   if (!port_)
     return;
@@ -948,10 +971,11 @@
     nomination = nomination_;
   }
 
-  auto req =
-      std::make_unique<ConnectionRequest>(requests_, this, BuildPingRequest());
+  bool has_delta = delta != nullptr;
+  auto req = std::make_unique<ConnectionRequest>(
+      requests_, this, BuildPingRequest(std::move(delta)));
 
-  if (ShouldSendGoogPing(req->msg())) {
+  if (!has_delta && ShouldSendGoogPing(req->msg())) {
     auto message = std::make_unique<IceMessage>(GOOG_PING_REQUEST, req->id());
     message->AddMessageIntegrity32(remote_candidate_.password());
     req.reset(new ConnectionRequest(requests_, this, std::move(message)));
@@ -966,7 +990,8 @@
   num_pings_sent_++;
 }
 
-std::unique_ptr<IceMessage> Connection::BuildPingRequest() {
+std::unique_ptr<IceMessage> Connection::BuildPingRequest(
+    std::unique_ptr<StunByteStringAttribute> delta) {
   auto message = std::make_unique<IceMessage>(STUN_BINDING_REQUEST);
   // Note that the order of attributes does not impact the parsing on the
   // receiver side. The attribute is retrieved then by iterating and matching
@@ -1022,6 +1047,13 @@
     list->AddTypeAtIndex(kSupportGoogPingVersionRequestIndex, kGoogPingVersion);
     message->AddAttribute(std::move(list));
   }
+
+  if (delta) {
+    RTC_DCHECK(delta->type() == STUN_ATTR_GOOG_DELTA);
+    RTC_LOG(LS_INFO) << "Sending GOOG_DELTA: len: " << delta->length();
+    message->AddAttribute(std::move(delta));
+  }
+
   message->AddMessageIntegrity(remote_candidate_.password());
   message->AddFingerprint();
 
@@ -1393,6 +1425,34 @@
       cached_stun_binding_ = request->msg()->Clone();
     }
   }
+
+  // Did we send a delta ?
+  const bool sent_goog_delta =
+      request->msg()->GetByteString(STUN_ATTR_GOOG_DELTA) != nullptr;
+  // Did we get a GOOG_DELTA_ACK ?
+  const StunUInt64Attribute* delta_ack =
+      response->GetUInt64(STUN_ATTR_GOOG_DELTA_ACK);
+
+  if (goog_delta_ack_consumer_) {
+    if (sent_goog_delta && delta_ack) {
+      RTC_LOG(LS_VERBOSE) << "Got GOOG_DELTA_ACK len: " << delta_ack->length();
+      (*goog_delta_ack_consumer_)(delta_ack);
+    } else if (sent_goog_delta) {
+      // We sent DELTA but did not get a DELTA_ACK.
+      // This means that remote does not support GOOG_DELTA
+      RTC_LOG(LS_INFO) << "NO DELTA ACK => disable GOOG_DELTA";
+      (*goog_delta_ack_consumer_)(
+          webrtc::RTCError(webrtc::RTCErrorType::UNSUPPORTED_OPERATION));
+    } else if (delta_ack) {
+      // We did NOT send DELTA but got a DELTA_ACK.
+      // That is internal error.
+      RTC_LOG(LS_ERROR) << "DELTA ACK w/o DELTA => disable GOOG_DELTA";
+      (*goog_delta_ack_consumer_)(
+          webrtc::RTCError(webrtc::RTCErrorType::INTERNAL_ERROR));
+    }
+  } else if (delta_ack) {
+    RTC_LOG(LS_ERROR) << "Discard GOOG_DELTA_ACK, no consumer";
+  }
 }
 
 void Connection::OnConnectionRequestErrorResponse(ConnectionRequest* request,
diff --git a/p2p/base/connection.h b/p2p/base/connection.h
index 4e6c7d9..8e439855 100644
--- a/p2p/base/connection.h
+++ b/p2p/base/connection.h
@@ -13,6 +13,7 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/strings/string_view.h"
@@ -205,12 +206,15 @@
 
   // Called when this connection should try checking writability again.
   int64_t last_ping_sent() const;
-  void Ping(int64_t now);
+  void Ping(int64_t now,
+            std::unique_ptr<StunByteStringAttribute> delta = nullptr);
   void ReceivedPingResponse(
       int rtt,
       absl::string_view request_id,
       const absl::optional<uint32_t>& nomination = absl::nullopt);
-  std::unique_ptr<IceMessage> BuildPingRequest() RTC_RUN_ON(network_thread_);
+  std::unique_ptr<IceMessage> BuildPingRequest(
+      std::unique_ptr<StunByteStringAttribute> delta)
+      RTC_RUN_ON(network_thread_);
 
   int64_t last_ping_response_received() const;
   const absl::optional<std::string>& last_ping_id_received() const;
@@ -319,7 +323,7 @@
 
   std::unique_ptr<IceMessage> BuildPingRequestForTest() {
     RTC_DCHECK_RUN_ON(network_thread_);
-    return BuildPingRequest();
+    return BuildPingRequest(nullptr);
   }
 
   // Public for unit tests.
@@ -333,6 +337,20 @@
     remote_candidate_.set_password(pwd);
   }
 
+  void SetStunDictConsumer(
+      std::function<std::unique_ptr<StunAttribute>(
+          const StunByteStringAttribute*)> goog_delta_consumer,
+      std::function<void(webrtc::RTCErrorOr<const StunUInt64Attribute*>)>
+          goog_delta_ack_consumer) {
+    goog_delta_consumer_ = std::move(goog_delta_consumer);
+    goog_delta_ack_consumer_ = std::move(goog_delta_ack_consumer);
+  }
+
+  void ClearStunDictConsumer() {
+    goog_delta_consumer_ = absl::nullopt;
+    goog_delta_ack_consumer_ = absl::nullopt;
+  }
+
  protected:
   // A ConnectionRequest is a simple STUN ping used to determine writability.
   class ConnectionRequest;
@@ -475,6 +493,13 @@
   const IceFieldTrials* field_trials_;
   rtc::EventBasedExponentialMovingAverage rtt_estimate_
       RTC_GUARDED_BY(network_thread_);
+
+  absl::optional<std::function<std::unique_ptr<StunAttribute>(
+      const StunByteStringAttribute*)>>
+      goog_delta_consumer_;
+  absl::optional<
+      std::function<void(webrtc::RTCErrorOr<const StunUInt64Attribute*>)>>
+      goog_delta_ack_consumer_;
 };
 
 // ProxyConnection defers all the interesting work to the port.
diff --git a/p2p/base/ice_transport_internal.h b/p2p/base/ice_transport_internal.h
index 55f1238..98deb49 100644
--- a/p2p/base/ice_transport_internal.h
+++ b/p2p/base/ice_transport_internal.h
@@ -14,6 +14,7 @@
 #include <stdint.h>
 
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/strings/string_view.h"
@@ -24,6 +25,7 @@
 #include "p2p/base/connection.h"
 #include "p2p/base/packet_transport_internal.h"
 #include "p2p/base/port.h"
+#include "p2p/base/stun_dictionary.h"
 #include "p2p/base/transport_description.h"
 #include "rtc_base/network_constants.h"
 #include "rtc_base/system/rtc_export.h"
@@ -293,6 +295,11 @@
   virtual absl::optional<const CandidatePair> GetSelectedCandidatePair()
       const = 0;
 
+  virtual absl::optional<std::reference_wrapper<StunDictionaryWriter>>
+  GetDictionaryWriter() {
+    return absl::nullopt;
+  }
+
   sigslot::signal1<IceTransportInternal*> SignalGatheringState;
 
   // Handles sending and receiving of candidates.
@@ -330,6 +337,37 @@
 
   // Invoked when the transport is being destroyed.
   sigslot::signal1<IceTransportInternal*> SignalDestroyed;
+
+  // Invoked when remote dictionary has been updated,
+  // i.e. modifications to attributes from remote ice agent has
+  // reflected in our StunDictionaryView.
+  template <typename F>
+  void AddDictionaryViewUpdatedCallback(const void* tag, F&& callback) {
+    dictionary_view_updated_callback_list_.AddReceiver(
+        tag, std::forward<F>(callback));
+  }
+  void RemoveDictionaryViewUpdatedCallback(const void* tag) {
+    dictionary_view_updated_callback_list_.RemoveReceivers(tag);
+  }
+
+  // Invoked when local dictionary has been synchronized,
+  // i.e. remote ice agent has reported acknowledged updates from us.
+  template <typename F>
+  void AddDictionaryWriterSyncedCallback(const void* tag, F&& callback) {
+    dictionary_writer_synced_callback_list_.AddReceiver(
+        tag, std::forward<F>(callback));
+  }
+  void RemoveDictionaryWriterSyncedCallback(const void* tag) {
+    dictionary_writer_synced_callback_list_.RemoveReceivers(tag);
+  }
+
+ protected:
+  webrtc::CallbackList<IceTransportInternal*,
+                       const StunDictionaryView&,
+                       rtc::ArrayView<uint16_t>>
+      dictionary_view_updated_callback_list_;
+  webrtc::CallbackList<IceTransportInternal*, const StunDictionaryWriter&>
+      dictionary_writer_synced_callback_list_;
 };
 
 }  // namespace cricket
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index af4b800..d916983 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -286,6 +286,13 @@
 
   connection->set_ice_event_log(&ice_event_log_);
   connection->SetIceFieldTrials(&ice_field_trials_);
+  connection->SetStunDictConsumer(
+      [this](const StunByteStringAttribute* delta) {
+        return GoogDeltaReceived(delta);
+      },
+      [this](webrtc::RTCErrorOr<const StunUInt64Attribute*> delta_ack) {
+        GoogDeltaAckReceived(std::move(delta_ack));
+      });
   LogCandidatePairConfig(connection,
                          webrtc::IceCandidatePairConfigType::kAdded);
 
@@ -727,7 +734,10 @@
       &ice_field_trials_.dead_connection_timeout_ms,
       // Stop gathering on strongly connected.
       "stop_gather_on_strongly_connected",
-      &ice_field_trials_.stop_gather_on_strongly_connected)
+      &ice_field_trials_.stop_gather_on_strongly_connected,
+      // GOOG_DELTA
+      "enable_goog_delta", &ice_field_trials_.enable_goog_delta,
+      "answer_goog_delta", &ice_field_trials_.answer_goog_delta)
       ->Parse(field_trials->Lookup("WebRTC-IceFieldTrials"));
 
   if (ice_field_trials_.dead_connection_timeout_ms < 30000) {
@@ -782,6 +792,10 @@
 
   ice_field_trials_.extra_ice_ping =
       field_trials->IsEnabled("WebRTC-ExtraICEPing");
+
+  if (!ice_field_trials_.enable_goog_delta) {
+    stun_dict_writer_.Disable();
+  }
 }
 
 const IceConfig& P2PTransportChannel::config() const {
@@ -2058,7 +2072,7 @@
   conn->set_nomination(nomination);
   conn->set_use_candidate_attr(use_candidate_attr);
   last_ping_sent_ms_ = rtc::TimeMillis();
-  conn->Ping(last_ping_sent_ms_);
+  conn->Ping(last_ping_sent_ms_, stun_dict_writer_.CreateDelta());
 }
 
 uint32_t P2PTransportChannel::GetNominationAttr(Connection* conn) const {
@@ -2129,11 +2143,12 @@
   }
 }
 
-void P2PTransportChannel::RemoveConnection(const Connection* connection) {
+void P2PTransportChannel::RemoveConnection(Connection* connection) {
   RTC_DCHECK_RUN_ON(network_thread_);
   auto it = absl::c_find(connections_, connection);
   RTC_DCHECK(it != connections_.end());
   connections_.erase(it);
+  connection->ClearStunDictConsumer();
   ice_controller_->OnConnectionDestroyed(connection);
 }
 
@@ -2307,4 +2322,34 @@
                                         conn->ToLogDescription());
 }
 
+std::unique_ptr<StunAttribute> P2PTransportChannel::GoogDeltaReceived(
+    const StunByteStringAttribute* delta) {
+  auto error = stun_dict_view_.ApplyDelta(*delta);
+  if (error.ok()) {
+    auto& result = error.value();
+    RTC_LOG(LS_INFO) << "Applied GOOG_DELTA";
+    dictionary_view_updated_callback_list_.Send(this, stun_dict_view_,
+                                                result.second);
+    return std::move(result.first);
+  } else {
+    RTC_LOG(LS_ERROR) << "Failed to apply GOOG_DELTA: "
+                      << error.error().message();
+  }
+  return nullptr;
+}
+
+void P2PTransportChannel::GoogDeltaAckReceived(
+    webrtc::RTCErrorOr<const StunUInt64Attribute*> error_or_ack) {
+  if (error_or_ack.ok()) {
+    RTC_LOG(LS_ERROR) << "Applied GOOG_DELTA_ACK";
+    auto ack = error_or_ack.value();
+    stun_dict_writer_.ApplyDeltaAck(*ack);
+    dictionary_writer_synced_callback_list_.Send(this, stun_dict_writer_);
+  } else {
+    stun_dict_writer_.Disable();
+    RTC_LOG(LS_ERROR) << "Failed GOOG_DELTA_ACK: "
+                      << error_or_ack.error().message();
+  }
+}
+
 }  // namespace cricket
diff --git a/p2p/base/p2p_transport_channel.h b/p2p/base/p2p_transport_channel.h
index a0729c1..dc27b20 100644
--- a/p2p/base/p2p_transport_channel.h
+++ b/p2p/base/p2p_transport_channel.h
@@ -61,6 +61,7 @@
 #include "p2p/base/port_allocator.h"
 #include "p2p/base/port_interface.h"
 #include "p2p/base/regathering_controller.h"
+#include "p2p/base/stun_dictionary.h"
 #include "p2p/base/transport_description.h"
 #include "rtc_base/async_packet_socket.h"
 #include "rtc_base/checks.h"
@@ -216,7 +217,7 @@
   int check_receiving_interval() const;
   absl::optional<rtc::NetworkRoute> network_route() const override;
 
-  void RemoveConnection(const Connection* connection);
+  void RemoveConnection(Connection* connection);
 
   // Helper method used only in unittest.
   rtc::DiffServCodePoint DefaultDscpValue() const;
@@ -254,6 +255,11 @@
     return ss.Release();
   }
 
+  absl::optional<std::reference_wrapper<StunDictionaryWriter>>
+  GetDictionaryWriter() override {
+    return stun_dict_writer_;
+  }
+
  private:
   P2PTransportChannel(
       absl::string_view transport_name,
@@ -494,6 +500,10 @@
       Candidate candidate,
       const webrtc::AsyncDnsResolverResult& result);
 
+  std::unique_ptr<StunAttribute> GoogDeltaReceived(
+      const StunByteStringAttribute*);
+  void GoogDeltaAckReceived(webrtc::RTCErrorOr<const StunUInt64Attribute*>);
+
   // Bytes/packets sent/received on this channel.
   uint64_t bytes_sent_ = 0;
   uint64_t bytes_received_ = 0;
@@ -509,6 +519,12 @@
 
   // Parsed field trials.
   IceFieldTrials ice_field_trials_;
+
+  // A dictionary of attributes that will be reflected to peer.
+  StunDictionaryWriter stun_dict_writer_;
+
+  // A dictionary that tracks attributes from peer.
+  StunDictionaryView stun_dict_view_;
 };
 
 }  // namespace cricket
diff --git a/p2p/base/p2p_transport_channel_ice_field_trials.h b/p2p/base/p2p_transport_channel_ice_field_trials.h
index f19823b..96a7756 100644
--- a/p2p/base/p2p_transport_channel_ice_field_trials.h
+++ b/p2p/base/p2p_transport_channel_ice_field_trials.h
@@ -70,6 +70,10 @@
 
   bool piggyback_ice_check_acknowledgement = false;
   bool extra_ice_ping = false;
+
+  // Announce/enable GOOG_DELTA
+  bool enable_goog_delta = true;  // send GOOG DELTA
+  bool answer_goog_delta = true;  // answer GOOG DELTA
 };
 
 }  // namespace cricket
diff --git a/p2p/base/p2p_transport_channel_unittest.cc b/p2p/base/p2p_transport_channel_unittest.cc
index 02cc483..ca8ca8d 100644
--- a/p2p/base/p2p_transport_channel_unittest.cc
+++ b/p2p/base/p2p_transport_channel_unittest.cc
@@ -65,6 +65,7 @@
 using ::testing::InSequence;
 using ::testing::InvokeArgument;
 using ::testing::InvokeWithoutArgs;
+using ::testing::MockFunction;
 using ::testing::Return;
 using ::testing::ReturnRef;
 using ::testing::SaveArg;
@@ -3417,6 +3418,38 @@
                              kDefaultTimeout, clock);
 }
 
+TEST_F(P2PTransportChannelMultihomedTest, StunDictionaryPerformsSync) {
+  rtc::ScopedFakeClock clock;
+  AddAddress(0, kPublicAddrs[0], "eth0", rtc::ADAPTER_TYPE_CELLULAR);
+  AddAddress(0, kAlternateAddrs[0], "vpn0", rtc::ADAPTER_TYPE_VPN,
+             rtc::ADAPTER_TYPE_ETHERNET);
+  AddAddress(1, kPublicAddrs[1]);
+
+  // Create channels and let them go writable, as usual.
+  CreateChannels();
+
+  MockFunction<void(IceTransportInternal*, const StunDictionaryView&,
+                    rtc::ArrayView<uint16_t>)>
+      view_updated_func;
+  ep2_ch1()->AddDictionaryViewUpdatedCallback(
+      "tag", view_updated_func.AsStdFunction());
+  MockFunction<void(IceTransportInternal*, const StunDictionaryWriter&)>
+      writer_synced_func;
+  ep1_ch1()->AddDictionaryWriterSyncedCallback(
+      "tag", writer_synced_func.AsStdFunction());
+  auto& dict_writer = ep1_ch1()->GetDictionaryWriter()->get();
+  dict_writer.SetByteString(12)->CopyBytes("keso");
+  EXPECT_CALL(view_updated_func, Call)
+      .WillOnce([&](auto* channel, auto& view, auto keys) {
+        EXPECT_EQ(keys.size(), 1u);
+        EXPECT_EQ(keys[0], 12);
+        EXPECT_EQ(view.GetByteString(12)->string_view(), "keso");
+      });
+  EXPECT_CALL(writer_synced_func, Call).Times(1);
+  EXPECT_TRUE_SIMULATED_WAIT(CheckConnected(ep1_ch1(), ep2_ch1()),
+                             kMediumTimeout, clock);
+}
+
 // A collection of tests which tests a single P2PTransportChannel by sending
 // pings.
 class P2PTransportChannelPingTest : public ::testing::Test,
diff --git a/p2p/base/port_unittest.cc b/p2p/base/port_unittest.cc
index 1b1c156..b27afe2 100644
--- a/p2p/base/port_unittest.cc
+++ b/p2p/base/port_unittest.cc
@@ -20,6 +20,7 @@
 #include <utility>
 #include <vector>
 
+#include "absl/memory/memory.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/optional.h"
 #include "api/candidate.h"
@@ -3833,7 +3834,6 @@
 
   void OnConnectionStateChange(Connection* connection) { num_state_changes_++; }
 
- private:
   std::unique_ptr<TestPort> lport_;
   std::unique_ptr<TestPort> rport_;
 };
@@ -3922,4 +3922,93 @@
   EXPECT_EQ(num_state_changes_, 2);
 }
 
+// Test normal happy case.
+// Sending a delta and getting a delta ack in response.
+TEST_F(ConnectionTest, SendReceiveGoogDelta) {
+  constexpr int64_t ms = 10;
+  Connection* lconn = CreateConnection(ICEROLE_CONTROLLING);
+  Connection* rconn = CreateConnection(ICEROLE_CONTROLLED);
+
+  std::unique_ptr<StunByteStringAttribute> delta =
+      absl::WrapUnique(new StunByteStringAttribute(STUN_ATTR_GOOG_DELTA));
+  delta->CopyBytes("DELTA");
+
+  std::unique_ptr<StunAttribute> delta_ack =
+      absl::WrapUnique(new StunUInt64Attribute(STUN_ATTR_GOOG_DELTA_ACK, 133));
+
+  bool received_goog_delta = false;
+  bool received_goog_delta_ack = false;
+  lconn->SetStunDictConsumer(
+      // DeltaReceived
+      [](const StunByteStringAttribute* delta)
+          -> std::unique_ptr<StunAttribute> { return nullptr; },
+      // DeltaAckReceived
+      [&](webrtc::RTCErrorOr<const StunUInt64Attribute*> error_or_ack) {
+        received_goog_delta_ack = true;
+        EXPECT_TRUE(error_or_ack.ok());
+        EXPECT_EQ(error_or_ack.value()->value(), 133ull);
+      });
+
+  rconn->SetStunDictConsumer(
+      // DeltaReceived
+      [&](const StunByteStringAttribute* delta)
+          -> std::unique_ptr<StunAttribute> {
+        received_goog_delta = true;
+        EXPECT_EQ(delta->string_view(), "DELTA");
+        return std::move(delta_ack);
+      },
+      // DeltaAckReceived
+      [](webrtc::RTCErrorOr<const StunUInt64Attribute*> error_or__ack) {});
+
+  lconn->Ping(rtc::TimeMillis(), std::move(delta));
+  ASSERT_TRUE_WAIT(lport_->last_stun_msg(), kDefaultTimeout);
+  ASSERT_TRUE(lport_->last_stun_buf());
+  rconn->OnReadPacket(lport_->last_stun_buf()->data<char>(),
+                      lport_->last_stun_buf()->size(), /* packet_time_us */ -1);
+  EXPECT_TRUE(received_goog_delta);
+
+  clock_.AdvanceTime(webrtc::TimeDelta::Millis(ms));
+  ASSERT_TRUE_WAIT(rport_->last_stun_msg(), kDefaultTimeout);
+  ASSERT_TRUE(rport_->last_stun_buf());
+  lconn->OnReadPacket(rport_->last_stun_buf()->data<char>(),
+                      rport_->last_stun_buf()->size(), /* packet_time_us */ -1);
+  EXPECT_TRUE(received_goog_delta_ack);
+}
+
+// Test that sending a goog delta and not getting
+// a delta ack in reply gives an error callback.
+TEST_F(ConnectionTest, SendGoogDeltaNoReply) {
+  constexpr int64_t ms = 10;
+  Connection* lconn = CreateConnection(ICEROLE_CONTROLLING);
+  Connection* rconn = CreateConnection(ICEROLE_CONTROLLED);
+
+  std::unique_ptr<StunByteStringAttribute> delta =
+      absl::WrapUnique(new StunByteStringAttribute(STUN_ATTR_GOOG_DELTA));
+  delta->CopyBytes("DELTA");
+
+  bool received_goog_delta_ack_error = false;
+  lconn->SetStunDictConsumer(
+      // DeltaReceived
+      [](const StunByteStringAttribute* delta)
+          -> std::unique_ptr<StunAttribute> { return nullptr; },
+      // DeltaAckReceived
+      [&](webrtc::RTCErrorOr<const StunUInt64Attribute*> error_or_ack) {
+        received_goog_delta_ack_error = true;
+        EXPECT_FALSE(error_or_ack.ok());
+      });
+
+  lconn->Ping(rtc::TimeMillis(), std::move(delta));
+  ASSERT_TRUE_WAIT(lport_->last_stun_msg(), kDefaultTimeout);
+  ASSERT_TRUE(lport_->last_stun_buf());
+  rconn->OnReadPacket(lport_->last_stun_buf()->data<char>(),
+                      lport_->last_stun_buf()->size(), /* packet_time_us */ -1);
+
+  clock_.AdvanceTime(webrtc::TimeDelta::Millis(ms));
+  ASSERT_TRUE_WAIT(rport_->last_stun_msg(), kDefaultTimeout);
+  ASSERT_TRUE(rport_->last_stun_buf());
+  lconn->OnReadPacket(rport_->last_stun_buf()->data<char>(),
+                      rport_->last_stun_buf()->size(), /* packet_time_us */ -1);
+  EXPECT_TRUE(received_goog_delta_ack_error);
+}
+
 }  // namespace cricket
diff --git a/p2p/base/stun_dictionary.cc b/p2p/base/stun_dictionary.cc
index 7e27a37..bf6a1e4 100644
--- a/p2p/base/stun_dictionary.cc
+++ b/p2p/base/stun_dictionary.cc
@@ -233,7 +233,15 @@
   return 0;
 }
 
+void StunDictionaryWriter::Disable() {
+  disabled_ = true;
+}
+
 void StunDictionaryWriter::Delete(int key) {
+  if (disabled_) {
+    return;
+  }
+
   if (dictionary_) {
     if (dictionary_->attrs_.find(key) == dictionary_->attrs_.end()) {
       return;
@@ -262,6 +270,9 @@
 }
 
 void StunDictionaryWriter::Set(std::unique_ptr<StunAttribute> attr) {
+  if (disabled_) {
+    return;
+  }
   int key = attr->type();
   // remove any pending updates.
   pending_.erase(
@@ -284,6 +295,9 @@
 // Create an StunByteStringAttribute containing the pending (e.g not ack:ed)
 // modifications.
 std::unique_ptr<StunByteStringAttribute> StunDictionaryWriter::CreateDelta() {
+  if (disabled_) {
+    return nullptr;
+  }
   if (pending_.empty()) {
     return nullptr;
   }
diff --git a/p2p/base/stun_dictionary.h b/p2p/base/stun_dictionary.h
index ae2ad75..f93a1f1 100644
--- a/p2p/base/stun_dictionary.h
+++ b/p2p/base/stun_dictionary.h
@@ -62,7 +62,7 @@
   const StunUInt16ListAttribute* GetUInt16List(int key) const;
 
   bool empty() const { return attrs_.empty(); }
-  int size() const { return attrs_.size(); }
+  size_t size() const { return attrs_.size(); }
   int bytes_stored() const { return bytes_stored_; }
   void set_max_bytes_stored(int max_bytes_stored) {
     max_bytes_stored_ = max_bytes_stored;
@@ -175,9 +175,17 @@
   const StunDictionaryView* dictionary() { return dictionary_.get(); }
   const StunDictionaryView* operator->() { return dictionary_.get(); }
 
+  // Disable writer,
+  // i.e CreateDelta always return null, and no modifications are made.
+  // This is called if remote peer does not support GOOG_DELTA.
+  void Disable();
+  bool disabled() const { return disabled_; }
+
  private:
   void Set(std::unique_ptr<StunAttribute> attr);
 
+  bool disabled_ = false;
+
   // version of modification.
   int64_t version_ = 1;