Skip to content

Commit 041efc6

Browse files
committed
feat: racenonce for deduplicating connection attempts
1 parent 7d5fef2 commit 041efc6

File tree

6 files changed

+188
-5
lines changed

6 files changed

+188
-5
lines changed

quinn-proto/src/config/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,9 @@ impl fmt::Debug for ValidationTokenConfig {
525525
}
526526
}
527527

528+
/// Racenonce
529+
pub(crate) type Racenonce = [u8; 32];
530+
528531
/// Configuration for outgoing connections
529532
///
530533
/// Default values should be suitable for most internet applications.
@@ -545,6 +548,8 @@ pub struct ClientConfig {
545548

546549
/// QUIC protocol version to use
547550
pub(crate) version: u32,
551+
552+
pub(crate) racenonce: Option<Racenonce>,
548553
}
549554

550555
impl ClientConfig {
@@ -558,6 +563,7 @@ impl ClientConfig {
558563
RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid()
559564
}),
560565
version: 1,
566+
racenonce: None,
561567
}
562568
}
563569

@@ -597,6 +603,12 @@ impl ClientConfig {
597603
self.version = version;
598604
self
599605
}
606+
607+
/// Set the racenonce.
608+
pub fn racenonce(&mut self, racenonce: [u8; 32]) -> &mut Self {
609+
self.racenonce = Some(racenonce);
610+
self
611+
}
600612
}
601613

602614
#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]

quinn-proto/src/connection/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
cid_generator::ConnectionIdGenerator,
2222
cid_queue::CidQueue,
2323
coding::BufMutExt,
24-
config::{ServerConfig, TransportConfig},
24+
config::{Racenonce, ServerConfig, TransportConfig},
2525
congestion::Controller,
2626
crypto::{self, KeyPair, Keys, PacketKey},
2727
frame::{self, Close, Datagram, FrameStruct, NewToken, ObservedAddr},
@@ -4278,6 +4278,10 @@ impl Connection {
42784278
new_tokens.push(remote_addr);
42794279
}
42804280
}
4281+
4282+
pub(crate) fn racenonce(&self) -> Option<Racenonce> {
4283+
self.peer_params.racenonce
4284+
}
42814285
}
42824286

42834287
impl fmt::Debug for Connection {

quinn-proto/src/endpoint.rs

+30-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
ResetToken, Side, Transmit, TransportConfig, TransportError,
2020
cid_generator::ConnectionIdGenerator,
2121
coding::BufMutExt,
22-
config::{ClientConfig, EndpointConfig, ServerConfig},
22+
config::{ClientConfig, EndpointConfig, Racenonce, ServerConfig},
2323
connection::{Connection, ConnectionError, SideArgs},
2424
crypto::{self, Keys, UnsupportedVersion},
2525
frame,
@@ -356,6 +356,7 @@ impl Endpoint {
356356
&self.config,
357357
self.local_cid_generator.as_ref(),
358358
loc_cid,
359+
config.racenonce,
359360
None,
360361
&mut self.rng,
361362
);
@@ -612,6 +613,7 @@ impl Endpoint {
612613
&self.config,
613614
self.local_cid_generator.as_ref(),
614615
loc_cid,
616+
None,
615617
Some(&server_config),
616618
&mut self.rng,
617619
);
@@ -663,6 +665,31 @@ impl Endpoint {
663665
incoming.rest,
664666
) {
665667
Ok(()) => {
668+
if let Some(nonce) = conn.racenonce() {
669+
if self
670+
.connections
671+
.iter()
672+
.any(|(_idx, conn)| conn.racenonce == Some(nonce))
673+
{
674+
debug!("handshake failed: duplicate racenonce");
675+
self.handle_event(ch, EndpointEvent(EndpointEventInner::Drained));
676+
let e = TransportError::CONNECTION_REFUSED("duplicate racenonce");
677+
let response = self.initial_close(
678+
version,
679+
incoming.addresses,
680+
&incoming.crypto,
681+
&src_cid,
682+
e.clone(),
683+
buf,
684+
);
685+
return Err(AcceptError {
686+
cause: e.into(),
687+
response: Some(response),
688+
});
689+
} else {
690+
self.connections[ch].racenonce = Some(nonce);
691+
}
692+
}
666693
trace!(id = ch.0, icid = %dst_cid, "new connection");
667694

668695
for event in incoming_buffer.datagrams {
@@ -853,6 +880,7 @@ impl Endpoint {
853880
addresses,
854881
side,
855882
reset_token: None,
883+
racenonce: None,
856884
});
857885
debug_assert_eq!(id, ch.0, "connection handle allocation out of sync");
858886

@@ -1138,6 +1166,7 @@ pub(crate) struct ConnectionMeta {
11381166
/// Reset token provided by the peer for the CID we're currently sending to, and the address
11391167
/// being sent to
11401168
reset_token: Option<(SocketAddr, ResetToken)>,
1169+
racenonce: Option<Racenonce>,
11411170
}
11421171

11431172
/// Local connection IDs for a single path

quinn-proto/src/tests/mod.rs

+49
Original file line numberDiff line numberDiff line change
@@ -3569,3 +3569,52 @@ fn reject_short_idcid() {
35693569
panic!("expected an initial close");
35703570
};
35713571
}
3572+
3573+
#[test]
3574+
fn racenonce() {
3575+
let _guard = subscribe();
3576+
let mut pair = Pair::default();
3577+
3578+
println!("connect1");
3579+
// first connect with a racenonce: succeeds
3580+
{
3581+
let mut cfg = client_config();
3582+
cfg.racenonce([1u8; 32]);
3583+
pair.connect_with(cfg);
3584+
}
3585+
3586+
println!("connect2");
3587+
// second connect with the same racenonce: fails
3588+
{
3589+
let mut cfg = client_config();
3590+
cfg.racenonce([1u8; 32]);
3591+
3592+
let client_ch = pair.begin_connect(cfg);
3593+
pair.drive();
3594+
3595+
assert_matches!(
3596+
pair.server.assert_accept_error(),
3597+
ConnectionError::TransportError(TransportError {
3598+
code,
3599+
reason,
3600+
..
3601+
})
3602+
if code == TransportErrorCode::CONNECTION_REFUSED && &reason == "duplicate racenonce"
3603+
);
3604+
3605+
let client_conn = pair.client_conn_mut(client_ch);
3606+
assert_matches!(
3607+
client_conn.poll(),
3608+
Some(Event::ConnectionLost { reason: ConnectionError::ConnectionClosed(err) }) if err.error_code == TransportErrorCode::CONNECTION_REFUSED && err.reason.as_ref() == b"duplicate racenonce"
3609+
);
3610+
assert_matches!(client_conn.poll(), None);
3611+
}
3612+
3613+
// third connect with a different racenonce: succeeds
3614+
println!("connect3");
3615+
{
3616+
let mut cfg = client_config();
3617+
cfg.racenonce([2u8; 32]);
3618+
pair.connect_with(cfg);
3619+
}
3620+
}

quinn-proto/src/transport_parameters.rs

+28-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
cid_generator::ConnectionIdGenerator,
2222
cid_queue::CidQueue,
2323
coding::{BufExt, BufMutExt, UnexpectedEnd},
24-
config::{EndpointConfig, ServerConfig, TransportConfig},
24+
config::{EndpointConfig, Racenonce, ServerConfig, TransportConfig},
2525
connection::PathId,
2626
shared::ConnectionId,
2727
};
@@ -118,6 +118,9 @@ macro_rules! make_struct {
118118

119119
// Multipath extension
120120
pub(crate) initial_max_path_id: Option<PathId>,
121+
122+
// Racenonce
123+
pub(crate) racenonce: Option<Racenonce>,
121124
}
122125

123126
// We deliberately don't implement the `Default` trait, since that would be public, and
@@ -143,6 +146,7 @@ macro_rules! make_struct {
143146
write_order: None,
144147
address_discovery_role: address_discovery::Role::Disabled,
145148
initial_max_path_id: None,
149+
racenonce: None
146150
}
147151
}
148152
}
@@ -157,6 +161,7 @@ impl TransportParameters {
157161
endpoint_config: &EndpointConfig,
158162
cid_gen: &dyn ConnectionIdGenerator,
159163
initial_src_cid: ConnectionId,
164+
racenonce: Option<Racenonce>,
160165
server_config: Option<&ServerConfig>,
161166
rng: &mut impl RngCore,
162167
) -> Self {
@@ -193,6 +198,7 @@ impl TransportParameters {
193198
address_discovery_role: config.address_discovery_role,
194199
// TODO(@divma): TransportConfig or..?
195200
initial_max_path_id: config.initial_max_path_id.map(PathId::from),
201+
racenonce,
196202
..Self::default()
197203
}
198204
}
@@ -409,6 +415,13 @@ impl TransportParameters {
409415
w.write(val);
410416
}
411417
}
418+
TransportParameterId::Racenonce => {
419+
if let Some(val) = self.racenonce {
420+
w.write_var(id as u64);
421+
w.write_var(val.len() as u64);
422+
w.put_slice(&val);
423+
}
424+
}
412425
id => {
413426
macro_rules! write_params {
414427
{$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => {
@@ -535,6 +548,14 @@ impl TransportParameters {
535548
params.initial_max_path_id = Some(value);
536549
tracing::debug!(initial_max_path_id=%value, "multipath enabled");
537550
}
551+
TransportParameterId::Racenonce => {
552+
if len != 32 || params.racenonce.is_some() {
553+
return Err(Error::Malformed);
554+
}
555+
let mut val = [0; 32];
556+
r.copy_to_slice(&mut val);
557+
params.racenonce = Some(val);
558+
}
538559
_ => {
539560
macro_rules! parse {
540561
{$($(#[$doc:meta])* $name:ident ($id:ident) = $default:expr,)*} => {
@@ -703,11 +724,13 @@ pub(crate) enum TransportParameterId {
703724

704725
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath
705726
InitialMaxPathId = 0x0f739bbc1b666d0c,
727+
728+
Racenonce = 0x0f138193fac,
706729
}
707730

708731
impl TransportParameterId {
709732
/// Array with all supported transport parameter IDs
710-
const SUPPORTED: [Self; 23] = [
733+
const SUPPORTED: [Self; 24] = [
711734
Self::MaxIdleTimeout,
712735
Self::MaxUdpPayloadSize,
713736
Self::InitialMaxData,
@@ -731,6 +754,7 @@ impl TransportParameterId {
731754
Self::MinAckDelayDraft07,
732755
Self::ObservedAddr,
733756
Self::InitialMaxPathId,
757+
Self::Racenonce,
734758
];
735759
}
736760

@@ -772,6 +796,7 @@ impl TryFrom<u64> for TransportParameterId {
772796
id if Self::MinAckDelayDraft07 == id => Self::MinAckDelayDraft07,
773797
id if Self::ObservedAddr == id => Self::ObservedAddr,
774798
id if Self::InitialMaxPathId == id => Self::InitialMaxPathId,
799+
id if Self::Racenonce == id => Self::Racenonce,
775800
_ => return Err(()),
776801
};
777802
Ok(param)
@@ -812,6 +837,7 @@ mod test {
812837
min_ack_delay: Some(2_000u32.into()),
813838
address_discovery_role: address_discovery::Role::SendOnly,
814839
initial_max_path_id: Some(PathId::MAX),
840+
racenonce: Some([42u8; 32]),
815841
..TransportParameters::default()
816842
};
817843
params.write(&mut buf);

quinn/src/tests.rs

+64-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ use std::{
1616
use crate::runtime::TokioRuntime;
1717
use crate::{Duration, Instant};
1818
use bytes::Bytes;
19-
use proto::{RandomConnectionIdGenerator, crypto::rustls::QuicClientConfig};
19+
use proto::{
20+
ConnectionError, RandomConnectionIdGenerator, TransportErrorCode,
21+
crypto::rustls::QuicClientConfig,
22+
};
2023
use rand::{RngCore, SeedableRng, rngs::StdRng};
2124
use rustls::{
2225
RootCertStore,
@@ -310,6 +313,13 @@ impl EndpointFactory {
310313

311314
endpoint
312315
}
316+
317+
fn client_config(&self) -> ClientConfig {
318+
let mut roots = rustls::RootCertStore::empty();
319+
roots.add(self.cert.cert.der().clone()).unwrap();
320+
321+
ClientConfig::with_root_certificates(Arc::new(roots)).unwrap()
322+
}
313323
}
314324

315325
#[tokio::test]
@@ -892,3 +902,56 @@ async fn test_multipath_negotiated() {
892902

893903
tokio::join!(server_task, client_task);
894904
}
905+
906+
#[tokio::test]
907+
async fn racenonce() {
908+
let _guard = subscribe();
909+
let factory = EndpointFactory::new();
910+
let server = factory.endpoint("server");
911+
let server_addr = server.local_addr().unwrap();
912+
913+
let client = factory.endpoint("client1");
914+
915+
let client_task = async move {
916+
let mut config = factory.client_config();
917+
config.racenonce([1u8; 32]);
918+
let conn1 = client
919+
.connect_with(config, server_addr, "localhost")
920+
.unwrap()
921+
.await
922+
.unwrap();
923+
let mut config = factory.client_config();
924+
config.racenonce([1u8; 32]);
925+
let conn2 = client
926+
.connect_with(config, server_addr, "localhost")
927+
.unwrap()
928+
.await;
929+
assert!(matches!(conn2,
930+
Err(ConnectionError::ConnectionClosed(ref frame))
931+
if frame.error_code == TransportErrorCode::CONNECTION_REFUSED && frame.reason.as_ref() == b"duplicate racenonce"
932+
));
933+
let mut config = factory.client_config();
934+
config.racenonce([2u8; 32]);
935+
let conn3 = client
936+
.connect_with(config, server_addr, "localhost")
937+
.unwrap()
938+
.await
939+
.unwrap();
940+
drop(conn1);
941+
drop(conn2);
942+
drop(conn3);
943+
};
944+
let server_task = async move {
945+
let _client1 = server.accept().await.unwrap().await.unwrap();
946+
let incoming2 = server.accept().await.unwrap();
947+
let res = incoming2.accept();
948+
assert!(matches!(
949+
res,
950+
Err(ConnectionError::TransportError(err))
951+
if err.code == TransportErrorCode::CONNECTION_REFUSED && &err.reason == "duplicate racenonce"
952+
));
953+
let _client1 = server.accept().await.unwrap().await.unwrap();
954+
}
955+
.instrument(error_span!("server"));
956+
tokio::join!(client_task, server_task);
957+
}

0 commit comments

Comments
 (0)