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