Enforce "comprehension-required" STUN rules.

If a STUN attribute is in the "comprehension-required" range
(0x0000-0x7FFF), and the implementation does not recognize it, this
should be treated as an error (as per RFC5389), with different behavior
depending on the type of the message received.

Bug: webrtc:9063
Change-Id: Ic31b0cdd3c26772c21d770b44fe4ee4a1b47030a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/64500
Commit-Queue: Taylor <deadbeef@webrtc.org>
Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
Reviewed-by: Qingsi Wang <qingsi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#30925}
diff --git a/api/transport/stun.cc b/api/transport/stun.cc
index 5ed4900..b083f15 100644
--- a/api/transport/stun.cc
+++ b/api/transport/stun.cc
@@ -47,6 +47,7 @@
 const char STUN_ERROR_REASON_TRY_ALTERNATE_SERVER[] = "Try Alternate Server";
 const char STUN_ERROR_REASON_BAD_REQUEST[] = "Bad Request";
 const char STUN_ERROR_REASON_UNAUTHORIZED[] = "Unauthorized";
+const char STUN_ERROR_REASON_UNKNOWN_ATTRIBUTE[] = "Unknown Attribute";
 const char STUN_ERROR_REASON_FORBIDDEN[] = "Forbidden";
 const char STUN_ERROR_REASON_STALE_CREDENTIALS[] = "Stale Credentials";
 const char STUN_ERROR_REASON_ALLOCATION_MISMATCH[] = "Allocation Mismatch";
@@ -140,6 +141,18 @@
   length_ = 0;
 }
 
+std::vector<uint16_t> StunMessage::GetNonComprehendedAttributes() const {
+  std::vector<uint16_t> unknown_attributes;
+  for (auto& attr : attrs_) {
+    // "comprehension-required" range is 0x0000-0x7FFF.
+    if (attr->type() >= 0x0000 && attr->type() <= 0x7FFF &&
+        GetAttributeValueType(attr->type()) == STUN_VALUE_UNKNOWN) {
+      unknown_attributes.push_back(attr->type());
+    }
+  }
+  return unknown_attributes;
+}
+
 const StunAddressAttribute* StunMessage::GetAddress(int type) const {
   switch (type) {
     case STUN_ATTR_MAPPED_ADDRESS: {
diff --git a/api/transport/stun.h b/api/transport/stun.h
index 41f76a1..51ca306 100644
--- a/api/transport/stun.h
+++ b/api/transport/stun.h
@@ -163,6 +163,10 @@
   void SetType(int type) { type_ = static_cast<uint16_t>(type); }
   bool SetTransactionID(const std::string& str);
 
+  // Get a list of all of the attribute types in the "comprehension required"
+  // range that were not recognized.
+  std::vector<uint16_t> GetNonComprehendedAttributes() const;
+
   // Gets the desired attribute value, or NULL if no such attribute type exists.
   const StunAddressAttribute* GetAddress(int type) const;
   const StunUInt32Attribute* GetUInt32(int type) const;
diff --git a/p2p/base/port.cc b/p2p/base/port.cc
index a6eb333..0f2b2c6 100644
--- a/p2p/base/port.cc
+++ b/p2p/base/port.cc
@@ -469,6 +469,12 @@
     return false;
   }
 
+  // Get list of attributes in the "comprehension-required" range that were not
+  // comprehended. If one or more is found, the behavior differs based on the
+  // type of the incoming message; see below.
+  std::vector<uint16_t> unknown_attributes =
+      stun_msg->GetNonComprehendedAttributes();
+
   if (stun_msg->type() == STUN_BINDING_REQUEST) {
     // Check for the presence of USERNAME and MESSAGE-INTEGRITY (if ICE) first.
     // If not present, fail with a 400 Bad Request.
@@ -507,6 +513,15 @@
                                STUN_ERROR_REASON_UNAUTHORIZED);
       return true;
     }
+
+    // If a request contains unknown comprehension-required attributes, reply
+    // with an error. See RFC5389 section 7.3.1.
+    if (!unknown_attributes.empty()) {
+      SendUnknownAttributesErrorResponse(stun_msg.get(), addr,
+                                         unknown_attributes);
+      return true;
+    }
+
     out_username->assign(remote_ufrag);
   } else if ((stun_msg->type() == STUN_BINDING_RESPONSE) ||
              (stun_msg->type() == STUN_BINDING_ERROR_RESPONSE)) {
@@ -527,6 +542,15 @@
         return true;
       }
     }
+    // If a response contains unknown comprehension-required attributes, it's
+    // simply discarded and the transaction is considered failed. See RFC5389
+    // sections 7.3.3 and 7.3.4.
+    if (!unknown_attributes.empty()) {
+      RTC_LOG(LS_ERROR) << ToString()
+                        << ": Discarding STUN response due to unknown "
+                           "comprehension-required attribute";
+      return true;
+    }
     // NOTE: Username should not be used in verifying response messages.
     out_username->clear();
   } else if (stun_msg->type() == STUN_BINDING_INDICATION) {
@@ -534,6 +558,15 @@
                         << StunMethodToString(stun_msg->type()) << ": from "
                         << addr.ToSensitiveString();
     out_username->clear();
+
+    // If an indication contains unknown comprehension-required attributes,[]
+    // it's simply discarded. See RFC5389 section 7.3.2.
+    if (!unknown_attributes.empty()) {
+      RTC_LOG(LS_ERROR) << ToString()
+                        << ": Discarding STUN indication due to "
+                           "unknown comprehension-required attribute";
+      return true;
+    }
     // No stun attributes will be verified, if it's stun indication message.
     // Returning from end of the this method.
   } else if (stun_msg->type() == GOOG_PING_REQUEST) {
@@ -749,6 +782,44 @@
                    << addr.ToSensitiveString();
 }
 
+void Port::SendUnknownAttributesErrorResponse(
+    StunMessage* request,
+    const rtc::SocketAddress& addr,
+    const std::vector<uint16_t>& unknown_types) {
+  RTC_DCHECK(request->type() == STUN_BINDING_REQUEST);
+
+  // Fill in the response message.
+  StunMessage response;
+  response.SetType(STUN_BINDING_ERROR_RESPONSE);
+  response.SetTransactionID(request->transaction_id());
+
+  auto error_attr = StunAttribute::CreateErrorCode();
+  error_attr->SetCode(STUN_ERROR_UNKNOWN_ATTRIBUTE);
+  error_attr->SetReason(STUN_ERROR_REASON_UNKNOWN_ATTRIBUTE);
+  response.AddAttribute(std::move(error_attr));
+
+  std::unique_ptr<StunUInt16ListAttribute> unknown_attr =
+      StunAttribute::CreateUnknownAttributes();
+  for (uint16_t type : unknown_types) {
+    unknown_attr->AddType(type);
+  }
+  response.AddAttribute(std::move(unknown_attr));
+
+  response.AddMessageIntegrity(password_);
+  response.AddFingerprint();
+
+  // Send the response message.
+  rtc::ByteBufferWriter buf;
+  response.Write(&buf);
+  rtc::PacketOptions options(StunDscpValue());
+  options.info_signaled_after_sent.packet_type =
+      rtc::PacketType::kIceConnectivityCheckResponse;
+  SendTo(buf.Data(), buf.Length(), addr, options, false);
+  RTC_LOG(LS_ERROR) << ToString() << ": Sending STUN binding error: reason="
+                    << STUN_ERROR_UNKNOWN_ATTRIBUTE << " to "
+                    << addr.ToSensitiveString();
+}
+
 void Port::KeepAliveUntilPruned() {
   // If it is pruned, we won't bring it up again.
   if (state_ == State::INIT) {
diff --git a/p2p/base/port.h b/p2p/base/port.h
index 4200bed..bf1c041 100644
--- a/p2p/base/port.h
+++ b/p2p/base/port.h
@@ -295,6 +295,10 @@
                                 const rtc::SocketAddress& addr,
                                 int error_code,
                                 const std::string& reason) override;
+  void SendUnknownAttributesErrorResponse(
+      StunMessage* request,
+      const rtc::SocketAddress& addr,
+      const std::vector<uint16_t>& unknown_types);
 
   void set_proxy(const std::string& user_agent, const rtc::ProxyInfo& proxy) {
     user_agent_ = user_agent;
diff --git a/p2p/base/port_unittest.cc b/p2p/base/port_unittest.cc
index eaa2545..a7ac1fa 100644
--- a/p2p/base/port_unittest.cc
+++ b/p2p/base/port_unittest.cc
@@ -2222,6 +2222,110 @@
   EXPECT_EQ(0, port->last_stun_error_code());
 }
 
+// Test handling a STUN message with unknown attributes in the
+// "comprehension-required" range. Should respond with an error with the
+// unknown attributes' IDs.
+TEST_F(PortTest,
+       TestHandleStunRequestWithUnknownComprehensionRequiredAttribute) {
+  // Our port will act as the "remote" port.
+  std::unique_ptr<TestPort> port(CreateTestPort(kLocalAddr2, "rfrag", "rpass"));
+
+  std::unique_ptr<IceMessage> in_msg, out_msg;
+  auto buf = std::make_unique<ByteBufferWriter>();
+  rtc::SocketAddress addr(kLocalAddr1);
+  std::string username;
+
+  // Build ordinary message with valid ufrag/pass.
+  in_msg = CreateStunMessageWithUsername(STUN_BINDING_REQUEST, "rfrag:lfrag");
+  in_msg->AddMessageIntegrity("rpass");
+  // Add a couple attributes with ID in comprehension-required range.
+  in_msg->AddAttribute(StunAttribute::CreateUInt32(0x7777));
+  in_msg->AddAttribute(StunAttribute::CreateUInt32(0x4567));
+  // ... And one outside the range.
+  in_msg->AddAttribute(StunAttribute::CreateUInt32(0xdead));
+  in_msg->AddFingerprint();
+  WriteStunMessage(*in_msg, buf.get());
+  ASSERT_TRUE(port->GetStunMessage(buf->Data(), buf->Length(), addr, &out_msg,
+                                   &username));
+  IceMessage* error_response = port->last_stun_msg();
+  ASSERT_NE(nullptr, error_response);
+
+  // Verify that the "unknown attribute" error response has the right error
+  // code, and includes an attribute that lists out the unrecognized attribute
+  // types.
+  EXPECT_EQ(STUN_ERROR_UNKNOWN_ATTRIBUTE, error_response->GetErrorCodeValue());
+  const StunUInt16ListAttribute* unknown_attributes =
+      error_response->GetUnknownAttributes();
+  ASSERT_NE(nullptr, unknown_attributes);
+  ASSERT_EQ(2u, unknown_attributes->Size());
+  EXPECT_EQ(0x7777, unknown_attributes->GetType(0));
+  EXPECT_EQ(0x4567, unknown_attributes->GetType(1));
+}
+
+// Similar to the above, but with a response instead of a request. In this
+// case the response should just be ignored and transaction treated is failed.
+TEST_F(PortTest,
+       TestHandleStunResponseWithUnknownComprehensionRequiredAttribute) {
+  // Generic setup.
+  auto lport = CreateTestPort(kLocalAddr1, "lfrag", "lpass");
+  lport->SetIceRole(cricket::ICEROLE_CONTROLLING);
+  auto rport = CreateTestPort(kLocalAddr2, "rfrag", "rpass");
+  rport->SetIceRole(cricket::ICEROLE_CONTROLLED);
+  lport->PrepareAddress();
+  rport->PrepareAddress();
+  ASSERT_FALSE(lport->Candidates().empty());
+  ASSERT_FALSE(rport->Candidates().empty());
+  Connection* lconn =
+      lport->CreateConnection(rport->Candidates()[0], Port::ORIGIN_MESSAGE);
+  Connection* rconn =
+      rport->CreateConnection(lport->Candidates()[0], Port::ORIGIN_MESSAGE);
+
+  // Send request.
+  lconn->Ping(0);
+  ASSERT_TRUE_WAIT(lport->last_stun_msg() != NULL, kDefaultTimeout);
+  rconn->OnReadPacket(lport->last_stun_buf()->data<char>(),
+                      lport->last_stun_buf()->size(), /* packet_time_us */ -1);
+
+  // Intercept request and add comprehension required attribute.
+  ASSERT_TRUE_WAIT(rport->last_stun_msg() != NULL, kDefaultTimeout);
+  auto modified_response = rport->last_stun_msg()->Clone();
+  modified_response->AddAttribute(StunAttribute::CreateUInt32(0x7777));
+  modified_response->RemoveAttribute(STUN_ATTR_FINGERPRINT);
+  modified_response->AddFingerprint();
+  ByteBufferWriter buf;
+  WriteStunMessage(*modified_response, &buf);
+  lconn->OnReadPacket(buf.Data(), buf.Length(), /* packet_time_us */ -1);
+  // Response should have been ignored, leaving us unwritable still.
+  EXPECT_FALSE(lconn->writable());
+}
+
+// Similar to the above, but with an indication. As with a response, it should
+// just be ignored.
+TEST_F(PortTest,
+       TestHandleStunIndicationWithUnknownComprehensionRequiredAttribute) {
+  // Generic set up.
+  auto lport = CreateTestPort(kLocalAddr2, "lfrag", "lpass");
+  lport->SetIceRole(cricket::ICEROLE_CONTROLLING);
+  auto rport = CreateTestPort(kLocalAddr2, "rfrag", "rpass");
+  rport->SetIceRole(cricket::ICEROLE_CONTROLLED);
+  lport->PrepareAddress();
+  rport->PrepareAddress();
+  ASSERT_FALSE(lport->Candidates().empty());
+  ASSERT_FALSE(rport->Candidates().empty());
+  Connection* lconn =
+      lport->CreateConnection(rport->Candidates()[0], Port::ORIGIN_MESSAGE);
+
+  // Generate indication with comprehension required attribute and verify it
+  // doesn't update last_ping_received.
+  auto in_msg = CreateStunMessage(STUN_BINDING_INDICATION);
+  in_msg->AddAttribute(StunAttribute::CreateUInt32(0x7777));
+  in_msg->AddFingerprint();
+  ByteBufferWriter buf;
+  WriteStunMessage(*in_msg, &buf);
+  lconn->OnReadPacket(buf.Data(), buf.Length(), /* packet_time_us */ -1);
+  EXPECT_EQ(0u, lconn->last_ping_received());
+}
+
 // Test handling of STUN binding indication messages . STUN binding
 // indications are allowed only to the connection which is in read mode.
 TEST_F(PortTest, TestHandleStunBindingIndication) {
diff --git a/p2p/base/stun_request.cc b/p2p/base/stun_request.cc
index b4dba7d..d7c2336 100644
--- a/p2p/base/stun_request.cc
+++ b/p2p/base/stun_request.cc
@@ -125,7 +125,15 @@
   }
 
   StunRequest* request = iter->second;
-  if (msg->type() == GetStunSuccessResponseType(request->type())) {
+  if (!msg->GetNonComprehendedAttributes().empty()) {
+    // If a response contains unknown comprehension-required attributes, it's
+    // simply discarded and the transaction is considered failed. See RFC5389
+    // sections 7.3.3 and 7.3.4.
+    RTC_LOG(LS_ERROR) << ": Discarding response due to unknown "
+                         "comprehension-required attribute.";
+    delete request;
+    return false;
+  } else if (msg->type() == GetStunSuccessResponseType(request->type())) {
     request->OnResponse(msg);
   } else if (msg->type() == GetStunErrorResponseType(request->type())) {
     request->OnErrorResponse(msg);
diff --git a/p2p/base/stun_request_unittest.cc b/p2p/base/stun_request_unittest.cc
index 1f48c19..ce573f0 100644
--- a/p2p/base/stun_request_unittest.cc
+++ b/p2p/base/stun_request_unittest.cc
@@ -198,4 +198,22 @@
   delete res;
 }
 
+// If the response contains an attribute in the "comprehension required" range
+// which is not recognized, the transaction should be considered a failure and
+// the response should be ignored.
+TEST_F(StunRequestTest, TestUnrecognizedComprehensionRequiredAttribute) {
+  StunMessage* req = CreateStunMessage(STUN_BINDING_REQUEST, NULL);
+
+  manager_.Send(new StunRequestThunker(req, this));
+  StunMessage* res = CreateStunMessage(STUN_BINDING_ERROR_RESPONSE, req);
+  res->AddAttribute(StunAttribute::CreateUInt32(0x7777));
+  EXPECT_FALSE(manager_.CheckResponse(res));
+
+  EXPECT_EQ(nullptr, response_);
+  EXPECT_FALSE(success_);
+  EXPECT_FALSE(failure_);
+  EXPECT_FALSE(timeout_);
+  delete res;
+}
+
 }  // namespace cricket