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