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::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
27const LINGER: Duration = Duration::from_secs(1);
29const LINGER_BYTES: usize = 1 << 20;
30
31#[derive(Debug, Clone)]
33pub struct Limits {
34 pub max_head_bytes: usize,
36 pub handshake_timeout: Duration,
38 pub idle_timeout: Duration,
40 pub connect_timeout: Duration,
42 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#[derive(Clone, Copy)]
60pub struct Proxy<'a> {
61 pub policy: &'a CompiledPolicy<'a>,
63 pub resolver: &'a dyn Resolve,
65 pub recorder: &'a dyn Recorder,
67 pub limits: &'a Limits,
69 pub broker: &'a Broker,
71 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 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 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 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 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 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 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 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 self.recorder.unrecorded(&error);
263 false
264 }
265 }
266 }
267}
268
269struct Slot<'a>(&'a AtomicUsize);
272
273impl<'a> Slot<'a> {
274 fn take(active: &'a AtomicUsize, max: usize) -> Option<Self> {
275 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
288fn 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
307fn read_hello(
309 client: &TcpStream,
310 buf: &mut Vec<u8>,
311 deadline: Instant,
312) -> Result<Option<String>, String> {
313 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
327fn 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
360fn 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}