// This file implements functions necessary for address validation.
use std::{
net::{IpAddr, SocketAddr},
time::{Duration, Instant},
};
use neqo_common::{Buffer, Decoder, Encoder, Role, qinfo, qtrace}; use nss::{
constants::{TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3},
selfencrypt::SelfEncrypt,
}; use smallvec::SmallVec; use static_assertions::const_assert;
/// A prefix we add to Retry tokens to distinguish them from `NEW_TOKEN` tokens. const TOKEN_IDENTIFIER_RETRY: &[u8] = &[0x52, 0x65, 0x74, 0x72, 0x79]; /// A prefix on `NEW_TOKEN` tokens, that is maximally Hamming distant from `NEW_TOKEN`. /// Together, these need to have a low probability of collision, even if there is /// corruption of individual bits in transit. const TOKEN_IDENTIFIER_NEW_TOKEN: &[u8] = &[0xad, 0x9a, 0x8b, 0x8d, 0x86];
/// The maximum number of tokens we'll save from `NEW_TOKEN` frames. /// This should be the same as the value of `MAX_TICKETS` in `nss`. const MAX_NEW_TOKEN: usize = 4; /// The number of tokens we'll track for the purposes of looking for duplicates. /// This is based on how many might be received over a period where could be /// retransmissions. It should be at least `MAX_NEW_TOKEN`. const MAX_SAVED_TOKENS: usize = 8;
/// `ValidateAddress` determines what sort of address validation is performed. /// In short, this determines when a Retry packet is sent. #[derive(Debug, PartialEq, Eq)] pubenum ValidateAddress { /// Require address validation never.
Never, /// Require address validation unless a `NEW_TOKEN` token is provided.
NoToken, /// Require address validation even if a `NEW_TOKEN` token is provided.
Always,
}
pubstruct AddressValidation { /// What sort of validation is performed.
validation: ValidateAddress, /// A self-encryption object used for protecting Retry tokens.
self_encrypt: SelfEncrypt, /// When this object was created.
start_time: Instant,
}
fn encode_aad(peer_address: SocketAddr, retry: bool) -> Encoder { // Let's be "clever" by putting the peer's address in the AAD. // We don't need to encode these into the token as they should be // available when we need to check the token. letmut aad = Encoder::default(); if retry {
aad.encode(TOKEN_IDENTIFIER_RETRY);
} else {
aad.encode(TOKEN_IDENTIFIER_NEW_TOKEN);
} match peer_address.ip() {
IpAddr::V4(a) => {
aad.encode_byte(4);
aad.encode(a.octets());
}
IpAddr::V6(a) => {
aad.encode_byte(6);
aad.encode(a.octets());
}
} if retry {
aad.encode_uint(2, peer_address.port());
}
aad
}
// TODO(mt) rotate keys on a fixed schedule. let retry = dcid.is_some(); letmut data = Encoder::default(); let end = now
+ if retry {
EXPIRATION_RETRY
} else {
EXPIRATION_NEW_TOKEN
}; let end_millis = u32::try_from(end.duration_since(self.start_time).as_millis())?;
data.encode_uint(4, end_millis); iflet Some(dcid) = dcid {
data.encode(dcid);
}
// Include the token identifier ("Retry"/~) in the AAD, then keep it for plaintext. letmut buf = Self::encode_aad(peer_address, retry); let encrypted = self.self_encrypt.seal(buf.as_ref(), data.as_ref())?; #[cfg(feature = "build-fuzzing-corpus")] letmut corpus_data = buf.as_ref()[TOKEN_IDENTIFIER_RETRY.len()..].to_vec();
buf.truncate(TOKEN_IDENTIFIER_RETRY.len());
buf.encode(&encrypted); let token: Vec<u8> = buf.into(); #[cfg(feature = "build-fuzzing-corpus")]
{ if !retry { // Port is not validated for NEW_TOKEN, so use 0 as a placeholder.
corpus_data.extend_from_slice(&[0, 0]);
}
corpus_data.extend_from_slice(&token);
neqo_common::write_item_to_fuzzing_corpus("addr_valid", &corpus_data);
}
Ok(token)
}
/// This generates a token for use with Retry. pubfn generate_retry_token(
&self,
dcid: &ConnectionId,
peer_address: SocketAddr,
now: Instant,
) -> Res<Vec<u8>> { self.generate_token(Some(dcid), peer_address, now)
}
/// This generates a token for use with `NEW_TOKEN`. pubfn generate_new_token(&self, peer_address: SocketAddr, now: Instant) -> Res<Vec<u8>> { self.generate_token(None, peer_address, now)
}
pubfn set_validation(&mutself, validation: ValidateAddress) {
qtrace!("AddressValidation {self:p}: set to {validation:?}"); self.validation = validation;
}
/// Decrypts `token` and returns the connection ID it contains. /// Returns a tuple with a boolean indicating whether this thinks /// that the token was a Retry token, and a connection ID, that is /// None if the token wasn't successfully decrypted. fn decrypt_token(
&self,
token: &[u8],
peer_address: SocketAddr,
retry: bool,
now: Instant,
) -> Option<ConnectionId> { let peer_addr = Self::encode_aad(peer_address, retry); let data = self.self_encrypt.open(peer_addr.as_ref(), token).ok()?; letmut dec = Decoder::new(&data);
{ let d = dec.decode_uint::<u32>()?; let end = self.start_time + Duration::from_millis(u64::from(d)); if end < now {
qtrace!("Expired token: {end:?} vs. {now:?}"); return None;
}
}
Some(ConnectionId::from(dec.decode_remainder()))
}
/// Calculate the Hamming difference between our identifier and the target. /// Less than one difference per byte indicates that it is likely not a Retry. /// This generous interpretation allows for a lot of damage in transit. /// Note that if this check fails, then the token will be treated like it came /// from `NEW_TOKEN` instead. If there truly is corruption of packets that causes /// validation failure, it will be a failure that we try to recover from. fn is_likely_retry(token: &[u8]) -> bool { let difference: u32 = token
.iter()
.zip(TOKEN_IDENTIFIER_RETRY.iter())
.map(|(a, b)| (a ^ b).count_ones())
.sum();
usize::try_from(difference).expect("u32 fits in usize") < TOKEN_IDENTIFIER_RETRY.len()
}
if token.is_empty() { ifself.validation == ValidateAddress::Never {
qinfo!("AddressValidation: no token; accepting"); return AddressValidationResult::Pass;
}
qinfo!("AddressValidation: no token; validating"); return AddressValidationResult::Validate;
} if token.len() <= TOKEN_IDENTIFIER_RETRY.len() { // Treat bad tokens strictly.
qinfo!("AddressValidation: too short token"); return AddressValidationResult::Invalid;
} let retry = Self::is_likely_retry(token); let enc = &token[TOKEN_IDENTIFIER_RETRY.len()..]; // Note that this allows the token identifier part to be corrupted. // That's OK here as we don't depend on that being authenticated. #[expect(clippy::option_if_let_else, reason = "Alternative is less readable.")] matchself.decrypt_token(enc, peer_address, retry, now) {
Some(cid) => { if retry { // This is from Retry, so we should have an ODCID >= 8. if cid.len() >= 8 {
qinfo!("AddressValidation: valid Retry token for {cid}");
AddressValidationResult::ValidRetry(cid)
} else {
panic!("AddressValidation: Retry token with small CID {cid}");
}
} elseif cid.is_empty() { // An empty connection ID means NEW_TOKEN. ifself.validation == ValidateAddress::Always {
qinfo!("AddressValidation: valid NEW_TOKEN token; validating again");
AddressValidationResult::Validate
} else {
qinfo!("AddressValidation: valid NEW_TOKEN token; accepting");
AddressValidationResult::Pass
}
} else {
panic!("AddressValidation: NEW_TOKEN token with CID {cid}");
}
}
None => { // From here on, we have a token that we couldn't decrypt. // We've either lost the keys or we've received junk. if retry { // If this looked like a Retry, treat it as being bad.
qinfo!("AddressValidation: invalid Retry token; rejecting");
AddressValidationResult::Invalid
} elseifself.validation == ValidateAddress::Never { // We don't require validation, so OK.
qinfo!("AddressValidation: invalid NEW_TOKEN token; accepting");
AddressValidationResult::Pass
} else { // This might be an invalid NEW_TOKEN token, or a valid one // for which we have since lost the keys. Check again.
qinfo!("AddressValidation: invalid NEW_TOKEN token; validating again");
AddressValidationResult::Validate
}
}
}
}
}
#[expect(clippy::large_enum_variant, reason = "No way around it.")] pubenum NewTokenState {
Client { /// Tokens that haven't been taken yet.
pending: SmallVec<[Vec<u8>; MAX_NEW_TOKEN]>, /// Tokens that have been taken, saved so that we can discard duplicates.
old: SmallVec<[Vec<u8>; MAX_SAVED_TOKENS]>,
},
Server(NewTokenSender),
}
/// Is there a token available? pubfn has_token(&self) -> bool { matchself { Self::Client { pending, .. } => !pending.is_empty(), Self::Server(..) => false,
}
}
/// If this is a client, take a token if there is one. /// If this is a server, panic. pubfn take_token(&mutself) -> Option<&[u8]> { ifletSelf::Client { pending, old } = self {
pending.pop().map(|t| { if old.len() >= MAX_SAVED_TOKENS {
old.remove(0);
}
old.push(t);
old[old.len() - 1].as_slice()
})
} else {
unreachable!();
}
}
/// If this is a client, save a token. /// If this is a server, panic. pubfn save_token(&mutself, token: Vec<u8>) { ifletSelf::Client { pending, old } = self { for t in old.iter().rev().chain(pending.iter().rev()) { if t == &token {
qinfo!("NewTokenState discarding duplicate NEW_TOKEN"); return;
}
}
/// If this is a server, maybe send a frame. /// If this is a client, do nothing. pubfn write_frames<B: Buffer>(
&mutself,
builder: &mut packet::Builder<B>,
tokens: &mut recovery::Tokens,
stats: &mut FrameStats,
) { ifletSelf::Server(sender) = self {
sender.write_frames(builder, tokens, stats);
}
}
/// If this a server, buffer a `NEW_TOKEN` for sending. /// If this is a client, panic. pubfn send_new_token(&mutself, token: Vec<u8>) { ifletSelf::Server(sender) = self {
sender.send_new_token(token);
} else {
unreachable!();
}
}
/// If this a server, process a lost signal for a `NEW_TOKEN` frame. /// If this is a client, panic. pubfn lost(&mutself, seqno: usize) { ifletSelf::Server(sender) = self {
sender.lost(seqno);
} else {
unreachable!();
}
}
/// If this a server, process remove the acknowledged `NEW_TOKEN` frame. /// If this is a client, panic. pubfn acked(&mutself, seqno: usize) { ifletSelf::Server(sender) = self {
sender.acked(seqno);
} else {
unreachable!();
}
}
}
#[derive(Default)] pubstruct NewTokenSender { /// The unacknowledged `NEW_TOKEN` frames we are yet to send.
tokens: Vec<NewTokenFrameStatus>, /// A sequence number that is used to track individual tokens /// by reference (so that recovery tokens can be simple).
next_seqno: usize,
}
impl NewTokenSender { /// Add a token to be sent. pubfn send_new_token(&mutself, token: Vec<u8>) { self.tokens.push(NewTokenFrameStatus {
seqno: self.next_seqno,
token,
needs_sending: true,
}); self.next_seqno += 1;
}
pubfn write_frames<B: Buffer>(
&mutself,
builder: &mut packet::Builder<B>,
tokens: &mut recovery::Tokens,
stats: &mut FrameStats,
) { for t in &mutself.tokens { if t.needs_sending && t.len() <= builder.remaining() {
t.needs_sending = false;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.