Skip to content

Commit

Permalink
#2057: Use randomly generated GREASE transport parameter.
Browse files Browse the repository at this point in the history
  • Loading branch information
mstyura authored and djc committed Nov 21, 2024
1 parent a4c886c commit 2edf192
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 3 deletions.
2 changes: 2 additions & 0 deletions quinn-proto/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ impl Endpoint {
self.local_cid_generator.as_ref(),
loc_cid,
None,
&mut self.rng,
);
let tls = config
.crypto
Expand Down Expand Up @@ -632,6 +633,7 @@ impl Endpoint {
self.local_cid_generator.as_ref(),
loc_cid,
Some(&server_config),
&mut self.rng,
);
params.stateless_reset_token = Some(ResetToken::new(&*self.config.reset_key, &loc_cid));
params.original_dst_cid = Some(incoming.orig_dst_cid);
Expand Down
135 changes: 132 additions & 3 deletions quinn-proto/src/transport_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::{
};

use bytes::{Buf, BufMut};
use rand::{Rng as _, RngCore};
use thiserror::Error;

use crate::{
Expand Down Expand Up @@ -99,6 +100,10 @@ macro_rules! make_struct {
pub(crate) stateless_reset_token: Option<ResetToken>,
/// The server's preferred address for communication after handshake completion
pub(crate) preferred_address: Option<PreferredAddress>,
/// The randomly generated reserved transport parameter to sustain future extensibility
/// of transport parameter extensions.
/// When present, it is included during serialization but ignored during deserialization.
pub(crate) grease_transport_parameter: Option<ReservedTransportParameter>,
}

// We deliberately don't implement the `Default` trait, since that would be public, and
Expand All @@ -120,6 +125,7 @@ macro_rules! make_struct {
retry_src_cid: None,
stateless_reset_token: None,
preferred_address: None,
grease_transport_parameter: None,
}
}
}
Expand All @@ -135,6 +141,7 @@ impl TransportParameters {
cid_gen: &dyn ConnectionIdGenerator,
initial_src_cid: ConnectionId,
server_config: Option<&ServerConfig>,
rng: &mut impl RngCore,
) -> Self {
Self {
initial_src_cid: Some(initial_src_cid),
Expand All @@ -160,6 +167,7 @@ impl TransportParameters {
min_ack_delay: Some(
VarInt::from_u64(u64::try_from(TIMER_GRANULARITY.as_micros()).unwrap()).unwrap(),
),
grease_transport_parameter: Some(ReservedTransportParameter::random(rng)),
..Self::default()
}
}
Expand Down Expand Up @@ -300,9 +308,9 @@ impl TransportParameters {
}
apply_params!(write_params);

// Add a reserved parameter to keep people on their toes
w.write_var(31 * 5 + 27);
w.write_var(0);
if let Some(param) = self.grease_transport_parameter {
param.write(w);
}

if let Some(ref x) = self.stateless_reset_token {
w.write_var(0x02);
Expand Down Expand Up @@ -467,6 +475,81 @@ impl TransportParameters {
}
}

/// A reserved transport parameter.
///
/// It has an identifier of the form 31 * N + 27 for the integer value of N.
/// Such identifiers are reserved to exercise the requirement that unknown transport parameters be ignored.
/// The reserved transport parameter has no semantics and can carry arbitrary values.
/// It may be included in transport parameters sent to the peer, and should be ignored when received.
///
/// See spec: <https://www.rfc-editor.org/rfc/rfc9000.html#section-18.1>
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct ReservedTransportParameter {
/// The reserved identifier of the transport parameter
id: VarInt,

/// Buffer to store the parameter payload
payload: [u8; Self::MAX_PAYLOAD_LEN],

/// The number of bytes to include in the wire format from the `payload` buffer
payload_len: usize,
}

impl ReservedTransportParameter {
/// Generates a transport parameter with a random payload and a reserved ID.
///
/// The implementation is inspired by quic-go and quiche:
/// 1. <https://github.com/quic-go/quic-go/blob/3e0a67b2476e1819752f04d75968de042b197b56/internal/wire/transport_parameters.go#L338-L344>
/// 2. <https://github.com/google/quiche/blob/cb1090b20c40e2f0815107857324e99acf6ec567/quiche/quic/core/crypto/transport_parameters.cc#L843-L860>
fn random(rng: &mut impl RngCore) -> Self {
let id = Self::generate_reserved_id(rng);

let payload_len = rng.gen_range(0..Self::MAX_PAYLOAD_LEN);

let payload = {
let mut slice = [0u8; Self::MAX_PAYLOAD_LEN];
rng.fill_bytes(&mut slice[..payload_len]);
slice
};

Self {
id,
payload,
payload_len,
}
}

fn write(&self, w: &mut impl BufMut) {
w.write_var(self.id.0);
w.write_var(self.payload_len as u64);
w.put_slice(&self.payload[..self.payload_len]);
}

/// Generates a random reserved identifier of the form `31 * N + 27`, as required by RFC 9000.
/// Reserved transport parameter identifiers are used to test compliance with the requirement
/// that unknown transport parameters must be ignored by peers.
/// See: <https://www.rfc-editor.org/rfc/rfc9000.html#section-18.1> and <https://www.rfc-editor.org/rfc/rfc9000.html#section-22.3>
fn generate_reserved_id(rng: &mut impl RngCore) -> VarInt {
let id = {
let rand = rng.gen_range(0u64..(1 << 62) - 27);
let n = rand / 31;
31 * n + 27
};
debug_assert!(
id % 31 == 27,
"generated id does not have the form of 31 * N + 27"
);
VarInt::from_u64(id).expect(
"generated id does fit into range of allowed transport parameter IDs: [0; 2^62)",
)
}

/// The maximum length of the payload to include as the parameter payload.
/// This value is not a specification-imposed limit but is chosen to match
/// the limit used by other implementations of QUIC, e.g., quic-go and quiche.
const MAX_PAYLOAD_LEN: usize = 16;
}

fn decode_cid(len: usize, value: &mut Option<ConnectionId>, r: &mut impl Buf) -> Result<(), Error> {
if len > MAX_CID_SIZE || value.is_some() || r.remaining() < len {
return Err(Error::Malformed);
Expand Down Expand Up @@ -507,6 +590,52 @@ mod test {
);
}

#[test]
fn reserved_transport_parameter_generate_reserved_id() {
use rand::rngs::mock::StepRng;
let mut rngs = [
StepRng::new(0, 1),
StepRng::new(1, 1),
StepRng::new(27, 1),
StepRng::new(31, 1),
StepRng::new(u32::MAX as u64, 1),
StepRng::new(u32::MAX as u64 - 1, 1),
StepRng::new(u32::MAX as u64 + 1, 1),
StepRng::new(u32::MAX as u64 - 27, 1),
StepRng::new(u32::MAX as u64 + 27, 1),
StepRng::new(u32::MAX as u64 - 31, 1),
StepRng::new(u32::MAX as u64 + 31, 1),
StepRng::new(u64::MAX, 1),
StepRng::new(u64::MAX - 1, 1),
StepRng::new(u64::MAX - 27, 1),
StepRng::new(u64::MAX - 31, 1),
StepRng::new(1 << 62, 1),
StepRng::new((1 << 62) - 1, 1),
StepRng::new((1 << 62) + 1, 1),
StepRng::new((1 << 62) - 27, 1),
StepRng::new((1 << 62) + 27, 1),
StepRng::new((1 << 62) - 31, 1),
StepRng::new((1 << 62) + 31, 1),
];
for rng in &mut rngs {
let id = ReservedTransportParameter::generate_reserved_id(rng);
assert!(id.0 % 31 == 27)
}
}

#[test]
fn reserved_transport_parameter_ignored_when_read() {
let mut buf = Vec::new();
let reserved_parameter = ReservedTransportParameter::random(&mut rand::thread_rng());
assert!(reserved_parameter.payload_len < ReservedTransportParameter::MAX_PAYLOAD_LEN);
assert!(reserved_parameter.id.0 % 31 == 27);

reserved_parameter.write(&mut buf);
assert!(!buf.is_empty());
let read_params = TransportParameters::read(Side::Server, &mut buf.as_slice()).unwrap();
assert_eq!(read_params, TransportParameters::default());
}

#[test]
fn read_semantic_validation() {
#[allow(clippy::type_complexity)]
Expand Down

0 comments on commit 2edf192

Please sign in to comment.