s2n_quic_core/path/migration.rs
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5 event,
6 event::{
7 api::{Path, SocketAddress},
8 IntoEvent,
9 },
10 inet,
11};
12
13#[derive(Debug)]
14#[non_exhaustive]
15pub struct Attempt<'a> {
16 /// The path that the connection is currently actively using
17 pub active_path: Path<'a>,
18 /// Information about the packet triggering the migration attempt
19 pub packet: PacketInfo<'a>,
20}
21
22#[derive(Debug)]
23pub struct AttemptBuilder<'a> {
24 /// The path that the connection is currently actively using
25 pub active_path: Path<'a>,
26 /// Information about the packet triggering the migration attempt
27 pub packet: PacketInfo<'a>,
28}
29
30impl<'a> From<AttemptBuilder<'a>> for Attempt<'a> {
31 #[inline]
32 fn from(builder: AttemptBuilder<'a>) -> Self {
33 Self {
34 active_path: builder.active_path,
35 packet: builder.packet,
36 }
37 }
38}
39
40#[derive(Debug)]
41#[non_exhaustive]
42pub struct PacketInfo<'a> {
43 pub remote_address: SocketAddress<'a>,
44 pub local_address: SocketAddress<'a>,
45}
46
47#[derive(Debug)]
48pub struct PacketInfoBuilder<'a> {
49 pub remote_address: &'a inet::SocketAddress,
50 pub local_address: &'a inet::SocketAddress,
51}
52
53impl<'a> From<PacketInfoBuilder<'a>> for PacketInfo<'a> {
54 #[inline]
55 fn from(builder: PacketInfoBuilder<'a>) -> Self {
56 Self {
57 remote_address: builder.remote_address.into_event(),
58 local_address: builder.local_address.into_event(),
59 }
60 }
61}
62
63// TODO: Add an outcome that allows the connection to be closed/stateless reset https://github.com/aws/s2n-quic/issues/317
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum Outcome {
68 /// Allows the path migration attempt to continue
69 ///
70 /// Note that path validation will still be attempted as described in
71 /// [Section 8.2](https://datatracker.ietf.org/doc/html/rfc9000#section-8.2).
72 Allow,
73
74 /// Rejects a path migration attempt
75 ///
76 /// The connection will drop the packet that attempted to migrate and not reserve any state
77 /// for the new path.
78 Deny(DenyReason),
79 // Additional outcomes must be handled in the path::Manager
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum DenyReason {
84 // The new address uses a port that is blocked
85 BlockedPort,
86 // The new address uses a port in a different scope
87 PortScopeChanged,
88 // The new address uses an IP in a different scope
89 IpScopeChanged,
90 // All connection migrations are disabled
91 ConnectionMigrationDisabled,
92}
93
94impl IntoEvent<event::builder::ConnectionMigrationDenied> for DenyReason {
95 #[inline]
96 fn into_event(self) -> event::builder::ConnectionMigrationDenied {
97 let reason = match self {
98 DenyReason::BlockedPort => event::builder::MigrationDenyReason::BlockedPort,
99 DenyReason::PortScopeChanged => event::builder::MigrationDenyReason::PortScopeChanged,
100 DenyReason::IpScopeChanged => event::builder::MigrationDenyReason::IpScopeChange,
101 DenyReason::ConnectionMigrationDisabled => {
102 event::builder::MigrationDenyReason::ConnectionMigrationDisabled
103 }
104 };
105 event::builder::ConnectionMigrationDenied { reason }
106 }
107}
108
109/// Validates a path migration attempt from an active path to another
110pub trait Validator: 'static + Send {
111 /// Called on each connection migration attempt for a connection
112 fn on_migration_attempt(&mut self, attempt: &Attempt) -> Outcome;
113}
114
115pub mod default {
116 use super::*;
117 use crate::path::remote_port_blocked;
118
119 #[derive(Debug, Default)]
120 pub struct Validator;
121
122 impl super::Validator for Validator {
123 #[inline]
124 fn on_migration_attempt(&mut self, attempt: &Attempt) -> Outcome {
125 let active_addr = to_addr(&attempt.active_path.remote_addr);
126 let packet_addr = to_addr(&attempt.packet.remote_address);
127
128 // Block migrations to a port that is blocked
129 if remote_port_blocked(packet_addr.port()) {
130 return Outcome::Deny(DenyReason::BlockedPort);
131 }
132
133 //= https://www.rfc-editor.org/rfc/rfc9000#section-21.5.6
134 //# it might be possible over time to identify
135 //# specific UDP ports that are common targets of attacks or particular
136 //# patterns in datagrams that are used for attacks. Endpoints MAY
137 //# choose to avoid sending datagrams to these ports or not send
138 //# datagrams that match these patterns prior to validating the
139 //# destination address.
140
141 // NOTE: this may cause reachability issues if a peer or NAT use different
142 // port scopes for the same connection. Additional research may
143 // be required to determine if this countermeasure needs to be relaxed.
144 if PortScope::new(active_addr.port()) != PortScope::new(packet_addr.port()) {
145 return Outcome::Deny(DenyReason::PortScopeChanged);
146 }
147
148 //= https://www.rfc-editor.org/rfc/rfc9000#section-21.5.6
149 //# Endpoints MAY prevent connection attempts or
150 //# migration to a loopback address. Endpoints SHOULD NOT allow
151 //# connections or migration to a loopback address if the same service
152 //# was previously available at a different interface or if the address
153 //# was provided by a service at a non-loopback address.
154
155 //= https://www.rfc-editor.org/rfc/rfc9000#section-21.5.6
156 //# Similarly, endpoints could regard a change in address to a link-local
157 //# address [RFC4291] or an address in a private-use range [RFC1918] from
158 //# a global, unique-local [RFC4193], or non-private address as a
159 //# potential attempt at request forgery.
160
161 // Here, we ensure the ip scope match so peers are unable to change after
162 // establishing a connection
163 match (active_addr.unicast_scope(), packet_addr.unicast_scope()) {
164 (Some(a), Some(b)) if a == b => Outcome::Allow,
165 _ => Outcome::Deny(DenyReason::IpScopeChanged),
166 }
167 }
168 }
169
170 #[derive(Debug, PartialEq, Eq)]
171 enum PortScope {
172 System,
173 User,
174 Dynamic,
175 }
176
177 impl PortScope {
178 #[inline]
179 pub const fn new(value: u16) -> Self {
180 //= https://www.rfc-editor.org/rfc/rfc6335#section-6
181 //# o the System Ports, also known as the Well Known Ports, from 0-1023
182 //# (assigned by IANA)
183 //#
184 //# o the User Ports, also known as the Registered Ports, from 1024-
185 //# 49151 (assigned by IANA)
186 //#
187 //# o the Dynamic Ports, also known as the Private or Ephemeral Ports,
188 //# from 49152-65535 (never assigned)
189 match value {
190 0..=1023 => Self::System,
191 1024..=49151 => Self::User,
192 49152..=65535 => Self::Dynamic,
193 }
194 }
195 }
196
197 fn to_addr(addr: &SocketAddress) -> crate::inet::SocketAddress {
198 match addr {
199 SocketAddress::IpV4 { ip, port, .. } => {
200 crate::inet::SocketAddressV4::new(**ip, *port).into()
201 }
202 SocketAddress::IpV6 { ip, port, .. } => {
203 crate::inet::SocketAddressV6::new(**ip, *port).into()
204 }
205 }
206 }
207}
208
209pub mod disabled {
210 use super::*;
211
212 #[derive(Debug, Default)]
213 pub struct Validator;
214
215 impl super::Validator for Validator {
216 fn on_migration_attempt(&mut self, _attempt: &Attempt) -> Outcome {
217 // deny all migration attempts
218 Outcome::Deny(DenyReason::ConnectionMigrationDisabled)
219 }
220 }
221}
222
223#[cfg(any(test, feature = "testing"))]
224pub mod allow_all {
225 use super::*;
226
227 #[derive(Debug, Default)]
228 pub struct Validator;
229
230 impl super::Validator for Validator {
231 fn on_migration_attempt(&mut self, _attempt: &Attempt) -> Outcome {
232 // allow all migration attempts
233 Outcome::Allow
234 }
235 }
236}