@@ -2164,7 +2164,7 @@ impl Connection {
21642164 let span = trace_span ! ( "pkt" , %path_id) ;
21652165 let _guard = span. enter ( ) ;
21662166
2167- if self . update_network_path_or_discard ( network_path, path_id) {
2167+ if self . early_discard_packet ( network_path, path_id) {
21682168 // A return value of true indicates we should discard this packet.
21692169 return ;
21702170 }
@@ -2225,13 +2225,27 @@ impl Connection {
22252225 }
22262226 }
22272227
2228- /// Updates the network path for `path_id` .
2228+ /// Returns whether a packet can be discarded early .
22292229 ///
2230- /// Returns true if a packet coming in for this `path_id` over given `network_path` should be discarded.
2231- /// Returns false if the path was updated and the packet doesn't need to be discarded.
2232- fn update_network_path_or_discard ( & mut self , network_path : FourTuple , path_id : PathId ) -> bool {
2233- let remote_may_migrate = self . side . remote_may_migrate ( & self . state ) ;
2234- let local_ip_may_migrate = self . side . is_client ( ) ;
2230+ /// Packets sent on the wrong network path can be entirely ignored, saving further
2231+ /// processing.
2232+ ///
2233+ /// Returns true if a packet coming in for this `path_id` over given `network_path`
2234+ /// should be discarded.
2235+ fn early_discard_packet ( & mut self , network_path : FourTuple , path_id : PathId ) -> bool {
2236+ if self . is_handshaking ( ) && path_id != PathId :: ZERO {
2237+ debug ! ( %network_path, %path_id, "discarding multipath packet during handshake" ) ;
2238+ return true ;
2239+ }
2240+
2241+ // TODO(flub): In RFC9000 the server is allowed to send off-path probing packets
2242+ // once the client has been probing such a 4-tuple. These probes are currently
2243+ // not yet recognised and discarded here.
2244+ // See https://github.com/n0-computer/noq/issues/607.
2245+ let remote_may_migrate = self . remote_may_migrate ( ) ;
2246+
2247+ let local_ip_may_migrate = self . local_ip_may_migrate ( ) ;
2248+
22352249 // If this packet could initiate a migration and we're a client or a server that
22362250 // forbids migration, drop the datagram. This could be relaxed to heuristically
22372251 // permit NAT-rebinding-like migration.
@@ -2259,35 +2273,54 @@ impl Connection {
22592273 ) ;
22602274 return true ;
22612275 }
2262- // If the datagram indicates that we've changed our local IP, we update it.
2263- // This is alluded to in Section 5.2 of the Multipath RFC draft 18:
2264- // https://www.ietf.org/archive/id/draft-ietf-quic-multipath-18.html#name-using-multiple-paths-on-the
2265- // > Client receives the packet, recognizes a path migration, updates the source address of path 2 to 192.0.2.1.
2266- if let Some ( local_ip) = network_path. local_ip {
2267- if known_path
2268- . network_path
2269- . local_ip
2270- . is_some_and ( |ip| ip != local_ip)
2271- {
2272- debug ! (
2273- %path_id,
2274- %network_path,
2275- %known_path. network_path,
2276- "path's local address seemingly migrated"
2277- ) ;
2276+ }
2277+ false
2278+ }
2279+
2280+ /// Whether a remote is allowed to migrate.
2281+ ///
2282+ /// QUIC relies on stable endpoints during the handshake. So other than the server's
2283+ /// preferred_address transport parameter no side may migrate before the handshake is
2284+ /// completed.
2285+ ///
2286+ /// In RFC9000 only the client may migrate. If QNT is negotiated the server may migrate
2287+ /// as well.
2288+ ///
2289+ /// Additionally for iroh we allow the server to migrate once during the handshake as
2290+ /// long as the client has not received an authenticated Handshake packet. This allows
2291+ /// us to duplicate client Initial packets to multiple destinations. See
2292+ /// [`state::Handshake::allow_server_migration`].
2293+ fn remote_may_migrate ( & self ) -> bool {
2294+ match & self . side {
2295+ ConnectionSide :: Server { server_config } => {
2296+ server_config. migration && self . is_handshake_confirmed ( )
2297+ }
2298+ ConnectionSide :: Client { .. } => {
2299+ if let Some ( hs) = self . state . as_handshake ( ) {
2300+ hs. allow_server_migration
2301+ } else {
2302+ self . n0_nat_traversal . is_negotiated ( ) && self . is_handshake_confirmed ( )
22782303 }
2279- // We update the address without path validation on the client side.
2280- // https://www.ietf.org/archive/id/draft-ietf-quic-multipath-18.html#section-5.1
2281- // > Servers observing a 4-tuple change will perform path validation (see Section 9 of [QUIC-TRANSPORT]).
2282- // This sounds like it's *only* the server endpoints that do this.
2283- // TODO(matheus23): We should still consider doing a proper migration on the client side in the future.
2284- // For now, this preserves the behavior of this code pre 4-tuple tracking.
2285- known_path. network_path . local_ip = Some ( local_ip) ;
22862304 }
22872305 }
2288- false
22892306 }
22902307
2308+ /// Whether our local IP address is allowed to change with new incoming packets.
2309+ ///
2310+ /// Incoming packets show us the local IP address we received a packet on, which could
2311+ /// be different from what we thought due to e.g. NAT rebinding or moving from mobile
2312+ /// data to WiFi without being notified of the network change.
2313+ ///
2314+ /// This is only allowed to happen after the handshake is confirmed and when we are the
2315+ /// client. Unless QNT is negotiated in which case the server is also allowed to
2316+ /// migrate.
2317+ ///
2318+ /// Be aware that probing packets, which do not exist in Multipath without QNT, are
2319+ /// exempt from this.
2320+ fn local_ip_may_migrate ( & self ) -> bool {
2321+ ( self . side . is_client ( ) || self . n0_nat_traversal . is_negotiated ( ) )
2322+ && self . is_handshake_confirmed ( )
2323+ }
22912324 /// Process timer expirations
22922325 ///
22932326 /// Executes protocol logic, potentially preparing signals (including application `Event`s,
@@ -4051,6 +4084,11 @@ impl Connection {
40514084 }
40524085 }
40534086
4087+ /// Decrypts the packet and processes the payload.
4088+ ///
4089+ /// Processes the entire packet, starting with removing header protection, then handling
4090+ /// a stateless reset if needed, and decrypting and processing the frames in the payload
4091+ /// if not a stateless reset.
40544092 fn handle_decode (
40554093 & mut self ,
40564094 now : Instant ,
@@ -4076,6 +4114,12 @@ impl Connection {
40764114 }
40774115 }
40784116
4117+ /// Handles a packet with header protection removed.
4118+ ///
4119+ /// The packet body is still encrypted at this point.
4120+ ///
4121+ /// If the datagram was a stateless reset we may have failed to remove header protection
4122+ /// and thus `packet` may be `None`.
40794123 fn handle_packet (
40804124 & mut self ,
40814125 now : Instant ,
@@ -4098,31 +4142,10 @@ impl Connection {
40984142 ) ;
40994143 }
41004144
4101- if self . is_handshaking ( ) {
4102- if path_id != PathId :: ZERO {
4103- debug ! ( %network_path, %path_id, "discarding multipath packet during handshake" ) ;
4104- return ;
4105- }
4106- if network_path != self . path_data_mut ( path_id) . network_path {
4107- if let Some ( hs) = self . state . as_handshake ( ) {
4108- if hs. allow_server_migration {
4109- trace ! ( %network_path, prev = %self . path_data( path_id) . network_path, "server migrated to new remote" ) ;
4110- self . path_data_mut ( path_id) . network_path = network_path;
4111- self . qlog . emit_tuple_assigned ( path_id, network_path, now) ;
4112- } else {
4113- debug ! ( "discarding packet with unexpected remote during handshake" ) ;
4114- return ;
4115- }
4116- } else {
4117- debug ! ( "discarding packet with unexpected remote during handshake" ) ;
4118- return ;
4119- }
4120- }
4121- }
4122-
41234145 let was_closed = self . state . is_closed ( ) ;
41244146 let was_drained = self . state . is_drained ( ) ;
41254147
4148+ // Now decrypt the packet payload in-place.
41264149 let decrypted = match packet {
41274150 None => Err ( None ) ,
41284151 Some ( mut packet) => self
@@ -4152,13 +4175,34 @@ impl Connection {
41524175 }
41534176 }
41544177 Ok ( ( packet, number) ) => {
4178+ // We received an authenticated packet and decrypted it.
41554179 qlog. header ( & packet. header , number, path_id) ;
41564180 let span = match number {
41574181 Some ( pn) => trace_span ! ( "recv" , space = ?packet. header. space( ) , pn) ,
41584182 None => trace_span ! ( "recv" , space = ?packet. header. space( ) ) ,
41594183 } ;
41604184 let _guard = span. enter ( ) ;
41614185
4186+ // Now the packet is authenticated we do the migration during the
4187+ // handshake. See Handshake::allow_server_migration for details.
4188+ if self . is_handshaking ( ) && network_path != self . path_data_mut ( path_id) . network_path
4189+ {
4190+ if let Some ( hs) = self . state . as_handshake ( )
4191+ && hs. allow_server_migration
4192+ {
4193+ trace ! (
4194+ %network_path,
4195+ prev = %self . path_data( path_id) . network_path,
4196+ "server migrated to new remote" ,
4197+ ) ;
4198+ self . path_data_mut ( path_id) . network_path = network_path;
4199+ self . qlog . emit_tuple_assigned ( path_id, network_path, now) ;
4200+ } else {
4201+ debug ! ( "discarding packet with unexpected remote during handshake" ) ;
4202+ return ;
4203+ }
4204+ }
4205+
41624206 let dedup = self . spaces [ packet. header . space ( ) ]
41634207 . path_space_mut ( path_id)
41644208 . map ( |pns| & mut pns. dedup ) ;
@@ -4714,7 +4758,7 @@ impl Connection {
47144758 Ok ( ( ) )
47154759 }
47164760
4717- /// Processes the packet payload, always in the data space.
4761+ /// Processes the decrypted packet payload, always in the data space.
47184762 fn process_payload (
47194763 & mut self ,
47204764 now : Instant ,
@@ -4823,18 +4867,17 @@ impl Connection {
48234867 . path_mut ( path_id)
48244868 . expect ( "payload is processed only after the path becomes known" ) ;
48254869 path. path_responses . push ( number, challenge. 0 , network_path) ;
4826- // At this point, update_network_path_or_discard was already called, so
4827- // we don't need to be lenient about `local_ip` possibly mis-matching.
4828- if network_path == path. network_path {
4829- // PATH_CHALLENGE on active path, possible off-path packet forwarding
4830- // attack. Send a non-probing packet to recover the active path.
4831- // TODO(flub): No longer true! We now path_challege also to validate
4832- // the path if the path is new, without an RFC9000-style
4833- // migration involved. This means we add in an extra
4834- // IMMEDIATE_ACK on some challenges. It isn't really wrong to do
4835- // so, but it still is something untidy. We should instead
4836- // suppress this when we know the remote is still validating the
4837- // path.
4870+ // If we were passively migrated (e.g. NAT rebinding), our local_ip will
4871+ // not match. Once we processed a non-probing packet the local_ip will
4872+ // finally be updated.
4873+ if network_path. remote == path. network_path . remote {
4874+ // PATH_CHALLENGE on active path, possible off-path packet
4875+ // forwarding attack. Send a non-probing packet to recover the
4876+ // active path. See
4877+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-9.3.3-3. In
4878+ // rare cases NAT probes might also appear on-path and would also
4879+ // get a non-probing packet as response. There is little harm in
4880+ // this.
48384881 match self . peer_supports_ack_frequency ( ) {
48394882 true => self . immediate_ack ( path_id) ,
48404883 false => {
@@ -5454,20 +5497,48 @@ impl Connection {
54545497 self . connection_close_pending = true ;
54555498 }
54565499
5457- if Some ( number)
5500+ // For Multipath any packet triggers migration. For RFC9000 or QNT (+ Multipath)
5501+ // only non-probing packets trigger migration.
5502+ let migrate_on_any_packet =
5503+ self . is_multipath_negotiated ( ) && !self . n0_nat_traversal . is_negotiated ( ) ;
5504+
5505+ // Only migrate if this is the largest packet number seen.
5506+ let is_largest_received_pn = Some ( number)
54585507 == self . spaces [ SpaceId :: Data ]
54595508 . for_path ( path_id)
5460- . largest_received_packet_number
5461- && !is_probing_packet
5462- && network_path != self . path_data ( path_id) . network_path
5509+ . largest_received_packet_number ;
5510+
5511+ // If we receive a non-probing packet on a new local IP that means we had a NAT
5512+ // rebinding-like migration. We update our local address but do not otherwise
5513+ // validate the new path, we only need to validate the path if the peer migrates per
5514+ // RFC9000 §9: https://www.rfc-editor.org/rfc/rfc9000.html#section-9-4
5515+ if ( migrate_on_any_packet || !is_probing_packet)
5516+ && is_largest_received_pn
5517+ && self . local_ip_may_migrate ( )
5518+ && let Some ( new_local_ip) = network_path. local_ip
5519+ {
5520+ let path_data = self . path_data_mut ( path_id) ;
5521+ if path_data
5522+ . network_path
5523+ . local_ip
5524+ . is_some_and ( |ip| ip != new_local_ip)
5525+ {
5526+ debug ! (
5527+ %path_id,
5528+ new_4tuple = %network_path,
5529+ prev_4tuple = %path_data. network_path,
5530+ "local address passive migration"
5531+ ) ;
5532+ }
5533+ path_data. network_path . local_ip = Some ( new_local_ip)
5534+ }
5535+
5536+ // If the peer migrated to a new address, trigger migration.
5537+ if ( migrate_on_any_packet || !is_probing_packet)
5538+ && is_largest_received_pn
5539+ && network_path. remote != self . path_data ( path_id) . network_path . remote
5540+ && self . remote_may_migrate ( )
54635541 {
5464- let ConnectionSide :: Server { ref server_config } = self . side else {
5465- panic ! ( "packets from unknown remote should be dropped by clients" ) ;
5466- } ;
5467- debug_assert ! (
5468- server_config. migration,
5469- "migration-initiating packets should have been dropped immediately"
5470- ) ;
54715542 self . migrate ( path_id, now, network_path, migration_observed_addr) ;
54725543 // Break linkability, if possible
54735544 self . update_remote_cid ( path_id) ;
@@ -5477,14 +5548,23 @@ impl Connection {
54775548 Ok ( ( ) )
54785549 }
54795550
5551+ /// Migrates the 4-tuple of the path.
5552+ ///
5553+ /// This creates a new [`PathData`] for the migrated path and stores the previous
5554+ /// [`PathData`] in [`PathState::prev`].
54805555 fn migrate (
54815556 & mut self ,
54825557 path_id : PathId ,
54835558 now : Instant ,
54845559 network_path : FourTuple ,
54855560 observed_addr : Option < ObservedAddr > ,
54865561 ) {
5487- trace ! ( %network_path, %path_id, "migration initiated" ) ;
5562+ trace ! (
5563+ new_4tuple = %network_path,
5564+ prev_4tuple = %self . path_data( path_id) . network_path,
5565+ %path_id,
5566+ "migration initiated" ,
5567+ ) ;
54885568 self . path_generation_counter = self . path_generation_counter . wrapping_add ( 1 ) ;
54895569 // TODO(@divma): conditions for path migration in multipath are very specific, check them
54905570 // again to prevent path migrations that should actually create a new path
@@ -5648,7 +5728,9 @@ impl Connection {
56485728 } ;
56495729
56505730 if open_first && let Err ( e) = self . open_path ( network_path, status, now) {
5651- debug ! ( %e, "Failed to open new path for network change" ) ;
5731+ if self . side ( ) . is_client ( ) {
5732+ debug ! ( %e, "Failed to open new path for network change" ) ;
5733+ }
56525734 // if this fails, let the path try to recover itself
56535735 recoverable_paths. push ( ( path_id, remote) ) ;
56545736 continue ;
@@ -7252,19 +7334,6 @@ enum ConnectionSide {
72527334}
72537335
72547336impl ConnectionSide {
7255- fn remote_may_migrate ( & self , state : & State ) -> bool {
7256- match self {
7257- Self :: Server { server_config } => server_config. migration ,
7258- Self :: Client { .. } => {
7259- if let Some ( hs) = state. as_handshake ( ) {
7260- hs. allow_server_migration
7261- } else {
7262- false
7263- }
7264- }
7265- }
7266- }
7267-
72687337 fn is_client ( & self ) -> bool {
72697338 self . side ( ) . is_client ( )
72707339 }
0 commit comments