@@ -9,7 +9,14 @@ use crate::magicsock::Metrics as MagicsockMetrics;
99/// currently trusted.
1010///
1111/// If trust goes away, it can be brought back with another valid DISCO UDP pong.
12- const TRUST_UDP_ADDR_DURATION : Duration = Duration :: from_millis ( 6500 ) ;
12+ ///
13+ /// Increased from 6.5s to 12s to be more resilient under congestion.
14+ const TRUST_UDP_ADDR_DURATION : Duration = Duration :: from_secs ( 12 ) ;
15+
16+ /// Number of consecutive ping failures required before marking a path as outdated.
17+ ///
18+ /// This implements a tolerance to prevent temporary packet loss from causing path degradation.
19+ const PING_FAILURE_THRESHOLD : u8 = 3 ;
1320
1421/// Tracks a path's validity.
1522///
@@ -27,6 +34,7 @@ struct Inner {
2734 latest_pong : Instant ,
2835 latency : Duration ,
2936 trust_until : Instant ,
37+ consecutive_failures : u8 ,
3038 congestion_metrics : CongestionMetrics ,
3139}
3240
@@ -150,6 +158,7 @@ impl PathValidity {
150158 trust_until : pong_at + Source :: ReceivedPong . trust_duration ( ) ,
151159 latest_pong : pong_at,
152160 latency,
161+ consecutive_failures : 0 ,
153162 congestion_metrics : metrics,
154163 } ) )
155164 }
@@ -161,6 +170,7 @@ impl PathValidity {
161170 inner. trust_until = pong_at + Source :: ReceivedPong . trust_duration ( ) ;
162171 inner. latest_pong = pong_at;
163172 inner. latency = latency;
173+ inner. consecutive_failures = 0 ;
164174 inner. congestion_metrics . add_latency_sample ( latency) ;
165175 }
166176 None => {
@@ -226,6 +236,31 @@ impl PathValidity {
226236 Some ( self . 0 . as_ref ( ) ?. latest_pong )
227237 }
228238
239+ /// Record a ping failure (timeout or no response).
240+ ///
241+ /// Only marks the path as outdated after PING_FAILURE_THRESHOLD consecutive failures.
242+ pub ( super ) fn record_ping_failure ( & mut self ) {
243+ let Some ( state) = self . 0 . as_mut ( ) else {
244+ return ;
245+ } ;
246+ state. consecutive_failures = state. consecutive_failures . saturating_add ( 1 ) ;
247+ }
248+
249+ /// Check if path should be considered outdated based on consecutive failures.
250+ pub ( super ) fn should_mark_outdated ( & self ) -> bool {
251+ self . 0
252+ . as_ref ( )
253+ . map ( |state| state. consecutive_failures >= PING_FAILURE_THRESHOLD )
254+ . unwrap_or ( false )
255+ }
256+
257+ /// Reset consecutive failure counter (called when we receive activity).
258+ pub ( super ) fn reset_failures ( & mut self ) {
259+ if let Some ( state) = self . 0 . as_mut ( ) {
260+ state. consecutive_failures = 0 ;
261+ }
262+ }
263+
229264 /// Record that a ping was sent on this path.
230265 pub ( super ) fn record_ping_sent ( & mut self ) {
231266 if let Some ( state) = self . 0 . as_mut ( ) {
@@ -267,6 +302,14 @@ impl PathValidity {
267302 . and_then ( |state| state. congestion_metrics . avg_latency ( ) )
268303 }
269304
305+ /// Get the number of consecutive failures.
306+ pub ( super ) fn consecutive_failures ( & self ) -> u8 {
307+ self . 0
308+ . as_ref ( )
309+ . map ( |state| state. consecutive_failures )
310+ . unwrap_or ( 0 )
311+ }
312+
270313 /// Record congestion metrics to the metrics system.
271314 /// Should be called periodically or on significant events.
272315 pub ( super ) fn record_metrics ( & self , metrics : & MagicsockMetrics ) {
@@ -302,7 +345,7 @@ impl Inner {
302345mod tests {
303346 use n0_future:: time:: { Duration , Instant } ;
304347
305- use super :: { PathValidity , Source , TRUST_UDP_ADDR_DURATION } ;
348+ use super :: { PING_FAILURE_THRESHOLD , PathValidity , Source , TRUST_UDP_ADDR_DURATION } ;
306349
307350 #[ tokio:: test( start_paused = true ) ]
308351 async fn test_basic_path_validity_lifetime ( ) {
@@ -330,6 +373,32 @@ mod tests {
330373 assert ! ( !validity. is_valid( Instant :: now( ) ) ) ;
331374 assert ! ( validity. is_outdated( Instant :: now( ) ) ) ;
332375 }
376+
377+ #[ tokio:: test]
378+ async fn test_multiple_ping_failures ( ) {
379+ let mut validity = PathValidity :: new ( Instant :: now ( ) , Duration :: from_millis ( 20 ) ) ;
380+
381+ // First failure should not mark as outdated
382+ validity. record_ping_failure ( ) ;
383+ assert ! ( !validity. should_mark_outdated( ) ) ;
384+ assert_eq ! ( validity. consecutive_failures( ) , 1 ) ;
385+
386+ // Second failure should not mark as outdated
387+ validity. record_ping_failure ( ) ;
388+ assert ! ( !validity. should_mark_outdated( ) ) ;
389+ assert_eq ! ( validity. consecutive_failures( ) , 2 ) ;
390+
391+ // Third failure should mark as outdated (threshold = 3)
392+ validity. record_ping_failure ( ) ;
393+ assert ! ( validity. should_mark_outdated( ) ) ;
394+ assert_eq ! ( validity. consecutive_failures( ) , PING_FAILURE_THRESHOLD ) ;
395+
396+ // Receiving pong should reset failures
397+ validity. update_pong ( Instant :: now ( ) , Duration :: from_millis ( 20 ) ) ;
398+ assert_eq ! ( validity. consecutive_failures( ) , 0 ) ;
399+ assert ! ( !validity. should_mark_outdated( ) ) ;
400+ }
401+
333402 #[ tokio:: test]
334403 async fn test_congestion_metrics ( ) {
335404 let mut validity = PathValidity :: new ( Instant :: now ( ) , Duration :: from_millis ( 10 ) ) ;
0 commit comments