Skip to content

Commit 6bda48e

Browse files
authored
feat(s2n-quic-dc): import 10/17/24 (#2351)
1 parent 4626ffe commit 6bda48e

File tree

24 files changed

+1885
-48
lines changed

24 files changed

+1885
-48
lines changed

dc/s2n-quic-dc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ bytes = "1"
2525
crossbeam-channel = "0.5"
2626
crossbeam-epoch = "0.9"
2727
crossbeam-queue = { version = "0.3" }
28+
event-listener-strategy = "0.5"
2829
flurry = "0.5"
2930
libc = "0.2"
3031
num-rational = { version = "0.4", default-features = false }

dc/s2n-quic-dc/src/credentials.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,7 @@ pub use s2n_quic_core::varint::VarInt as KeyId;
1616
pub mod testing;
1717

1818
#[derive(
19-
Clone,
20-
Copy,
21-
Default,
22-
PartialEq,
23-
Eq,
24-
Hash,
25-
AsBytes,
26-
FromBytes,
27-
FromZeroes,
28-
Unaligned,
29-
PartialOrd,
30-
Ord,
19+
Clone, Copy, Default, PartialEq, Eq, AsBytes, FromBytes, FromZeroes, Unaligned, PartialOrd, Ord,
3120
)]
3221
#[cfg_attr(
3322
any(test, feature = "testing"),
@@ -36,6 +25,15 @@ pub mod testing;
3625
#[repr(C)]
3726
pub struct Id([u8; 16]);
3827

28+
impl std::hash::Hash for Id {
29+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
30+
// The ID has very high quality entropy already, so write just one half of it to keep hash
31+
// costs as low as possible. For the main use of the Hash impl in the fixed-size ID map
32+
// this translates to just directly using these bytes for the indexing.
33+
state.write_u64(u64::from_ne_bytes(self.0[..8].try_into().unwrap()));
34+
}
35+
}
36+
3937
impl fmt::Debug for Id {
4038
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4139
format_args!("{:#01x}", u128::from_be_bytes(self.0)).fmt(f)

dc/s2n-quic-dc/src/fixed_map.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! extent possible) reducing the likelihood.
99
1010
use core::{
11+
fmt::Debug,
1112
hash::Hash,
1213
sync::atomic::{AtomicU8, Ordering},
1314
};
@@ -21,7 +22,7 @@ pub struct Map<K, V, S = RandomState> {
2122

2223
impl<K, V, S> Map<K, V, S>
2324
where
24-
K: Hash + Eq,
25+
K: Hash + Eq + Debug,
2526
S: BuildHasher,
2627
{
2728
pub fn with_capacity(entries: usize, hasher: S) -> Self {
@@ -108,7 +109,7 @@ struct Slot<K, V> {
108109

109110
impl<K, V> Slot<K, V>
110111
where
111-
K: Hash + Eq,
112+
K: Hash + Eq + Debug,
112113
{
113114
fn new() -> Self {
114115
Slot {
@@ -139,6 +140,10 @@ where
139140
// If `new_key` isn't already in this slot, replace one of the existing entries with the
140141
// new key. For now we rotate through based on `next_write`.
141142
let replacement = self.next_write.fetch_add(1, Ordering::Relaxed) as usize % SLOT_CAPACITY;
143+
tracing::trace!(
144+
"evicting {:?} - bucket overflow",
145+
values[replacement].as_mut().unwrap().0
146+
);
142147
values[replacement] = Some((new_key, new_value));
143148
None
144149
}

dc/s2n-quic-dc/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod random;
1717
pub mod recovery;
1818
pub mod socket;
1919
pub mod stream;
20+
pub mod sync;
2021
pub mod task;
2122

2223
#[cfg(any(test, feature = "testing"))]

dc/s2n-quic-dc/src/path/secret.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,13 @@ pub mod stateless_reset;
1212

1313
pub use key::{open, seal};
1414
pub use map::Map;
15+
16+
/// The handshake operation may return immediately if state for the target is already cached,
17+
/// or perform an actual handshake if not.
18+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19+
pub enum HandshakeKind {
20+
/// Handshake was skipped because a secret was already present in the cache
21+
Cached,
22+
/// Handshake was performed to generate a new secret
23+
Fresh,
24+
}

dc/s2n-quic-dc/src/path/secret/map.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use s2n_quic_core::{
2222
};
2323
use std::{
2424
fmt,
25+
hash::{BuildHasherDefault, Hasher},
2526
net::{Ipv4Addr, SocketAddr},
2627
sync::{
2728
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
@@ -51,6 +52,24 @@ pub struct Map {
5152
pub(super) state: Arc<State>,
5253
}
5354

55+
#[derive(Default)]
56+
pub(super) struct NoopIdHasher(Option<u64>);
57+
58+
impl Hasher for NoopIdHasher {
59+
fn finish(&self) -> u64 {
60+
self.0.unwrap()
61+
}
62+
63+
fn write(&mut self, _bytes: &[u8]) {
64+
unimplemented!()
65+
}
66+
67+
fn write_u64(&mut self, x: u64) {
68+
debug_assert!(self.0.is_none());
69+
self.0 = Some(x);
70+
}
71+
}
72+
5473
// # Managing memory consumption
5574
//
5675
// For regular rotation with live peers, we retain at most two secrets: one derived from the most
@@ -93,7 +112,7 @@ pub(super) struct State {
93112
pub(super) requested_handshakes: flurry::HashSet<SocketAddr>,
94113

95114
// All known entries.
96-
pub(super) ids: fixed_map::Map<Id, Arc<Entry>>,
115+
pub(super) ids: fixed_map::Map<Id, Arc<Entry>, BuildHasherDefault<NoopIdHasher>>,
97116

98117
pub(super) signer: stateless_reset::Signer,
99118

@@ -232,7 +251,7 @@ impl State {
232251
}
233252

234253
impl Map {
235-
pub fn new(signer: stateless_reset::Signer) -> Self {
254+
pub fn new(signer: stateless_reset::Signer, capacity: usize) -> Self {
236255
// FIXME: Avoid unwrap and the whole socket.
237256
//
238257
// We only ever send on this socket - but we really should be sending on the same
@@ -244,11 +263,11 @@ impl Map {
244263
control_socket.set_nonblocking(true).unwrap();
245264
let state = State {
246265
// This is around 500MB with current entry size.
247-
max_capacity: 500_000,
266+
max_capacity: capacity,
248267
// FIXME: Allow configuring the rehandshake_period.
249268
rehandshake_period: Duration::from_secs(3600 * 24),
250-
peers: fixed_map::Map::with_capacity(500_000, Default::default()),
251-
ids: fixed_map::Map::with_capacity(500_000, Default::default()),
269+
peers: fixed_map::Map::with_capacity(capacity, Default::default()),
270+
ids: fixed_map::Map::with_capacity(capacity, Default::default()),
252271
requested_handshakes: Default::default(),
253272
cleaner: Cleaner::new(),
254273
signer,
@@ -301,6 +320,19 @@ impl Map {
301320
Some((sealer, credentials, state.parameters.clone()))
302321
}
303322

323+
/// Retrieve a sealer by path secret ID.
324+
///
325+
/// Generally callers should prefer to use one of the `pair` APIs; this is primarily useful for
326+
/// "response" datagrams which want to be bound to the exact same shared secret.
327+
///
328+
/// Note that unlike by-IP lookup this should typically not be done significantly after the
329+
/// original secret was used for decryption.
330+
pub fn seal_once_id(&self, id: Id) -> Option<(seal::Once, Credentials, ApplicationParams)> {
331+
let state = self.state.ids.get_by_key(&id)?;
332+
let (sealer, credentials) = state.uni_sealer();
333+
Some((sealer, credentials, state.parameters.clone()))
334+
}
335+
304336
pub fn open_once(
305337
&self,
306338
credentials: &Credentials,
@@ -485,7 +517,7 @@ impl Map {
485517
pub fn for_test_with_peers(
486518
peers: Vec<(schedule::Ciphersuite, dc::Version, SocketAddr)>,
487519
) -> (Self, Vec<Id>) {
488-
let provider = Self::new(stateless_reset::Signer::random());
520+
let provider = Self::new(stateless_reset::Signer::random(), peers.len() * 3);
489521
let mut secret = [0; 32];
490522
aws_lc_rs::rand::fill(&mut secret).unwrap();
491523
let mut stateless_reset = [0; control::TAG_LEN];

dc/s2n-quic-dc/src/path/secret/map/test.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ fn fake_entry(peer: u16) -> Arc<Entry> {
3232
#[test]
3333
fn cleans_after_delay() {
3434
let signer = stateless_reset::Signer::new(b"secret");
35-
let map = Map::new(signer);
35+
let map = Map::new(signer, 50);
3636

3737
// Stop background processing. We expect to manually invoke clean, and a background worker
3838
// might interfere with our state.
@@ -60,7 +60,7 @@ fn cleans_after_delay() {
6060
#[test]
6161
fn thread_shutdown() {
6262
let signer = stateless_reset::Signer::new(b"secret");
63-
let map = Map::new(signer);
63+
let map = Map::new(signer, 10);
6464
let state = Arc::downgrade(&map.state);
6565
drop(map);
6666

@@ -263,7 +263,7 @@ fn check_invariants() {
263263

264264
let mut model = Model::default();
265265
let signer = stateless_reset::Signer::new(b"secret");
266-
let mut map = Map::new(signer);
266+
let mut map = Map::new(signer, 10_000);
267267

268268
// Avoid background work interfering with testing.
269269
map.state.cleaner.stop();
@@ -293,7 +293,7 @@ fn check_invariants_no_overflow() {
293293

294294
let mut model = Model::default();
295295
let signer = stateless_reset::Signer::new(b"secret");
296-
let map = Map::new(signer);
296+
let map = Map::new(signer, 10_000);
297297

298298
// Avoid background work interfering with testing.
299299
map.state.cleaner.stop();
@@ -316,7 +316,7 @@ fn check_invariants_no_overflow() {
316316
#[ignore = "memory growth takes a long time to run"]
317317
fn no_memory_growth() {
318318
let signer = stateless_reset::Signer::new(b"secret");
319-
let map = Map::new(signer);
319+
let map = Map::new(signer, 100_000);
320320
map.state.cleaner.stop();
321321
for idx in 0..500_000 {
322322
// FIXME: this ends up 2**16 peers in the `peers` map
@@ -325,10 +325,15 @@ fn no_memory_growth() {
325325
}
326326

327327
#[test]
328-
#[cfg(all(target_pointer_width = "64", target_os = "linux"))]
329328
fn entry_size() {
329+
let mut should_check = true;
330+
331+
should_check &= cfg!(target_pointer_width = "64");
332+
should_check &= cfg!(target_os = "linux");
333+
should_check &= std::env::var("S2N_QUIC_RUN_VERSION_SPECIFIC_TESTS").is_ok();
334+
330335
// This gates to running only on specific GHA to reduce false positives.
331-
if std::env::var("S2N_QUIC_RUN_VERSION_SPECIFIC_TESTS").is_ok() {
336+
if should_check {
332337
assert_eq!(fake_entry(0).size(), 238);
333338
}
334339
}

dc/s2n-quic-dc/src/stream.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub const DEFAULT_INFLIGHT_TIMEOUT: Duration = Duration::from_secs(5);
1111
pub const MAX_DATAGRAM_SIZE: usize = 1 << 15; // 32k
1212

1313
pub mod application;
14+
pub mod client;
1415
pub mod crypto;
1516
pub mod endpoint;
1617
pub mod environment;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pub mod tokio;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::{
5+
path::secret,
6+
stream::{
7+
application::Stream,
8+
endpoint,
9+
environment::tokio::{self as env, Environment},
10+
socket::Protocol,
11+
},
12+
};
13+
use std::{io, net::SocketAddr};
14+
use tokio::net::TcpStream;
15+
16+
/// Connects using the UDP transport layer
17+
#[inline]
18+
pub async fn connect_udp<H>(
19+
handshake_addr: SocketAddr,
20+
handshake: H,
21+
acceptor_addr: SocketAddr,
22+
env: &Environment,
23+
map: &secret::Map,
24+
) -> io::Result<Stream>
25+
where
26+
H: core::future::Future<Output = io::Result<secret::HandshakeKind>>,
27+
{
28+
// ensure we have a secret for the peer
29+
handshake.await?;
30+
31+
let stream = endpoint::open_stream(
32+
env,
33+
handshake_addr.into(),
34+
env::UdpUnbound(acceptor_addr.into()),
35+
map,
36+
None,
37+
)?;
38+
39+
// build the stream inside the application context
40+
let mut stream = stream.build()?;
41+
42+
debug_assert_eq!(stream.protocol(), Protocol::Udp);
43+
44+
write_prelude(&mut stream).await?;
45+
46+
Ok(stream)
47+
}
48+
49+
/// Connects using the TCP transport layer
50+
#[inline]
51+
pub async fn connect_tcp<H>(
52+
handshake_addr: SocketAddr,
53+
handshake: H,
54+
acceptor_addr: SocketAddr,
55+
env: &Environment,
56+
map: &secret::Map,
57+
) -> io::Result<Stream>
58+
where
59+
H: core::future::Future<Output = io::Result<secret::HandshakeKind>>,
60+
{
61+
// Race TCP handshake with the TLS handshake
62+
let (socket, _) = tokio::try_join!(TcpStream::connect(acceptor_addr), handshake,)?;
63+
64+
let stream = endpoint::open_stream(
65+
env,
66+
handshake_addr.into(),
67+
env::TcpRegistered(socket),
68+
map,
69+
None,
70+
)?;
71+
72+
// build the stream inside the application context
73+
let mut stream = stream.build()?;
74+
75+
debug_assert_eq!(stream.protocol(), Protocol::Tcp);
76+
77+
write_prelude(&mut stream).await?;
78+
79+
Ok(stream)
80+
}
81+
82+
/// Connects with a pre-existing TCP stream
83+
///
84+
/// # Note
85+
///
86+
/// The provided `map` must contain a shared secret for the `handshake_addr`
87+
#[inline]
88+
pub async fn connect_tcp_with(
89+
handshake_addr: SocketAddr,
90+
stream: TcpStream,
91+
env: &Environment,
92+
map: &secret::Map,
93+
) -> io::Result<Stream> {
94+
let stream = endpoint::open_stream(
95+
env,
96+
handshake_addr.into(),
97+
env::TcpRegistered(stream),
98+
map,
99+
None,
100+
)?;
101+
102+
// build the stream inside the application context
103+
let mut stream = stream.build()?;
104+
105+
debug_assert_eq!(stream.protocol(), Protocol::Tcp);
106+
107+
write_prelude(&mut stream).await?;
108+
109+
Ok(stream)
110+
}
111+
112+
#[inline]
113+
async fn write_prelude(stream: &mut Stream) -> io::Result<()> {
114+
// TODO should we actually write the prelude here or should we do late sealer binding on
115+
// the first packet to reduce secret reordering on the peer
116+
117+
stream
118+
.write_from(&mut s2n_quic_core::buffer::reader::storage::Empty)
119+
.await
120+
.map(|_| ())
121+
}

0 commit comments

Comments
 (0)