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
//! 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: & = &;
/// 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