dcsctp: Detect the peer SCTP implementation

It's to be used for clients to record metrics and to e.g. attribute
metrics to which SCTP implementation the peer was using.

This is not explicitly signaled, so heuristics are used. These are not
guaranteed to come to the correct conclusion, and the data is not always
available.

Note: The behavior of dcSCTP will not change depending on the assumed
implementation - only by explicitly signaled capabilities.

Bug: webrtc:13216
Change-Id: I2f58054d17d53d947ed5845df7a08f974d42f918
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/233100
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35103}
diff --git a/net/dcsctp/public/dcsctp_socket.h b/net/dcsctp/public/dcsctp_socket.h
index 583d037..2b56094a 100644
--- a/net/dcsctp/public/dcsctp_socket.h
+++ b/net/dcsctp/public/dcsctp_socket.h
@@ -206,6 +206,31 @@
   absl::optional<uint32_t> peer_rwnd_bytes = absl::nullopt;
 };
 
+// Represent known SCTP implementations.
+enum class SctpImplementation {
+  // There is not enough information toto determine any SCTP implementation.
+  kUnknown,
+  // This implementation.
+  kDcsctp,
+  // https://github.com/sctplab/usrsctp.
+  kUsrSctp,
+  // Any other implementation.
+  kOther,
+};
+
+inline constexpr absl::string_view ToString(SctpImplementation implementation) {
+  switch (implementation) {
+    case SctpImplementation::kUnknown:
+      return "unknown";
+    case SctpImplementation::kDcsctp:
+      return "dcsctp";
+    case SctpImplementation::kUsrSctp:
+      return "usrsctp";
+    case SctpImplementation::kOther:
+      return "other";
+  }
+}
+
 // Callbacks that the DcSctpSocket will call synchronously to the owning
 // client. It is allowed to call back into the library from callbacks that start
 // with "On". It has been explicitly documented when it's not allowed to call
@@ -440,6 +465,15 @@
   // called on success.
   virtual absl::optional<DcSctpSocketHandoverState>
   GetHandoverStateAndClose() = 0;
+
+  // Returns the detected SCTP implementation of the peer. As this is not
+  // explicitly signalled during the connection establishment, heuristics is
+  // used to analyze e.g. the state cookie in the INIT-ACK chunk.
+  //
+  // If this method is called too early (before
+  // `DcSctpSocketCallbacks::OnConnected` has triggered), this will likely
+  // return `SctpImplementation::kUnknown`.
+  virtual SctpImplementation peer_implementation() const = 0;
 };
 }  // namespace dcsctp
 
diff --git a/net/dcsctp/public/mock_dcsctp_socket.h b/net/dcsctp/public/mock_dcsctp_socket.h
index eb1e8cc..d207899 100644
--- a/net/dcsctp/public/mock_dcsctp_socket.h
+++ b/net/dcsctp/public/mock_dcsctp_socket.h
@@ -73,6 +73,8 @@
               GetHandoverStateAndClose,
               (),
               (override));
+
+  MOCK_METHOD(SctpImplementation, peer_implementation, (), (const));
 };
 
 }  // namespace dcsctp
diff --git a/net/dcsctp/socket/dcsctp_socket.cc b/net/dcsctp/socket/dcsctp_socket.cc
index 7910a39..a1cc12d 100644
--- a/net/dcsctp/socket/dcsctp_socket.cc
+++ b/net/dcsctp/socket/dcsctp_socket.cc
@@ -138,6 +138,20 @@
   return TieTag(static_cast<uint64_t>(tie_tag_upper) << 32 |
                 static_cast<uint64_t>(tie_tag_lower));
 }
+
+SctpImplementation DeterminePeerImplementation(
+    rtc::ArrayView<const uint8_t> cookie) {
+  if (cookie.size() > 8) {
+    absl::string_view magic(reinterpret_cast<const char*>(cookie.data()), 8);
+    if (magic == "dcSCTP00") {
+      return SctpImplementation::kDcsctp;
+    }
+    if (magic == "KAME-BSD") {
+      return SctpImplementation::kUsrSctp;
+    }
+  }
+  return SctpImplementation::kOther;
+}
 }  // namespace
 
 DcSctpSocket::DcSctpSocket(absl::string_view log_prefix,
@@ -1149,6 +1163,8 @@
   Capabilities capabilities = GetCapabilities(options_, chunk->parameters());
   t1_init_->Stop();
 
+  peer_implementation_ = DeterminePeerImplementation(cookie->data());
+
   tcb_ = std::make_unique<TransmissionControlBlock>(
       timer_manager_, log_prefix_, options_, capabilities, callbacks_,
       send_queue_, connect_params_.verification_tag,
diff --git a/net/dcsctp/socket/dcsctp_socket.h b/net/dcsctp/socket/dcsctp_socket.h
index 508a8a6..c249864 100644
--- a/net/dcsctp/socket/dcsctp_socket.h
+++ b/net/dcsctp/socket/dcsctp_socket.h
@@ -101,7 +101,9 @@
   Metrics GetMetrics() const override;
   HandoverReadinessStatus GetHandoverReadiness() const override;
   absl::optional<DcSctpSocketHandoverState> GetHandoverStateAndClose() override;
-
+  SctpImplementation peer_implementation() const override {
+    return peer_implementation_;
+  }
   // Returns this socket's verification tag, or zero if not yet connected.
   VerificationTag verification_tag() const {
     return tcb_ != nullptr ? tcb_->my_verification_tag() : VerificationTag(0);
@@ -276,6 +278,8 @@
   State state_ = State::kClosed;
   // If the connection is established, contains a transmission control block.
   std::unique_ptr<TransmissionControlBlock> tcb_;
+
+  SctpImplementation peer_implementation_ = SctpImplementation::kUnknown;
 };
 }  // namespace dcsctp
 
diff --git a/net/dcsctp/socket/dcsctp_socket_test.cc b/net/dcsctp/socket/dcsctp_socket_test.cc
index 71eefd4..66876e4 100644
--- a/net/dcsctp/socket/dcsctp_socket_test.cc
+++ b/net/dcsctp/socket/dcsctp_socket_test.cc
@@ -255,10 +255,14 @@
       : options_(MakeOptionsForTest(enable_message_interleaving)),
         cb_a_("A"),
         cb_z_("Z"),
-        sock_a_(std::make_unique<DcSctpSocket>(
-            "A", cb_a_, GetPacketObserver("A"), options_)),
-        sock_z_(std::make_unique<DcSctpSocket>(
-            "Z", cb_z_, GetPacketObserver("Z"), options_)) {}
+        sock_a_(std::make_unique<DcSctpSocket>("A",
+                                               cb_a_,
+                                               GetPacketObserver("A"),
+                                               options_)),
+        sock_z_(std::make_unique<DcSctpSocket>("Z",
+                                               cb_z_,
+                                               GetPacketObserver("Z"),
+                                               options_)) {}
 
   void AdvanceTime(DurationMs duration) {
     cb_a_.AdvanceTime(duration);
@@ -2050,5 +2054,26 @@
   EXPECT_THAT(msg->payload(), testing::ElementsAre(1, 2, 3));
 }
 
+TEST_F(DcSctpSocketTest, CanDetectDcsctpImplementation) {
+  ConnectSockets();
+
+  EXPECT_EQ(sock_a_->peer_implementation(), SctpImplementation::kDcsctp);
+
+  // As A initiated the connection establishment, Z will not receive enough
+  // information to know about A's implementation
+  EXPECT_EQ(sock_z_->peer_implementation(), SctpImplementation::kUnknown);
+}
+
+TEST_F(DcSctpSocketTest, BothCanDetectDcsctpImplementation) {
+  EXPECT_CALL(cb_a_, OnConnected).Times(1);
+  EXPECT_CALL(cb_z_, OnConnected).Times(1);
+  sock_a_->Connect();
+  sock_z_->Connect();
+
+  ExchangeMessages(*sock_a_, cb_a_, *sock_z_, cb_z_);
+
+  EXPECT_EQ(sock_a_->peer_implementation(), SctpImplementation::kDcsctp);
+  EXPECT_EQ(sock_z_->peer_implementation(), SctpImplementation::kDcsctp);
+}
 }  // namespace
 }  // namespace dcsctp
diff --git a/net/dcsctp/socket/state_cookie_test.cc b/net/dcsctp/socket/state_cookie_test.cc
index eab41a7..870620b 100644
--- a/net/dcsctp/socket/state_cookie_test.cc
+++ b/net/dcsctp/socket/state_cookie_test.cc
@@ -36,5 +36,18 @@
   EXPECT_TRUE(deserialized.capabilities().reconfig);
 }
 
+TEST(StateCookieTest, ValidateMagicValue) {
+  Capabilities capabilities = {/*partial_reliability=*/true,
+                               /*message_interleaving=*/false,
+                               /*reconfig=*/true};
+  StateCookie cookie(VerificationTag(123), TSN(456),
+                     /*a_rwnd=*/789, TieTag(101112), capabilities);
+  std::vector<uint8_t> serialized = cookie.Serialize();
+  ASSERT_THAT(serialized, SizeIs(StateCookie::kCookieSize));
+
+  absl::string_view magic(reinterpret_cast<const char*>(serialized.data()), 8);
+  EXPECT_EQ(magic, "dcSCTP00");
+}
+
 }  // namespace
 }  // namespace dcsctp