Skip to content

Commit e8bbffe

Browse files
committed
Harden QUIC happy eyeballs scheduling
Signed-off-by: Andrzej Haczewski <ahaczewski@gmail.com>
1 parent a800fe3 commit e8bbffe

1 file changed

Lines changed: 124 additions & 1 deletion

File tree

lore-transport/src/quic/client.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pub struct EndpointConfig {
7373
const IDLE_TIMEOUT_MS: u32 = 30000;
7474
const KEEP_ALIVE_MS: u64 = 500;
7575
const HAPPY_EYEBALLS_DELAY_MS: u64 = 250;
76+
const HAPPY_EYEBALLS_MAX_IN_FLIGHT: usize = 10;
7677
pub const DEFAULT_EXPECTED_RTT_MS: u64 = 100;
7778

7879
#[derive(Clone, Debug)]
@@ -693,6 +694,7 @@ pub async fn connect(
693694
.to_socket_addrs()
694695
.internal_with(|| format!("remote {remote_url} is invalid"))?
695696
.collect();
697+
let remote_addrs = interleave_socket_addrs(remote_addrs);
696698
let server_name = config.sni_override.as_deref().unwrap_or(host.as_str());
697699

698700
let validate_certificate = url.scheme().ends_with("s");
@@ -772,6 +774,38 @@ pub async fn connect(
772774
Err(ProtocolError::internal(format!("connect: {remote_url}")))
773775
}
774776

777+
fn interleave_socket_addrs(remote_addrs: Vec<SocketAddr>) -> Vec<SocketAddr> {
778+
let Some(first) = remote_addrs.first() else {
779+
return remote_addrs;
780+
};
781+
let prefer_ipv6 = first.is_ipv6();
782+
let (preferred, fallback): (Vec<_>, Vec<_>) = remote_addrs
783+
.into_iter()
784+
.partition(|addr| addr.is_ipv6() == prefer_ipv6);
785+
let mut preferred = preferred.into_iter();
786+
let mut fallback = fallback.into_iter();
787+
let mut interleaved = Vec::with_capacity(preferred.len() + fallback.len());
788+
789+
loop {
790+
match preferred.next() {
791+
Some(addr) => interleaved.push(addr),
792+
None => {
793+
interleaved.extend(fallback);
794+
break;
795+
}
796+
}
797+
match fallback.next() {
798+
Some(addr) => interleaved.push(addr),
799+
None => {
800+
interleaved.extend(preferred);
801+
break;
802+
}
803+
}
804+
}
805+
806+
interleaved
807+
}
808+
775809
async fn connect_happy_eyeballs<T, F, Fut>(
776810
remote_addrs: Vec<SocketAddr>,
777811
attempt_delay: Duration,
@@ -802,6 +836,8 @@ where
802836
tokio::select! {
803837
result = attempts.next(), if !attempts.is_empty() => {
804838
if let Some(Some(connection)) = result {
839+
// Dropping `attempts` cancels the losing Quinn handshakes because each
840+
// production future owns its `Connecting` and `Endpoint`.
805841
return Some(connection);
806842
}
807843
if attempts.is_empty() {
@@ -813,7 +849,7 @@ where
813849
delay.as_mut().reset(tokio::time::Instant::now() + attempt_delay);
814850
}
815851
}
816-
_ = &mut delay => {
852+
_ = &mut delay, if attempts.len() < HAPPY_EYEBALLS_MAX_IN_FLIGHT => {
817853
let Some(addr) = next_addr.take() else {
818854
continue;
819855
};
@@ -1244,8 +1280,12 @@ mod tests {
12441280
use std::time::Duration;
12451281

12461282
use parking_lot::Mutex;
1283+
use tokio::sync::Semaphore;
1284+
use tokio::sync::mpsc;
12471285

1286+
use super::HAPPY_EYEBALLS_MAX_IN_FLIGHT;
12481287
use super::connect_happy_eyeballs;
1288+
use super::interleave_socket_addrs;
12491289

12501290
fn ipv6_addr() -> SocketAddr {
12511291
"[::1]:41337".parse().unwrap()
@@ -1255,6 +1295,41 @@ mod tests {
12551295
"127.0.0.1:41337".parse().unwrap()
12561296
}
12571297

1298+
#[test]
1299+
fn happy_eyeballs_interleaves_ipv6_first_addresses() {
1300+
let ipv6_second = "[::2]:41337".parse().unwrap();
1301+
let ipv6_third = "[::3]:41337".parse().unwrap();
1302+
let ipv4_second = "127.0.0.2:41337".parse().unwrap();
1303+
1304+
assert_eq!(
1305+
interleave_socket_addrs(vec![
1306+
ipv6_addr(),
1307+
ipv6_second,
1308+
ipv6_third,
1309+
ipv4_addr(),
1310+
ipv4_second,
1311+
]),
1312+
vec![
1313+
ipv6_addr(),
1314+
ipv4_addr(),
1315+
ipv6_second,
1316+
ipv4_second,
1317+
ipv6_third,
1318+
]
1319+
);
1320+
}
1321+
1322+
#[test]
1323+
fn happy_eyeballs_interleaves_ipv4_first_addresses() {
1324+
let ipv4_second = "127.0.0.2:41337".parse().unwrap();
1325+
let ipv6_second = "[::2]:41337".parse().unwrap();
1326+
1327+
assert_eq!(
1328+
interleave_socket_addrs(vec![ipv4_addr(), ipv4_second, ipv6_addr(), ipv6_second,]),
1329+
vec![ipv4_addr(), ipv6_addr(), ipv4_second, ipv6_second]
1330+
);
1331+
}
1332+
12581333
#[tokio::test]
12591334
async fn happy_eyeballs_starts_fallback_while_first_attempt_is_stalled() {
12601335
let attempts = Arc::new(Mutex::new(Vec::new()));
@@ -1334,4 +1409,52 @@ mod tests {
13341409

13351410
assert_eq!(result, None);
13361411
}
1412+
1413+
#[tokio::test]
1414+
async fn happy_eyeballs_bounds_in_flight_attempts() {
1415+
let remote_addrs: Vec<_> = (1..=HAPPY_EYEBALLS_MAX_IN_FLIGHT + 1)
1416+
.map(|port| SocketAddr::new(ipv6_addr().ip(), port as u16))
1417+
.collect();
1418+
let release = Arc::new(Semaphore::new(0));
1419+
let attempt_release = release.clone();
1420+
let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1421+
1422+
let task = lore_base::lore_spawn!(connect_happy_eyeballs(
1423+
remote_addrs.clone(),
1424+
Duration::from_millis(1),
1425+
move |addr| {
1426+
started_tx.send(addr).unwrap();
1427+
let attempt_release = attempt_release.clone();
1428+
async move {
1429+
attempt_release.acquire().await.unwrap().forget();
1430+
None::<SocketAddr>
1431+
}
1432+
},
1433+
));
1434+
1435+
for expected in remote_addrs.iter().take(HAPPY_EYEBALLS_MAX_IN_FLIGHT) {
1436+
assert_eq!(
1437+
tokio::time::timeout(Duration::from_secs(2), started_rx.recv())
1438+
.await
1439+
.expect("attempt should start"),
1440+
Some(*expected)
1441+
);
1442+
}
1443+
assert!(
1444+
tokio::time::timeout(Duration::from_millis(50), started_rx.recv())
1445+
.await
1446+
.is_err(),
1447+
"attempts above the in-flight limit should remain queued"
1448+
);
1449+
1450+
release.add_permits(1);
1451+
assert_eq!(
1452+
tokio::time::timeout(Duration::from_secs(2), started_rx.recv())
1453+
.await
1454+
.expect("queued attempt should start when a slot opens"),
1455+
Some(remote_addrs[HAPPY_EYEBALLS_MAX_IN_FLIGHT])
1456+
);
1457+
1458+
task.abort();
1459+
}
13371460
}

0 commit comments

Comments
 (0)