@@ -788,3 +788,90 @@ fn regression_peer_ignored_path_abandon() {
788788 pair. server_conn_mut( server_ch)
789789 ) ) ) ;
790790}
791+
792+ /// A regression test that used to put quinn into a state of sending PATH_CHALLENGE
793+ /// from the client side indefinitely.
794+ ///
795+ /// The test uses passive migrations to establish a situation in which the client
796+ /// expects the server to respond to a PATH_CHALLENGE that was sent on the interface
797+ /// 1.1.1.1, but the server cannot respond to it anymore, because the client was
798+ /// involuntarily migrated to path 1.1.1.2.
799+ ///
800+ /// Here's a log line that shows the client skipping the path response due to it
801+ /// being delivered "on the wrong network path" (after migration).
802+ ///
803+ /// > DEBUG client:pkt{path_id=1}:recv{space=Data pn=3}:frame{ty=PATH_RESPONSE}:
804+ /// > iroh_quinn_proto::connection: 4704:
805+ /// > ignoring invalid PATH_RESPONSE
806+ /// > response=PATH_RESPONSE(ece9dc07f89ded7e)
807+ /// > network_path=(local: ::ffff:1.1.1.2, remote: [::ffff:2.2.2.0]:4433)
808+ /// > expected=(local: ::ffff:1.1.1.1, remote: [::ffff:2.2.2.0]:4433)
809+ ///
810+ /// The client will then never clear out the PATH_CHALLENGE from the "pending"
811+ /// challenges, and so it will never fully clear the path challenge timer.
812+ ///
813+ /// This issue was fixed by making sure to clear out challenges that were probing
814+ /// 4-tuples that are different from the current network path.
815+ #[ test]
816+ fn regression_never_idle4 ( ) {
817+ let prefix = "regression_never_idle4" ;
818+ let seed = [ 0u8 ; 32 ] ;
819+ let interactions = vec ! [
820+ // Open path 1 with the same remote address as path 0
821+ TestOp :: OpenPath {
822+ side: Side :: Client ,
823+ status: PathStatus :: Backup ,
824+ addr_idx: 0 ,
825+ } ,
826+ // Sets path 0 to backup, but generally just sends *something*
827+ TestOp :: PathSetStatus {
828+ side: Side :: Client ,
829+ path_idx: 0 ,
830+ status: PathStatus :: Backup ,
831+ } ,
832+ // Sends the two packets (opening path 1 & setting path status on path 0)
833+ TestOp :: Drive { side: Side :: Client } ,
834+ // But loses those two packets
835+ TestOp :: DropInbound { side: Side :: Server } ,
836+ // Client's interface 0 now migrates from 1.1.1.0 to 1.1.1.1
837+ TestOp :: PassiveMigration {
838+ side: Side :: Client ,
839+ addr_idx: 0 ,
840+ } ,
841+ TestOp :: AdvanceTime ,
842+ // Client closes path 0, path 1 is now the only remaining path for the client.
843+ // It will now always choose path 1 to send, even though it's not validated.
844+ TestOp :: ClosePath {
845+ side: Side :: Client ,
846+ path_idx: 0 ,
847+ error_code: 0 ,
848+ } ,
849+ // Send out the packet containing the PATH_ABANDON
850+ TestOp :: Drive { side: Side :: Client } ,
851+ // Migrate the first interface from 1.1.1.1 to 1.1.1.2 now.
852+ TestOp :: PassiveMigration {
853+ side: Side :: Client ,
854+ addr_idx: 0 ,
855+ } ,
856+ ] ;
857+ let routes = RoutingTable :: from_routes (
858+ vec ! [
859+ ( "[::ffff:1.1.1.0]:44433" . parse( ) . unwrap( ) , 0 ) ,
860+ ( "[::ffff:1.1.1.1]:44433" . parse( ) . unwrap( ) , 0 ) ,
861+ ] ,
862+ vec ! [ ( "[::ffff:2.2.2.0]:4433" . parse( ) . unwrap( ) , 0 ) ] ,
863+ ) ;
864+
865+ let _guard = subscribe ( ) ;
866+ let mut pair = setup_deterministic_with_multipath ( seed, routes, prefix) ;
867+ let ( client_ch, server_ch) =
868+ run_random_interaction ( & mut pair, interactions, multipath_transport_config ( prefix) ) ;
869+
870+ assert ! ( !pair. drive_bounded( 100 ) , "connection never became idle" ) ;
871+ assert ! ( allowed_error( poll_to_close(
872+ pair. client_conn_mut( client_ch)
873+ ) ) ) ;
874+ assert ! ( allowed_error( poll_to_close(
875+ pair. server_conn_mut( server_ch)
876+ ) ) ) ;
877+ }
0 commit comments