gfe-relnote: In QUIC, use QuicNetworkBlackholeDetector which detects both path degrading and network blackhole. Protected by gfe2_reloadable_flag_quic_use_blackhole_detector.

Path degrading is only armed after handshake completes. And blackhole detection now is based on time rather than event driven (i.e., 5RTO, 6PTO, etc)

PiperOrigin-RevId: 302526709
Change-Id: I43e776e18979d4f8ce4f26708107b4ef9356aa1a
diff --git a/quic/core/quic_connection.cc b/quic/core/quic_connection.cc
index 5f2b031..dcb8ef8 100644
--- a/quic/core/quic_connection.cc
+++ b/quic/core/quic_connection.cc
@@ -332,7 +332,8 @@
       max_consecutive_ptos_(0),
       bytes_received_before_address_validation_(0),
       bytes_sent_before_address_validation_(0),
-      address_validated_(false) {
+      address_validated_(false),
+      blackhole_detector_(this, &arena_, alarm_factory_) {
   QUIC_DLOG(INFO) << ENDPOINT << "Created connection with server connection ID "
                   << server_connection_id
                   << " and version: " << ParsedQuicVersionToString(version());
@@ -2335,8 +2336,23 @@
     debug_visitor_->OnPacketSent(*packet, packet->transmission_type,
                                  packet_send_time);
   }
-  if (IsRetransmittable(*packet) == HAS_RETRANSMITTABLE_DATA) {
-    if (!is_path_degrading_ && !path_degrading_alarm_->IsSet()) {
+  if (IsRetransmittable(*packet) == HAS_RETRANSMITTABLE_DATA &&
+      !is_termination_packet) {
+    // Start blackhole/path degrading detections if the sent packet is not
+    // termination packet and contains retransmittable data.
+    if (use_blackhole_detector_) {
+      // Do not restart detection if detection is in progress indicating no
+      // forward progress has been made since last event (i.e., packet was sent
+      // or new packets were acknowledged).
+      if (!blackhole_detector_.IsDetectionInProgress()) {
+        // Try to start detections if no detection in progress. This could
+        // because either both detections are inactive when sending last packet
+        // or this connection just gets out of quiescence.
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_use_blackhole_detector, 1, 4);
+        blackhole_detector_.RestartDetection(GetPathDegradingDeadline(),
+                                             GetNetworkBlackholeDeadline());
+      }
+    } else if (!is_path_degrading_ && !path_degrading_alarm_->IsSet()) {
       // This is the first retransmittable packet on the working path.
       // Start the path degrading alarm to detect new path degrading.
       SetPathDegradingAlarm();
@@ -2640,20 +2656,26 @@
 
   QuicPacketNumber previous_created_packet_number =
       packet_creator_.packet_number();
-  if (close_connection_after_five_rtos_ &&
-      sent_packet_manager_.GetConsecutiveRtoCount() >= 4) {
-    // Close on the 5th consecutive RTO, so after 4 previous RTOs have occurred.
-    CloseConnection(QUIC_TOO_MANY_RTOS, "5 consecutive retransmission timeouts",
-                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
-    return;
-  }
-  if (sent_packet_manager_.pto_enabled() && max_consecutive_ptos_ > 0 &&
-      sent_packet_manager_.GetConsecutivePtoCount() >= max_consecutive_ptos_) {
-    CloseConnection(QUIC_TOO_MANY_RTOS,
-                    quiche::QuicheStrCat(max_consecutive_ptos_ + 1,
-                                         "consecutive retransmission timeouts"),
-                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
-    return;
+  if (!use_blackhole_detector_) {
+    if (close_connection_after_five_rtos_ &&
+        sent_packet_manager_.GetConsecutiveRtoCount() >= 4) {
+      // Close on the 5th consecutive RTO, so after 4 previous RTOs have
+      // occurred.
+      CloseConnection(QUIC_TOO_MANY_RTOS,
+                      "5 consecutive retransmission timeouts",
+                      ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+      return;
+    }
+    if (sent_packet_manager_.pto_enabled() && max_consecutive_ptos_ > 0 &&
+        sent_packet_manager_.GetConsecutivePtoCount() >=
+            max_consecutive_ptos_) {
+      CloseConnection(
+          QUIC_TOO_MANY_RTOS,
+          quiche::QuicheStrCat(max_consecutive_ptos_ + 1,
+                               "consecutive retransmission timeouts"),
+          ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+      return;
+    }
   }
 
   const auto retransmission_mode =
@@ -3032,6 +3054,10 @@
   mtu_discovery_alarm_->Cancel();
   path_degrading_alarm_->Cancel();
   process_undecryptable_packets_alarm_->Cancel();
+  if (use_blackhole_detector_) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_use_blackhole_detector, 4, 4);
+    blackhole_detector_.StopDetection();
+  }
 }
 
 QuicByteCount QuicConnection::max_packet_length() const {
@@ -3222,6 +3248,7 @@
 }
 
 void QuicConnection::SetPathDegradingAlarm() {
+  DCHECK(!use_blackhole_detector_);
   if (perspective_ == Perspective::IS_SERVER) {
     return;
   }
@@ -3748,7 +3775,23 @@
   // Always reset the retransmission alarm when an ack comes in, since we now
   // have a better estimate of the current rtt than when it was set.
   SetRetransmissionAlarm();
-  MaybeSetPathDegradingAlarm(acked_new_packet);
+  if (use_blackhole_detector_) {
+    if (acked_new_packet) {
+      is_path_degrading_ = false;
+      if (sent_packet_manager_.HasInFlightPackets()) {
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_use_blackhole_detector, 2, 4);
+        // Restart detections if forward progress has been made.
+        blackhole_detector_.RestartDetection(GetPathDegradingDeadline(),
+                                             GetNetworkBlackholeDeadline());
+      } else {
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_use_blackhole_detector, 3, 4);
+        // Stop detections in quiecense.
+        blackhole_detector_.StopDetection();
+      }
+    }
+  } else {
+    MaybeSetPathDegradingAlarm(acked_new_packet);
+  }
 
   if (send_stop_waiting) {
     ++stop_waiting_count_;
@@ -3758,6 +3801,7 @@
 }
 
 void QuicConnection::MaybeSetPathDegradingAlarm(bool acked_new_packet) {
+  DCHECK(!use_blackhole_detector_);
   if (!sent_packet_manager_.HasInFlightPackets()) {
     // There are no retransmittable packets on the wire, so it's impossible to
     // say if the connection has degraded.
@@ -4134,5 +4178,54 @@
   framer_.SetExpectedClientConnectionIdLength(client_connection_id_.length());
 }
 
+void QuicConnection::OnPathDegradingDetected() {
+  DCHECK(use_blackhole_detector_);
+  is_path_degrading_ = true;
+  visitor_->OnPathDegrading();
+}
+
+void QuicConnection::OnBlackholeDetected() {
+  DCHECK(use_blackhole_detector_);
+  CloseConnection(QUIC_TOO_MANY_RTOS, "Network blackhole detected.",
+                  ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+}
+
+QuicTime QuicConnection::GetPathDegradingDeadline() const {
+  DCHECK(use_blackhole_detector_);
+  if (!ShouldDetectPathDegrading()) {
+    return QuicTime::Zero();
+  }
+  return clock_->ApproximateNow() +
+         sent_packet_manager_.GetPathDegradingDelay();
+}
+
+bool QuicConnection::ShouldDetectPathDegrading() const {
+  DCHECK(use_blackhole_detector_);
+  return connected_ && handshake_timeout_.IsInfinite() &&
+         perspective_ == Perspective::IS_CLIENT && !is_path_degrading_;
+}
+
+QuicTime QuicConnection::GetNetworkBlackholeDeadline() const {
+  DCHECK(use_blackhole_detector_);
+  if (!ShouldDetectBlackhole()) {
+    return QuicTime::Zero();
+  }
+  return clock_->ApproximateNow() +
+         sent_packet_manager_.GetNetworkBlackholeDelay();
+}
+
+bool QuicConnection::ShouldDetectBlackhole() const {
+  DCHECK(use_blackhole_detector_);
+  if (!connected_) {
+    return false;
+  }
+  if (!handshake_timeout_.IsInfinite()) {
+    // No blackhole detection before handshake completes.
+    return false;
+  }
+  return close_connection_after_five_rtos_ ||
+         (sent_packet_manager_.pto_enabled() && max_consecutive_ptos_ > 0);
+}
+
 #undef ENDPOINT  // undef for jumbo builds
 }  // namespace quic
diff --git a/quic/core/quic_connection.h b/quic/core/quic_connection.h
index fcd177a..cd478a6 100644
--- a/quic/core/quic_connection.h
+++ b/quic/core/quic_connection.h
@@ -36,6 +36,7 @@
 #include "net/third_party/quiche/src/quic/core/quic_connection_stats.h"
 #include "net/third_party/quiche/src/quic/core/quic_framer.h"
 #include "net/third_party/quiche/src/quic/core/quic_mtu_discovery.h"
+#include "net/third_party/quiche/src/quic/core/quic_network_blackhole_detector.h"
 #include "net/third_party/quiche/src/quic/core/quic_one_block_arena.h"
 #include "net/third_party/quiche/src/quic/core/quic_packet_creator.h"
 #include "net/third_party/quiche/src/quic/core/quic_packet_writer.h"
@@ -348,7 +349,8 @@
     : public QuicFramerVisitorInterface,
       public QuicBlockedWriterInterface,
       public QuicPacketCreator::DelegateInterface,
-      public QuicSentPacketManager::NetworkChangeVisitor {
+      public QuicSentPacketManager::NetworkChangeVisitor,
+      public QuicNetworkBlackholeDetector::Delegate {
  public:
   // Constructs a new QuicConnection for |connection_id| and
   // |initial_peer_address| using |writer| to write packets. |owns_writer|
@@ -575,6 +577,10 @@
   void OnCongestionChange() override;
   void OnPathMtuIncreased(QuicPacketLength packet_size) override;
 
+  // QuicNetworkBlackholeDetector::Delegate
+  void OnPathDegradingDetected() override;
+  void OnBlackholeDetected() override;
+
   // Please note, this is not a const function. For logging purpose, please use
   // ack_frame().
   const QuicFrame GetUpdatedAckFrame();
@@ -1221,6 +1227,20 @@
   // reverted to a previous(smaller) value to avoid write errors in the future.
   bool ShouldIgnoreWriteError();
 
+  // Returns path degrading deadline. QuicTime::Zero() means no path degrading
+  // detection is needed.
+  QuicTime GetPathDegradingDeadline() const;
+
+  // Returns true if path degrading should be detected.
+  bool ShouldDetectPathDegrading() const;
+
+  // Returns network blackhole deadline. QuicTime::Zero() means no blackhole
+  // detection is needed.
+  QuicTime GetNetworkBlackholeDeadline() const;
+
+  // Returns true if network blackhole should be detected.
+  bool ShouldDetectBlackhole() const;
+
   QuicFramer framer_;
 
   // Contents received in the current packet, especially used to identify
@@ -1379,6 +1399,8 @@
   // An alarm that fires when an MTU probe should be sent.
   QuicArenaScopedPtr<QuicAlarm> mtu_discovery_alarm_;
   // An alarm that fires when this connection is considered degrading.
+  // TODO(fayang): Remove this when deprecating quic_use_blackhole_detector
+  // flag.
   QuicArenaScopedPtr<QuicAlarm> path_degrading_alarm_;
   // An alarm that fires to process undecryptable packets when new decyrption
   // keys are available.
@@ -1562,6 +1584,11 @@
   QuicCoalescedPacket coalesced_packet_;
 
   QuicConnectionMtuDiscoverer mtu_discoverer_;
+
+  QuicNetworkBlackholeDetector blackhole_detector_;
+
+  const bool use_blackhole_detector_ =
+      GetQuicReloadableFlag(quic_use_blackhole_detector);
 };
 
 }  // namespace quic
diff --git a/quic/core/quic_connection_test.cc b/quic/core/quic_connection_test.cc
index cd83e94..1b1511b 100644
--- a/quic/core/quic_connection_test.cc
+++ b/quic/core/quic_connection_test.cc
@@ -863,6 +863,34 @@
         QuicConnectionPeer::GetProcessUndecryptablePacketsAlarm(this));
   }
 
+  TestAlarmFactory::TestAlarm* GetBlackholeDetectorAlarm() {
+    DCHECK(GetQuicReloadableFlag(quic_use_blackhole_detector));
+    return reinterpret_cast<TestAlarmFactory::TestAlarm*>(
+        QuicConnectionPeer::GetBlackholeDetectorAlarm(this));
+  }
+
+  void PathDegradingTimeout() {
+    DCHECK(PathDegradingDetectionInProgress());
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      GetBlackholeDetectorAlarm()->Fire();
+    } else {
+      GetPathDegradingAlarm()->Fire();
+    }
+  }
+
+  bool PathDegradingDetectionInProgress() {
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      return QuicConnectionPeer::GetPathDegradingDeadline(this).IsInitialized();
+    }
+    return GetPathDegradingAlarm()->IsSet();
+  }
+
+  bool BlackholeDetectionInProgress() {
+    DCHECK(GetQuicReloadableFlag(quic_use_blackhole_detector));
+    return QuicConnectionPeer::GetBlackholeDetectionDeadline(this)
+        .IsInitialized();
+  }
+
   void SetMaxTailLossProbes(size_t max_tail_loss_probes) {
     QuicSentPacketManagerPeer::SetMaxTailLossProbes(
         QuicConnectionPeer::GetSentPacketManager(this), max_tail_loss_probes);
@@ -4224,6 +4252,8 @@
   QuicTagVector options;
   options.push_back(kTLPR);
   config.SetConnectionOptionsToSend(options);
+  QuicConfigPeer::ReceiveIdleNetworkTimeout(&config, SERVER,
+                                            kDefaultIdleTimeoutSecs);
   connection_.SetFromConfig(config);
   connection_.SetMaxTailLossProbes(1);
 
@@ -4236,7 +4266,7 @@
   EXPECT_TRUE(connection_.connected());
   EXPECT_CALL(visitor_, ShouldKeepConnectionAlive())
       .WillRepeatedly(Return(true));
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
   EXPECT_FALSE(connection_.GetPingAlarm()->IsSet());
 
@@ -4250,7 +4280,7 @@
 
   // Path degrading alarm should be set when there is a retransmittable packet
   // on the wire.
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
 
   // Verify the path degrading delay.
   // First TLP with stream data.
@@ -4283,7 +4313,7 @@
 
   // Path degrading alarm should be cancelled as there is no more
   // reretransmittable packets on the wire.
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   // The ping alarm should be set to the retransmittable_on_wire_timeout.
   EXPECT_TRUE(connection_.GetPingAlarm()->IsSet());
   EXPECT_EQ(retransmittable_on_wire_timeout,
@@ -4297,7 +4327,7 @@
   // The retransmission alarm and the path degrading alarm should be set as
   // there is a retransmittable packet (PING) on the wire,
   EXPECT_TRUE(connection_.GetRetransmissionAlarm()->IsSet());
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
 
   // Verify the retransmission delay.
   QuicTime::Delta min_rto_timeout =
@@ -6022,6 +6052,8 @@
   QuicTagVector connection_options;
   connection_options.push_back(k5RTO);
   config.SetConnectionOptionsToSend(connection_options);
+  QuicConfigPeer::ReceiveIdleNetworkTimeout(&config, SERVER,
+                                            kDefaultIdleTimeoutSecs);
   connection_.SetFromConfig(config);
 
   // Send stream data.
@@ -6036,6 +6068,10 @@
     EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
     EXPECT_TRUE(connection_.connected());
   }
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_CALL(visitor_, OnPathDegrading());
+    connection_.PathDegradingTimeout();
+  }
 
   EXPECT_EQ(2u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
   EXPECT_EQ(4u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
@@ -6043,7 +6079,12 @@
   EXPECT_CALL(visitor_,
               OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
   EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(AtLeast(1));
-  connection_.GetRetransmissionAlarm()->Fire();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    ASSERT_TRUE(connection_.BlackholeDetectionInProgress());
+    connection_.GetBlackholeDetectorAlarm()->Fire();
+  } else {
+    connection_.GetRetransmissionAlarm()->Fire();
+  }
   EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
   EXPECT_FALSE(connection_.connected());
   TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
@@ -7783,32 +7824,37 @@
 
 TEST_P(QuicConnectionTest, PathDegradingAlarmForCryptoPacket) {
   EXPECT_TRUE(connection_.connected());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
 
   EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(1);
   connection_.SendCryptoStreamData();
 
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
   QuicTime::Delta delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
                               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 
   // Fire the path degrading alarm, path degrading signal should be sent to
   // the visitor.
   EXPECT_CALL(visitor_, OnPathDegrading());
   clock_.AdvanceTime(delay);
-  connection_.GetPathDegradingAlarm()->Fire();
+  connection_.PathDegradingTimeout();
   EXPECT_TRUE(connection_.IsPathDegrading());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
 }
 
 // Includes regression test for b/69979024.
 TEST_P(QuicConnectionTest, PathDegradingAlarmForNonCryptoPackets) {
   EXPECT_TRUE(connection_.connected());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
 
   const char data[] = "data";
@@ -7822,25 +7868,38 @@
         GetNthClientInitiatedStreamId(1, connection_.transport_version()), data,
         offset, NO_FIN);
     offset += data_size;
-    EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+    EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
     // Check the deadline of the path degrading alarm.
     QuicTime::Delta delay =
         QuicConnectionPeer::GetSentPacketManager(&connection_)
             ->GetPathDegradingDelay();
-    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                         clock_.ApproximateNow());
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                           clock_.ApproximateNow());
+    } else {
+      EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                           clock_.ApproximateNow());
+    }
 
     // Send a second packet. The path degrading alarm's deadline should remain
     // the same.
     // Regression test for b/69979024.
     clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
     QuicTime prev_deadline = connection_.GetPathDegradingAlarm()->deadline();
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      prev_deadline = connection_.GetBlackholeDetectorAlarm()->deadline();
+    }
     connection_.SendStreamDataWithString(
         GetNthClientInitiatedStreamId(1, connection_.transport_version()), data,
         offset, NO_FIN);
     offset += data_size;
-    EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
-    EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+    EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      EXPECT_EQ(prev_deadline,
+                connection_.GetBlackholeDetectorAlarm()->deadline());
+    } else {
+      EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+    }
 
     // Now receive an ACK of the first packet. This should advance the path
     // degrading alarm's deadline since forward progress has been made.
@@ -7852,12 +7911,17 @@
     QuicAckFrame frame = InitAckFrame(
         {{QuicPacketNumber(1u + 2u * i), QuicPacketNumber(2u + 2u * i)}});
     ProcessAckPacket(&frame);
-    EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+    EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
     // Check the deadline of the path degrading alarm.
     delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
                 ->GetPathDegradingDelay();
-    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                         clock_.ApproximateNow());
+    if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+      EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                           clock_.ApproximateNow());
+    } else {
+      EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                           clock_.ApproximateNow());
+    }
 
     if (i == 0) {
       // Now receive an ACK of the second packet. Since there are no more
@@ -7867,14 +7931,14 @@
       EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
       frame = InitAckFrame({{QuicPacketNumber(2), QuicPacketNumber(3)}});
       ProcessAckPacket(&frame);
-      EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+      EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
     } else {
       // Advance time to the path degrading alarm's deadline and simulate
       // firing the alarm.
       clock_.AdvanceTime(delay);
       EXPECT_CALL(visitor_, OnPathDegrading());
-      connection_.GetPathDegradingAlarm()->Fire();
-      EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+      connection_.PathDegradingTimeout();
+      EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
     }
   }
   EXPECT_TRUE(connection_.IsPathDegrading());
@@ -7890,7 +7954,7 @@
   EXPECT_CALL(visitor_, ShouldKeepConnectionAlive())
       .WillRepeatedly(Return(true));
 
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
   EXPECT_FALSE(connection_.GetPingAlarm()->IsSet());
 
@@ -7904,11 +7968,16 @@
   // Now there's a retransmittable packet on the wire, so the path degrading
   // alarm should be set.
   // The retransmittable-on-wire alarm should not be set.
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   QuicTime::Delta delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
                               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
   ASSERT_TRUE(connection_.sent_packet_manager().HasInFlightPackets());
   // The ping alarm is set for the ping timeout, not the shorter
   // retransmittable_on_wire_timeout.
@@ -7927,7 +7996,7 @@
   // No more retransmittable packets on the wire, so the path degrading alarm
   // should be cancelled, and the ping alarm should be set to the
   // retransmittable_on_wire_timeout.
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_TRUE(connection_.GetPingAlarm()->IsSet());
   EXPECT_EQ(retransmittable_on_wire_timeout,
             connection_.GetPingAlarm()->deadline() - clock_.ApproximateNow());
@@ -7941,11 +8010,16 @@
 
   // Now there's a retransmittable packet (PING) on the wire, so the path
   // degrading alarm should be set.
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 }
 
 // This test verifies that the connection marks path as degrading and does not
@@ -7953,7 +8027,7 @@
 // degraded path.
 TEST_P(QuicConnectionTest, NoPathDegradingAlarmIfPathIsDegrading) {
   EXPECT_TRUE(connection_.connected());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
 
   const char data[] = "data";
@@ -7964,21 +8038,34 @@
   // the path degrading alarm should be set.
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   // Check the deadline of the path degrading alarm.
   QuicTime::Delta delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
                               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 
   // Send a second packet. The path degrading alarm's deadline should remain
   // the same.
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
   QuicTime prev_deadline = connection_.GetPathDegradingAlarm()->deadline();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    prev_deadline = connection_.GetBlackholeDetectorAlarm()->deadline();
+  }
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
-  EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(prev_deadline,
+              connection_.GetBlackholeDetectorAlarm()->deadline());
+  } else {
+    EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+  }
 
   // Now receive an ACK of the first packet. This should advance the path
   // degrading alarm's deadline since forward progress has been made.
@@ -7988,29 +8075,34 @@
   QuicAckFrame frame =
       InitAckFrame({{QuicPacketNumber(1u), QuicPacketNumber(2u)}});
   ProcessAckPacket(&frame);
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   // Check the deadline of the path degrading alarm.
   delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 
   // Advance time to the path degrading alarm's deadline and simulate
   // firing the path degrading alarm. This path will be considered as
   // degrading.
   clock_.AdvanceTime(delay);
   EXPECT_CALL(visitor_, OnPathDegrading()).Times(1);
-  connection_.GetPathDegradingAlarm()->Fire();
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  connection_.PathDegradingTimeout();
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_TRUE(connection_.IsPathDegrading());
 
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   // Send a third packet. The path degrading alarm is no longer set but path
   // should still be marked as degrading.
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_TRUE(connection_.IsPathDegrading());
 }
 
@@ -8019,7 +8111,7 @@
 // after path has been marked degrading.
 TEST_P(QuicConnectionTest, UnmarkPathDegradingOnForwardProgress) {
   EXPECT_TRUE(connection_.connected());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_FALSE(connection_.IsPathDegrading());
 
   const char data[] = "data";
@@ -8030,21 +8122,34 @@
   // the path degrading alarm should be set.
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   // Check the deadline of the path degrading alarm.
   QuicTime::Delta delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
                               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 
   // Send a second packet. The path degrading alarm's deadline should remain
   // the same.
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
   QuicTime prev_deadline = connection_.GetPathDegradingAlarm()->deadline();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    prev_deadline = connection_.GetBlackholeDetectorAlarm()->deadline();
+  }
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
-  EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(prev_deadline,
+              connection_.GetBlackholeDetectorAlarm()->deadline());
+  } else {
+    EXPECT_EQ(prev_deadline, connection_.GetPathDegradingAlarm()->deadline());
+  }
 
   // Now receive an ACK of the first packet. This should advance the path
   // degrading alarm's deadline since forward progress has been made.
@@ -8054,28 +8159,33 @@
   QuicAckFrame frame =
       InitAckFrame({{QuicPacketNumber(1u), QuicPacketNumber(2u)}});
   ProcessAckPacket(&frame);
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
   // Check the deadline of the path degrading alarm.
   delay = QuicConnectionPeer::GetSentPacketManager(&connection_)
               ->GetPathDegradingDelay();
-  EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
-                       clock_.ApproximateNow());
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_EQ(delay, connection_.GetBlackholeDetectorAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  } else {
+    EXPECT_EQ(delay, connection_.GetPathDegradingAlarm()->deadline() -
+                         clock_.ApproximateNow());
+  }
 
   // Advance time to the path degrading alarm's deadline and simulate
   // firing the alarm.
   clock_.AdvanceTime(delay);
   EXPECT_CALL(visitor_, OnPathDegrading()).Times(1);
-  connection_.GetPathDegradingAlarm()->Fire();
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  connection_.PathDegradingTimeout();
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_TRUE(connection_.IsPathDegrading());
 
   // Send a third packet. The path degrading alarm is no longer set but path
   // should still be marked as degrading.
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   connection_.SendStreamDataWithString(1, data, offset, NO_FIN);
   offset += data_size;
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
   EXPECT_TRUE(connection_.IsPathDegrading());
 
   // Now receive an ACK of the second packet. This should unmark the path as
@@ -8085,7 +8195,7 @@
   frame = InitAckFrame({{QuicPacketNumber(2), QuicPacketNumber(3)}});
   ProcessAckPacket(&frame);
   EXPECT_FALSE(connection_.IsPathDegrading());
-  EXPECT_TRUE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_TRUE(connection_.PathDegradingDetectionInProgress());
 }
 
 TEST_P(QuicConnectionTest, NoPathDegradingOnServer) {
@@ -8096,13 +8206,13 @@
   QuicPacketCreatorPeer::SetSendVersionInPacket(creator_, false);
 
   EXPECT_FALSE(connection_.IsPathDegrading());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
 
   // Send data.
   const char data[] = "data";
   connection_.SendStreamDataWithString(1, data, 0, NO_FIN);
   EXPECT_FALSE(connection_.IsPathDegrading());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
 
   // Ack data.
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(5));
@@ -8111,7 +8221,7 @@
       InitAckFrame({{QuicPacketNumber(1u), QuicPacketNumber(2u)}});
   ProcessAckPacket(&frame);
   EXPECT_FALSE(connection_.IsPathDegrading());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
 }
 
 TEST_P(QuicConnectionTest, NoPathDegradingAfterSendingAck) {
@@ -8125,7 +8235,7 @@
   EXPECT_FALSE(connection_.sent_packet_manager().unacked_packets().empty());
   EXPECT_FALSE(connection_.sent_packet_manager().HasInFlightPackets());
   EXPECT_FALSE(connection_.IsPathDegrading());
-  EXPECT_FALSE(connection_.GetPathDegradingAlarm()->IsSet());
+  EXPECT_FALSE(connection_.PathDegradingDetectionInProgress());
 }
 
 TEST_P(QuicConnectionTest, MultipleCallsToCloseConnection) {
@@ -9714,6 +9824,8 @@
   connection_options.push_back(k1PTO);
   connection_options.push_back(k6PTO);
   config.SetConnectionOptionsToSend(connection_options);
+  QuicConfigPeer::ReceiveIdleNetworkTimeout(&config, SERVER,
+                                            kDefaultIdleTimeoutSecs);
   EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
   connection_.SetFromConfig(config);
   EXPECT_FALSE(connection_.GetRetransmissionAlarm()->IsSet());
@@ -9732,6 +9844,10 @@
     EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
     EXPECT_TRUE(connection_.connected());
   }
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_CALL(visitor_, OnPathDegrading());
+    connection_.PathDegradingTimeout();
+  }
 
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
@@ -9739,7 +9855,12 @@
   // Closes connection on 6th PTO.
   EXPECT_CALL(visitor_,
               OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
-  connection_.GetRetransmissionAlarm()->Fire();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    ASSERT_TRUE(connection_.BlackholeDetectionInProgress());
+    connection_.GetBlackholeDetectorAlarm()->Fire();
+  } else {
+    connection_.GetRetransmissionAlarm()->Fire();
+  }
   EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
   EXPECT_FALSE(connection_.connected());
   TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
@@ -9751,6 +9872,8 @@
   connection_options.push_back(k2PTO);
   connection_options.push_back(k7PTO);
   config.SetConnectionOptionsToSend(connection_options);
+  QuicConfigPeer::ReceiveIdleNetworkTimeout(&config, SERVER,
+                                            kDefaultIdleTimeoutSecs);
   EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
   connection_.SetFromConfig(config);
   EXPECT_FALSE(connection_.GetRetransmissionAlarm()->IsSet());
@@ -9767,6 +9890,10 @@
     EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
     EXPECT_TRUE(connection_.connected());
   }
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_CALL(visitor_, OnPathDegrading());
+    connection_.PathDegradingTimeout();
+  }
 
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
@@ -9775,7 +9902,12 @@
   EXPECT_CALL(visitor_,
               OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
   EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(AtLeast(1));
-  connection_.GetRetransmissionAlarm()->Fire();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    ASSERT_TRUE(connection_.BlackholeDetectionInProgress());
+    connection_.GetBlackholeDetectorAlarm()->Fire();
+  } else {
+    connection_.GetRetransmissionAlarm()->Fire();
+  }
   EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
   EXPECT_FALSE(connection_.connected());
   TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
@@ -9786,6 +9918,8 @@
   QuicTagVector connection_options;
   connection_options.push_back(k2PTO);
   connection_options.push_back(k8PTO);
+  QuicConfigPeer::ReceiveIdleNetworkTimeout(&config, SERVER,
+                                            kDefaultIdleTimeoutSecs);
   config.SetConnectionOptionsToSend(connection_options);
   EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
   connection_.SetFromConfig(config);
@@ -9803,6 +9937,10 @@
     EXPECT_TRUE(connection_.GetTimeoutAlarm()->IsSet());
     EXPECT_TRUE(connection_.connected());
   }
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    EXPECT_CALL(visitor_, OnPathDegrading());
+    connection_.PathDegradingTimeout();
+  }
 
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveTlpCount());
   EXPECT_EQ(0u, connection_.sent_packet_manager().GetConsecutiveRtoCount());
@@ -9811,7 +9949,12 @@
   EXPECT_CALL(visitor_,
               OnConnectionClosed(_, ConnectionCloseSource::FROM_SELF));
   EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(AtLeast(1));
-  connection_.GetRetransmissionAlarm()->Fire();
+  if (GetQuicReloadableFlag(quic_use_blackhole_detector)) {
+    ASSERT_TRUE(connection_.BlackholeDetectionInProgress());
+    connection_.GetBlackholeDetectorAlarm()->Fire();
+  } else {
+    connection_.GetRetransmissionAlarm()->Fire();
+  }
   EXPECT_FALSE(connection_.GetTimeoutAlarm()->IsSet());
   EXPECT_FALSE(connection_.connected());
   TestConnectionCloseQuicErrorCode(QUIC_TOO_MANY_RTOS);
diff --git a/quic/core/quic_network_blackhole_detector.cc b/quic/core/quic_network_blackhole_detector.cc
new file mode 100644
index 0000000..9206ae4
--- /dev/null
+++ b/quic/core/quic_network_blackhole_detector.cc
@@ -0,0 +1,78 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/third_party/quiche/src/quic/core/quic_network_blackhole_detector.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_constants.h"
+
+namespace quic {
+
+namespace {
+
+class AlarmDelegate : public QuicAlarm::Delegate {
+ public:
+  explicit AlarmDelegate(QuicNetworkBlackholeDetector* detector)
+      : detector_(detector) {}
+  AlarmDelegate(const AlarmDelegate&) = delete;
+  AlarmDelegate& operator=(const AlarmDelegate&) = delete;
+
+  void OnAlarm() override { detector_->OnAlarm(); }
+
+ private:
+  QuicNetworkBlackholeDetector* detector_;
+};
+
+}  // namespace
+
+QuicNetworkBlackholeDetector::QuicNetworkBlackholeDetector(
+    Delegate* delegate,
+    QuicConnectionArena* arena,
+    QuicAlarmFactory* alarm_factory)
+    : delegate_(delegate),
+      path_degrading_deadline_(QuicTime::Zero()),
+      blackhole_deadline_(QuicTime::Zero()),
+      alarm_(
+          alarm_factory->CreateAlarm(arena->New<AlarmDelegate>(this), arena)) {}
+
+void QuicNetworkBlackholeDetector::OnAlarm() {
+  if (path_degrading_deadline_.IsInitialized()) {
+    path_degrading_deadline_ = QuicTime::Zero();
+    delegate_->OnPathDegradingDetected();
+    // Switch to blackhole detection mode.
+    alarm_->Update(blackhole_deadline_, kAlarmGranularity);
+    return;
+  }
+  if (blackhole_deadline_.IsInitialized()) {
+    blackhole_deadline_ = QuicTime::Zero();
+    delegate_->OnBlackholeDetected();
+  }
+}
+
+void QuicNetworkBlackholeDetector::StopDetection() {
+  alarm_->Cancel();
+  path_degrading_deadline_ = QuicTime::Zero();
+  blackhole_deadline_ = QuicTime::Zero();
+}
+
+void QuicNetworkBlackholeDetector::RestartDetection(
+    QuicTime path_degrading_deadline,
+    QuicTime blackhole_deadline) {
+  path_degrading_deadline_ = path_degrading_deadline;
+  blackhole_deadline_ = blackhole_deadline;
+  QUIC_BUG_IF(path_degrading_deadline_.IsInitialized() &&
+              blackhole_deadline_.IsInitialized() &&
+              path_degrading_deadline_ > blackhole_deadline_)
+      << "Path degrading timeout is later than blackhole detection timeout";
+  alarm_->Update(path_degrading_deadline_, kAlarmGranularity);
+  if (alarm_->IsSet()) {
+    return;
+  }
+  alarm_->Update(blackhole_deadline_, kAlarmGranularity);
+}
+
+bool QuicNetworkBlackholeDetector::IsDetectionInProgress() const {
+  return alarm_->IsSet();
+}
+
+}  // namespace quic
diff --git a/quic/core/quic_network_blackhole_detector.h b/quic/core/quic_network_blackhole_detector.h
new file mode 100644
index 0000000..b839d42
--- /dev/null
+++ b/quic/core/quic_network_blackhole_detector.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_CORE_QUIC_NETWORK_BLACKHOLE_DETECTOR_H_
+#define QUICHE_QUIC_CORE_QUIC_NETWORK_BLACKHOLE_DETECTOR_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_alarm.h"
+#include "net/third_party/quiche/src/quic/core/quic_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/core/quic_one_block_arena.h"
+#include "net/third_party/quiche/src/quic/core/quic_time.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_export.h"
+
+namespace quic {
+
+namespace test {
+class QuicConnectionPeer;
+class QuicNetworkBlackholeDetectorPeer;
+}  // namespace test
+
+// QuicNetworkBlackholeDetector can detect path degrading and/or network
+// blackhole. If both detections are in progress, detector will be in path
+// degrading detection mode. After reporting path degrading detected, detector
+// switches to blackhole detection mode. So blackhole detection deadline must
+// be later than path degrading deadline.
+class QUIC_EXPORT_PRIVATE QuicNetworkBlackholeDetector {
+ public:
+  class QUIC_EXPORT_PRIVATE Delegate {
+   public:
+    virtual ~Delegate() {}
+
+    // Called when the path degrading alarm fires.
+    virtual void OnPathDegradingDetected() = 0;
+
+    // Called when the path blackhole alarm fires.
+    virtual void OnBlackholeDetected() = 0;
+  };
+
+  QuicNetworkBlackholeDetector(Delegate* delegate,
+                               QuicConnectionArena* arena,
+                               QuicAlarmFactory* alarm_factory);
+
+  // Called to stop all detections.
+  void StopDetection();
+
+  // Called to restart path degrading or/and blackhole detections. Please note,
+  // if both deadlines are set, |blackhole_deadline| must be later than
+  // |path_degrading_deadline|.
+  void RestartDetection(QuicTime path_degrading_deadline,
+                        QuicTime blackhole_deadline);
+
+  // Called when |alarm_| fires.
+  void OnAlarm();
+
+  // Returns true if |alarm_| is set.
+  bool IsDetectionInProgress() const;
+
+ private:
+  friend class test::QuicConnectionPeer;
+  friend class test::QuicNetworkBlackholeDetectorPeer;
+
+  Delegate* delegate_;  // Not owned.
+
+  // Time that Delegate::OnPathDegrading will be called. 0 means no path
+  // degrading detection is in progress.
+  QuicTime path_degrading_deadline_;
+  // Time that Delegate::OnBlackholeDetected will be called. 0 means no
+  // blackhole detection is in progress.
+  QuicTime blackhole_deadline_;
+
+  QuicArenaScopedPtr<QuicAlarm> alarm_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_CORE_QUIC_NETWORK_BLACKHOLE_DETECTOR_H_
diff --git a/quic/core/quic_network_blackhole_detector_test.cc b/quic/core/quic_network_blackhole_detector_test.cc
new file mode 100644
index 0000000..eca50e3
--- /dev/null
+++ b/quic/core/quic_network_blackhole_detector_test.cc
@@ -0,0 +1,121 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/third_party/quiche/src/quic/core/quic_network_blackhole_detector.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_one_block_arena.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_test_utils.h"
+
+namespace quic {
+namespace test {
+
+class QuicNetworkBlackholeDetectorPeer {
+ public:
+  static QuicAlarm* GetAlarm(QuicNetworkBlackholeDetector* detector) {
+    return detector->alarm_.get();
+  }
+};
+
+namespace {
+class MockDelegate : public QuicNetworkBlackholeDetector::Delegate {
+ public:
+  MOCK_METHOD0(OnPathDegradingDetected, void());
+  MOCK_METHOD0(OnBlackholeDetected, void());
+};
+
+const size_t kPathDegradingDelayInSeconds = 5;
+const size_t kBlackholeDelayInSeconds = 10;
+
+class QuicNetworkBlackholeDetectorTest : public QuicTest {
+ public:
+  QuicNetworkBlackholeDetectorTest()
+      : detector_(&delegate_, &arena_, &alarm_factory_),
+        alarm_(static_cast<MockAlarmFactory::TestAlarm*>(
+            QuicNetworkBlackholeDetectorPeer::GetAlarm(&detector_))),
+        path_degrading_delay_(
+            QuicTime::Delta::FromSeconds(kPathDegradingDelayInSeconds)),
+        blackhole_delay_(
+            QuicTime::Delta::FromSeconds(kBlackholeDelayInSeconds)) {
+    clock_.AdvanceTime(QuicTime::Delta::FromSeconds(1));
+  }
+
+ protected:
+  testing::StrictMock<MockDelegate> delegate_;
+  QuicConnectionArena arena_;
+  MockAlarmFactory alarm_factory_;
+
+  QuicNetworkBlackholeDetector detector_;
+
+  MockAlarmFactory::TestAlarm* alarm_;
+  MockClock clock_;
+  const QuicTime::Delta path_degrading_delay_;
+  const QuicTime::Delta blackhole_delay_;
+};
+
+TEST_F(QuicNetworkBlackholeDetectorTest, StartAndFire) {
+  EXPECT_FALSE(detector_.IsDetectionInProgress());
+
+  detector_.RestartDetection(clock_.Now() + path_degrading_delay_,
+                             clock_.Now() + blackhole_delay_);
+  EXPECT_TRUE(detector_.IsDetectionInProgress());
+  EXPECT_EQ(clock_.Now() + path_degrading_delay_, alarm_->deadline());
+
+  // Fire path degrading alarm.
+  clock_.AdvanceTime(path_degrading_delay_);
+  EXPECT_CALL(delegate_, OnPathDegradingDetected());
+  alarm_->Fire();
+  // Verify blackhole detection is still in progress.
+  EXPECT_TRUE(detector_.IsDetectionInProgress());
+  EXPECT_EQ(clock_.Now() + blackhole_delay_ - path_degrading_delay_,
+            alarm_->deadline());
+
+  // Fire blackhole detection alarm.
+  clock_.AdvanceTime(blackhole_delay_ - path_degrading_delay_);
+  EXPECT_CALL(delegate_, OnBlackholeDetected());
+  alarm_->Fire();
+  EXPECT_FALSE(detector_.IsDetectionInProgress());
+}
+
+TEST_F(QuicNetworkBlackholeDetectorTest, RestartAndStop) {
+  detector_.RestartDetection(clock_.Now() + path_degrading_delay_,
+                             clock_.Now() + blackhole_delay_);
+
+  clock_.AdvanceTime(QuicTime::Delta::FromSeconds(1));
+  detector_.RestartDetection(clock_.Now() + path_degrading_delay_,
+                             clock_.Now() + blackhole_delay_);
+  EXPECT_EQ(clock_.Now() + path_degrading_delay_, alarm_->deadline());
+
+  detector_.StopDetection();
+  EXPECT_FALSE(detector_.IsDetectionInProgress());
+}
+
+TEST_F(QuicNetworkBlackholeDetectorTest, PathDegradingFiresAndRestart) {
+  EXPECT_FALSE(detector_.IsDetectionInProgress());
+  detector_.RestartDetection(clock_.Now() + path_degrading_delay_,
+                             clock_.Now() + blackhole_delay_);
+  EXPECT_TRUE(detector_.IsDetectionInProgress());
+  EXPECT_EQ(clock_.Now() + path_degrading_delay_, alarm_->deadline());
+
+  // Fire path degrading alarm.
+  clock_.AdvanceTime(path_degrading_delay_);
+  EXPECT_CALL(delegate_, OnPathDegradingDetected());
+  alarm_->Fire();
+  // Verify blackhole detection is still in progress.
+  EXPECT_TRUE(detector_.IsDetectionInProgress());
+  EXPECT_EQ(clock_.Now() + blackhole_delay_ - path_degrading_delay_,
+            alarm_->deadline());
+
+  // After 100ms, restart detections on forward progress.
+  clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(100));
+  detector_.RestartDetection(clock_.Now() + path_degrading_delay_,
+                             clock_.Now() + blackhole_delay_);
+  // Verify alarm is armed based on path degrading deadline.
+  EXPECT_EQ(clock_.Now() + path_degrading_delay_, alarm_->deadline());
+}
+
+}  // namespace
+
+}  // namespace test
+}  // namespace quic
diff --git a/quic/core/quic_sent_packet_manager.cc b/quic/core/quic_sent_packet_manager.cc
index 4a041d8..d87d6c7 100644
--- a/quic/core/quic_sent_packet_manager.cc
+++ b/quic/core/quic_sent_packet_manager.cc
@@ -32,6 +32,9 @@
 // The path degrading delay is the sum of this number of consecutive RTO delays.
 const size_t kNumRetransmissionDelaysForPathDegradingDelay = 2;
 
+// The blachkhole delay is the sum of this number of consecutive RTO delays.
+const size_t kNumRetransmissionDelaysForBlackholeDelay = 5;
+
 // Ensure the handshake timer isnt't faster than 10ms.
 // This limits the tenth retransmitted packet to 10s after the initial CHLO.
 static const int64_t kMinHandshakeTimeoutMs = 10;
@@ -1107,6 +1110,11 @@
       max_tail_loss_probes_ + kNumRetransmissionDelaysForPathDegradingDelay);
 }
 
+const QuicTime::Delta QuicSentPacketManager::GetNetworkBlackholeDelay() const {
+  return GetNConsecutiveRetransmissionTimeoutDelay(
+      max_tail_loss_probes_ + kNumRetransmissionDelaysForBlackholeDelay);
+}
+
 const QuicTime::Delta QuicSentPacketManager::GetCryptoRetransmissionDelay()
     const {
   // This is equivalent to the TailLossProbeDelay, but slightly more aggressive
diff --git a/quic/core/quic_sent_packet_manager.h b/quic/core/quic_sent_packet_manager.h
index ed51a9c..5f1de70 100644
--- a/quic/core/quic_sent_packet_manager.h
+++ b/quic/core/quic_sent_packet_manager.h
@@ -213,6 +213,9 @@
   // notify the session that this connection is degrading.
   const QuicTime::Delta GetPathDegradingDelay() const;
 
+  // Returns the current delay for detecting network blackhole.
+  const QuicTime::Delta GetNetworkBlackholeDelay() const;
+
   const RttStats* GetRttStats() const { return &rtt_stats_; }
 
   // Returns the estimated bandwidth calculated by the congestion algorithm.
diff --git a/quic/test_tools/quic_config_peer.cc b/quic/test_tools/quic_config_peer.cc
index 41527c2..20463ab 100644
--- a/quic/test_tools/quic_config_peer.cc
+++ b/quic/test_tools/quic_config_peer.cc
@@ -97,5 +97,14 @@
   config->max_packet_size_.SetReceivedValue(max_packet_size);
 }
 
+// static
+void QuicConfigPeer::ReceiveIdleNetworkTimeout(QuicConfig* config,
+                                               HelloType hello_type,
+                                               uint32_t idle_timeout_seconds) {
+  std::string error_details;
+  config->idle_network_timeout_seconds_.ReceiveValue(
+      idle_timeout_seconds, hello_type, &error_details);
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quic/test_tools/quic_config_peer.h b/quic/test_tools/quic_config_peer.h
index 61e2ef2..b52bf42 100644
--- a/quic/test_tools/quic_config_peer.h
+++ b/quic/test_tools/quic_config_peer.h
@@ -58,6 +58,10 @@
 
   static void SetReceivedMaxPacketSize(QuicConfig* config,
                                        uint32_t max_packet_size);
+
+  static void ReceiveIdleNetworkTimeout(QuicConfig* config,
+                                        HelloType hello_type,
+                                        uint32_t idle_timeout_seconds);
 };
 
 }  // namespace test
diff --git a/quic/test_tools/quic_connection_peer.cc b/quic/test_tools/quic_connection_peer.cc
index b38b64e..21a0105 100644
--- a/quic/test_tools/quic_connection_peer.cc
+++ b/quic/test_tools/quic_connection_peer.cc
@@ -348,5 +348,29 @@
   return count;
 }
 
+// static
+QuicNetworkBlackholeDetector& QuicConnectionPeer::GetBlackholeDetector(
+    QuicConnection* connection) {
+  return connection->blackhole_detector_;
+}
+
+// static
+QuicAlarm* QuicConnectionPeer::GetBlackholeDetectorAlarm(
+    QuicConnection* connection) {
+  return connection->blackhole_detector_.alarm_.get();
+}
+
+// static
+QuicTime QuicConnectionPeer::GetPathDegradingDeadline(
+    QuicConnection* connection) {
+  return connection->blackhole_detector_.path_degrading_deadline_;
+}
+
+// static
+QuicTime QuicConnectionPeer::GetBlackholeDetectionDeadline(
+    QuicConnection* connection) {
+  return connection->blackhole_detector_.blackhole_deadline_;
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quic/test_tools/quic_connection_peer.h b/quic/test_tools/quic_connection_peer.h
index e0a97b8..ab14190 100644
--- a/quic/test_tools/quic_connection_peer.h
+++ b/quic/test_tools/quic_connection_peer.h
@@ -135,6 +135,15 @@
                                         const std::string& details);
 
   static size_t GetNumEncryptionLevels(QuicConnection* connection);
+
+  static QuicNetworkBlackholeDetector& GetBlackholeDetector(
+      QuicConnection* connection);
+
+  static QuicAlarm* GetBlackholeDetectorAlarm(QuicConnection* connection);
+
+  static QuicTime GetPathDegradingDeadline(QuicConnection* connection);
+
+  static QuicTime GetBlackholeDetectionDeadline(QuicConnection* connection);
 };
 
 }  // namespace test