Skip to content

Commit

Permalink
Allow server to use NEW_TOKEN frames
Browse files Browse the repository at this point in the history
When a path becomes validated, the server may send the client NEW_TOKEN
frames. These may cause an Incoming to be validated.

- Adds TokenPayload::Validation variant
- Adds relevant configuration to ServerConfig
- Adds `TokenLog` object to server to mitigate token reuse

As of this commit, the only provided implementation of TokenLog is
NoneTokenLog, which is equivalent to the lack of a token log, and is the
default.
  • Loading branch information
gretchenfrage committed Jan 26, 2025
1 parent 78bfa5b commit 195a781
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 15 deletions.
123 changes: 121 additions & 2 deletions quinn-proto/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use crate::{
cid_generator::{ConnectionIdGenerator, HashedConnectionIdGenerator},
crypto::{self, HandshakeTokenKey, HmacKey},
shared::ConnectionId,
Duration, RandomConnectionIdGenerator, SystemTime, VarInt, VarIntBoundsExceeded,
DEFAULT_SUPPORTED_VERSIONS, MAX_CID_SIZE,
Duration, NoneTokenLog, RandomConnectionIdGenerator, SystemTime, TokenLog, VarInt,
VarIntBoundsExceeded, DEFAULT_SUPPORTED_VERSIONS, MAX_CID_SIZE,
};

mod transport;
Expand Down Expand Up @@ -197,6 +197,9 @@ pub struct ServerConfig {
/// Must be set to use TLS 1.3 only.
pub crypto: Arc<dyn crypto::ServerConfig>,

/// Configuration for sending and handling validation tokens
pub validation_token: ValidationTokenConfig,

/// Used to generate one-time AEAD keys to protect handshake tokens
pub(crate) token_key: Arc<dyn HandshakeTokenKey>,

Expand Down Expand Up @@ -234,6 +237,8 @@ impl ServerConfig {

migration: true,

validation_token: ValidationTokenConfig::default(),

preferred_address_v4: None,
preferred_address_v6: None,

Expand All @@ -251,6 +256,15 @@ impl ServerConfig {
self
}

/// Set a custom [`ValidationTokenConfig`]
pub fn validation_token_config(
&mut self,
validation_token: ValidationTokenConfig,
) -> &mut Self {
self.validation_token = validation_token;
self
}

/// Private key used to authenticate data included in handshake tokens
pub fn token_key(&mut self, value: Arc<dyn HandshakeTokenKey>) -> &mut Self {
self.token_key = value;
Expand Down Expand Up @@ -392,6 +406,7 @@ impl fmt::Debug for ServerConfig {
// crypto not debug
// token not debug
.field("retry_token_lifetime", &self.retry_token_lifetime)
.field("validation_token", &self.validation_token)
.field("migration", &self.migration)
.field("preferred_address_v4", &self.preferred_address_v4)
.field("preferred_address_v6", &self.preferred_address_v6)
Expand All @@ -406,6 +421,110 @@ impl fmt::Debug for ServerConfig {
}
}

/// Configuration for sending and handling validation tokens in incoming connections
///
/// Default values should be suitable for most internet applications.
///
/// ## QUIC Tokens
///
/// The QUIC protocol defines a concept of "[address validation][1]". Essentially, one side of a
/// QUIC connection may appear to be receiving QUIC packets from a particular remote UDP address,
/// but it will only consider that remote address "validated" once it has convincing evidence that
/// the address is not being [spoofed][2].
///
/// Validation is important primarily because of QUIC's "anti-amplification limit." This limit
/// prevents a QUIC server from sending a client more than three times the number of bytes it has
/// received from the client on a given address until that address is validated. This is designed
/// to mitigate the ability of attackers to use QUIC-based servers as reflectors in [amplification
/// attacks][3].
///
/// A path may become validated in several ways. The server is always considered validated by the
/// client. The client usually begins in an unvalidated state upon first connecting or migrating,
/// but then becomes validated through various mechanisms that usually take one network round trip.
/// However, in some cases, a client which has previously attempted to connect to a server may have
/// been given a one-time use cryptographically secured "token" that it can send in a subsequent
/// connection attempt to be validated immediately.
///
/// There are two ways these tokens can originate:
///
/// - If the server responds to an incoming connection with `retry`, a "retry token" is minted and
/// sent to the client, which the client immediately uses to attempt to connect again. Retry
/// tokens operate on short timescales, such as 15 seconds.
/// - If a client's path within an active connection is validated, the server may send the client
/// one or more "validation tokens," which the client may store for use in later connections to
/// the same server. Validation tokens may be valid for much longer lifetimes than retry token.
///
/// The usage of validation tokens is most impactful in situations where 0-RTT data is also being
/// used--in particular, in situations where the server sends the client more than three times more
/// 0.5-RTT data than it has received 0-RTT data. Since the successful completion of a connection
/// handshake implicitly causes the client's address to be validated, transmission of 0.5-RTT data
/// is the main situation where a server might be sending application data to an address that could
/// be validated by token usage earlier than it would become validated without token usage.
///
/// [1]: https://www.rfc-editor.org/rfc/rfc9000.html#section-8
/// [2]: https://en.wikipedia.org/wiki/IP_address_spoofing
/// [3]: https://en.wikipedia.org/wiki/Denial-of-service_attack#Amplification
///
/// These tokens should not be confused with "stateless reset tokens," which are similarly named
/// but entirely unrelated.
#[derive(Clone)]
pub struct ValidationTokenConfig {
pub(crate) lifetime: Duration,
pub(crate) log: Arc<dyn TokenLog>,
pub(crate) sent: u32,
}

impl ValidationTokenConfig {
/// Duration after an address validation token was issued for which it's considered valid
///
/// This refers only to tokens sent in NEW_TOKEN frames, in contrast to retry tokens.
///
/// Defaults to 2 weeks.
pub fn lifetime(&mut self, value: Duration) -> &mut Self {
self.lifetime = value;
self
}

/// Set a custom [`TokenLog`]
///
/// Defaults to [`NoneTokenLog`], which makes the server ignore all address validation tokens
/// (that is, tokens originating from NEW_TOKEN frames--retry tokens may still be accepted).
pub fn log(&mut self, log: Arc<dyn TokenLog>) -> &mut Self {
self.log = log;
self
}

/// Number of address validation tokens sent to a client when its path is validated
///
/// This refers only to tokens sent in NEW_TOKEN frames, in contrast to retry tokens.
///
/// Defaults to 0.
pub fn sent(&mut self, value: u32) -> &mut Self {
self.sent = value;
self
}
}

impl Default for ValidationTokenConfig {
fn default() -> Self {
Self {
lifetime: Duration::from_secs(2 * 7 * 24 * 60 * 60),
log: Arc::new(NoneTokenLog),
sent: 0,
}
}
}

impl fmt::Debug for ValidationTokenConfig {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("ServerValidationTokenConfig")
.field("lifetime", &self.lifetime)
// log not debug
.field("sent", &self.sent)
.finish_non_exhaustive()
}
}

/// Configuration for outgoing connections
///
/// Default values should be suitable for most internet applications.
Expand Down
62 changes: 58 additions & 4 deletions quinn-proto/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::{
ConnectionEvent, ConnectionEventInner, ConnectionId, DatagramConnectionEvent, EcnCodepoint,
EndpointEvent, EndpointEventInner,
},
token::ResetToken,
token::{ResetToken, Token, TokenPayload},
transport_parameters::TransportParameters,
Dir, Duration, EndpointConfig, Frame, Instant, Side, StreamId, Transmit, TransportError,
TransportErrorCode, VarInt, INITIAL_MTU, MAX_CID_SIZE, MAX_STREAM_COUNT, MIN_INITIAL_SIZE,
Expand Down Expand Up @@ -277,7 +277,7 @@ impl Connection {
now,
if pref_addr_cid.is_some() { 2 } else { 1 },
),
path: PathData::new(remote, allow_mtud, None, now, path_validated, &config),
path: PathData::new(remote, allow_mtud, None, now, &config),
allow_mtud,
local_ip,
prev_path: None,
Expand Down Expand Up @@ -351,6 +351,9 @@ impl Connection {
stats: ConnectionStats::default(),
version,
};
if path_validated {
this.on_path_validated();
}
if side.is_client() {
// Kick off the connection
this.write_crypto();
Expand Down Expand Up @@ -2435,7 +2438,7 @@ impl Connection {
);
return Ok(());
}
self.path.validated = true;
self.on_path_validated();

self.process_early_payload(now, packet)?;
if self.state.is_closed() {
Expand Down Expand Up @@ -2965,7 +2968,6 @@ impl Connection {
self.allow_mtud,
Some(peer_max_udp_payload_size),
now,
false,
&self.config,
)
};
Expand Down Expand Up @@ -3242,6 +3244,45 @@ impl Connection {
self.datagrams.send_blocked = false;
}

// NEW_TOKEN
while let Some(remote_addr) = space.pending.new_tokens.pop() {
debug_assert_eq!(space_id, SpaceId::Data);
let ConnectionSide::Server { server_config } = &self.side else {
panic!("NEW_TOKEN frames should not be enqueued by clients");
};

if remote_addr != self.path.remote {
// NEW_TOKEN frames contain tokens bound to a client's IP address, and are only
// useful if used from the same IP address. Thus, we abandon enqueued NEW_TOKEN
// frames upon an path change. Instead, when the new path becomes validated,
// NEW_TOKEN frames may be enqueued for the new path instead.
continue;
}

let token = Token::new(
TokenPayload::Validation {
ip: remote_addr.ip(),
issued: server_config.time_source.now(),
},
&mut self.rng,
);
let new_token = NewToken {
token: token.encode(&*server_config.token_key).into(),
};

if buf.len() + new_token.size() >= max_size {
space.pending.new_tokens.push(remote_addr);
break;
}

new_token.encode(buf);
sent.retransmits
.get_or_create()
.new_tokens
.push(remote_addr);
self.stats.frame_tx.new_token += 1;
}

// STREAM
if space_id == SpaceId::Data {
sent.stream_frames =
Expand Down Expand Up @@ -3603,6 +3644,19 @@ impl Connection {
// but that would needlessly prevent sending datagrams during 0-RTT.
key.map_or(16, |x| x.tag_len())
}

/// Mark the path as validated, and enqueue NEW_TOKEN frames to be sent as appropriate
fn on_path_validated(&mut self) {
self.path.validated = true;
let ConnectionSide::Server { server_config } = &self.side else {
return;
};
let new_tokens = &mut self.spaces[SpaceId::Data as usize].pending.new_tokens;
new_tokens.clear();
for _ in 0..server_config.validation_token.sent {
new_tokens.push(self.path.remote);
}
}
}

impl fmt::Debug for Connection {
Expand Down
3 changes: 1 addition & 2 deletions quinn-proto/src/connection/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ impl PathData {
allow_mtud: bool,
peer_max_udp_payload_size: Option<u16>,
now: Instant,
validated: bool,
config: &TransportConfig,
) -> Self {
let congestion = config
Expand All @@ -70,7 +69,7 @@ impl PathData {
congestion,
challenge: None,
challenge_pending: false,
validated,
validated: false,
total_sent: 0,
total_recvd: 0,
mtud: config
Expand Down
21 changes: 20 additions & 1 deletion quinn-proto/src/connection/spaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tracing::trace;
use super::assembler::Assembler;
use crate::{
connection::StreamsState, crypto::Keys, frame, packet::SpaceId, range_set::ArrayRangeSet,
shared::IssuedCid, Dir, Duration, Instant, StreamId, TransportError, VarInt,
shared::IssuedCid, Dir, Duration, Instant, SocketAddr, StreamId, TransportError, VarInt,
};

pub(super) struct PacketSpace {
Expand Down Expand Up @@ -309,6 +309,23 @@ pub struct Retransmits {
pub(super) retire_cids: Vec<u64>,
pub(super) ack_frequency: bool,
pub(super) handshake_done: bool,
/// For each enqueued NEW_TOKEN frame, a copy of the path's remote address
///
/// There are 2 reasons this is unusual:
///
/// - If the path changes, NEW_TOKEN frames bound for the old path are not retransmitted on the
/// new path. That is why this field stores the remote address: so that ones for old paths
/// can be filtered out.
/// - If a token is lost, a new randomly generated token is re-transmitted, rather than the
/// original. This is so that if both transmissions are received, the client won't risk
/// sending the same token twice. That is why this field does _not_ store any actual token.
///
/// It is true that a QUIC endpoint will only want to effectively have NEW_TOKEN frames
/// enqueued for its current path at a given point in time. Based on that, we could conceivably
/// change this from a vector to an `Option<(SocketAddr, usize)>` or just a `usize` or
/// something. However, due to the architecture of Quinn, it is considerably simpler to not do
/// that; consider what such a change would mean for implementing `BitOrAssign` on Self.
pub(super) new_tokens: Vec<SocketAddr>,
}

impl Retransmits {
Expand All @@ -326,6 +343,7 @@ impl Retransmits {
&& self.retire_cids.is_empty()
&& !self.ack_frequency
&& !self.handshake_done
&& self.new_tokens.is_empty()
}
}

Expand All @@ -347,6 +365,7 @@ impl ::std::ops::BitOrAssign for Retransmits {
self.retire_cids.extend(rhs.retire_cids);
self.ack_frequency |= rhs.ack_frequency;
self.handshake_done |= rhs.handshake_done;
self.new_tokens.extend_from_slice(&rhs.new_tokens);
}
}

Expand Down
12 changes: 12 additions & 0 deletions quinn-proto/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,18 @@ pub(crate) struct NewToken {
pub(crate) token: Bytes,
}

impl NewToken {
pub(crate) fn encode<W: BufMut>(&self, out: &mut W) {
out.write(FrameType::NEW_TOKEN);
out.write_var(self.token.len() as u64);
out.put_slice(&self.token);
}

pub(crate) fn size(&self) -> usize {
1 + VarInt::from_u64(self.token.len() as u64).unwrap().size() + self.token.len()
}
}

pub(crate) struct Iter {
bytes: Bytes,
last_ty: Option<FrameType>,
Expand Down
3 changes: 2 additions & 1 deletion quinn-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub use rustls;
mod config;
pub use config::{
AckFrequencyConfig, ClientConfig, ConfigError, EndpointConfig, IdleTimeout, MtuDiscoveryConfig,
ServerConfig, StdSystemTime, TimeSource, TransportConfig,
ServerConfig, StdSystemTime, TimeSource, TransportConfig, ValidationTokenConfig,
};

pub mod crypto;
Expand Down Expand Up @@ -86,6 +86,7 @@ pub use crate::cid_generator::{

mod token;
use token::ResetToken;
pub use token::{NoneTokenLog, TokenLog, TokenReuseError};

#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
Expand Down
Loading

0 comments on commit 195a781

Please sign in to comment.