Skip to main content

vane_core/
conn_context.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3use std::sync::OnceLock;
4use std::time::Instant;
5
6use bytes::Bytes;
7use parking_lot::Mutex;
8use rustls_pki_types::CertificateDer;
9
10#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
11pub struct ConnId(pub u64);
12
13impl std::fmt::Display for ConnId {
14	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15		write!(f, "{:016x}", self.0)
16	}
17}
18
19#[derive(
20	Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, serde::Serialize, serde::Deserialize,
21)]
22pub enum Transport {
23	Tcp,
24	Udp,
25}
26
27#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
28pub enum HttpVersion {
29	Http1_0,
30	Http1_1,
31	Http2,
32	Http3,
33}
34
35#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
36pub enum TlsVersion {
37	Tls12,
38	Tls13,
39}
40
41#[derive(Clone, Debug, Default)]
42pub struct TlsInfo {
43	/// Negotiated SNI value, lower-cased. Stored as `Arc<str>` so
44	/// predicate readers can hand out borrowed slices and the
45	/// listener-side population path can reuse the same arc across
46	/// multiple `TlsInfo` snapshots for the same connection.
47	pub sni: Option<Arc<str>>,
48	/// Negotiated ALPN bytes. `Arc<[u8]>` for the same reason — the
49	/// hot `alpn.clone()` patterns now bump a refcount.
50	pub alpn: Option<Arc<[u8]>>,
51	pub version: Option<TlsVersion>,
52	pub peer_cert: Option<Arc<PeerCertificate>>,
53	/// Whether the client's request arrived (in part or wholly) as
54	/// TLS 1.3 0-RTT (early data). Set at handshake completion in the
55	/// engine's `run_tls` from rustls's `is_early_data_accepted()`.
56	/// The L7 executor consults this together with the matched rule's
57	/// `allow_zero_rtt` to decide whether to short-circuit the request
58	/// with a synthetic 425 Too Early. See
59	/// `spec/crates/engine-tls.md` § _TLS 1.3 0-RTT (early data)_.
60	pub zero_rtt_used: bool,
61}
62
63/// Verified client certificate captured at TLS handshake time, with
64/// every predicate-readable field pre-extracted so the per-Check
65/// dispatch is allocation-light. Built once by the engine's
66/// post-handshake population (`run_tls`); the seven
67/// `tls.peer_cert.*` predicates read pre-computed strings off this
68/// struct rather than re-parsing the DER on every test.
69///
70/// `leaf_der` retains the raw DER bytes so future predicates (or a
71/// post-MVP debug surface) can re-derive any field x509-parser
72/// exposes; the seven currently-spec'd fields are pre-extracted.
73///
74/// All `String`-typed fields are byte-for-byte canonical: hex digests
75/// are ASCII-lowercase; `serial` is hex (lowercase, no leading-zero
76/// stripping). See `spec/crates/core.md` §
77/// _Predicate_ for the canonical formats.
78#[derive(Clone, Debug, Default)]
79pub struct PeerCertificate {
80	/// Raw leaf cert DER. Retained for future predicates that need
81	/// fields not pre-extracted; current readers should use the
82	/// pre-extracted scalar fields below.
83	pub leaf_der: Bytes,
84	/// All `String` / `Vec<String>` predicate-readable fields are
85	/// stored as `Arc<str>` / `Arc<[Arc<str>]>` so per-Check
86	/// predicate dispatch can hand out borrowed `&str` slices
87	/// instead of cloning ~30ns Strings inside the connection
88	/// `Mutex<Option<TlsInfo>>` guard.
89	pub subject_cn: Option<Arc<str>>,
90	pub san_dns: Arc<[Arc<str>]>,
91	pub fingerprint_sha256: Arc<str>,
92	pub spki_sha256: Arc<str>,
93	pub issuer_cn: Option<Arc<str>>,
94	pub serial: Arc<str>,
95}
96
97impl PeerCertificate {
98	/// Pre-extract every `tls.peer_cert.*` predicate-readable field
99	/// from a raw leaf cert DER. Returns `None` when the bytes are
100	/// not a parseable X.509v3 certificate; the caller treats that as
101	/// "no verified peer cert" (sound-by-default per spec).
102	#[must_use]
103	pub fn from_der(leaf_der: &CertificateDer<'_>) -> Option<Self> {
104		use sha2::{Digest, Sha256};
105		use x509_parser::prelude::*;
106
107		let bytes = leaf_der.as_ref();
108		let (_, cert) = X509Certificate::from_der(bytes).ok()?;
109		let tbs = &cert.tbs_certificate;
110
111		let subject_cn =
112			tbs.subject().iter_common_name().next().and_then(|attr| attr.as_str().ok().map(Arc::from));
113		let issuer_cn =
114			tbs.issuer().iter_common_name().next().and_then(|attr| attr.as_str().ok().map(Arc::from));
115
116		// SAN dNSName entries — RFC 5280 §4.2.1.6. Other GeneralName
117		// variants (URI, RFC822, etc.) are not exposed via this path
118		// per the predicate-schema table.
119		let mut san_dns: Vec<Arc<str>> = Vec::new();
120		if let Ok(Some(san_ext)) = tbs.subject_alternative_name() {
121			for name in &san_ext.value.general_names {
122				if let GeneralName::DNSName(d) = name {
123					san_dns.push(Arc::from(*d));
124				}
125			}
126		}
127		let san_dns: Arc<[Arc<str>]> = san_dns.into();
128
129		let mut hasher = Sha256::new();
130		hasher.update(bytes);
131		let fingerprint_sha256: Arc<str> = Arc::from(hex_lower(&hasher.finalize()));
132
133		let spki_sha256: Arc<str> = {
134			let spki_der = tbs.subject_pki.raw;
135			let mut h = Sha256::new();
136			h.update(spki_der);
137			Arc::from(hex_lower(&h.finalize()))
138		};
139
140		// Serial: x509-parser gives BigUint; canonicalise as
141		// lowercase hex, big-endian, no leading-zero stripping (per
142		// spec). `to_bytes_be` returns the minimal-length
143		// representation; pad nothing — operators copy the value out
144		// verbatim from `openssl x509 -serial` when matching.
145		let serial: Arc<str> = Arc::from(hex_lower(&tbs.serial.to_bytes_be()));
146
147		Some(Self {
148			leaf_der: Bytes::copy_from_slice(bytes),
149			subject_cn,
150			san_dns,
151			fingerprint_sha256,
152			spki_sha256,
153			issuer_cn,
154			serial,
155		})
156	}
157}
158
159fn hex_lower(bytes: &[u8]) -> String {
160	use std::fmt::Write as _;
161	let mut s = String::with_capacity(bytes.len() * 2);
162	for b in bytes {
163		let _ = write!(s, "{b:02x}");
164	}
165	s
166}
167
168/// Per-connection state shared between the listener and the executor.
169///
170/// `#[non_exhaustive]` so downstream crates cannot construct a
171/// `ConnContext` via a struct literal — the listener layer owns
172/// construction. Existing field-access patterns (`conn.id`,
173/// `conn.tls.lock()`, ...) remain available to workspace crates so
174/// the engine's hot path stays unchanged; new code should prefer the
175/// accessor methods below for the locked fields.
176#[non_exhaustive]
177pub struct ConnContext {
178	pub id: ConnId,
179	pub remote: SocketAddr,
180	pub local: SocketAddr,
181	pub transport: Transport,
182	pub entered_at: Instant,
183
184	pub tls: Mutex<Option<TlsInfo>>,
185	pub http_version: OnceLock<HttpVersion>,
186
187	pub user: Mutex<http::Extensions>,
188}
189
190impl ConnContext {
191	/// Construct a fresh per-connection context. The TLS / user-
192	/// extension mutexes and the http-version once-lock all start
193	/// empty; the listener fills them as the handshake progresses.
194	///
195	/// `#[non_exhaustive]` on the struct blocks external struct
196	/// literals, so this constructor is the only public entry point
197	/// for building a `ConnContext` from outside `vane-core`.
198	#[must_use]
199	pub fn new(
200		id: ConnId,
201		remote: SocketAddr,
202		local: SocketAddr,
203		transport: Transport,
204		entered_at: Instant,
205	) -> Self {
206		Self {
207			id,
208			remote,
209			local,
210			transport,
211			entered_at,
212			tls: Mutex::new(None),
213			http_version: OnceLock::new(),
214			user: Mutex::new(http::Extensions::new()),
215		}
216	}
217
218	/// Lock-and-read access to the TLS state. The returned guard is a
219	/// `parking_lot::MutexGuard` — drop it as soon as the read is
220	/// done to release the lock for other tasks.
221	pub fn tls(&self) -> parking_lot::MutexGuard<'_, Option<TlsInfo>> {
222		self.tls.lock()
223	}
224
225	/// Closure-scoped mutable access to the per-connection
226	/// extension map. Prefer this over directly grabbing
227	/// `conn.user.lock()` — the closure bound makes the lock window
228	/// visible at the call site, which matters because the executor
229	/// holds the same mutex across several dispatch arms.
230	pub fn with_user<R>(&self, f: impl FnOnce(&mut http::Extensions) -> R) -> R {
231		let mut guard = self.user.lock();
232		f(&mut guard)
233	}
234}
235
236#[cfg(test)]
237mod tests {
238	use super::*;
239
240	#[test]
241	fn conn_id_display_pads_zero_to_sixteen_hex_digits() {
242		let rendered = format!("{}", ConnId(0));
243		assert_eq!(rendered, "0000000000000000");
244		assert_eq!(rendered.len(), 16);
245	}
246
247	#[test]
248	fn conn_id_display_is_lowercase_hex() {
249		let rendered = format!("{}", ConnId(0x0bad_f00d_dead_beef));
250		assert_eq!(rendered, "0badf00ddeadbeef");
251		assert!(rendered.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)));
252	}
253
254	#[test]
255	fn conn_id_display_zero_pads_small_values() {
256		// non-zero top nibble would mean no left padding; a small value exercises
257		// the {:016x} pad path explicitly.
258		let rendered = format!("{}", ConnId(1));
259		assert_eq!(rendered, "0000000000000001");
260	}
261
262	#[test]
263	fn conn_id_display_renders_u64_max() {
264		let rendered = format!("{}", ConnId(u64::MAX));
265		assert_eq!(rendered, "ffffffffffffffff");
266		assert_eq!(rendered.len(), 16);
267	}
268
269	#[test]
270	fn conn_id_serde_round_trip() {
271		let id = ConnId(0x1234_5678_9abc_def0);
272		let encoded = serde_json::to_string(&id).expect("serialize");
273		let decoded: ConnId = serde_json::from_str(&encoded).expect("deserialize");
274		assert_eq!(decoded, id);
275	}
276
277	#[test]
278	fn tls_version_variants_are_exhaustive_at_two() {
279		// Adding a TlsVersion variant without updating this arm would be a
280		// compile error — the spec (spec/crates/engine-tls.md) constrains accepted versions
281		// to 1.2 and 1.3 only.
282		for v in [TlsVersion::Tls12, TlsVersion::Tls13] {
283			let matched = match v {
284				TlsVersion::Tls12 => "1.2",
285				TlsVersion::Tls13 => "1.3",
286			};
287			assert!(!matched.is_empty());
288		}
289	}
290
291	#[test]
292	fn tls_version_serde_round_trip_per_variant() {
293		for v in [TlsVersion::Tls12, TlsVersion::Tls13] {
294			let encoded = serde_json::to_string(&v).expect("serialize");
295			let decoded: TlsVersion = serde_json::from_str(&encoded).expect("deserialize");
296			assert_eq!(decoded, v);
297		}
298	}
299
300	#[test]
301	fn transport_serde_round_trip_per_variant() {
302		for t in [Transport::Tcp, Transport::Udp] {
303			let encoded = serde_json::to_string(&t).expect("serialize");
304			let decoded: Transport = serde_json::from_str(&encoded).expect("deserialize");
305			assert_eq!(decoded, t);
306		}
307	}
308
309	#[test]
310	fn http_version_serde_round_trip_per_variant() {
311		for v in [HttpVersion::Http1_0, HttpVersion::Http1_1, HttpVersion::Http2, HttpVersion::Http3] {
312			let encoded = serde_json::to_string(&v).expect("serialize");
313			let decoded: HttpVersion = serde_json::from_str(&encoded).expect("deserialize");
314			assert_eq!(decoded, v);
315		}
316	}
317}