Reland "Added OnIceCandidateError to API and implementation"

This is a reland of 9469c784dbf732472e3b2a60a5fcca0a2f432313

Original change's description:
> Added OnIceCandidateError to API and implementation
>
> Bug: webrtc:3098
> Change-Id: I27ffd015ebf9e8130c1288f7331b0e2fdafb01ef
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/135953
> Commit-Queue: Steve Anton <steveanton@webrtc.org>
> Reviewed-by: Amit Hilbuch <amithi@webrtc.org>
> Reviewed-by: Qingsi Wang <qingsi@webrtc.org>
> Reviewed-by: Henrik Boström <hbos@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#28173}

TBR=steveanton@webrtc.org

Bug: webrtc:3098
Change-Id: I77af2065fc1479273f399e2b3d919f98fe8ac23d
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/140641
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28179}
diff --git a/api/peer_connection_interface.h b/api/peer_connection_interface.h
index a23b25d..ceec13a 100644
--- a/api/peer_connection_interface.h
+++ b/api/peer_connection_interface.h
@@ -1172,6 +1172,14 @@
   // A new ICE candidate has been gathered.
   virtual void OnIceCandidate(const IceCandidateInterface* candidate) = 0;
 
+  // Gathering of an ICE candidate failed.
+  // See https://w3c.github.io/webrtc-pc/#event-icecandidateerror
+  // |host_candidate| is a stringified socket address.
+  virtual void OnIceCandidateError(const std::string& host_candidate,
+                                   const std::string& url,
+                                   int error_code,
+                                   const std::string& error_text) {}
+
   // Ice candidates have been removed.
   // TODO(honghaiz): Make this a pure virtual method when all its subclasses
   // implement it.
diff --git a/p2p/base/ice_transport_internal.h b/p2p/base/ice_transport_internal.h
index 89b2107..e3d98db 100644
--- a/p2p/base/ice_transport_internal.h
+++ b/p2p/base/ice_transport_internal.h
@@ -270,6 +270,9 @@
   sigslot::signal2<IceTransportInternal*, const Candidate&>
       SignalCandidateGathered;
 
+  sigslot::signal2<IceTransportInternal*, const IceCandidateErrorEvent&>
+      SignalCandidateError;
+
   sigslot::signal2<IceTransportInternal*, const Candidates&>
       SignalCandidatesRemoved;
 
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index d469ff7..d510820 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -180,6 +180,8 @@
   session->SignalPortsPruned.connect(this, &P2PTransportChannel::OnPortsPruned);
   session->SignalCandidatesReady.connect(
       this, &P2PTransportChannel::OnCandidatesReady);
+  session->SignalCandidateError.connect(this,
+                                        &P2PTransportChannel::OnCandidateError);
   session->SignalCandidatesRemoved.connect(
       this, &P2PTransportChannel::OnCandidatesRemoved);
   session->SignalCandidatesAllocationDone.connect(
@@ -878,6 +880,13 @@
   }
 }
 
+void P2PTransportChannel::OnCandidateError(
+    PortAllocatorSession* session,
+    const IceCandidateErrorEvent& event) {
+  RTC_DCHECK(network_thread_ == rtc::Thread::Current());
+  SignalCandidateError(this, event);
+}
+
 void P2PTransportChannel::OnCandidatesAllocationDone(
     PortAllocatorSession* session) {
   RTC_DCHECK_RUN_ON(network_thread_);
diff --git a/p2p/base/p2p_transport_channel.h b/p2p/base/p2p_transport_channel.h
index 5e537ce..0bcbe10 100644
--- a/p2p/base/p2p_transport_channel.h
+++ b/p2p/base/p2p_transport_channel.h
@@ -307,6 +307,8 @@
                      const std::vector<PortInterface*>& ports);
   void OnCandidatesReady(PortAllocatorSession* session,
                          const std::vector<Candidate>& candidates);
+  void OnCandidateError(PortAllocatorSession* session,
+                        const IceCandidateErrorEvent& event);
   void OnCandidatesRemoved(PortAllocatorSession* session,
                            const std::vector<Candidate>& candidates);
   void OnCandidatesAllocationDone(PortAllocatorSession* session);
diff --git a/p2p/base/port.h b/p2p/base/port.h
index a0e2606..8e6281f 100644
--- a/p2p/base/port.h
+++ b/p2p/base/port.h
@@ -15,6 +15,7 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/types/optional.h"
@@ -130,6 +131,23 @@
   bool operator!=(const ProtocolAddress& o) const { return !(*this == o); }
 };
 
+struct IceCandidateErrorEvent {
+  IceCandidateErrorEvent() = default;
+  IceCandidateErrorEvent(std::string host_candidate,
+                         std::string url,
+                         int error_code,
+                         std::string error_text)
+      : host_candidate(std::move(host_candidate)),
+        url(std::move(url)),
+        error_code(error_code),
+        error_text(std::move(error_text)) {}
+
+  std::string host_candidate;
+  std::string url;
+  int error_code = 0;
+  std::string error_text;
+};
+
 typedef std::set<rtc::SocketAddress> ServerAddresses;
 
 // Represents a local communication mechanism that can be used to create
@@ -227,9 +245,10 @@
   // Fired when candidates are discovered by the port. When all candidates
   // are discovered that belong to port SignalAddressReady is fired.
   sigslot::signal2<Port*, const Candidate&> SignalCandidateReady;
-
   // Provides all of the above information in one handy object.
   const std::vector<Candidate>& Candidates() const override;
+  // Fired when candidate discovery failed using certain server.
+  sigslot::signal2<Port*, const IceCandidateErrorEvent&> SignalCandidateError;
 
   // SignalPortComplete is sent when port completes the task of candidates
   // allocation.
diff --git a/p2p/base/port_allocator.h b/p2p/base/port_allocator.h
index d0605b6..d78b6cb 100644
--- a/p2p/base/port_allocator.h
+++ b/p2p/base/port_allocator.h
@@ -271,6 +271,8 @@
       SignalPortsPruned;
   sigslot::signal2<PortAllocatorSession*, const std::vector<Candidate>&>
       SignalCandidatesReady;
+  sigslot::signal2<PortAllocatorSession*, const IceCandidateErrorEvent&>
+      SignalCandidateError;
   // Candidates should be signaled to be removed when the port that generated
   // the candidates is removed.
   sigslot::signal2<PortAllocatorSession*, const std::vector<Candidate>&>
diff --git a/p2p/base/stun.cc b/p2p/base/stun.cc
index d289774..3a44909 100644
--- a/p2p/base/stun.cc
+++ b/p2p/base/stun.cc
@@ -61,6 +61,7 @@
 const char TURN_MAGIC_COOKIE_VALUE[] = {'\x72', '\xC6', '\x4B', '\xC6'};
 const char EMPTY_TRANSACTION_ID[] = "0000000000000000";
 const uint32_t STUN_FINGERPRINT_XOR_VALUE = 0x5354554E;
+const int SERVER_NOT_REACHABLE_ERROR = 701;
 
 // StunMessage
 
diff --git a/p2p/base/stun.h b/p2p/base/stun.h
index caaa474..33410b5 100644
--- a/p2p/base/stun.h
+++ b/p2p/base/stun.h
@@ -581,6 +581,9 @@
   STUN_ERROR_WRONG_CREDENTIALS = 441,
   STUN_ERROR_UNSUPPORTED_PROTOCOL = 442
 };
+
+extern const int SERVER_NOT_REACHABLE_ERROR;
+
 extern const char STUN_ERROR_REASON_FORBIDDEN[];
 extern const char STUN_ERROR_REASON_ALLOCATION_MISMATCH[];
 extern const char STUN_ERROR_REASON_WRONG_CREDENTIALS[];
diff --git a/p2p/base/stun_port.cc b/p2p/base/stun_port.cc
index 65112f2..4662b0d 100644
--- a/p2p/base/stun_port.cc
+++ b/p2p/base/stun_port.cc
@@ -79,7 +79,10 @@
                         << " reason=" << attr->reason();
     }
 
-    port_->OnStunBindingOrResolveRequestFailed(server_addr_);
+    port_->OnStunBindingOrResolveRequestFailed(
+        server_addr_, attr ? attr->number() : STUN_ERROR_GLOBAL_FAILURE,
+        attr ? attr->reason()
+             : "STUN binding response with no error code attribute.");
 
     int64_t now = rtc::TimeMillis();
     if (WithinLifetime(now) &&
@@ -93,8 +96,9 @@
     RTC_LOG(LS_ERROR) << "Binding request timed out from "
                       << port_->GetLocalAddress().ToSensitiveString() << " ("
                       << port_->Network()->name() << ")";
-
-    port_->OnStunBindingOrResolveRequestFailed(server_addr_);
+    port_->OnStunBindingOrResolveRequestFailed(
+        server_addr_, SERVER_NOT_REACHABLE_ERROR,
+        "STUN allocate request timed out.");
   }
 
  private:
@@ -104,6 +108,7 @@
     int lifetime = port_->stun_keepalive_lifetime();
     return lifetime < 0 || rtc::TimeDiff(now, start_time_) <= lifetime;
   }
+
   UDPPort* port_;
   const rtc::SocketAddress server_addr_;
 
@@ -445,7 +450,8 @@
     RTC_LOG(LS_WARNING) << ToString()
                         << ": StunPort: stun host lookup received error "
                         << error;
-    OnStunBindingOrResolveRequestFailed(input);
+    OnStunBindingOrResolveRequestFailed(input, SERVER_NOT_REACHABLE_ERROR,
+                                        "STUN host lookup received error.");
     return;
   }
 
@@ -469,8 +475,10 @@
     } else {
       // Since we can't send stun messages to the server, we should mark this
       // port ready.
-      RTC_LOG(LS_WARNING) << "STUN server address is incompatible.";
-      OnStunBindingOrResolveRequestFailed(stun_addr);
+      const char* reason = "STUN server address is incompatible.";
+      RTC_LOG(LS_WARNING) << reason;
+      OnStunBindingOrResolveRequestFailed(stun_addr, SERVER_NOT_REACHABLE_ERROR,
+                                          reason);
     }
   }
 }
@@ -530,7 +538,14 @@
 }
 
 void UDPPort::OnStunBindingOrResolveRequestFailed(
-    const rtc::SocketAddress& stun_server_addr) {
+    const rtc::SocketAddress& stun_server_addr,
+    int error_code,
+    const std::string& reason) {
+  rtc::StringBuilder url;
+  url << "stun:" << stun_server_addr.ToString();
+  SignalCandidateError(
+      this, IceCandidateErrorEvent(GetLocalAddress().ToSensitiveString(),
+                                   url.str(), error_code, reason));
   if (bind_request_failed_servers_.find(stun_server_addr) !=
       bind_request_failed_servers_.end()) {
     return;
diff --git a/p2p/base/stun_port.h b/p2p/base/stun_port.h
index 1ee0727..3c42349 100644
--- a/p2p/base/stun_port.h
+++ b/p2p/base/stun_port.h
@@ -222,7 +222,9 @@
       const rtc::SocketAddress& stun_server_addr,
       const rtc::SocketAddress& stun_reflected_addr);
   void OnStunBindingOrResolveRequestFailed(
-      const rtc::SocketAddress& stun_server_addr);
+      const rtc::SocketAddress& stun_server_addr,
+      int error_code,
+      const std::string& reason);
 
   // Sends STUN requests to the server.
   void OnSendPacket(const void* data, size_t size, StunRequest* req);
diff --git a/p2p/base/stun_port_unittest.cc b/p2p/base/stun_port_unittest.cc
index a23c063..98e41f9 100644
--- a/p2p/base/stun_port_unittest.cc
+++ b/p2p/base/stun_port_unittest.cc
@@ -88,6 +88,8 @@
     stun_port_->SignalPortComplete.connect(this,
                                            &StunPortTestBase::OnPortComplete);
     stun_port_->SignalPortError.connect(this, &StunPortTestBase::OnPortError);
+    stun_port_->SignalCandidateError.connect(
+        this, &StunPortTestBase::OnCandidateError);
   }
 
   void CreateSharedUdpPort(const rtc::SocketAddress& server_addr,
@@ -145,6 +147,10 @@
     done_ = true;
     error_ = true;
   }
+  void OnCandidateError(cricket::Port* port,
+                        const cricket::IceCandidateErrorEvent& event) {
+    error_event_ = event;
+  }
   void SetKeepaliveDelay(int delay) { stun_keepalive_delay_ = delay; }
 
   void SetKeepaliveLifetime(int lifetime) {
@@ -167,6 +173,9 @@
   bool error_;
   int stun_keepalive_delay_;
   int stun_keepalive_lifetime_;
+
+ protected:
+  cricket::IceCandidateErrorEvent error_event_;
 };
 
 class StunPortTestWithRealClock : public StunPortTestBase {};
@@ -212,6 +221,15 @@
   EXPECT_TRUE_SIMULATED_WAIT(done(), kTimeoutMs, fake_clock);
   EXPECT_TRUE(error());
   EXPECT_EQ(0U, port()->Candidates().size());
+  EXPECT_EQ_SIMULATED_WAIT(error_event_.error_code,
+                           cricket::SERVER_NOT_REACHABLE_ERROR, kTimeoutMs,
+                           fake_clock);
+  ASSERT_NE(error_event_.error_text.find("."), std::string::npos);
+  ASSERT_NE(
+      error_event_.host_candidate.find(kLocalAddr.HostAsSensitiveURIString()),
+      std::string::npos);
+  std::string server_url = "stun:" + kBadAddr.ToString();
+  ASSERT_EQ(error_event_.url, server_url);
 }
 
 // Test that we can get an address from a STUN server specified by a hostname.
@@ -237,6 +255,8 @@
   EXPECT_TRUE_WAIT(done(), kTimeoutMs);
   EXPECT_TRUE(error());
   EXPECT_EQ(0U, port()->Candidates().size());
+  EXPECT_EQ_WAIT(error_event_.error_code, cricket::SERVER_NOT_REACHABLE_ERROR,
+                 kTimeoutMs);
 }
 
 // This test verifies keepalive response messages don't result in
@@ -303,6 +323,9 @@
   PrepareAddress();
   EXPECT_TRUE_SIMULATED_WAIT(done(), kTimeoutMs, fake_clock);
   EXPECT_EQ(1U, port()->Candidates().size());
+  std::string server_url = "stun:" + kBadAddr.ToString();
+  ASSERT_EQ_SIMULATED_WAIT(error_event_.url, server_url, kTimeoutMs,
+                           fake_clock);
 }
 
 // Test that two candidates are allocated if the two STUN servers return
diff --git a/p2p/base/turn_port.cc b/p2p/base/turn_port.cc
index bcf574e..f910497 100644
--- a/p2p/base/turn_port.cc
+++ b/p2p/base/turn_port.cc
@@ -326,7 +326,8 @@
   if (credentials_.username.empty() || credentials_.password.empty()) {
     RTC_LOG(LS_ERROR) << "Allocation can't be started without setting the"
                          " TURN server credentials for the user.";
-    OnAllocateError();
+    OnAllocateError(STUN_ERROR_UNAUTHORIZED,
+                    "Missing TURN server credentials.");
     return;
   }
 
@@ -343,7 +344,8 @@
       RTC_LOG(LS_ERROR) << "IP address family does not match. server: "
                         << server_address_.address.family()
                         << " local: " << Network()->GetBestIP().family();
-      OnAllocateError();
+      OnAllocateError(STUN_ERROR_GLOBAL_FAILURE,
+                      "IP address family does not match.");
       return;
     }
 
@@ -355,7 +357,8 @@
                      << server_address_.address.ToSensitiveString();
     if (!CreateTurnClientSocket()) {
       RTC_LOG(LS_ERROR) << "Failed to create TURN client socket";
-      OnAllocateError();
+      OnAllocateError(STUN_ERROR_GLOBAL_FAILURE,
+                      "Failed to create TURN client socket.");
       return;
     }
     if (server_address_.proto == PROTO_UDP) {
@@ -473,7 +476,9 @@
                           << socket_address.ipaddr().ToString()
                           << ", rather than an address associated with network:"
                           << Network()->ToString() << ". Discarding TURN port.";
-      OnAllocateError();
+      OnAllocateError(
+          STUN_ERROR_GLOBAL_FAILURE,
+          "Address not associated with the desired network interface.");
       return;
     }
   }
@@ -501,7 +506,8 @@
     RTC_LOG(LS_WARNING) << ToString() << ": Giving up on the port after "
                         << allocate_mismatch_retries_
                         << " retries for STUN_ERROR_ALLOCATION_MISMATCH";
-    OnAllocateError();
+    OnAllocateError(STUN_ERROR_ALLOCATION_MISMATCH,
+                    "Maximum retries reached for allocation mismatch.");
     return;
   }
 
@@ -786,7 +792,8 @@
   if (resolver_->GetError() != 0 && (server_address_.proto == PROTO_TCP ||
                                      server_address_.proto == PROTO_TLS)) {
     if (!CreateTurnClientSocket()) {
-      OnAllocateError();
+      OnAllocateError(SERVER_NOT_REACHABLE_ERROR,
+                      "TURN host lookup received error.");
     }
     return;
   }
@@ -800,7 +807,8 @@
     RTC_LOG(LS_WARNING) << ToString() << ": TURN host lookup received error "
                         << resolver_->GetError();
     error_ = resolver_->GetError();
-    OnAllocateError();
+    OnAllocateError(SERVER_NOT_REACHABLE_ERROR,
+                    "TURN host lookup received error.");
     return;
   }
   // Signal needs both resolved and unresolved address. After signal is sent
@@ -847,14 +855,20 @@
              ProtoToString(server_address_.proto),  // The first hop protocol.
              "",  // TCP canddiate type, empty for turn candidates.
              RELAY_PORT_TYPE, GetRelayPreference(server_address_.proto),
-             server_priority_, ReconstructedServerUrl(), true);
+             server_priority_, ReconstructedServerUrl(false /* use_hostname */),
+             true);
 }
 
-void TurnPort::OnAllocateError() {
+void TurnPort::OnAllocateError(int error_code, const std::string& reason) {
   // We will send SignalPortError asynchronously as this can be sent during
   // port initialization. This way it will not be blocking other port
   // creation.
   thread()->Post(RTC_FROM_HERE, this, MSG_ALLOCATE_ERROR);
+  SignalCandidateError(
+      this,
+      IceCandidateErrorEvent(GetLocalAddress().ToSensitiveString(),
+                             ReconstructedServerUrl(true /* use_hostname */),
+                             error_code, reason));
 }
 
 void TurnPort::OnRefreshError() {
@@ -887,7 +901,7 @@
 
 void TurnPort::Close() {
   if (!ready()) {
-    OnAllocateError();
+    OnAllocateError(SERVER_NOT_REACHABLE_ERROR, "");
   }
   request_manager_.Clear();
   // Stop the port from creating new connections.
@@ -941,7 +955,8 @@
 }
 
 void TurnPort::OnAllocateRequestTimeout() {
-  OnAllocateError();
+  OnAllocateError(SERVER_NOT_REACHABLE_ERROR,
+                  "TURN allocate request timed out.");
 }
 
 void TurnPort::HandleDataIndication(const char* data,
@@ -1249,7 +1264,7 @@
   return true;
 }
 
-std::string TurnPort::ReconstructedServerUrl() {
+std::string TurnPort::ReconstructedServerUrl(bool use_hostname) {
   // draft-petithuguenin-behave-turn-uris-01
   // turnURI       = scheme ":" turn-host [ ":" turn-port ]
   //                 [ "?transport=" transport ]
@@ -1272,8 +1287,10 @@
       break;
   }
   rtc::StringBuilder url;
-  url << scheme << ":" << server_address_.address.ipaddr().ToString() << ":"
-      << server_address_.address.port() << "?transport=" << transport;
+  url << scheme << ":"
+      << (use_hostname ? server_address_.address.hostname()
+                       : server_address_.address.ipaddr().ToString())
+      << ":" << server_address_.address.port() << "?transport=" << transport;
   return url.Release();
 }
 
@@ -1388,7 +1405,8 @@
                           << ": Received TURN allocate error response, id="
                           << rtc::hex_encode(id()) << ", code=" << error_code
                           << ", rtt=" << Elapsed();
-      port_->OnAllocateError();
+      const StunErrorCodeAttribute* attr = response->GetErrorCode();
+      port_->OnAllocateError(error_code, attr ? attr->reason() : "");
   }
 }
 
@@ -1404,7 +1422,8 @@
     RTC_LOG(LS_WARNING) << port_->ToString()
                         << ": Failed to authenticate with the server "
                            "after challenge.";
-    port_->OnAllocateError();
+    const StunErrorCodeAttribute* attr = response->GetErrorCode();
+    port_->OnAllocateError(STUN_ERROR_UNAUTHORIZED, attr ? attr->reason() : "");
     return;
   }
 
@@ -1437,7 +1456,7 @@
   // According to RFC 5389 section 11, there are use cases where
   // authentication of response is not possible, we're not validating
   // message integrity.
-
+  const StunErrorCodeAttribute* error_code_attr = response->GetErrorCode();
   // Get the alternate server address attribute value.
   const StunAddressAttribute* alternate_server_attr =
       response->GetAddress(STUN_ATTR_ALTERNATE_SERVER);
@@ -1445,11 +1464,13 @@
     RTC_LOG(LS_WARNING) << port_->ToString()
                         << ": Missing STUN_ATTR_ALTERNATE_SERVER "
                            "attribute in try alternate error response";
-    port_->OnAllocateError();
+    port_->OnAllocateError(STUN_ERROR_TRY_ALTERNATE,
+                           error_code_attr ? error_code_attr->reason() : "");
     return;
   }
   if (!port_->SetAlternateServer(alternate_server_attr->GetAddress())) {
-    port_->OnAllocateError();
+    port_->OnAllocateError(STUN_ERROR_TRY_ALTERNATE,
+                           error_code_attr ? error_code_attr->reason() : "");
     return;
   }
 
diff --git a/p2p/base/turn_port.h b/p2p/base/turn_port.h
index e929d3e..5edbb1c 100644
--- a/p2p/base/turn_port.h
+++ b/p2p/base/turn_port.h
@@ -310,7 +310,7 @@
   void OnStunAddress(const rtc::SocketAddress& address);
   void OnAllocateSuccess(const rtc::SocketAddress& address,
                          const rtc::SocketAddress& stun_address);
-  void OnAllocateError();
+  void OnAllocateError(int error_code, const std::string& reason);
   void OnAllocateRequestTimeout();
 
   void HandleDataIndication(const char* data,
@@ -349,7 +349,7 @@
   bool FailAndPruneConnection(const rtc::SocketAddress& address);
 
   // Reconstruct the URL of the server which the candidate is gathered from.
-  std::string ReconstructedServerUrl();
+  std::string ReconstructedServerUrl(bool use_hostname);
 
   void TurnCustomizerMaybeModifyOutgoingStunMessage(StunMessage* message);
   bool TurnCustomizerAllowChannelData(const void* data,
diff --git a/p2p/base/turn_port_unittest.cc b/p2p/base/turn_port_unittest.cc
index e713b2a..b51a126 100644
--- a/p2p/base/turn_port_unittest.cc
+++ b/p2p/base/turn_port_unittest.cc
@@ -66,6 +66,7 @@
 static const SocketAddress kTurnUdpIPv6IntAddr(
     "2400:4030:1:2c00:be30:abcd:efab:cdef",
     cricket::TURN_SERVER_PORT);
+static const SocketAddress kTurnInvalidAddr("www.google.invalid", 3478);
 
 static const char kCandidateFoundation[] = "foundation";
 static const char kIceUfrag1[] = "TESTICEUFRAG0001";
@@ -176,6 +177,10 @@
 
   void OnTurnPortComplete(Port* port) { turn_ready_ = true; }
   void OnTurnPortError(Port* port) { turn_error_ = true; }
+  void OnCandidateError(Port* port,
+                        const cricket::IceCandidateErrorEvent& event) {
+    error_event_ = event;
+  }
   void OnTurnUnknownAddress(PortInterface* port,
                             const SocketAddress& addr,
                             ProtocolType proto,
@@ -316,6 +321,8 @@
     turn_port_->SignalPortComplete.connect(this,
                                            &TurnPortTest::OnTurnPortComplete);
     turn_port_->SignalPortError.connect(this, &TurnPortTest::OnTurnPortError);
+    turn_port_->SignalCandidateError.connect(this,
+                                             &TurnPortTest::OnCandidateError);
     turn_port_->SignalUnknownAddress.connect(
         this, &TurnPortTest::OnTurnUnknownAddress);
     turn_port_->SignalCreatePermissionResult.connect(
@@ -755,6 +762,7 @@
   std::vector<rtc::Buffer> udp_packets_;
   rtc::PacketOptions options;
   std::unique_ptr<webrtc::TurnCustomizer> turn_customizer_;
+  cricket::IceCandidateErrorEvent error_event_;
 };
 
 TEST_F(TurnPortTest, TestTurnPortType) {
@@ -802,6 +810,17 @@
   EXPECT_NE(0, turn_port_->Candidates()[0].address().port());
 }
 
+// Test bad credentials.
+TEST_F(TurnPortTest, TestTurnBadCredentials) {
+  CreateTurnPort(kTurnUsername, "bad", kTurnUdpProtoAddr);
+  turn_port_->PrepareAddress();
+  EXPECT_TRUE_SIMULATED_WAIT(turn_error_, kSimulatedRtt * 3, fake_clock_);
+  ASSERT_EQ(0U, turn_port_->Candidates().size());
+  EXPECT_EQ_SIMULATED_WAIT(error_event_.error_code, STUN_ERROR_UNAUTHORIZED,
+                           kSimulatedRtt * 3, fake_clock_);
+  EXPECT_EQ(error_event_.error_text, "Unauthorized");
+}
+
 // Testing a normal UDP allocation using TCP connection.
 TEST_F(TurnPortTest, TestTurnTcpAllocate) {
   turn_server_.AddInternalSocket(kTurnTcpIntAddr, PROTO_TCP);
@@ -861,6 +880,15 @@
   // Shouldn't take more than 1 RTT to realize the bound address isn't the one
   // expected.
   EXPECT_TRUE_SIMULATED_WAIT(turn_error_, kSimulatedRtt, fake_clock_);
+  EXPECT_EQ_SIMULATED_WAIT(error_event_.error_code, STUN_ERROR_GLOBAL_FAILURE,
+                           kSimulatedRtt, fake_clock_);
+  ASSERT_NE(error_event_.error_text.find("."), std::string::npos);
+  ASSERT_NE(
+      error_event_.host_candidate.find(kLocalAddr2.HostAsSensitiveURIString()),
+      std::string::npos);
+  std::string server_url =
+      "turn:" + kTurnTcpIntAddr.ToString() + "?transport=tcp";
+  ASSERT_EQ(error_event_.url, server_url);
 }
 
 // A caveat for the above logic: if the socket ends up bound to one of the IPs
@@ -921,14 +949,18 @@
 TEST_F(TurnPortTest, TestTurnTcpOnAddressResolveFailure) {
   turn_server_.AddInternalSocket(kTurnTcpIntAddr, PROTO_TCP);
   CreateTurnPort(kTurnUsername, kTurnPassword,
-                 ProtocolAddress(rtc::SocketAddress("www.google.invalid", 3478),
-                                 PROTO_TCP));
+                 ProtocolAddress(kTurnInvalidAddr, PROTO_TCP));
   turn_port_->PrepareAddress();
   EXPECT_TRUE_WAIT(turn_error_, kResolverTimeout);
   // As VSS doesn't provide a DNS resolution, name resolve will fail. TurnPort
   // will proceed in creating a TCP socket which will fail as there is no
   // server on the above domain and error will be set to SOCKET_ERROR.
   EXPECT_EQ(SOCKET_ERROR, turn_port_->error());
+  EXPECT_EQ_SIMULATED_WAIT(error_event_.error_code, SERVER_NOT_REACHABLE_ERROR,
+                           kSimulatedRtt, fake_clock_);
+  std::string server_url =
+      "turn:" + kTurnInvalidAddr.ToString() + "?transport=tcp";
+  ASSERT_EQ(error_event_.url, server_url);
 }
 
 // Testing turn port will attempt to create TLS socket on address resolution
@@ -936,8 +968,7 @@
 TEST_F(TurnPortTest, TestTurnTlsOnAddressResolveFailure) {
   turn_server_.AddInternalSocket(kTurnTcpIntAddr, PROTO_TLS);
   CreateTurnPort(kTurnUsername, kTurnPassword,
-                 ProtocolAddress(rtc::SocketAddress("www.google.invalid", 3478),
-                                 PROTO_TLS));
+                 ProtocolAddress(kTurnInvalidAddr, PROTO_TLS));
   turn_port_->PrepareAddress();
   EXPECT_TRUE_WAIT(turn_error_, kResolverTimeout);
   EXPECT_EQ(SOCKET_ERROR, turn_port_->error());
@@ -947,8 +978,7 @@
 // and return allocate failure.
 TEST_F(TurnPortTest, TestTurnUdpOnAddressResolveFailure) {
   CreateTurnPort(kTurnUsername, kTurnPassword,
-                 ProtocolAddress(rtc::SocketAddress("www.google.invalid", 3478),
-                                 PROTO_UDP));
+                 ProtocolAddress(kTurnInvalidAddr, PROTO_UDP));
   turn_port_->PrepareAddress();
   EXPECT_TRUE_WAIT(turn_error_, kResolverTimeout);
   // Error from turn port will not be socket error.
diff --git a/p2p/client/basic_port_allocator.cc b/p2p/client/basic_port_allocator.cc
index 4f418ee..b1f147d 100644
--- a/p2p/client/basic_port_allocator.cc
+++ b/p2p/client/basic_port_allocator.cc
@@ -953,6 +953,8 @@
 
   port->SignalCandidateReady.connect(
       this, &BasicPortAllocatorSession::OnCandidateReady);
+  port->SignalCandidateError.connect(
+      this, &BasicPortAllocatorSession::OnCandidateError);
   port->SignalPortComplete.connect(this,
                                    &BasicPortAllocatorSession::OnPortComplete);
   port->SignalDestroyed.connect(this,
@@ -1024,6 +1026,15 @@
   }
 }
 
+void BasicPortAllocatorSession::OnCandidateError(
+    Port* port,
+    const IceCandidateErrorEvent& event) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  RTC_DCHECK(FindPort(port));
+
+  SignalCandidateError(this, event);
+}
+
 Port* BasicPortAllocatorSession::GetBestTurnPortForNetwork(
     const std::string& network_name) const {
   RTC_DCHECK_RUN_ON(network_thread_);
diff --git a/p2p/client/basic_port_allocator.h b/p2p/client/basic_port_allocator.h
index 26eea1e..13611e7 100644
--- a/p2p/client/basic_port_allocator.h
+++ b/p2p/client/basic_port_allocator.h
@@ -231,6 +231,7 @@
                         AllocationSequence* seq,
                         bool prepare_address);
   void OnCandidateReady(Port* port, const Candidate& c);
+  void OnCandidateError(Port* port, const IceCandidateErrorEvent& event);
   void OnPortComplete(Port* port);
   void OnPortError(Port* port);
   void OnProtocolEnabled(AllocationSequence* seq, ProtocolType proto);
diff --git a/pc/jsep_transport_controller.cc b/pc/jsep_transport_controller.cc
index 93949c6..56d9a47 100644
--- a/pc/jsep_transport_controller.cc
+++ b/pc/jsep_transport_controller.cc
@@ -523,6 +523,8 @@
       this, &JsepTransportController::OnTransportGatheringState_n);
   dtls->ice_transport()->SignalCandidateGathered.connect(
       this, &JsepTransportController::OnTransportCandidateGathered_n);
+  dtls->ice_transport()->SignalCandidateError.connect(
+      this, &JsepTransportController::OnTransportCandidateError_n);
   dtls->ice_transport()->SignalCandidatesRemoved.connect(
       this, &JsepTransportController::OnTransportCandidatesRemoved_n);
   dtls->ice_transport()->SignalRoleConflict.connect(
@@ -1377,6 +1379,14 @@
       });
 }
 
+void JsepTransportController::OnTransportCandidateError_n(
+    cricket::IceTransportInternal* transport,
+    const cricket::IceCandidateErrorEvent& event) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+
+  invoker_.AsyncInvoke<void>(RTC_FROM_HERE, signaling_thread_,
+                             [this, event] { SignalIceCandidateError(event); });
+}
 void JsepTransportController::OnTransportCandidatesRemoved_n(
     cricket::IceTransportInternal* transport,
     const cricket::Candidates& candidates) {
diff --git a/pc/jsep_transport_controller.h b/pc/jsep_transport_controller.h
index 995c703..fcae153 100644
--- a/pc/jsep_transport_controller.h
+++ b/pc/jsep_transport_controller.h
@@ -235,6 +235,9 @@
   sigslot::signal2<const std::string&, const std::vector<cricket::Candidate>&>
       SignalIceCandidatesGathered;
 
+  sigslot::signal1<const cricket::IceCandidateErrorEvent&>
+      SignalIceCandidateError;
+
   sigslot::signal1<const std::vector<cricket::Candidate>&>
       SignalIceCandidatesRemoved;
 
@@ -375,6 +378,9 @@
   void OnTransportGatheringState_n(cricket::IceTransportInternal* transport);
   void OnTransportCandidateGathered_n(cricket::IceTransportInternal* transport,
                                       const cricket::Candidate& candidate);
+  void OnTransportCandidateError_n(
+      cricket::IceTransportInternal* transport,
+      const cricket::IceCandidateErrorEvent& event);
   void OnTransportCandidatesRemoved_n(cricket::IceTransportInternal* transport,
                                       const cricket::Candidates& candidates);
   void OnTransportRoleConflict_n(cricket::IceTransportInternal* transport);
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index fde0433..b94b883 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -1075,6 +1075,8 @@
       this, &PeerConnection::OnTransportControllerGatheringState);
   transport_controller_->SignalIceCandidatesGathered.connect(
       this, &PeerConnection::OnTransportControllerCandidatesGathered);
+  transport_controller_->SignalIceCandidateError.connect(
+      this, &PeerConnection::OnTransportControllerCandidateError);
   transport_controller_->SignalIceCandidatesRemoved.connect(
       this, &PeerConnection::OnTransportControllerCandidatesRemoved);
   transport_controller_->SignalDtlsHandshakeError.connect(
@@ -4169,6 +4171,16 @@
   Observer()->OnIceCandidate(candidate.get());
 }
 
+void PeerConnection::OnIceCandidateError(const std::string& host_candidate,
+                                         const std::string& url,
+                                         int error_code,
+                                         const std::string& error_text) {
+  if (IsClosed()) {
+    return;
+  }
+  Observer()->OnIceCandidateError(host_candidate, url, error_code, error_text);
+}
+
 void PeerConnection::OnIceCandidatesRemoved(
     const std::vector<cricket::Candidate>& candidates) {
   if (IsClosed()) {
@@ -6107,6 +6119,12 @@
   }
 }
 
+void PeerConnection::OnTransportControllerCandidateError(
+    const cricket::IceCandidateErrorEvent& event) {
+  OnIceCandidateError(event.host_candidate, event.url, event.error_code,
+                      event.error_text);
+}
+
 void PeerConnection::OnTransportControllerCandidatesRemoved(
     const std::vector<cricket::Candidate>& candidates) {
   // Sanity check.
diff --git a/pc/peer_connection.h b/pc/peer_connection.h
index d2fb768..e7362d9 100644
--- a/pc/peer_connection.h
+++ b/pc/peer_connection.h
@@ -413,12 +413,18 @@
       PeerConnectionInterface::PeerConnectionState new_state)
       RTC_RUN_ON(signaling_thread());
 
-  // Called any time the IceGatheringState changes
+  // Called any time the IceGatheringState changes.
   void OnIceGatheringChange(IceGatheringState new_state)
       RTC_RUN_ON(signaling_thread());
   // New ICE candidate has been gathered.
   void OnIceCandidate(std::unique_ptr<IceCandidateInterface> candidate)
       RTC_RUN_ON(signaling_thread());
+  // Gathering of an ICE candidate failed.
+  void OnIceCandidateError(const std::string& host_candidate,
+                           const std::string& url,
+                           int error_code,
+                           const std::string& error_text)
+      RTC_RUN_ON(signaling_thread());
   // Some local ICE candidates have been removed.
   void OnIceCandidatesRemoved(const std::vector<cricket::Candidate>& candidates)
       RTC_RUN_ON(signaling_thread());
@@ -1000,6 +1006,9 @@
       const std::string& transport_name,
       const std::vector<cricket::Candidate>& candidates)
       RTC_RUN_ON(signaling_thread());
+  void OnTransportControllerCandidateError(
+      const cricket::IceCandidateErrorEvent& event)
+      RTC_RUN_ON(signaling_thread());
   void OnTransportControllerCandidatesRemoved(
       const std::vector<cricket::Candidate>& candidates)
       RTC_RUN_ON(signaling_thread());
diff --git a/pc/peer_connection_integrationtest.cc b/pc/peer_connection_integrationtest.cc
index 395c437..3eeeeb8 100644
--- a/pc/peer_connection_integrationtest.cc
+++ b/pc/peer_connection_integrationtest.cc
@@ -565,6 +565,9 @@
   const cricket::Candidate& last_candidate_gathered() const {
     return last_candidate_gathered_;
   }
+  const cricket::IceCandidateErrorEvent& error_event() const {
+    return error_event_;
+  }
 
   // Sets the mDNS responder for the owned fake network manager and keeps a
   // reference to the responder.
@@ -958,6 +961,13 @@
     SendIceMessage(candidate->sdp_mid(), candidate->sdp_mline_index(), ice_sdp);
     last_candidate_gathered_ = candidate->candidate();
   }
+  void OnIceCandidateError(const std::string& host_candidate,
+                           const std::string& url,
+                           int error_code,
+                           const std::string& error_text) override {
+    error_event_ = cricket::IceCandidateErrorEvent(host_candidate, url,
+                                                   error_code, error_text);
+  }
   void OnDataChannel(
       rtc::scoped_refptr<DataChannelInterface> data_channel) override {
     RTC_LOG(LS_INFO) << debug_name_ << ": OnDataChannel";
@@ -990,6 +1000,7 @@
   int signaling_delay_ms_ = 0;
   bool signal_ice_candidates_ = true;
   cricket::Candidate last_candidate_gathered_;
+  cricket::IceCandidateErrorEvent error_event_;
 
   // Store references to the video sources we've created, so that we can stop
   // them, if required.
@@ -5154,6 +5165,46 @@
   ClosePeerConnections();
 }
 
+TEST_P(PeerConnectionIntegrationTest, OnIceCandidateError) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-GatherOnCandidateFilterChanged/Enabled/");
+  static const rtc::SocketAddress turn_server_internal_address{"88.88.88.0",
+                                                               3478};
+  static const rtc::SocketAddress turn_server_external_address{"88.88.88.1", 0};
+
+  CreateTurnServer(turn_server_internal_address, turn_server_external_address);
+
+  webrtc::PeerConnectionInterface::IceServer ice_server;
+  ice_server.urls.push_back("turn:88.88.88.0:3478");
+  ice_server.username = "test";
+  ice_server.password = "123";
+
+  PeerConnectionInterface::RTCConfiguration caller_config;
+  caller_config.servers.push_back(ice_server);
+  caller_config.type = webrtc::PeerConnectionInterface::kRelay;
+  caller_config.continual_gathering_policy = PeerConnection::GATHER_CONTINUALLY;
+
+  PeerConnectionInterface::RTCConfiguration callee_config;
+  callee_config.servers.push_back(ice_server);
+  callee_config.type = webrtc::PeerConnectionInterface::kRelay;
+  callee_config.continual_gathering_policy = PeerConnection::GATHER_CONTINUALLY;
+
+  ASSERT_TRUE(
+      CreatePeerConnectionWrappersWithConfig(caller_config, callee_config));
+
+  // Do normal offer/answer and wait for ICE to complete.
+  ConnectFakeSignaling();
+  caller()->AddAudioVideoTracks();
+  callee()->AddAudioVideoTracks();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+  EXPECT_EQ_WAIT(401, caller()->error_event().error_code, kDefaultTimeout);
+  EXPECT_EQ("Unauthorized", caller()->error_event().error_text);
+  EXPECT_EQ("turn:88.88.88.0:3478?transport=udp", caller()->error_event().url);
+  EXPECT_NE(std::string::npos,
+            caller()->error_event().host_candidate.find(":"));
+}
+
 INSTANTIATE_TEST_SUITE_P(PeerConnectionIntegrationTest,
                          PeerConnectionIntegrationTest,
                          Values(SdpSemantics::kPlanB,