moqtap_proxy/error.rs
1//! Proxy error types.
2
3use moqtap_client::transport::TransportError;
4use moqtap_codec::error::CodecError;
5use moqtap_codec::version::DraftVersion;
6
7use crate::transport::TransportProfileError;
8use crate::types::Leg;
9
10/// Errors from the proxy layer.
11#[derive(Debug, thiserror::Error)]
12pub enum ProxyError {
13 /// Error from the QUIC/WebTransport listener.
14 #[error("listener error: {0}")]
15 Listener(String),
16 /// Something was asked of a proxy's listener before there was one.
17 ///
18 /// A [`TransparentProxy`](crate::proxy::TransparentProxy) binds its
19 /// endpoint inside `run()`, so a
20 /// [`ProxyControl`](crate::control::ProxyControl) taken beforehand —
21 /// which is the usual case, since `run()` does not return until the
22 /// proxy is finished — is live before the endpoint is. This is the
23 /// answer during that window, and again after `run()` has returned and
24 /// the endpoint is gone.
25 ///
26 /// Distinct from [`ProxyError::Listener`] on purpose: that one means a
27 /// listener existed and something about it failed, and the two call for
28 /// opposite responses. A caller waiting for a proxy to come up retries
29 /// on this and gives up on the other.
30 #[error("the proxy has not bound a listener")]
31 NotBound,
32 /// Error from the underlying transport.
33 #[error("transport error: {0}")]
34 Transport(#[from] TransportError),
35 /// Error decoding a MoQT frame.
36 #[error("codec error: {0}")]
37 Codec(#[from] CodecError),
38 /// Failed to connect to the upstream relay.
39 #[error("upstream connection failed: {0}")]
40 UpstreamConnect(String),
41 /// A socket was supplied for an upstream connection that cannot be
42 /// made over it.
43 ///
44 /// Distinct from [`ProxyError::UpstreamConnect`] because nothing was
45 /// attempted: the session refuses to connect at all rather than
46 /// connecting over a socket the caller did not supply. Silently
47 /// ignoring the socket would let a caller arm loss, delay or a
48 /// bandwidth cap on the relay leg, watch a clean run, and conclude the
49 /// impairment had no effect — when in fact it never reached the wire.
50 #[error(
51 "upstream socket unsupported: a WebTransport upstream builds its endpoint inside the \
52 WebTransport library, which exposes no way to supply a userspace socket, so the socket \
53 supplied for this session cannot be honoured"
54 )]
55 UpstreamSocketUnsupported,
56 /// One leg was given both a raw `quinn::TransportConfig` and a
57 /// [`TransportProfile`](crate::transport::TransportProfile).
58 ///
59 /// Refused where the leg is built, rather than merged, and the reason
60 /// is a missing trait rather than a policy anyone chose.
61 /// `quinn::TransportConfig` has no `Clone` and no getter for any field,
62 /// so nothing in this crate can take the caller's config and hand back
63 /// a modified copy of it. Code that looked like a merge would in fact
64 /// be starting from `quinn::TransportConfig::default()` and discarding
65 /// everything the embedding program configured — and the tests would
66 /// stay green while it happened, because every field set *in the
67 /// profile* does arrive. Only the fields set in the config vanish, and
68 /// nothing reports that.
69 ///
70 /// A caller who wants both writes the base config and applies the
71 /// profile over it with `TransportProfile::apply_to(&mut base)`, then
72 /// sets the result as this leg's `transport_config`. Where the base has
73 /// to be rebuilt for each connection,
74 /// [`TransportInstaller`](crate::transport::TransportInstaller) is that
75 /// same call behind a trait the leg can hold.
76 #[error(
77 "the {leg:?} leg was given both a transport_config and a transport_profile, which cannot \
78 be combined: quinn::TransportConfig can be neither cloned nor read back, so a merge \
79 would silently discard the config and keep only the profile. Apply the profile to your \
80 own config with TransportProfile::apply_to(&mut base) and set the result as this leg's \
81 transport_config, or supply a TransportInstaller that builds the base itself"
82 )]
83 TransportConfigAndProfile {
84 /// The leg holding the contradiction.
85 leg: Leg,
86 },
87 /// One leg was given both a raw `quinn::TransportConfig` and a
88 /// [`QlogSpec`](crate::qlog::QlogSpec).
89 ///
90 /// Its own variant rather than a widening of
91 /// [`ProxyError::TransportConfigAndProfile`], because the two refusals
92 /// have different fixes and a caller — or a test — that could not tell
93 /// them apart would be sent to the wrong half of its configuration. A
94 /// single variant carrying a string to say which pair was named is not
95 /// something a `match` can check.
96 ///
97 /// # Why the pair cannot be honoured
98 ///
99 /// quinn accepts a capture sink in exactly one place: a method that
100 /// **mutates** a `quinn::TransportConfig`. There is no setter on an
101 /// endpoint, none on a `quinn::ServerConfig` or a `quinn::ClientConfig`,
102 /// and none on a live connection. A leg's raw config arrives as an
103 /// `Arc<quinn::TransportConfig>`, and that type has no `Clone`, so it
104 /// cannot be copied and mutated; `Arc::get_mut` is the only other way in
105 /// and it hands back nothing whenever a second handle exists — which it
106 /// does on this crate's own path, since
107 /// [`TransparentProxy`](crate::proxy::TransparentProxy) clones the `Arc`
108 /// into the listener config it builds while the original stays in its
109 /// template.
110 ///
111 /// So a leg holding both could do exactly one thing: take the spec,
112 /// count it as configured, and deliver it nowhere. The connection would
113 /// come up, the leg would report success, and the file the caller was
114 /// watching would never be written to at all — the failure this crate
115 /// exists to make impossible, in the one place a caller has no way of
116 /// noticing it.
117 ///
118 /// A caller who wants both attaches the sink to their own config with
119 /// `QlogSpec::attach_to(&mut base)` *before* the `Arc` is made — that is
120 /// the last moment a mutable reference to it exists — and sets the
121 /// result as this leg's raw config. A caller who has no config of their
122 /// own drops the field and lets the leg build one, which is what a leg
123 /// carrying a spec alone does.
124 #[cfg(feature = "qlog")]
125 #[error(
126 "the {leg:?} leg was given both a transport_config and a qlog spec, which cannot be \
127 combined: quinn installs a capture sink by mutating a quinn::TransportConfig, and this \
128 leg's config arrives behind an Arc that can be neither cloned nor mutated, so the spec \
129 would be accepted and delivered nowhere. Attach the sink to your own config with \
130 QlogSpec::attach_to(&mut base) before wrapping it in an Arc and set the result as this \
131 leg's transport_config, or drop the transport_config and let the leg build one"
132 )]
133 TransportConfigAndQlog {
134 /// The leg holding the contradiction.
135 leg: Leg,
136 },
137 /// A leg's [`QlogSpec`](crate::qlog::QlogSpec) could not become a
138 /// capture.
139 ///
140 /// The leg is refused rather than opened without the sink, for the
141 /// reason every refusal in this area exists: a connection that came up
142 /// anyway would run, report success, and leave the caller's capture
143 /// empty — and an empty capture is indistinguishable from a capture of
144 /// a connection that carried nothing, which is exactly the question a
145 /// capture is usually read to answer.
146 ///
147 /// Both of [`QlogError`](crate::qlog::QlogError)'s variants reach here.
148 /// `NoWriter` is a spec nobody finished writing, refused before a sink
149 /// is built or an endpoint exists; `NotStarted` is a writer that refused
150 /// the preamble, which is the first thing written and therefore the
151 /// first place a full disk or an unwritable path shows up.
152 #[cfg(feature = "qlog")]
153 #[error("the {leg:?} leg's qlog capture could not be started: {source}")]
154 Qlog {
155 /// The leg holding the spec.
156 leg: Leg,
157 /// Why the spec could not become a capture.
158 source: crate::qlog::QlogError,
159 },
160 /// A [`TransparentProxy`](crate::proxy::TransparentProxy) was built from
161 /// a template carrying a [`QlogSpec`](crate::qlog::QlogSpec), which it
162 /// cannot deliver to any leg.
163 ///
164 /// # Why a proxy template cannot carry one
165 ///
166 /// A `TransparentProxy` holds its two configurations as a **template**
167 /// and copies them: the listener's once, when it binds, and the
168 /// session's once per accepted connection. A `QlogSpec` owns a
169 /// `Box<dyn Write>`, has no `Clone`, and is consumed the moment it
170 /// becomes a sink, so there is nothing a copy could hand over — and one
171 /// writer cannot be divided between the connections a proxy accepts in
172 /// any case. One sink shared by two connections writes both into one
173 /// file, behind one preamble, with no record saying where the first ends.
174 ///
175 /// # Why it is refused rather than dropped
176 /// Because the alternative is this crate's cardinal failure with nothing to
177 /// give it away. A proxy that quietly left the field behind would bind,
178 /// accept, forward and report success, and the only symptom would be a file
179 /// that was never created — which a caller reads as *the run produced no
180 /// events* rather than as *the capture was never installed*. The
181 /// contradiction is knowable before a socket exists, so it is answered
182 /// there.
183 ///
184 /// The fix is to capture the leg where one writer per connection is
185 /// expressible: build the
186 /// [`ListenerConfig`](crate::listener::ListenerConfig) and call
187 /// [`Listener::bind`](crate::listener::Listener::bind) for the client
188 /// leg, or drive a [`ProxySession`](crate::session::ProxySession) with
189 /// its own spec for the relay leg.
190 #[cfg(feature = "qlog")]
191 #[error(
192 "a TransparentProxy template was given a qlog spec for the {leg:?} leg, which it cannot \
193 deliver: a proxy copies its configuration — the listener's once, the session's once per \
194 accepted connection — and a QlogSpec owns its writer, has no Clone and is consumed when \
195 it becomes a sink, so the copy would carry nothing and the capture would never be \
196 installed. Capture the client leg by building a ListenerConfig and calling \
197 Listener::bind, or the relay leg by driving a ProxySession, one spec per connection"
198 )]
199 QlogOnProxyTemplate {
200 /// The leg whose template holds the spec.
201 leg: Leg,
202 },
203 /// A leg's [`TransportProfile`](crate::transport::TransportProfile)
204 /// could not be turned into a `quinn::TransportConfig`.
205 ///
206 /// The leg is refused rather than opened with quinn's defaults: a
207 /// connection that came up anyway would run with parameters nobody
208 /// chose and report success, and the profile that was ignored is
209 /// precisely the record of what the run was supposed to be.
210 #[error("the {leg:?} leg's transport profile was refused: {source}")]
211 TransportProfile {
212 /// The leg holding the profile.
213 leg: Leg,
214 /// Why the profile could not be honoured.
215 source: TransportProfileError,
216 },
217 /// A shaping class keys on a field this session's draft does not carry,
218 /// so the class could never claim a unit.
219 ///
220 /// Refused before the session dials, rather than left to be discovered
221 /// from a report during the run. The rule is not merely unlikely to
222 /// match — on this draft there is no traffic at all that could satisfy
223 /// it, so a session carrying one would pace nothing the author asked
224 /// for, count itself as shaping, and finish clean. That is the whole
225 /// failure this crate exists to make impossible, arrived at through a
226 /// configuration file rather than a bug.
227 ///
228 /// Distinct from
229 /// [`ShapeError::InertMatcher`](crate::shape::ShapeError::InertMatcher),
230 /// and the distinction is where the answer lives: an empty value set is
231 /// a property of the configuration alone, so the constructor rejects it
232 /// with no draft in hand; a key the draft does not carry needs a draft
233 /// to judge, which only exists once a session is being started.
234 #[error("{source}")]
235 ShapeRuleUnsupported {
236 /// The class, the draft, the stream kind and the key.
237 #[from]
238 source: crate::capability::UnsupportedMatcherKey,
239 },
240 /// The session is configured for a MoQT draft this build did not
241 /// compile a codec for.
242 ///
243 /// [`DraftVersion`] carries all thirteen variants under every feature
244 /// set, so a draft that was never compiled is still a value a
245 /// configuration can hold — and
246 /// `ProxySessionConfig::default().draft` holds one of them. On a build
247 /// made with a reduced draft set, nothing about such a configuration
248 /// looks wrong.
249 ///
250 /// # What the session would do instead
251 ///
252 /// Run, and forward everything uninterpreted. The dispatch enums fall
253 /// through to their catch-all arm and answer
254 /// `CodecError::UnsupportedDraft`, which is not an
255 /// incomplete-input error, so the object framer takes its terminal
256 /// arm, latches [`BypassReason::DecodeError`](crate::framer::BypassReason)
257 /// and pumps the stream through as bytes. No object reaches a hook, no
258 /// shaping class claims anything, no `ProxyEvent::Object` is emitted —
259 /// and the run completes, reports success, and produces a stream of
260 /// zeroes that is indistinguishable from a session nothing was sent on.
261 /// Control frames fare no better and are quieter still: the control
262 /// parser skips a frame it cannot decode and emits nothing at all.
263 ///
264 /// So it is refused where the session starts, beside the shaping
265 /// admission check and before the relay is dialled, because a byte pump
266 /// reporting success is precisely what this crate exists to make
267 /// impossible.
268 ///
269 /// # It names the draft that was resolved, not the one configured
270 ///
271 /// Drafts 15 and later are settled by the client's ALPN, so the draft a
272 /// session will actually frame with may not be the one in its
273 /// configuration. This carries the resolved one, which is the one that
274 /// is missing.
275 ///
276 /// The fix is a build that carries the draft — the `draftNN` feature of
277 /// this crate, which forwards to both the codec and the client — or a
278 /// configuration naming one this build has.
279 #[error(
280 "this session is configured for {draft:?}, which this build did not compile: it would \
281 forward every stream uninterpreted, surface no object to any hook, claim nothing with \
282 any shaping class, and report success. Build with the matching draftNN feature, or \
283 configure a draft this build carries"
284 )]
285 DraftNotCompiled {
286 /// The draft this build cannot frame.
287 draft: DraftVersion,
288 },
289 /// TLS configuration error.
290 #[error("TLS config error: {0}")]
291 TlsConfig(String),
292 /// Certificate generation error.
293 #[error("certificate generation error: {0}")]
294 CertGen(String),
295 /// Session was closed.
296 #[error("session closed: {0}")]
297 SessionClosed(String),
298 /// Proxy is shutting down.
299 #[error("proxy shutdown")]
300 Shutdown,
301}