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