1use 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
28const MALFORMED_CHUNKS: &str = "malformed chunked request body";
30
31const LINGER: Duration = Duration::from_secs(1);
33const LINGER_BYTES: usize = 1 << 20;
34
35#[derive(Debug, Clone)]
37pub struct Limits {
38 pub max_head_bytes: usize,
40 pub handshake_timeout: Duration,
42 pub idle_timeout: Duration,
44 pub connect_timeout: Duration,
46 pub max_connections: usize,
48}
49
50impl Limits {
51 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#[derive(Clone, Copy)]
98pub struct Proxy<'a> {
99 pub policy: &'a CompiledPolicy<'a>,
101 pub resolver: &'a dyn Resolve,
103 pub recorder: &'a dyn Recorder,
105 pub limits: &'a Limits,
107 pub broker: &'a Broker,
109 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 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 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 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 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 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 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 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 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 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 self.recorder.unrecorded(&error);
327 false
328 }
329 }
330 }
331}
332
333struct Slot<'a>(&'a AtomicUsize);
336
337impl<'a> Slot<'a> {
338 fn take(active: &'a AtomicUsize, max: usize) -> Option<Self> {
339 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
352fn 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
371fn read_hello(
373 client: &TcpStream,
374 buf: &mut Vec<u8>,
375 deadline: Instant,
376) -> Result<Option<String>, String> {
377 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
391fn 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
424fn 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 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;