moqtap_proxy/proxy.rs
1//! Transparent proxy orchestrator — accept loop and session management.
2//!
3//! The proxy binds a single listener that accepts raw-QUIC MoQT and
4//! WebTransport clients simultaneously, negotiated via ALPN. Each
5//! accepted connection is handed to a [`ProxySession`] that forwards
6//! traffic to the configured upstream relay.
7
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11use tokio_util::sync::CancellationToken;
12
13use crate::control::{ControlPlane, LegSetup, ProxyControl};
14use crate::error::ProxyError;
15use crate::event::{ProxyEvent, SessionId};
16use crate::hook::{NoOpHook, ProxyHook};
17use crate::listener::{AcceptedConn, Listener, ListenerConfig};
18use crate::observer::ProxyObserver;
19use crate::session::{ProxySession, ProxySessionConfig, UpstreamTransportType};
20
21/// Configuration for the transparent proxy.
22pub struct ProxyConfig {
23 /// Listener configuration (bind address, certs).
24 pub listener: ListenerConfig,
25 /// Per-session configuration (upstream address, TLS, transport).
26 pub session: ProxySessionConfig,
27}
28
29/// A transparent MoQT proxy that accepts client connections and forwards
30/// traffic to an upstream relay.
31///
32/// Each accepted connection spawns a [`ProxySession`] that handles
33/// bidirectional stream forwarding with inline MoQT frame parsing. The
34/// client-facing transport (raw QUIC vs WebTransport) is chosen by the
35/// client via ALPN and dispatched automatically — no configuration
36/// required.
37pub struct TransparentProxy {
38 config: ProxyConfig,
39 observer: Arc<dyn ProxyObserver>,
40 hook: Arc<dyn ProxyHook>,
41 cancel: CancellationToken,
42 next_session_id: AtomicU64,
43 /// What a [`ProxyControl`] reads and what every session registers with.
44 ///
45 /// Constructed with the proxy rather than with the listener, because
46 /// [`TransparentProxy::control`] has to answer before `run()` is
47 /// awaited: a caller that owns the proxy is usually the caller about to
48 /// give up its thread of control to the accept loop, so a handle that
49 /// could only be taken afterwards could not be taken at all.
50 control: Arc<ControlPlane>,
51 /// The socket the client-facing endpoint is bound over, when the caller
52 /// supplied one.
53 ///
54 /// `None` — the ordinary case — makes `run()` bind its own socket at
55 /// [`ListenerConfig::bind_addr`], which is what this proxy has always
56 /// done. `Some(_)` is set only by
57 /// `TransparentProxy::set_impaired_socket` — named in plain code font
58 /// because it exists only under the `impair` feature, so a link to it
59 /// from this always-compiled field would not resolve — which is also
60 /// where the handle that arms it is recorded, so the socket and the
61 /// thing that impairs it can only arrive together.
62 client_socket: Option<Arc<dyn quinn::AsyncUdpSocket>>,
63}
64
65impl TransparentProxy {
66 /// Create a new proxy with the given configuration and observer.
67 pub fn new(config: ProxyConfig, observer: Arc<dyn ProxyObserver>) -> Self {
68 let control = ControlPlane::new(LegSetup::from_config(&config));
69 Self {
70 config,
71 observer,
72 hook: Arc::new(NoOpHook),
73 cancel: CancellationToken::new(),
74 next_session_id: AtomicU64::new(1),
75 control,
76 client_socket: None,
77 }
78 }
79
80 /// Create a new proxy with a custom hook for frame mutation.
81 pub fn with_hook(
82 config: ProxyConfig,
83 observer: Arc<dyn ProxyObserver>,
84 hook: Arc<dyn ProxyHook>,
85 ) -> Self {
86 let control = ControlPlane::new(LegSetup::from_config(&config));
87 Self {
88 config,
89 observer,
90 hook,
91 cancel: CancellationToken::new(),
92 next_session_id: AtomicU64::new(1),
93 control,
94 client_socket: None,
95 }
96 }
97
98 /// Run one leg over `socket`, and let
99 /// [`ProxyControl::set_impair`](crate::control::ProxyControl::set_impair)
100 /// arm `handle` on it.
101 ///
102 /// Both halves at once, deliberately. `quinn`'s `AsyncUdpSocket` is a
103 /// trait object with no downcast, and `quinn_netem` exposes no accessor
104 /// from a wrapped socket back to its handle, so nothing anywhere can
105 /// check that a handle belongs to the socket beside it. Taking them as
106 /// two independent settings would make a mismatched pair expressible —
107 /// and a mismatched pair arms an impairment that is reported as applied
108 /// and crossed by nobody's traffic, which is the exact failure the whole
109 /// impairment surface exists to make loud. Taking them together does not
110 /// *prove* they match, but it removes every way of supplying them that
111 /// does not.
112 ///
113 /// Call before [`TransparentProxy::run`]. The client leg's socket is
114 /// consumed when the endpoint is bound and the relay leg's when a
115 /// session dials, so a socket set afterwards reaches the client leg
116 /// never and the relay leg only from the next session on.
117 ///
118 /// # What each leg does with it
119 ///
120 /// [`Leg::Client`](crate::transport::Leg::Client) binds the listener's
121 /// endpoint over it, which covers raw-QUIC and WebTransport clients
122 /// alike — the proxy builds one QUIC endpoint on that side and hands
123 /// `h3` connections to the WebTransport library already connected, so
124 /// there is no second datagram path.
125 ///
126 /// [`Leg::Upstream`](crate::transport::Leg::Upstream) sets
127 /// [`ProxySessionConfig::upstream_socket`], which **every** session this
128 /// proxy accepts then builds its relay endpoint over. Two endpoints
129 /// reading one socket take each other's datagrams, so an upstream socket
130 /// set here is only sound for a proxy handling one client at a time — a
131 /// capture or impairment harness rather than a fan-out deployment. That
132 /// is a property of the socket seam and not of this call; it is repeated
133 /// here because this is now the easiest way to reach it.
134 ///
135 /// A WebTransport upstream refuses a socket outright when it dials, with
136 /// [`ProxyError::UpstreamSocketUnsupported`], rather than connecting
137 /// around it.
138 #[cfg(feature = "impair")]
139 pub fn set_impaired_socket(
140 &mut self,
141 leg: crate::types::Leg,
142 socket: Arc<dyn quinn::AsyncUdpSocket>,
143 handle: quinn_netem::ImpairHandle,
144 ) {
145 match leg {
146 crate::types::Leg::Client => self.client_socket = Some(socket),
147 crate::types::Leg::Upstream => self.config.session.upstream_socket = Some(socket),
148 }
149 self.control.set_impair_handle(leg, handle);
150 }
151
152 /// Returns a cancellation token that can be used to trigger shutdown.
153 pub fn cancel_token(&self) -> CancellationToken {
154 self.cancel.clone()
155 }
156
157 /// A handle onto this proxy — the address it bound, and the sessions it
158 /// is running.
159 ///
160 /// Callable at any point after the proxy is constructed, and meant to be
161 /// called **before** [`TransparentProxy::run`] is awaited: `run()` does
162 /// not return until the proxy is finished, so a caller that waited for
163 /// it would never get a handle to a proxy that was doing anything. Every
164 /// question a handle answers is defined for a proxy that has not bound
165 /// yet — see [`ProxyControl`].
166 ///
167 /// Cheap, and cheap to keep: the handle is one `Arc` onto state the
168 /// proxy already holds, and clones of it share that state rather than
169 /// copying it.
170 pub fn control(&self) -> ProxyControl {
171 ProxyControl::new(Arc::clone(&self.control))
172 }
173
174 /// Run the proxy accept loop. Blocks until cancelled or a fatal
175 /// listener error occurs.
176 pub async fn run(&self) -> Result<(), ProxyError> {
177 // Behind an `Arc`, and published, because the bound endpoint is no
178 // longer only this loop's business: it is the one place the port a
179 // `bind_addr` of `:0` resolved to can be read, and a `ProxyControl`
180 // taken before this function was even called has to be able to read
181 // it. The guard un-publishes on every way out of this function —
182 // cancellation below, the `?` on a fatal accept error, and this
183 // whole future being dropped by whoever spawned it — so a handle
184 // never reports an address for an endpoint that is gone.
185 // Before the bind, because a template carrying a capture spec is a
186 // contradiction in what the caller wrote rather than a fact about
187 // the world, and because a proxy that came up anyway would be this
188 // crate's cardinal failure with nothing to give it away: it would
189 // bind, accept, forward, report success, and leave the file the
190 // caller was watching uncreated. Both legs are checked here rather
191 // than at the two copies below, so the answer arrives before a
192 // socket exists and does not depend on a connection being accepted.
193 #[cfg(feature = "qlog")]
194 {
195 if self.config.listener.qlog.is_some() {
196 return Err(ProxyError::QlogOnProxyTemplate { leg: crate::types::Leg::Client });
197 }
198 if self.config.session.upstream_qlog.is_some() {
199 return Err(ProxyError::QlogOnProxyTemplate { leg: crate::types::Leg::Upstream });
200 }
201 }
202
203 let config = self.listener_config();
204 let listener = Arc::new(match &self.client_socket {
205 // A supplied socket replaces the bind and nothing else: the same
206 // certificate, the same ALPN list and the same transport
207 // parameters follow, and `bind_addr` is then ignored because the
208 // socket is already bound.
209 Some(socket) => Listener::bind_with_socket(config, Arc::clone(socket))?,
210 None => Listener::bind(config)?,
211 });
212 let _bound = self.control.publish_listener(Arc::clone(&listener));
213
214 loop {
215 tokio::select! {
216 result = listener.accept() => {
217 self.dispatch(result?);
218 }
219 _ = self.cancel.cancelled() => {
220 listener.close();
221 return Ok(());
222 }
223 }
224 }
225 }
226
227 /// Spawn a session for an accepted connection, picking the right
228 /// entry point based on the negotiated transport.
229 fn dispatch(&self, accepted: AcceptedConn) {
230 match accepted {
231 AcceptedConn::Quic { conn, alpn } => {
232 let session_id = self.next_session_id();
233 let client_addr = conn.remote_address();
234 self.emit_session_started(session_id, client_addr, "QUIC");
235 let session = self.new_session(session_id, alpn);
236 tokio::spawn(async move {
237 let _ = session.run(conn).await;
238 });
239 }
240 #[cfg(feature = "webtransport")]
241 AcceptedConn::WebTransport(conn) => {
242 let session_id = self.next_session_id();
243 let client_addr = conn.remote_address();
244 self.emit_session_started(session_id, client_addr, "WebTransport");
245 // WebTransport carries no moqt-* ALPN (always h3) so the
246 // session falls back to config.draft and/or SETUP-peek.
247 let session = self.new_session(session_id, Vec::new());
248 tokio::spawn(async move {
249 let _ = session.run_webtransport(conn).await;
250 });
251 }
252 }
253 }
254
255 // ── Helpers ────────────────────────────────────────────────
256
257 fn next_session_id(&self) -> SessionId {
258 SessionId(self.next_session_id.fetch_add(1, Ordering::Relaxed))
259 }
260
261 fn emit_session_started(
262 &self,
263 session_id: SessionId,
264 client_addr: std::net::SocketAddr,
265 client_transport: &str,
266 ) {
267 if self.observer.wants_events() {
268 self.observer.on_event(&ProxyEvent::SessionStarted {
269 session_id,
270 client_addr,
271 client_transport: client_transport.to_string(),
272 });
273 }
274 }
275
276 fn new_session(&self, session_id: SessionId, client_alpn: Vec<u8>) -> ProxySession {
277 let mut session = ProxySession::new(
278 session_id,
279 self.session_config(),
280 client_alpn,
281 Arc::clone(&self.observer),
282 Arc::clone(&self.hook),
283 self.cancel.child_token(),
284 );
285 // Attached here rather than taken as a seventh constructor argument:
286 // a session built directly, which is how this crate's own tests
287 // drive one, belongs to no proxy and therefore to no control plane,
288 // and making the plane a parameter would force every such caller to
289 // conjure one that lists a session nobody can reach.
290 session.attach_control(Arc::clone(&self.control));
291 session
292 }
293
294 // ── The two hand-written copies ────────────────────────────────
295 //
296 // This proxy holds one template per leg and copies it — once for the
297 // listener it binds, once per connection it accepts — because neither
298 // config is `Clone`: the listener owns a `PrivateKeyDer`, which is
299 // cloned through `clone_key`, and the session config owns
300 // `Arc<dyn>`s that are cheap to share but not derivable.
301 // A hand-written copy is exactly where a field added later gets forgotten,
302 // and this crate's cardinal failure is a setting that is accepted, reported
303 // as applied, and silently reaches nothing: the caller sets a window, the
304 // proxy comes up, and the run is believed. So both functions below start by
305 // **destructuring the template with no `..`**. That is not decoration — a
306 // field added to either config stops this file compiling with *pattern does
307 // not mention field*, at the one place that has to learn about it, rather
308 // than passing the build and dropping the value at run time. Do not add
309 // `..` to either pattern; it would put the trap back.
310 //
311 // Two fields no longer come from the template unconditionally, and both
312 // read the control plane first: the client leg's transport parameters
313 // and the session's shape profile. A live request that set either of
314 // them has to win over the value the proxy was built with, or the
315 // request would apply to sessions already running and not to the ones
316 // accepted after it — the reverse of what every caller expects.
317 //
318 // And one field per leg is deliberately *not* carried: the qlog spec,
319 // which owns a writer, has no `Clone` and is consumed when it becomes a
320 // sink. A template that is copied — once here, once per accepted
321 // connection — has nothing it could hand over. The pattern still names
322 // it, so the decision is visible where the copy is made rather than
323 // inferred from its absence, and both fields say so in their own
324 // documentation.
325 //
326 // Neither copy has to *report* that, because `run` has already refused a
327 // template holding either spec before it binds. These two `None`s are
328 // therefore what a template with no spec in it produces, and never a
329 // value being dropped: if the refusal above is ever removed, these lines
330 // become exactly the silent failure the paragraph above is about.
331
332 /// A fresh [`ListenerConfig`] carrying every field of this proxy's
333 /// template — every field but one, and the exception is stated at the
334 /// pattern below rather than left to be noticed.
335 fn listener_config(&self) -> ListenerConfig {
336 let ListenerConfig {
337 bind_addr,
338 cert_chain,
339 key_der,
340 transport_config,
341 transport_profile,
342 installer,
343 // Discarded rather than bound, because there is nothing a copy
344 // could do with it. A `QlogSpec` owns a `Box<dyn Write>`, has
345 // no `Clone`, and is consumed the moment it becomes a sink, so
346 // a template cannot hand one to anything and this copy leaves
347 // the field `None`. Naming it here is still what the pattern is
348 // for: the next field added to `ListenerConfig` stops this file
349 // compiling, and this one had to be *decided* rather than
350 // forgotten.
351 #[cfg(feature = "qlog")]
352 qlog: _,
353 } = &self.config.listener;
354
355 // A profile installed through the control plane has already been
356 // built — through this leg's own installer — so it arrives as a
357 // finished config and displaces both of the template's transport
358 // fields. Leaving `transport_profile` beside it would be the one
359 // combination `Listener::bind` refuses outright, and this proxy
360 // would stop binding.
361 let live = self.control.client_transport();
362 let (transport_config, transport_profile) = match live {
363 Some(config) => (Some(config), None),
364 None => (transport_config.clone(), transport_profile.clone()),
365 };
366
367 ListenerConfig {
368 bind_addr: *bind_addr,
369 cert_chain: cert_chain.clone(),
370 key_der: key_der.clone_key(),
371 transport_config,
372 transport_profile,
373 installer: installer.clone(),
374 // The one field of either template that a copy cannot carry.
375 // A spec set on a `ProxyConfig` therefore reaches no leg, which
376 // is said on the field itself as well: capture a client leg by
377 // building the `ListenerConfig` and calling `Listener::bind`
378 // directly, which is also the only place one writer per
379 // connection is expressible.
380 #[cfg(feature = "qlog")]
381 qlog: None,
382 }
383 }
384
385 /// A fresh [`ProxySessionConfig`] carrying every field of this proxy's
386 /// template, built once per accepted connection.
387 fn session_config(&self) -> ProxySessionConfig {
388 let ProxySessionConfig {
389 draft,
390 upstream_transport,
391 upstream_addr,
392 skip_upstream_cert_verify,
393 upstream_ca_certs,
394 upstream_connect_timeout_secs,
395 upstream_transport_config,
396 upstream_transport_profile,
397 upstream_installer,
398 // Discarded for the same reason the client leg's spec is, and
399 // it bites harder here: this copy is made once per accepted
400 // connection, and one spec — one writer, consumed when it
401 // becomes a sink — cannot be divided between them. A relay leg
402 // is captured by driving `ProxySession` directly, one spec per
403 // session.
404 #[cfg(feature = "qlog")]
405 upstream_qlog: _,
406 upstream_socket,
407 egress,
408 shape,
409 } = &self.config.session;
410
411 // As on the client leg, a live profile displaces both of the
412 // template's transport fields rather than joining one of them: a leg
413 // naming a raw config and a profile at once is refused when it
414 // dials, so leaving the template's config in place would turn every
415 // session accepted after the request into an upstream-connect
416 // failure. Stored as a *profile* rather than as the config it built,
417 // so each session still runs the installer once for its own
418 // connection exactly as a configured profile does.
419 let live_transport = self.control.upstream_transport();
420 let (upstream_transport_config, upstream_transport_profile) = match live_transport {
421 Some(profile) => (None, Some(profile)),
422 None => (upstream_transport_config.clone(), upstream_transport_profile.clone()),
423 };
424
425 ProxySessionConfig {
426 draft: *draft,
427 upstream_transport: upstream_transport.clone(),
428 upstream_addr: upstream_addr.clone(),
429 skip_upstream_cert_verify: *skip_upstream_cert_verify,
430 upstream_ca_certs: upstream_ca_certs.clone(),
431 upstream_connect_timeout_secs: *upstream_connect_timeout_secs,
432 upstream_transport_config,
433 upstream_transport_profile,
434 upstream_installer: upstream_installer.clone(),
435 // Not carried, as on the listener above and for the same
436 // reason. A spec set on a proxy template reaches no session.
437 #[cfg(feature = "qlog")]
438 upstream_qlog: None,
439 // Every session this proxy accepts gets a clone of the *same*
440 // socket, and each builds its own upstream endpoint over it.
441 // Two endpoints reading one socket take each other's
442 // datagrams, so a socket set here is only sound for a proxy
443 // handling one client connection at a time — a capture or
444 // impairment harness, not a fan-out deployment. A caller
445 // wanting one socket per session drives `ProxySession`
446 // directly, which is where the seam is.
447 upstream_socket: upstream_socket.clone(),
448 egress: *egress,
449 // Cloned, not moved: this builds one config *per accepted
450 // connection* from a template the proxy keeps. `ShapeProfile`
451 // is not `Copy` either — it owns its bucket and class vectors
452 // — so the clone is explicit here exactly as
453 // `upstream_ca_certs`' is above.
454 //
455 // A profile installed through the control plane wins over the
456 // template, and it wins for good: once set, every session
457 // accepted afterwards shapes with it, including on a proxy whose
458 // template had no profile at all. That is what makes
459 // `set_shape` reach a proxy uniformly rather than only the
460 // sessions that happened to be running when it was called.
461 shape: self.control.shape().snapshot().1.or_else(|| shape.clone()),
462 }
463 }
464}
465
466impl LegSetup {
467 /// What the control plane needs to know about the two legs, read off the
468 /// configuration the proxy is being built with.
469 ///
470 /// The installers are shared rather than copied, so a live
471 /// [`TransportProfile`](crate::transport::TransportProfile) is built by
472 /// the same installer a configured one would have been.
473 fn from_config(config: &ProxyConfig) -> Self {
474 Self {
475 client_installer: config.listener.installer.clone(),
476 upstream_installer: config.session.upstream_installer.clone(),
477 // Read once, here, because it cannot change: the upstream
478 // transport is a field of the template and no request replaces
479 // it. A WebTransport upstream builds its endpoint inside the
480 // WebTransport library, which takes no `quinn::TransportConfig`
481 // and returns no endpoint to install one on, so a transport
482 // profile aimed at that leg has nowhere to go.
483 upstream_webtransport: matches!(
484 config.session.upstream_transport,
485 UpstreamTransportType::WebTransport { .. }
486 ),
487 }
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use std::time::Duration;
494
495 use moqtap_codec::version::DraftVersion;
496 use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
497
498 use super::*;
499 use crate::action::EgressConfig;
500 use crate::observer::NoOpProxyObserver;
501 use crate::session::UpstreamTransportType;
502 use crate::shape::{BucketConfig, ClassRule, Discipline, QueueConfig, ShapeProfile};
503 use crate::transport::{TransportInstaller, TransportProfile, TransportProfileError};
504
505 /// An installer the copy tests only ever compare by address.
506 struct MarkerInstaller;
507
508 impl TransportInstaller for MarkerInstaller {
509 fn build(
510 &self,
511 profile: &TransportProfile,
512 ) -> Result<quinn::TransportConfig, TransportProfileError> {
513 profile.into_config()
514 }
515 }
516
517 fn marker_profile(initial_mtu: u16) -> TransportProfile {
518 TransportProfile { initial_mtu: Some(initial_mtu), ..Default::default() }
519 }
520
521 /// A socket to hand the session template, so the field being carried is
522 /// checkable by address rather than by "both are `None`".
523 fn abstract_socket() -> Arc<dyn quinn::AsyncUdpSocket> {
524 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("an ephemeral port");
525 quinn::default_runtime()
526 .expect("these tests run under tokio")
527 .wrap_udp_socket(socket)
528 .expect("wrap the socket")
529 }
530
531 /// A template in which **no carryable field holds its default**.
532 ///
533 /// That is the whole design of these two tests. A copy that dropped a
534 /// field would leave the copy holding a default, and a template built
535 /// from defaults could not tell the two apart — every assertion would
536 /// pass against a copy that carried nothing at all. So every field
537 /// below is set to something the corresponding `Default` is not.
538 ///
539 /// The two qlog specs are the exception, and they are marked where they
540 /// are set: a spec cannot be copied by anything, so there is no version
541 /// of this fixture in which the copy carrying it is the correct
542 /// behaviour to check for.
543 fn distinctive_config(socket: Arc<dyn quinn::AsyncUdpSocket>) -> ProxyConfig {
544 let egress = EgressConfig {
545 max_pending_bytes: 12_345,
546 max_hold: Duration::from_secs(7),
547 ..Default::default()
548 };
549
550 ProxyConfig {
551 listener: ListenerConfig {
552 bind_addr: "127.0.0.1:4443".parse().expect("a literal address"),
553 cert_chain: vec![CertificateDer::from(vec![1u8; 10])],
554 key_der: PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(vec![2u8; 10])),
555 // Not both at once — the two are mutually exclusive on a
556 // leg — so the raw config is the upstream template's and
557 // the profile is this one's, and each is carried by only
558 // one of the two copies.
559 transport_config: None,
560 transport_profile: Some(marker_profile(1350)),
561 installer: Some(Arc::new(MarkerInstaller)),
562 // The one field left at its default, because a spec cannot
563 // be carried by a copy at all — it owns a writer, has no
564 // `Clone`, and is consumed when it becomes a sink. Setting
565 // one here would make the fixture describe a template whose
566 // copy is *documented* to drop it, which is a fact about
567 // the type rather than a copy worth checking.
568 #[cfg(feature = "qlog")]
569 qlog: None,
570 },
571 session: ProxySessionConfig {
572 draft: DraftVersion::Draft19,
573 upstream_transport: UpstreamTransportType::WebTransport {
574 url: "https://relay.invalid:4443/moq".to_string(),
575 },
576 upstream_addr: "relay.invalid:4443".to_string(),
577 skip_upstream_cert_verify: true,
578 upstream_ca_certs: vec![vec![3u8; 4]],
579 upstream_connect_timeout_secs: 11,
580 upstream_transport_config: Some(Arc::new(quinn::TransportConfig::default())),
581 upstream_transport_profile: None,
582 upstream_installer: Some(Arc::new(MarkerInstaller)),
583 // Left at its default for the reason given on the listener
584 // half above.
585 #[cfg(feature = "qlog")]
586 upstream_qlog: None,
587 upstream_socket: Some(socket),
588 egress,
589 // One bucket and one class naming it, which is the smallest
590 // profile there is: an empty one is `ShapeError::NoClasses`,
591 // because a profile with no classes shapes nothing. The
592 // names are distinctive for the same reason every other
593 // field here is — a copy that dropped this would otherwise
594 // be indistinguishable from one that carried it.
595 shape: Some(
596 ShapeProfile::try_new(
597 vec![BucketConfig { name: "marker".to_string(), ..Default::default() }],
598 vec![ClassRule {
599 name: "marker class".to_string(),
600 bucket: "marker".to_string(),
601 ..Default::default()
602 }],
603 QueueConfig::default(),
604 Discipline::Fifo,
605 )
606 .expect("one class naming its own bucket is a valid profile"),
607 ),
608 },
609 }
610 }
611
612 fn proxy(config: ProxyConfig) -> TransparentProxy {
613 TransparentProxy::new(config, Arc::new(NoOpProxyObserver))
614 }
615
616 /// The listener template survives the copy `run` makes of it.
617 ///
618 /// A field this copy drops is not a compile error and not a test
619 /// failure anywhere else in the crate: the caller sets it, the proxy
620 /// binds, and the setting reaches nothing. The destructuring pattern in
621 /// `listener_config` is the first line of defence and this is the
622 /// second — it fails if a field is named in the pattern and then
623 /// written from the wrong place, which the compiler cannot see.
624 #[tokio::test]
625 async fn the_listener_copy_carries_every_field() {
626 let template = distinctive_config(abstract_socket());
627 let expected_installer =
628 Arc::clone(template.listener.installer.as_ref().expect("set above"));
629 let proxy = proxy(template);
630 let copy = proxy.listener_config();
631 let template = &proxy.config.listener;
632
633 assert_eq!(copy.bind_addr, template.bind_addr);
634 assert_eq!(copy.cert_chain, template.cert_chain);
635 assert_eq!(
636 copy.key_der.secret_der(),
637 template.key_der.secret_der(),
638 "`clone_key` rather than `clone`: the key is the one field that cannot be derived"
639 );
640 assert!(copy.transport_config.is_none(), "the template names no raw config");
641 assert_eq!(copy.transport_profile, template.transport_profile);
642 assert!(
643 Arc::ptr_eq(copy.installer.as_ref().expect("carried"), &expected_installer),
644 "the copy shares the caller's installer rather than substituting the default one"
645 );
646 }
647
648 /// The session template survives the copy made per accepted
649 /// connection.
650 #[tokio::test]
651 async fn the_session_copy_carries_every_field() {
652 let template = distinctive_config(abstract_socket());
653 let expected_installer =
654 Arc::clone(template.session.upstream_installer.as_ref().expect("set above"));
655 let expected_raw =
656 Arc::clone(template.session.upstream_transport_config.as_ref().expect("set above"));
657 let expected_socket =
658 Arc::clone(template.session.upstream_socket.as_ref().expect("set above"));
659 let proxy = proxy(template);
660 let copy = proxy.session_config();
661 let template = &proxy.config.session;
662
663 assert_eq!(copy.draft, template.draft);
664 assert_eq!(
665 format!("{:?}", copy.upstream_transport),
666 format!("{:?}", template.upstream_transport),
667 "`UpstreamTransportType` has no `PartialEq`, so the comparison is its `Debug`"
668 );
669 assert_eq!(copy.upstream_addr, template.upstream_addr);
670 assert_eq!(copy.skip_upstream_cert_verify, template.skip_upstream_cert_verify);
671 assert_eq!(copy.upstream_ca_certs, template.upstream_ca_certs);
672 assert_eq!(copy.upstream_connect_timeout_secs, template.upstream_connect_timeout_secs);
673 assert!(Arc::ptr_eq(
674 copy.upstream_transport_config.as_ref().expect("carried"),
675 &expected_raw
676 ));
677 assert!(copy.upstream_transport_profile.is_none(), "the template names no profile");
678 assert!(
679 Arc::ptr_eq(copy.upstream_installer.as_ref().expect("carried"), &expected_installer),
680 "the copy shares the caller's installer rather than substituting the default one"
681 );
682 assert!(
683 Arc::ptr_eq(copy.upstream_socket.as_ref().expect("carried"), &expected_socket),
684 "a dropped socket is the loudest of these failures: every impairment armed on it \
685 would be applied to nothing and the run would look clean"
686 );
687 assert_eq!(copy.egress, template.egress);
688 assert_eq!(copy.shape, template.shape);
689 }
690
691 /// The mutually exclusive halves of the two templates, swapped.
692 ///
693 /// The pair above sets the profile on one leg and the raw config on the
694 /// other, so between them every one of the four fields is checked —
695 /// but only in one arrangement each. This runs the other arrangement,
696 /// so neither copy can be carrying a field by reading it off the wrong
697 /// leg.
698 #[tokio::test]
699 async fn the_copies_carry_the_other_arrangement_too() {
700 let mut template = distinctive_config(abstract_socket());
701 template.listener.transport_profile = None;
702 template.listener.transport_config = Some(Arc::new(quinn::TransportConfig::default()));
703 template.session.upstream_transport_config = None;
704 template.session.upstream_transport_profile = Some(marker_profile(1400));
705
706 let expected_raw =
707 Arc::clone(template.listener.transport_config.as_ref().expect("set above"));
708 let proxy = proxy(template);
709
710 let listener = proxy.listener_config();
711 assert!(
712 Arc::ptr_eq(listener.transport_config.as_ref().expect("carried"), &expected_raw),
713 "the client leg's raw config has to reach the listener unmodified"
714 );
715 assert!(listener.transport_profile.is_none());
716
717 let session = proxy.session_config();
718 assert_eq!(session.upstream_transport_profile, Some(marker_profile(1400)));
719 assert!(session.upstream_transport_config.is_none());
720 }
721}