Skip to main content

openmoat_proxy/
server.rs

1//! Accepting connections and deciding each one.
2//!
3//! Order per connection: read the head → check it for brokered secrets bound
4//! elsewhere → decide the host → resolve and check the addresses → connect →
5//! (CONNECT) answer 200, read the `ClientHello` and check its SNI → record →
6//! relay, watching a plain-HTTP body for brokered secrets too. Nothing reaches
7//! the destination before the record is written; a failed record closes the
8//! connection.
9
10use std::io::{self, ErrorKind, Read as _, Write as _};
11use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::thread;
14use std::time::{Duration, Instant};
15
16use openmoat_core::{CompiledPolicy, Decision, Verdict};
17use zeroize::Zeroizing;
18
19use crate::audit::{Connection, Recorder};
20use crate::body::{self, BodyEnd, Framing};
21use crate::broker::{Broker, Watch};
22use crate::decide::{self, RULE_REQUEST, RULE_SNI};
23use crate::request::{self, Request, Target};
24use crate::sni::{self, Hello};
25use crate::tunnel::{self, Forward};
26use crate::upstream::{self, Refusal, Resolve};
27
28/// Why a request whose chunked body cannot be followed is refused.
29const MALFORMED_CHUNKS: &str = "malformed chunked request body";
30
31/// How long, and how much input, a refused connection is drained for.
32const LINGER: Duration = Duration::from_secs(1);
33const LINGER_BYTES: usize = 1 << 20;
34
35/// Bounds on what one client can make the proxy hold.
36#[derive(Debug, Clone)]
37pub struct Limits {
38    /// Largest request head, in bytes.
39    pub max_head_bytes: usize,
40    /// Time allowed for the request head, and for the `ClientHello` after it.
41    pub handshake_timeout: Duration,
42    /// A relayed connection with no bytes either way for this long is closed.
43    pub idle_timeout: Duration,
44    /// Time allowed to connect to the destination.
45    pub connect_timeout: Duration,
46    /// Connections served at once; more are answered 503 and closed.
47    pub max_connections: usize,
48}
49
50impl Limits {
51    /// Refuse limits under which no connection could be served: a zero limit,
52    /// or a timeout too large to set a deadline with.
53    fn check(&self) -> io::Result<()> {
54        let now = Instant::now();
55        let sizes = [
56            ("max_head_bytes", self.max_head_bytes),
57            ("max_connections", self.max_connections),
58        ];
59        let timeouts = [
60            ("handshake_timeout", self.handshake_timeout),
61            ("idle_timeout", self.idle_timeout),
62            ("connect_timeout", self.connect_timeout),
63        ];
64        let bad = sizes
65            .iter()
66            .find(|(_, n)| *n == 0)
67            .map(|(name, _)| name)
68            .or_else(|| {
69                timeouts
70                    .iter()
71                    .find(|(_, d)| d.is_zero() || now.checked_add(*d).is_none())
72                    .map(|(name, _)| name)
73            });
74        match bad {
75            Some(name) => Err(io::Error::new(
76                ErrorKind::InvalidInput,
77                format!("proxy limit `{name}` is zero or too large"),
78            )),
79            None => Ok(()),
80        }
81    }
82}
83
84impl Default for Limits {
85    fn default() -> Self {
86        Self {
87            max_head_bytes: 16 * 1024,
88            handshake_timeout: Duration::from_secs(10),
89            idle_timeout: Duration::from_mins(2),
90            connect_timeout: Duration::from_secs(10),
91            max_connections: 256,
92        }
93    }
94}
95
96/// The proxy: a policy, a resolver, an audit sink and limits.
97#[derive(Clone, Copy)]
98pub struct Proxy<'a> {
99    /// The policy hosts are decided by.
100    pub policy: &'a CompiledPolicy<'a>,
101    /// Name resolution.
102    pub resolver: &'a dyn Resolve,
103    /// Where every decision is recorded.
104    pub recorder: &'a dyn Recorder,
105    /// Resource limits.
106    pub limits: &'a Limits,
107    /// Brokered secrets; [`Broker::default`] for none.
108    pub broker: &'a Broker,
109    /// Loopback addresses the proxy may connect to despite the loopback ban.
110    /// Empty in `moat proxy`; integration tests list their local server.
111    pub loopback_ok: &'a [SocketAddr],
112}
113
114impl std::fmt::Debug for Proxy<'_> {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("Proxy")
117            .field("limits", &self.limits)
118            .field("broker", &self.broker)
119            .field("loopback_ok", &self.loopback_ok)
120            .finish_non_exhaustive()
121    }
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 addresses = &addresses;
133        let active = AtomicUsize::new(0);
134        thread::scope(|scope| {
135            loop {
136                let client = match listener.accept() {
137                    Ok((client, _)) => client,
138                    Err(e) if is_transient(&e) => continue,
139                    Err(e) => return Err(e),
140                };
141                let _ = client.set_write_timeout(Some(self.limits.handshake_timeout));
142                let slot = Slot::take(&active, self.limits.max_connections);
143                let Some(slot) = slot else {
144                    respond(&client, 503, "too many connections");
145                    continue;
146                };
147                // A thread that cannot start drops (closes) this connection;
148                // `scope.spawn` would panic and take the proxy down instead.
149                let _ = thread::Builder::new().spawn_scoped(scope, move || {
150                    let _slot = slot;
151                    self.handle(&client, addresses);
152                });
153            }
154        })
155    }
156
157    fn handle(&self, client: &TcpStream, addresses: &CompiledPolicy<'_>) {
158        let started = Instant::now();
159        let timeout = Some(self.limits.handshake_timeout);
160        if client.set_read_timeout(timeout).is_err() || client.set_write_timeout(timeout).is_err() {
161            return;
162        }
163        let mut buf = Vec::new();
164        let deadline = started + self.limits.handshake_timeout;
165        let request = match read_head(client, &mut buf, self.limits.max_head_bytes, deadline) {
166            Ok(request) => request,
167            Err(why) => {
168                let decision = Decision::single(Verdict::Deny, RULE_REQUEST, why);
169                self.record(None, &decision, started);
170                refuse(client, 400, &decision.reasons[0]);
171                return;
172            }
173        };
174        // `buf` holds the head and any body bytes read with it. Watching all of
175        // it before `rest` is split off leaves its end in the watch's tail, so a
176        // secret split between `rest` and the next body chunk is still found.
177        let mut watch = Watch::new(self.broker, request.host());
178        if let Some(leak) = watch.next(&buf) {
179            let decision = leak.decision(request.host());
180            self.record(Some(&request), &decision, started);
181            refuse(client, 403, &summary(&decision));
182            return;
183        }
184        let rest = buf.split_off(request.head_len);
185        let decision = decide::host(self.policy, request.host());
186        if decision.verdict != Verdict::Allow {
187            self.record(Some(&request), &decision, started);
188            refuse(client, 403, &summary(&decision));
189            return;
190        }
191        // A plain-HTTP request is one request: its body is forwarded, and
192        // nothing the client sends after it (body.rs). A tunnel carries TLS,
193        // where the proxy cannot see a secret or a second request.
194        let mut body = BodyEnd::new(match &request.target {
195            Target::Http { head, .. } => body::framing(head),
196            Target::Connect { .. } => Framing::None,
197        });
198        let Ok(body_in_rest) = body.feed(&rest) else {
199            let decision = Decision::single(Verdict::Deny, RULE_REQUEST, MALFORMED_CHUNKS);
200            self.record(Some(&request), &decision, started);
201            refuse(client, 400, MALFORMED_CHUNKS);
202            return;
203        };
204        let upstream = match upstream::connect(
205            self.resolver,
206            request.host(),
207            request.port(),
208            addresses,
209            self.loopback_ok,
210            self.limits.connect_timeout,
211        ) {
212            Ok(upstream) => upstream,
213            Err(refusal) => {
214                let decision = refusal.decision(request.host());
215                self.record(Some(&request), &decision, started);
216                let status = if matches!(refusal, Refusal::Address(..)) {
217                    403
218                } else {
219                    502
220                };
221                refuse(client, status, &summary(&decision));
222                return;
223            }
224        };
225        // The body keeps being watched for brokered secrets as it goes by.
226        let mut inspect = |chunk: &[u8]| {
227            let Ok(n) = body.feed(chunk) else {
228                return Forward::Stop;
229            };
230            if let Some(leak) = watch.next(&chunk[..n]) {
231                self.record(Some(&request), &leak.decision(request.host()), started);
232                return Forward::Stop;
233            }
234            if n < chunk.len() || body.done() {
235                Forward::Part(n)
236            } else {
237                Forward::All
238            }
239        };
240        let (first_bytes, inspect) = match &request.target {
241            Target::Connect { host, .. } => {
242                match self.check_tunnel(client, host, rest, &decision, &request, started) {
243                    Some(hello) => (Zeroizing::new(hello), None),
244                    None => return,
245                }
246            }
247            Target::Http { head, host, .. } => {
248                let mut decision = decision.clone();
249                decision
250                    .reasons
251                    .extend(self.broker.withheld(host).iter().map(|id| {
252                        format!("secret `{id}` not injected: plain HTTP needs `plain_http: true`")
253                    }));
254                if !self.record(Some(&request), &decision, started) {
255                    refuse(client, 403, "audit log unavailable");
256                    return;
257                }
258                let inspect: tunnel::Inspect<'_> = &mut inspect;
259                // Only the owner host's head carries a value; it is zeroed
260                // with `first_bytes` once written.
261                let injected = self.broker.inject(host, head);
262                let head = injected.as_deref().map_or(head.as_slice(), Vec::as_slice);
263                let body_bytes = &rest[..body_in_rest];
264                (Zeroizing::new([head, body_bytes].concat()), Some(inspect))
265            }
266        };
267        if (&upstream).write_all(&first_bytes).is_ok() {
268            tunnel::relay(client, &upstream, self.limits.idle_timeout, inspect);
269        }
270    }
271
272    /// Answer the CONNECT, then hold the tunnel's first bytes until its SNI
273    /// is known to name the CONNECT host. Returns the bytes to forward. Every
274    /// outcome is recorded, a client that cannot be answered too: the
275    /// upstream connection is already open.
276    fn check_tunnel(
277        &self,
278        client: &TcpStream,
279        host: &str,
280        mut hello: Vec<u8>,
281        allowed: &Decision,
282        request: &Request,
283        started: Instant,
284    ) -> Option<Vec<u8>> {
285        let answered = (&*client).write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n");
286        let deadline = Instant::now() + self.limits.handshake_timeout;
287        let read = answered
288            .map_err(|e| format!("answering the CONNECT: {e}"))
289            .and_then(|()| read_hello(client, &mut hello, deadline));
290        let refusal = match read {
291            Ok(Some(name)) if name == host => None,
292            Ok(Some(name)) => Some(format!(
293                "TLS server name {name} does not match CONNECT {host}"
294            )),
295            // An IP literal is never sent as SNI (RFC 6066 §3). The bytes are
296            // still a complete `ClientHello`: anything else is an `Err`.
297            Ok(None) if host.parse::<std::net::IpAddr>().is_ok() => None,
298            Ok(None) => Some(format!(
299                "TLS ClientHello for CONNECT {host} has no server name"
300            )),
301            Err(why) => Some(why),
302        };
303        if let Some(why) = refusal {
304            let decision = Decision::single(Verdict::Deny, RULE_SNI, why);
305            self.record(Some(request), &decision, started);
306            return None;
307        }
308        self.record(Some(request), allowed, started)
309            .then_some(hello)
310    }
311
312    /// Record a decision; `false` when it could not be recorded, which the
313    /// caller must treat as a deny.
314    fn record(&self, request: Option<&Request>, decision: &Decision, started: Instant) -> bool {
315        let connection = Connection {
316            method: request.map(|r| r.method.as_str()),
317            destination: request.map(|r| (r.host(), r.port())),
318            decision,
319            latency_us: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
320        };
321        match self.recorder.record(&connection) {
322            Ok(()) => true,
323            Err(error) => {
324                // The caller refuses the connection; the recorder reports the
325                // failure elsewhere, since the audit log is what failed.
326                self.recorder.unrecorded(&error);
327                false
328            }
329        }
330    }
331}
332
333/// One of the `max_connections` slots, released when dropped, even if the
334/// connection's thread panics.
335struct Slot<'a>(&'a AtomicUsize);
336
337impl<'a> Slot<'a> {
338    fn take(active: &'a AtomicUsize, max: usize) -> Option<Self> {
339        // A slot taken over the cap is given back at once; the count never
340        // stays above `max`.
341        let slot = Self(active);
342        (active.fetch_add(1, Ordering::AcqRel) < max).then_some(slot)
343    }
344}
345
346impl Drop for Slot<'_> {
347    fn drop(&mut self) {
348        self.0.fetch_sub(1, Ordering::AcqRel);
349    }
350}
351
352/// Read until a complete request head is in `buf`.
353fn read_head(
354    client: &TcpStream,
355    buf: &mut Vec<u8>,
356    max: usize,
357    deadline: Instant,
358) -> Result<Request, String> {
359    loop {
360        match request::parse(buf) {
361            Ok(Some(request)) => return Ok(request),
362            Ok(None) if buf.len() >= max => {
363                return Err(format!("request head larger than {max} bytes"));
364            }
365            Ok(None) => read_more(client, buf, max, deadline)?,
366            Err(e) => return Err(e.to_string()),
367        }
368    }
369}
370
371/// Read until `buf` holds a complete `ClientHello`; its SNI, if any.
372fn read_hello(
373    client: &TcpStream,
374    buf: &mut Vec<u8>,
375    deadline: Instant,
376) -> Result<Option<String>, String> {
377    // Records add 5 bytes per 16 KiB of handshake, so this covers MAX_HELLO.
378    let max = sni::MAX_HELLO + 1024;
379    loop {
380        match sni::server_name(buf) {
381            Ok(Hello::ServerName(name)) => return Ok(name),
382            Ok(Hello::Incomplete) if buf.len() >= max => {
383                return Err(sni::HelloError::TooLarge.to_string());
384            }
385            Ok(Hello::Incomplete) => read_more(client, buf, max, deadline)?,
386            Err(e) => return Err(e.to_string()),
387        }
388    }
389}
390
391/// One read, bounded by `deadline` for the whole head or `ClientHello`, so a
392/// client trickling one byte at a time cannot hold its slot indefinitely.
393fn read_more(
394    client: &TcpStream,
395    buf: &mut Vec<u8>,
396    max: usize,
397    deadline: Instant,
398) -> Result<(), String> {
399    let left = deadline.saturating_duration_since(Instant::now());
400    if left.is_zero() || client.set_read_timeout(Some(left)).is_err() {
401        return Err("the client did not finish its handshake in time".to_owned());
402    }
403    let mut chunk = [0u8; 4096];
404    let want = chunk.len().min(max.saturating_sub(buf.len())).max(1);
405    match (&*client).read(&mut chunk[..want]) {
406        Ok(0) => Err("client closed the connection early".to_owned()),
407        Ok(n) => {
408            buf.extend_from_slice(&chunk[..n]);
409            Ok(())
410        }
411        Err(e) if e.kind() == ErrorKind::Interrupted => Ok(()),
412        Err(e) => Err(format!("reading from the client: {e}")),
413    }
414}
415
416fn summary(decision: &Decision) -> String {
417    format!(
418        "{} ({})",
419        decision.reasons.join("; "),
420        decision.rules.join(", ")
421    )
422}
423
424/// Answer `status` from a connection's own thread, then close it so the
425/// client can read the answer.
426///
427/// Closing a socket with unread input resets the connection, and on Windows
428/// the reset discards the answer before the client reads it. So stop writing,
429/// then drain what the client still sends, bounded by [`LINGER`] and
430/// [`LINGER_BYTES`], before the socket is dropped.
431fn refuse(client: &TcpStream, status: u16, message: &str) {
432    respond(client, status, message);
433    let _ = client.shutdown(Shutdown::Write);
434    let until = Instant::now() + LINGER;
435    let mut left = LINGER_BYTES;
436    let mut sink = [0; 8192];
437    while left > 0 {
438        let rest = until.saturating_duration_since(Instant::now());
439        if rest.is_zero() || client.set_read_timeout(Some(rest)).is_err() {
440            return;
441        }
442        match (&*client).read(&mut sink) {
443            Ok(0) | Err(_) => return,
444            Ok(n) => left = left.saturating_sub(n),
445        }
446    }
447}
448
449fn respond(client: &TcpStream, status: u16, message: &str) {
450    let reason = match status {
451        400 => "Bad Request",
452        403 => "Forbidden",
453        502 => "Bad Gateway",
454        _ => "Service Unavailable",
455    };
456    // The message may quote the request; no control byte of it is echoed.
457    let message: String = message
458        .chars()
459        .map(|c| if c.is_control() { '?' } else { c })
460        .collect();
461    let body = format!("moat proxy: {message}\n");
462    let response = format!(
463        "HTTP/1.1 {status} {reason}\r\nContent-Type: text/plain; charset=utf-8\r\n\
464         Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
465        body.len()
466    );
467    let _ = (&*client).write_all(response.as_bytes());
468}
469
470fn is_transient(e: &io::Error) -> bool {
471    matches!(
472        e.kind(),
473        ErrorKind::Interrupted | ErrorKind::ConnectionAborted | ErrorKind::ConnectionReset
474    )
475}
476
477#[cfg(test)]
478mod tests;