Skip to content

Commit

Permalink
udp: harden ConnectionValidator
Browse files Browse the repository at this point in the history
  • Loading branch information
greatest-ape committed Dec 25, 2023
1 parent 5870b53 commit ba8bdd6
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
socket if sending a request to a swarm worker failed
* Reuse allocations in swarm response channel
* Remove config key `network.poll_event_capacity`
* Harden ConnectionValidator to make IP spoofing even more costly

### aquatic_http

Expand Down
58 changes: 35 additions & 23 deletions crates/udp/src/workers/socket/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,27 @@ use aquatic_udp_protocol::ConnectionId;

use crate::config::Config;

/// HMAC (BLAKE3) based ConnectionID creator and validator
/// HMAC (BLAKE3) based ConnectionId creator and validator
///
/// Structure of created ConnectionID (bytes making up inner i64):
/// - &[0..4]: connection expiration time as number of seconds after
/// ConnectionValidator instance was created, encoded as u32 bytes.
/// Value fits around 136 years.
/// - &[4..8]: truncated keyed BLAKE3 hash of above 4 bytes and octets of
/// client IP address
///
/// The purpose of using ConnectionIDs is to prevent IP spoofing, mainly to
/// The purpose of using ConnectionIds is to make IP spoofing costly, mainly to
/// prevent the tracker from being used as an amplification vector for DDoS
/// attacks. By including 32 bits of BLAKE3 keyed hash output in its contents,
/// such abuse should be rendered impractical.
/// attacks. By including 32 bits of BLAKE3 keyed hash output in the Ids, an
/// attacker would have to make on average 2^31 attemps to correctly guess a
/// single hash. Furthermore, such a hash would only be valid for at most
/// `max_connection_age` seconds, a short duration to get value for the
/// bandwidth spent brute forcing it.
///
/// Structure of created ConnectionID (bytes making up inner i64):
/// - &[0..4]: ConnectionId creation time as number of seconds after
/// ConnectionValidator instance was created, encoded as u32 bytes. A u32
/// fits around 136 years in seconds.
/// - &[4..8]: truncated keyed BLAKE3 hash of:
/// - previous 4 bytes
/// - octets of client IP address
#[derive(Clone)]
pub struct ConnectionValidator {
start_time: Instant,
max_connection_age: u32,
max_connection_age: u64,
keyed_hasher: blake3::Hasher,
}

Expand All @@ -44,19 +48,18 @@ impl ConnectionValidator {
Ok(Self {
keyed_hasher,
start_time: Instant::now(),
max_connection_age: config.cleaning.max_connection_age,
max_connection_age: config.cleaning.max_connection_age.into(),
})
}

pub fn create_connection_id(&mut self, source_addr: CanonicalSocketAddr) -> ConnectionId {
let valid_until =
(self.start_time.elapsed().as_secs() as u32 + self.max_connection_age).to_ne_bytes();
let elapsed = (self.start_time.elapsed().as_secs() as u32).to_ne_bytes();

let hash = self.hash(valid_until, source_addr.get().ip());
let hash = self.hash(elapsed, source_addr.get().ip());

let mut connection_id_bytes = [0u8; 8];

(&mut connection_id_bytes[..4]).copy_from_slice(&valid_until);
(&mut connection_id_bytes[..4]).copy_from_slice(&elapsed);
(&mut connection_id_bytes[4..]).copy_from_slice(&hash);

ConnectionId::new(i64::from_ne_bytes(connection_id_bytes))
Expand All @@ -68,18 +71,27 @@ impl ConnectionValidator {
connection_id: ConnectionId,
) -> bool {
let bytes = connection_id.0.get().to_ne_bytes();
let (valid_until, hash) = bytes.split_at(4);
let valid_until: [u8; 4] = valid_until.try_into().unwrap();
let (elapsed, hash) = bytes.split_at(4);
let elapsed: [u8; 4] = elapsed.try_into().unwrap();

if !constant_time_eq(hash, &self.hash(valid_until, source_addr.get().ip())) {
if !constant_time_eq(hash, &self.hash(elapsed, source_addr.get().ip())) {
return false;
}

u32::from_ne_bytes(valid_until) > self.start_time.elapsed().as_secs() as u32
let tracker_elapsed = u64::from(self.start_time.elapsed().as_secs());
let client_elapsed = u64::from(u32::from_ne_bytes(elapsed));
let client_expiration_time = client_elapsed + self.max_connection_age;

// In addition to checking if the client connection is expired,
// disallow client_elapsed values that are in future and thus could not
// have been sent by the tracker. This prevents brute forcing with
// `u32::MAX` as 'elapsed' part of ConnectionId to find a hash that
// works until the tracker is restarted.
(client_expiration_time > tracker_elapsed) & (client_elapsed <= tracker_elapsed)
}

fn hash(&mut self, valid_until: [u8; 4], ip_addr: IpAddr) -> [u8; 4] {
self.keyed_hasher.update(&valid_until);
fn hash(&mut self, elapsed: [u8; 4], ip_addr: IpAddr) -> [u8; 4] {
self.keyed_hasher.update(&elapsed);

match ip_addr {
IpAddr::V4(ip) => self.keyed_hasher.update(&ip.octets()),
Expand Down

0 comments on commit ba8bdd6

Please sign in to comment.