Skip to main content

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}