Skip to main content

openmoat_proxy/
server.rs

1//! Accepting connections and deciding each one.
2//!
3//! Order per connection: read the head → (session mode) verify its credential
4//! and take the session's share of the slots → check it for brokered secrets
5//! bound elsewhere → decide the host → (session mode) apply the session's
6//! taint → resolve and check the addresses → connect → (CONNECT) answer 200,
7//! read the `ClientHello` and check its SNI → record → relay, watching a
8//! plain-HTTP body for brokered secrets too. Nothing reaches the destination
9//! before the record is written; a failed record closes the connection.
10
11mod limits;
12mod wire;
13
14use std::io::{self, ErrorKind, Write as _};
15use std::net::{SocketAddr, TcpListener, TcpStream};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::thread;
18use std::time::Instant;
19
20use openmoat_core::{CompiledPolicy, Decision, Verdict};
21use zeroize::Zeroizing;
22
23pub use self::limits::Limits;
24use self::wire::{read_head, read_hello, refuse, respond, summary};
25use crate::audit::{Connection, Recorder};
26use crate::body::{self, BodyEnd, Framing};
27use crate::broker::{Broker, Watch};
28use crate::decide::{self, RULE_REQUEST, RULE_SNI};
29use crate::local::{self, LocalService};
30use crate::request::{Request, Target};
31use crate::session::{RULE_SESSION, Sessions};
32use crate::slots::{PENDING, Share, Shares};
33use crate::tunnel::{self, Forward};
34use crate::upstream::{self, Refusal, Resolve};
35
36/// Why a request whose chunked body cannot be followed is refused.
37const MALFORMED_CHUNKS: &str = "malformed chunked request body";
38
39/// The proxy: a policy, a resolver, an audit sink and limits.
40#[derive(Clone, Copy)]
41pub struct Proxy<'a> {
42    /// The policy hosts are decided by.
43    pub policy: &'a CompiledPolicy<'a>,
44    /// Name resolution.
45    pub resolver: &'a dyn Resolve,
46    /// Where every decision is recorded.
47    pub recorder: &'a dyn Recorder,
48    /// Resource limits.
49    pub limits: &'a Limits,
50    /// Brokered secrets; [`Broker::default`] for none.
51    pub broker: &'a Broker,
52    /// Loopback addresses the proxy may connect to despite the loopback ban.
53    /// Empty in `moat proxy`; integration tests list their local server.
54    pub loopback_ok: &'a [SocketAddr],
55    /// Forward a `ClientHello` that carries an encrypted server name (ECH,
56    /// ESNI); refused when false (`sandbox.proxy_allow_ech`).
57    pub allow_ech: bool,
58    /// Session mode (ADR-027): every request must carry one of these
59    /// credentials, and is decided with that session's taint and connection
60    /// share. `None` serves any local client, as the standalone `moat proxy`
61    /// does for the host sandboxes, which cannot send a credential.
62    pub sessions: Option<&'a Sessions<'a>>,
63    /// A service of this process that shares the port (ADR-028): `moat run`
64    /// answers its sandboxed hooks here. `None` in `moat proxy`.
65    pub local: Option<&'a dyn LocalService>,
66}
67
68impl std::fmt::Debug for Proxy<'_> {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("Proxy")
71            .field("limits", &self.limits)
72            .field("broker", &self.broker)
73            .field("loopback_ok", &self.loopback_ok)
74            .field("allow_ech", &self.allow_ech)
75            .field("sessions", &self.sessions)
76            .field("local", &self.local.is_some())
77            .finish_non_exhaustive()
78    }
79}
80
81/// What every connection of one [`Proxy::serve`] shares.
82struct Served<'s> {
83    addresses: &'s CompiledPolicy<'s>,
84    shares: Shares,
85}
86
87/// One connection's session (`None` without session mode) and start time,
88/// which each of its audit rows carries.
89#[derive(Clone, Copy)]
90struct Turn<'s> {
91    session: Option<&'s str>,
92    started: Instant,
93}
94
95impl Proxy<'_> {
96    /// Serve connections from `listener` until accepting fails.
97    /// Fails at once when the [`Limits`] are zero or too large.
98    pub fn serve(&self, listener: &TcpListener) -> io::Result<()> {
99        self.limits.check()?;
100        let address_policy = decide::address_policy(self.policy.policy());
101        let addresses = CompiledPolicy::compile(&address_policy, self.policy.context())
102            .map_err(io::Error::other)?;
103        let served = Served {
104            addresses: &addresses,
105            shares: Shares::new(self.limits.max_connections),
106        };
107        let served = &served;
108        let active = AtomicUsize::new(0);
109        thread::scope(|scope| {
110            loop {
111                let client = match listener.accept() {
112                    Ok((client, _)) => client,
113                    Err(e) if is_transient(&e) => continue,
114                    Err(e) => return Err(e),
115                };
116                let _ = client.set_write_timeout(Some(self.limits.handshake_timeout));
117                let slot = Slot::take(&active, self.limits.max_connections);
118                let Some(slot) = slot else {
119                    respond(&client, 503, "too many connections");
120                    continue;
121                };
122                // Until it authenticates, a connection counts against the
123                // share of all such connections (#555).
124                let pending = if self.sessions.is_some() {
125                    let Some(share) = served.shares.take(PENDING) else {
126                        respond(&client, 503, "too many connections waiting to authenticate");
127                        continue;
128                    };
129                    Some(share)
130                } else {
131                    None
132                };
133                // A thread that cannot start drops (closes) this connection;
134                // `scope.spawn` would panic and take the proxy down instead.
135                let _ = thread::Builder::new().spawn_scoped(scope, move || {
136                    let _slot = slot;
137                    self.handle(&client, served, pending);
138                });
139            }
140        })
141    }
142
143    fn handle(&self, client: &TcpStream, served: &Served<'_>, pending: Option<Share<'_>>) {
144        let started = Instant::now();
145        let timeout = Some(self.limits.handshake_timeout);
146        if client.set_read_timeout(timeout).is_err() || client.set_write_timeout(timeout).is_err() {
147            return;
148        }
149        // The head may hold the session's credential: zeroed once dropped.
150        let mut buf = Zeroizing::new(Vec::new());
151        let deadline = started + self.limits.handshake_timeout;
152        // Still counted as waiting to authenticate: the service checks its own token.
153        if let Some(local) = self.local
154            && !local.prefix().is_empty()
155            && local::starts_with(client, local.prefix(), deadline)
156        {
157            local.serve(client);
158            return;
159        }
160        let request = match read_head(client, &mut buf, self.limits.max_head_bytes, deadline) {
161            Ok(request) => request,
162            Err(why) => {
163                let decision = Decision::single(Verdict::Deny, RULE_REQUEST, why);
164                let turn = Turn {
165                    session: None,
166                    started,
167                };
168                self.record(turn, None, &decision);
169                refuse(client, 400, &decision.reasons[0]);
170                return;
171            }
172        };
173        let Some((turn, _share)) = self.admit(client, &request, served, started) else {
174            return;
175        };
176        drop(pending);
177        // `buf` holds the head and any body bytes read with it. Watching all of
178        // it before `rest` is split off leaves its end in the watch's tail, so a
179        // secret split between `rest` and the next body chunk is still found.
180        let mut watch = Watch::new(self.broker, request.host());
181        if let Some(leak) = watch.next(&buf) {
182            let decision = leak.decision(request.host());
183            self.record(turn, Some(&request), &decision);
184            refuse(client, 403, &summary(&decision));
185            return;
186        }
187        let rest = Zeroizing::new(buf.split_off(request.head_len));
188        let decision = decide::host(self.policy, request.host());
189        if decision.verdict != Verdict::Allow {
190            self.record(turn, Some(&request), &decision);
191            refuse(client, 403, &summary(&decision));
192            return;
193        }
194        if let Some(refusal) = self.tainted(turn, &request) {
195            self.record(turn, Some(&request), &refusal);
196            refuse(client, 403, &summary(&refusal));
197            return;
198        }
199        // A plain-HTTP request is one request: its body is forwarded, and
200        // nothing the client sends after it (body.rs). A tunnel carries TLS,
201        // where the proxy cannot see a secret or a second request.
202        let mut body = BodyEnd::new(match &request.target {
203            Target::Http { head, .. } => body::framing(head),
204            Target::Connect { .. } => Framing::None,
205        });
206        let Ok(body_in_rest) = body.feed(&rest) else {
207            let decision = Decision::single(Verdict::Deny, RULE_REQUEST, MALFORMED_CHUNKS);
208            self.record(turn, Some(&request), &decision);
209            refuse(client, 400, MALFORMED_CHUNKS);
210            return;
211        };
212        let upstream = match upstream::connect(
213            self.resolver,
214            request.host(),
215            request.port(),
216            served.addresses,
217            self.loopback_ok,
218            self.limits.connect_timeout,
219        ) {
220            Ok(upstream) => upstream,
221            Err(refusal) => {
222                let decision = refusal.decision(request.host());
223                self.record(turn, Some(&request), &decision);
224                let status = if matches!(refusal, Refusal::Address(..) | Refusal::Denied(..)) {
225                    403
226                } else {
227                    502
228                };
229                refuse(client, status, &summary(&decision));
230                return;
231            }
232        };
233        // The body keeps being watched for brokered secrets as it goes by.
234        let mut inspect = |chunk: &[u8]| {
235            let Ok(n) = body.feed(chunk) else {
236                return Forward::Stop;
237            };
238            if let Some(leak) = watch.next(&chunk[..n]) {
239                self.record(turn, Some(&request), &leak.decision(request.host()));
240                return Forward::Stop;
241            }
242            if n < chunk.len() || body.done() {
243                Forward::Part(n)
244            } else {
245                Forward::All
246            }
247        };
248        let (first_bytes, inspect) = match &request.target {
249            Target::Connect { host, .. } => {
250                match self.check_tunnel(client, host, rest.to_vec(), &decision, &request, turn) {
251                    Some(hello) => (Zeroizing::new(hello), None),
252                    None => return,
253                }
254            }
255            Target::Http { head, host, .. } => {
256                let mut decision = decision.clone();
257                decision
258                    .reasons
259                    .extend(self.broker.withheld(host).iter().map(|id| {
260                        format!("secret `{id}` not injected: plain HTTP needs `plain_http: true`")
261                    }));
262                if !self.record(turn, Some(&request), &decision) {
263                    refuse(client, 403, "audit log unavailable");
264                    return;
265                }
266                let inspect: tunnel::Inspect<'_> = &mut inspect;
267                // Only the owner host's head carries a value; it is zeroed
268                // with `first_bytes` once written.
269                let injected = self.broker.inject(host, head);
270                let head = injected.as_deref().map_or(head.as_slice(), Vec::as_slice);
271                let body_bytes = &rest[..body_in_rest];
272                (Zeroizing::new([head, body_bytes].concat()), Some(inspect))
273            }
274        };
275        if (&upstream).write_all(&first_bytes).is_ok() {
276            tunnel::relay(client, &upstream, self.limits.idle_timeout, inspect);
277        }
278    }
279
280    /// The connection's session and its share of the slots, or `None` once
281    /// the request has been refused and recorded: a missing or invalid
282    /// credential gets `407`, a session holding its share `503`. Without
283    /// session mode every request is admitted, with no session.
284    fn admit<'s>(
285        &'s self,
286        client: &TcpStream,
287        request: &Request,
288        served: &'s Served<'_>,
289        started: Instant,
290    ) -> Option<(Turn<'s>, Option<Share<'s>>)> {
291        let mut turn = Turn {
292            session: None,
293            started,
294        };
295        let Some(sessions) = self.sessions else {
296            return Some((turn, None));
297        };
298        let session = match sessions.verify(&request.credentials) {
299            Ok(session) => session,
300            Err(why) => {
301                let decision = Decision::single(Verdict::Deny, RULE_SESSION, why.to_string());
302                self.record(turn, Some(request), &decision);
303                refuse(client, 407, &summary(&decision));
304                return None;
305            }
306        };
307        turn.session = Some(session);
308        if let Some(share) = served.shares.take(session) {
309            return Some((turn, Some(share)));
310        }
311        let decision = Decision::single(
312            Verdict::Deny,
313            RULE_SESSION,
314            format!(
315                "the session already holds {} connections, half of the proxy's",
316                served.shares.cap()
317            ),
318        );
319        self.record(turn, Some(request), &decision);
320        refuse(client, 503, &summary(&decision));
321        None
322    }
323
324    /// The refusal the connection's session taint calls for, if any. A
325    /// session whose history cannot be read is refused (fail closed).
326    fn tainted(&self, turn: Turn<'_>, request: &Request) -> Option<Decision> {
327        let (Some(sessions), Some(session)) = (self.sessions, turn.session) else {
328            return None;
329        };
330        match sessions.taint(session) {
331            Ok(taint) => decide::tainted(self.policy, request.host(), request.port(), &taint),
332            Err(why) => Some(Decision::single(
333                Verdict::Deny,
334                RULE_SESSION,
335                format!("the session's history cannot be read, so its taint is unknown: {why}"),
336            )),
337        }
338    }
339
340    /// Answer the CONNECT, then hold the tunnel's first bytes until its SNI
341    /// is known to name the CONNECT host. Returns the bytes to forward. Every
342    /// outcome is recorded, a client that cannot be answered too: the
343    /// upstream connection is already open.
344    fn check_tunnel(
345        &self,
346        client: &TcpStream,
347        host: &str,
348        mut hello: Vec<u8>,
349        allowed: &Decision,
350        request: &Request,
351        turn: Turn<'_>,
352    ) -> Option<Vec<u8>> {
353        let answered = (&*client).write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n");
354        let deadline = Instant::now() + self.limits.handshake_timeout;
355        let read = answered
356            .map_err(|e| format!("answering the CONNECT: {e}"))
357            .and_then(|()| read_hello(client, &mut hello, deadline));
358        let refusal = match read {
359            // With ECH the server acts on the encrypted inner name, which may
360            // be another site behind the same provider as the allowed host.
361            Ok((_, true)) if !self.allow_ech => Some(format!(
362                "TLS ClientHello for CONNECT {host} carries an encrypted server name (ECH), \
363                 so the server may serve another site; `sandbox.proxy_allow_ech: true` \
364                 in the policy allows it"
365            )),
366            Ok((Some(name), _)) if name == host => None,
367            Ok((Some(name), _)) => Some(format!(
368                "TLS server name {name} does not match CONNECT {host}"
369            )),
370            // An IP literal is never sent as SNI (RFC 6066 §3). The bytes are
371            // still a complete `ClientHello`: anything else is an `Err`.
372            Ok((None, _)) if host.parse::<std::net::IpAddr>().is_ok() => None,
373            Ok((None, _)) => Some(format!(
374                "TLS ClientHello for CONNECT {host} has no server name"
375            )),
376            Err(why) => Some(why),
377        };
378        if let Some(why) = refusal {
379            let decision = Decision::single(Verdict::Deny, RULE_SNI, why);
380            self.record(turn, Some(request), &decision);
381            return None;
382        }
383        self.record(turn, Some(request), allowed).then_some(hello)
384    }
385
386    /// Record a decision; `false` when it could not be recorded, which the
387    /// caller must treat as a deny.
388    fn record(&self, turn: Turn<'_>, request: Option<&Request>, decision: &Decision) -> bool {
389        let connection = Connection {
390            session: turn.session,
391            method: request.map(|r| r.method.as_str()),
392            destination: request.map(|r| (r.host(), r.port())),
393            decision,
394            latency_us: u64::try_from(turn.started.elapsed().as_micros()).unwrap_or(u64::MAX),
395        };
396        match self.recorder.record(&connection) {
397            Ok(()) => true,
398            Err(error) => {
399                // The caller refuses the connection; the recorder reports the
400                // failure elsewhere, since the audit log is what failed.
401                self.recorder.unrecorded(&error);
402                false
403            }
404        }
405    }
406}
407
408/// One of the `max_connections` slots, released when dropped, even if the
409/// connection's thread panics.
410struct Slot<'a>(&'a AtomicUsize);
411
412impl<'a> Slot<'a> {
413    fn take(active: &'a AtomicUsize, max: usize) -> Option<Self> {
414        // A slot taken over the cap is given back at once; the count never
415        // stays above `max`.
416        let slot = Self(active);
417        (active.fetch_add(1, Ordering::AcqRel) < max).then_some(slot)
418    }
419}
420
421impl Drop for Slot<'_> {
422    fn drop(&mut self) {
423        self.0.fetch_sub(1, Ordering::AcqRel);
424    }
425}
426
427fn is_transient(e: &io::Error) -> bool {
428    matches!(
429        e.kind(),
430        ErrorKind::Interrupted | ErrorKind::ConnectionAborted | ErrorKind::ConnectionReset
431    )
432}
433
434#[cfg(test)]
435mod tests;