dcsctp: Resend SHUTDOWN ACK on timer expiration

If the T2-shutdown timer expires while the receiver is in the
SHUTDOWN-ACK-SENT state, it MUST resend the SHUTDOWN ACK chunk.

Previously, OnShutdownTimerExpiry would always call SendShutdown(),
meaning that if the timer expired in the SHUTDOWN-ACK-SENT state, it
would incorrectly send a SHUTDOWN chunk instead of a SHUTDOWN ACK.
This CL fixes this by checking the socket state in
OnShutdownTimerExpiry.

Bug: webrtc:500087337
Change-Id: I603c415a162bf7d194ef06a9c5e74dbf3b0dafe8
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/461920
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Reviewed-by: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47337}
diff --git a/net/dcsctp/socket/dcsctp_socket.cc b/net/dcsctp/socket/dcsctp_socket.cc
index 0bcdbd5..cc883ba 100644
--- a/net/dcsctp/socket/dcsctp_socket.cc
+++ b/net/dcsctp/socket/dcsctp_socket.cc
@@ -1036,6 +1036,8 @@
 }
 
 TimeDelta DcSctpSocket::OnShutdownTimerExpiry() {
+  RTC_DCHECK(state_ == State::kShutdownSent ||
+             state_ == State::kShutdownAckSent);
   RTC_DLOG(LS_VERBOSE) << log_prefix() << "Timer " << t2_shutdown_->name()
                        << " has expired: " << t2_shutdown_->expiration_count()
                        << "/"
@@ -1058,10 +1060,19 @@
     return TimeDelta::Zero();
   }
 
-  // https://tools.ietf.org/html/rfc4960#section-9.2
-  // "If the timer expires, the endpoint must resend the SHUTDOWN with the
-  // updated last sequential TSN received from its peer."
-  SendShutdown();
+  if (state_ == State::kShutdownAckSent) {
+    // https://tools.ietf.org/html/rfc4960#section-9.2
+    // "... and start a T2-shutdown timer of its own, entering the
+    // SHUTDOWN-ACK-SENT state.  If the timer expires, the endpoint must
+    // resend the SHUTDOWN ACK."
+    SendShutdownAck();
+  } else {
+    // https://tools.ietf.org/html/rfc4960#section-9.2
+    // "It shall then start the T2-shutdown timer and enter the SHUTDOWN-SENT
+    // state.  If the timer expires, the endpoint must resend the SHUTDOWN
+    // with the updated last sequential TSN received from its peer."
+    SendShutdown();
+  }
   RTC_DCHECK(IsConsistent());
   return tcb_->current_rto();
 }
diff --git a/net/dcsctp/socket/dcsctp_socket_test.cc b/net/dcsctp/socket/dcsctp_socket_test.cc
index fcab267..7d50182 100644
--- a/net/dcsctp/socket/dcsctp_socket_test.cc
+++ b/net/dcsctp/socket/dcsctp_socket_test.cc
@@ -40,6 +40,7 @@
 #include "net/dcsctp/packet/chunk/reconfig_chunk.h"
 #include "net/dcsctp/packet/chunk/sack_chunk.h"
 #include "net/dcsctp/packet/chunk/shutdown_chunk.h"
+#include "net/dcsctp/packet/chunk/shutdown_ack_chunk.h"
 #include "net/dcsctp/packet/data.h"
 #include "net/dcsctp/packet/error_cause/unrecognized_chunk_type_cause.h"
 #include "net/dcsctp/packet/parameter/heartbeat_info_parameter.h"
@@ -776,6 +777,31 @@
   EXPECT_TRUE(a.cb.ConsumeSentPacket().empty());
 }
 
+TEST(DcSctpSocketTest, T2ShutdownTimerExpiresResendsShutdownAck) {
+  SocketUnderTest a("A");
+  SocketUnderTest z("Z");
+
+  ConnectSockets(a, z);
+
+  z.socket.Shutdown();
+
+  // A receives SHUTDOWN, sends SHUTDOWN_ACK.
+  // A is now in SHUTDOWN-ACK-SENT state.
+  a.socket.ReceivePacket(z.cb.ConsumeSentPacket());
+
+  // Drop the SHUTDOWN_ACK sent by A.
+  EXPECT_THAT(a.cb.ConsumeSentPacket(),
+              HasChunks(ElementsAre(IsChunkType(ShutdownAckChunk::kType))));
+
+  EXPECT_EQ(a.socket.state(), SocketState::kShuttingDown);
+  EXPECT_EQ(z.socket.state(), SocketState::kShuttingDown);
+
+  // Advance time to trigger timer expiry, which makes A resend SHUTDOWN_ACK.
+  AdvanceTime(a, z, a.options.rto_initial.ToTimeDelta());
+  EXPECT_THAT(a.cb.ConsumeSentPacket(),
+              HasChunks(ElementsAre(IsChunkType(ShutdownAckChunk::kType))));
+}
+
 TEST(DcSctpSocketTest, EstablishConnectionWhileSendingData) {
   SocketUnderTest a("A");
   SocketUnderTest z("Z");