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