1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! Which headers cross the tunnel, and which stop at its edge.
//!
//! Pure: transforms over an owned header list, no I/O and no knowledge of
//! how those headers were parsed or will be written back. Header names are
//! compared ASCII-case-insensitively throughout, because that is what they
//! are.
//!
//! The list is a `Vec` of pairs rather than a map on purpose. Headers may
//! legitimately repeat — `Set-Cookie` most obviously — and order is
//! observable, so collapsing them into a map would silently rewrite traffic
//! this crate has no business rewriting.
/// Headers that describe *this* connection rather than the message, and so
/// must not be forwarded onto a different one. RFC 9110 §7.6.1.
///
/// `Transfer-Encoding` is on the list, and stripping it is only correct
/// because the edge re-frames the body it forwards. A component that
/// stripped it and then copied the body through unchanged would be
/// forwarding chunked bytes with nothing left to say so — the framing
/// confusion that request smuggling is built out of. The check for a
/// message that arrives with conflicting framing to begin with belongs to
/// the edge, not here: it is a question about the whole message, and this
/// module only sees the headers.
const HOP_BY_HOP: & = &;
/// Headers describing a proxy chain in front of the client.
///
/// Stripped inbound and never added outbound. modelpipe is a private tunnel
/// between two machines someone owns, not a reverse proxy in front of a
/// fleet: there is no chain to describe, and forwarding a client-supplied
/// one into a local backend hands an attacker a free way to claim any
/// origin address they like to whatever reads the backend's logs.
const FORWARDING: & = &;
/// Headers this edge sets to say that a request came through the tunnel,
/// and from which peer.
///
/// Stripped inbound and then set outbound, which is the *replace* half of
/// the rule [`FORWARDING`] applies and not the *append* RFC 9110 §7.6.3
/// describes for `Via`. The RFC's chain is worth describing when there is
/// one; a private tunnel between two machines has exactly one hop, and a
/// field a client may extend is a field a client may forge. So the backend
/// sees what this edge says and nothing a peer said before it — which is
/// what lets a backend *restrict* on these (refuse a route to tunnelled
/// requests, count them) but never *trust* them for more than that: a
/// local client forging them only denies itself.
const TUNNEL_MARKERS: & = &;
/// The `Via` this edge sets. The protocol version is the one the backend
/// hop speaks, as the RFC asks; the pseudonym is the product.
pub const VIA: &str = "1.1 modelpipe";
/// The header carrying the connecting peer's fingerprint — the same twelve
/// hex characters the `peer` log field shows, so a backend can name a
/// device the way the operator's log does.
pub const PEER_HEADER: &str = "X-Modelpipe-Peer";
/// The header carrying the name of the token that admitted the request,
/// when one added by name did — so a backend can tell one paired machine
/// from another without holding their tokens itself. Absent when the
/// primary admitted, which is what a backend with one client sees and is
/// how it stays unaware that names exist.
pub const DEVICE_HEADER: &str = "X-Modelpipe-Device";
/// Fields a `Connection` header may not nominate, whatever it says.
///
/// RFC 9110 §7.6.1 forbids a sender from naming a field that is meaningful
/// to every recipient, and this is the recipient half of that rule: these
/// describe the *message*, not the hop, so deleting one on a peer's say-so
/// changes what the message means rather than what the connection does.
///
/// `content-length` is the one that matters. Framing is decided from the
/// headers as they arrived; the strip happens afterwards; and the
/// serializer re-declares framing only for chunked. So honouring
/// `Connection: content-length` deletes the length from the head while the
/// body is still forwarded under it — the edge emitting a body beneath a
/// head that declares none, which is the framing confusion the module
/// comment above says stripping must not create.
const NEVER_NOMINABLE: & = &;
/// Remove the hop-by-hop headers, including the ones this message nominates.
///
/// `Connection` may name further headers as hop-by-hop for this connection
/// only, and honouring that is not optional: a header a peer marked
/// connection-scoped is one it did not intend to reach anybody else. A
/// nomination of a message-level field is the exception and is ignored —
/// see [`NEVER_NOMINABLE`].
pub
/// Whether a field name is one the edge removes from a head.
///
/// The head strip's own rule, and one half of the trailer rule: trailers go
/// through [`is_forbidden_in_trailer`] below, which is this plus
/// [`NEVER_NOMINABLE`]. The two are separate because they answer different
/// questions — what this hop removes from a head it is forwarding, and what
/// a field is not allowed to be *at all* when it arrives after the body.
pub
/// Whether a field name is one a *trailer* may not carry.
///
/// Everything [`is_stripped`] covers, plus [`NEVER_NOMINABLE`] — which is
/// where `content-length` and `host` live, and which `is_stripped` does not
/// consult. `body.rs` filtered trailers through `is_stripped` alone while
/// the comment above that filter said a backend could not use one to put
/// back "`Connection` or a second `Content-Length`". Half of that was true:
/// `Connection` is hop-by-hop and was caught, `Content-Length` is neither
/// hop-by-hop nor a forwarding header and sailed through.
///
/// The rule is RFC 9110 §6.5.1: a trailer may not carry a field that
/// affects message framing, routing, authentication, or processing. These
/// two lists are this crate's spelling of the first two, and a trailer
/// restating either is the head strip undone a few hundred bytes later —
/// on the one part of the message nothing else filters.
///
/// Still deliberately ignores `Connection` nominations, for the reason
/// [`is_stripped`] gives: those describe the head they arrived with, and
/// re-reading one here would hand a peer the message-rewriting lever
/// [`NEVER_NOMINABLE`] exists to take away.
pub
/// Remove any inbound description of a proxy chain, and add none.
pub
/// Replace `Host` with the backend's authority.
///
/// The client's `Host` names the local listener it connected to, which the
/// backend has never heard of. Rewriting rather than passing through is also
/// what keeps a name-based backend from being addressed as something it is
/// not: whatever the client asked for, what arrives is the authority the
/// operator configured.
///
/// Every existing `Host` is removed first — a request carrying two is
/// ambiguous, and resolving it by appending a third would be worse.
pub
/// Present `token` to the backend as the bearer, in place of whatever the
/// client sent.
///
/// Every inbound `Authorization` is removed first, for the reason
/// [`set_host`] removes every `Host`: what the backend reads must be what
/// this edge wrote. Here that is the point rather than a tidiness — the
/// value removed is a device's own credential, and the backend is the one
/// party that must never see it.
pub
/// Mark the request as tunnelled, from `peer`.
///
/// Every inbound copy of either marker is removed first, for the reason
/// [`set_host`] removes every `Host`: what the backend reads must be what
/// this edge wrote. Appended rather than inserted at the front, so `Host`
/// keeps the first position the tests pin it to.
pub