Skip to content

Commit 1cef63e

Browse files
committed
tokio-quiche: Add test for active connection migration
Now that the tokio-quiche work loop issues extra connection IDs, we can properly support active connection migration. This is not a feature available to tokio-quiche clients today, but other QUIC clients talking to tokio-quiche servers can take advantage of it.
1 parent cde5b5d commit 1cef63e

File tree

2 files changed

+73
-49
lines changed

2 files changed

+73
-49
lines changed

tokio-quiche/tests/fixtures/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use futures::StreamExt;
5252
use regex::Regex;
5353
use std::collections::HashMap;
5454
use std::collections::HashSet;
55+
use std::net::SocketAddr;
5556
use std::sync::atomic::AtomicBool;
5657
use std::sync::atomic::AtomicUsize;
5758
use std::sync::atomic::Ordering;
@@ -266,6 +267,15 @@ where
266267
url
267268
}
268269

270+
pub fn extract_host_ipv4(url: &str) -> SocketAddr {
271+
let url = url::Url::parse(url).expect("url should be valid");
272+
match (url.host(), url.port()) {
273+
(Some(url::Host::Ipv4(addr)), Some(port)) =>
274+
SocketAddr::new(addr.into(), port),
275+
_ => panic!("invalid server address"),
276+
}
277+
}
278+
269279
pub fn map_responses(
270280
responses: Vec<HashMap<u64, String>>,
271281
) -> HashMap<usize, HashSet<usize>> {

tokio-quiche/tests/integration_tests/migration.rs

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,51 +25,55 @@
2525
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626

2727
use h3i::quiche;
28+
use tokio_quiche::quic::SimpleConnectionIdGenerator;
29+
use tokio_quiche::ConnectionIdGenerator as _;
2830

2931
use crate::fixtures::*;
3032

3133
#[tokio::test]
32-
/// Tests that client can migrate passively.
34+
async fn test_passive_migration() {
35+
run_migration_test(false).await;
36+
}
37+
38+
#[tokio::test]
39+
async fn test_active_migration() {
40+
run_migration_test(true).await;
41+
}
42+
43+
/// Tests that the client can migrate either actively or passively.
3344
///
34-
/// This means that the client's address somehow changes, but the client is not
35-
/// necessarily aware of it happening (e.g. due to NAT rebinding). This differs
36-
/// from "active" migration because in that case the client would explicitly
37-
/// provide a new connection ID that the server is then expected to use for the
38-
/// new path.
45+
/// Active migration means the client intentionally chooses a new local address
46+
/// to switch to. In this case, it must select a new DCID that was previously
47+
/// issued to it by the server. The server then also switches to a new DCID
48+
/// that was generated by the client.
49+
///
50+
/// Passive migration occurs when the client's address changes while keeping
51+
/// the same source and destination CIDs. The client is not necessarily aware
52+
/// of the change (e.g. due to NAT rebinding). Under these circumstances, the
53+
/// endpoints are allowed to keep talking to each other with the existing
54+
/// DCIDs.
3955
///
4056
/// The test simply binds a UDP socket on one address which is then used to
41-
/// complete the handshake and send an initial HTTP/3 request, then uses a new
42-
/// socket bound to a different port to send an additional HTTP/3 request, if
43-
/// both requests complete that means that the client was successfully migrated
44-
/// to the new address.
57+
/// complete the handshake and send an initial HTTP/3 request. Next, it
58+
/// switches (actively or passively) to a new socket bound to a different port
59+
/// and sends an additional HTTP/3 request. If both requests complete that
60+
/// means that the client was successfully migrated to the new address.
4561
///
4662
/// This requires using "plain" quiche as a client to properly control when and
4763
/// where packets are sent to, which is not possible using h3i.
48-
async fn test_passive_migration() {
64+
async fn run_migration_test(active: bool) {
4965
let mut quic_settings = QuicSettings::default();
5066
quic_settings.active_connection_id_limit = 2;
51-
quic_settings.disable_active_migration = true;
67+
quic_settings.disable_active_migration = !active;
5268
quic_settings.disable_dcid_reuse = false;
5369

54-
let hook = TestConnectionHook::new();
55-
5670
let url = start_server_with_settings(
5771
quic_settings,
5872
Http3Settings::default(),
59-
hook,
73+
TestConnectionHook::new(),
6074
handle_connection,
6175
);
62-
63-
let url = url::Url::parse(&url).unwrap();
64-
65-
let server_addr = match url.host().unwrap() {
66-
url::Host::Ipv4(addr) => std::net::SocketAddr::new(
67-
std::net::IpAddr::V4(addr),
68-
url.port().unwrap(),
69-
),
70-
71-
_ => panic!("invalid server address"),
72-
};
76+
let server_addr = extract_host_ipv4(&url);
7377

7478
let mut client_config =
7579
quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();
@@ -80,15 +84,15 @@ async fn test_passive_migration() {
8084
client_config.set_initial_max_stream_data_uni(1500);
8185
client_config.set_initial_max_streams_bidi(10);
8286
client_config.set_initial_max_streams_uni(3);
87+
client_config.set_active_connection_id_limit(2);
88+
// We don't need to allow the server to initiate connection migration
8389
client_config.set_disable_active_migration(true);
8490
client_config.verify_peer(false);
8591

86-
let mut client_scid = [0; quiche::MAX_CONN_ID_LEN];
87-
boring::rand::rand_bytes(&mut client_scid[..]).unwrap();
88-
let client_scid = quiche::ConnectionId::from_ref(&client_scid);
89-
let client_addr = "127.0.0.1:12345".parse().unwrap();
92+
let client_scid = SimpleConnectionIdGenerator.new_connection_id();
9093

91-
let socket = tokio::net::UdpSocket::bind(client_addr).await.unwrap();
94+
let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
95+
let client_addr = socket.local_addr().unwrap();
9296

9397
let mut conn = quiche::connect(
9498
Some("test.com"),
@@ -99,10 +103,16 @@ async fn test_passive_migration() {
99103
)
100104
.unwrap();
101105

106+
if active {
107+
// Supply a second SCID to the server to facilitate active migration
108+
let extra_scid = SimpleConnectionIdGenerator.new_connection_id();
109+
conn.new_scid(&extra_scid, 0xAABBCCDDEEFF0123454678, false)
110+
.unwrap();
111+
}
112+
102113
// Handshake.
103114
while !conn.is_established() {
104115
emit_flight(&socket, &mut conn).await;
105-
106116
process_flight(&socket, client_addr, &mut conn).await;
107117
}
108118

@@ -126,16 +136,28 @@ async fn test_passive_migration() {
126136

127137
assert_eq!(process_h3_events(&mut h3_conn, &mut conn), (true, true));
128138

129-
// Client "migrates" to new address.
130-
let migrated_addr: std::net::SocketAddr = "127.0.0.1:54321".parse().unwrap();
139+
// Client migrates to new address.
131140
let migrated_socket =
132-
tokio::net::UdpSocket::bind(migrated_addr).await.unwrap();
141+
tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
142+
let migrated_addr = migrated_socket.local_addr().unwrap();
143+
assert_ne!(
144+
client_addr, migrated_addr,
145+
"migrated socket must use a different local address",
146+
);
147+
148+
let client_addr = if active {
149+
// We actively switch the connection to `migrated_addr` and report that
150+
// address for all packets we receive from now on.
151+
conn.migrate_source(migrated_addr)
152+
.expect("active migration should succeed");
153+
migrated_addr
154+
} else {
155+
// We keep using the original client address to simulate the fact that the
156+
// client doesn't know that the path changes (e.g. due to NAT rebinding).
157+
client_addr
158+
};
133159

134160
// Client sends second request on the new address.
135-
//
136-
// Note that even though we use the `migrated_socket`, we still use the
137-
// original client address (`client_addr`) to simulate the fact that the
138-
// client doesn't know that the path changes (e.g. due to NAT rebinding).
139161
h3_conn.send_request(&mut conn, &req, true).unwrap();
140162
emit_flight(&migrated_socket, &mut conn).await;
141163

@@ -181,18 +203,12 @@ async fn process_flight(
181203
) {
182204
let mut buf = [0; 65535];
183205

184-
let mut did_recv = false;
206+
socket.readable().await.unwrap();
185207

186208
loop {
187-
if !did_recv {
188-
socket.readable().await.unwrap();
189-
}
190-
191209
let (len, from) = match socket.try_recv_from(&mut buf) {
192210
Ok(v) => v,
193-
194211
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
195-
196212
Err(e) => panic!("failed to receive packets: {e:?}"),
197213
};
198214

@@ -206,8 +222,6 @@ async fn process_flight(
206222

207223
// Process potentially coalesced packets.
208224
let _ = conn.recv(&mut buf[..len], recv_info).unwrap();
209-
210-
did_recv = true;
211225
}
212226
}
213227

@@ -224,7 +238,7 @@ fn process_h3_events(
224238

225239
Ok((stream_id, quiche::h3::Event::Data)) => {
226240
// Drain stream and drop the data.
227-
while let Ok(_) = h3_conn.recv_body(conn, stream_id, &mut buf) {}
241+
while h3_conn.recv_body(conn, stream_id, &mut buf).is_ok() {}
228242
},
229243

230244
Ok((_, quiche::h3::Event::Finished)) => {

0 commit comments

Comments
 (0)