1mod limits;
12mod wire;
13
14use std::io::{self, ErrorKind, Write as _};
15use std::net::{SocketAddr, TcpListener, TcpStream};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::thread;
18use std::time::Instant;
19
20use openmoat_core::{CompiledPolicy, Decision, Verdict};
21use zeroize::Zeroizing;
22
23pub use self::limits::Limits;
24use self::wire::{read_head, read_hello, refuse, respond, summary};
25use crate::audit::{Connection, Recorder};
26use crate::body::{self, BodyEnd, Framing};
27use crate::broker::{Broker, Watch};
28use crate::decide::{self, RULE_REQUEST, RULE_SNI};
29use crate::local::{self, LocalService};
30use crate::request::{Request, Target};
31use crate::session::{RULE_SESSION, Sessions};
32use crate::slots::{PENDING, Share, Shares};
33use crate::tunnel::{self, Forward};
34use crate::upstream::{self, Refusal, Resolve};
35
36const MALFORMED_CHUNKS: &str = "malformed chunked request body";
38
39#[derive(Clone, Copy)]
41pub struct Proxy<'a> {
42 pub policy: &'a CompiledPolicy<'a>,
44 pub resolver: &'a dyn Resolve,
46 pub recorder: &'a dyn Recorder,
48 pub limits: &'a Limits,
50 pub broker: &'a Broker,
52 pub loopback_ok: &'a [SocketAddr],
55 pub allow_ech: bool,
58 pub sessions: Option<&'a Sessions<'a>>,
63 pub local: Option<&'a dyn LocalService>,
66}
67
68impl std::fmt::Debug for Proxy<'_> {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("Proxy")
71 .field("limits", &self.limits)
72 .field("broker", &self.broker)
73 .field("loopback_ok", &self.loopback_ok)
74 .field("allow_ech", &self.allow_ech)
75 .field("sessions", &self.sessions)
76 .field("local", &self.local.is_some())
77 .finish_non_exhaustive()
78 }
79}
80
81struct Served<'s> {
83 addresses: &'s CompiledPolicy<'s>,
84 shares: Shares,
85}
86
87#[derive(Clone, Copy)]
90struct Turn<'s> {
91 session: Option<&'s str>,
92 started: Instant,
93}
94
95impl Proxy<'_> {
96 pub fn serve(&self, listener: &TcpListener) -> io::Result<()> {
99 self.limits.check()?;
100 let address_policy = decide::address_policy(self.policy.policy());
101 let addresses = CompiledPolicy::compile(&address_policy, self.policy.context())
102 .map_err(io::Error::other)?;
103 let served = Served {
104 addresses: &addresses,
105 shares: Shares::new(self.limits.max_connections),
106 };
107 let served = &served;
108 let active = AtomicUsize::new(0);
109 thread::scope(|scope| {
110 loop {
111 let client = match listener.accept() {
112 Ok((client, _)) => client,
113 Err(e) if is_transient(&e) => continue,
114 Err(e) => return Err(e),
115 };
116 let _ = client.set_write_timeout(Some(self.limits.handshake_timeout));
117 let slot = Slot::take(&active, self.limits.max_connections);
118 let Some(slot) = slot else {
119 respond(&client, 503, "too many connections");
120 continue;
121 };
122 let pending = if self.sessions.is_some() {
125 let Some(share) = served.shares.take(PENDING) else {
126 respond(&client, 503, "too many connections waiting to authenticate");
127 continue;
128 };
129 Some(share)
130 } else {
131 None
132 };
133 let _ = thread::Builder::new().spawn_scoped(scope, move || {
136 let _slot = slot;
137 self.handle(&client, served, pending);
138 });
139 }
140 })
141 }
142
143 fn handle(&self, client: &TcpStream, served: &Served<'_>, pending: Option<Share<'_>>) {
144 let started = Instant::now();
145 let timeout = Some(self.limits.handshake_timeout);
146 if client.set_read_timeout(timeout).is_err() || client.set_write_timeout(timeout).is_err() {
147 return;
148 }
149 let mut buf = Zeroizing::new(Vec::new());
151 let deadline = started + self.limits.handshake_timeout;
152 if let Some(local) = self.local
154 && !local.prefix().is_empty()
155 && local::starts_with(client, local.prefix(), deadline)
156 {
157 local.serve(client);
158 return;
159 }
160 let request = match read_head(client, &mut buf, self.limits.max_head_bytes, deadline) {
161 Ok(request) => request,
162 Err(why) => {
163 let decision = Decision::single(Verdict::Deny, RULE_REQUEST, why);
164 let turn = Turn {
165 session: None,
166 started,
167 };
168 self.record(turn, None, &decision);
169 refuse(client, 400, &decision.reasons[0]);
170 return;
171 }
172 };
173 let Some((turn, _share)) = self.admit(client, &request, served, started) else {
174 return;
175 };
176 drop(pending);
177 let mut watch = Watch::new(self.broker, request.host());
181 if let Some(leak) = watch.next(&buf) {
182 let decision = leak.decision(request.host());
183 self.record(turn, Some(&request), &decision);
184 refuse(client, 403, &summary(&decision));
185 return;
186 }
187 let rest = Zeroizing::new(buf.split_off(request.head_len));
188 let decision = decide::host(self.policy, request.host());
189 if decision.verdict != Verdict::Allow {
190 self.record(turn, Some(&request), &decision);
191 refuse(client, 403, &summary(&decision));
192 return;
193 }
194 if let Some(refusal) = self.tainted(turn, &request) {
195 self.record(turn, Some(&request), &refusal);
196 refuse(client, 403, &summary(&refusal));
197 return;
198 }
199 let mut body = BodyEnd::new(match &request.target {
203 Target::Http { head, .. } => body::framing(head),
204 Target::Connect { .. } => Framing::None,
205 });
206 let Ok(body_in_rest) = body.feed(&rest) else {
207 let decision = Decision::single(Verdict::Deny, RULE_REQUEST, MALFORMED_CHUNKS);
208 self.record(turn, Some(&request), &decision);
209 refuse(client, 400, MALFORMED_CHUNKS);
210 return;
211 };
212 let upstream = match upstream::connect(
213 self.resolver,
214 request.host(),
215 request.port(),
216 served.addresses,
217 self.loopback_ok,
218 self.limits.connect_timeout,
219 ) {
220 Ok(upstream) => upstream,
221 Err(refusal) => {
222 let decision = refusal.decision(request.host());
223 self.record(turn, Some(&request), &decision);
224 let status = if matches!(refusal, Refusal::Address(..) | Refusal::Denied(..)) {
225 403
226 } else {
227 502
228 };
229 refuse(client, status, &summary(&decision));
230 return;
231 }
232 };
233 let mut inspect = |chunk: &[u8]| {
235 let Ok(n) = body.feed(chunk) else {
236 return Forward::Stop;
237 };
238 if let Some(leak) = watch.next(&chunk[..n]) {
239 self.record(turn, Some(&request), &leak.decision(request.host()));
240 return Forward::Stop;
241 }
242 if n < chunk.len() || body.done() {
243 Forward::Part(n)
244 } else {
245 Forward::All
246 }
247 };
248 let (first_bytes, inspect) = match &request.target {
249 Target::Connect { host, .. } => {
250 match self.check_tunnel(client, host, rest.to_vec(), &decision, &request, turn) {
251 Some(hello) => (Zeroizing::new(hello), None),
252 None => return,
253 }
254 }
255 Target::Http { head, host, .. } => {
256 let mut decision = decision.clone();
257 decision
258 .reasons
259 .extend(self.broker.withheld(host).iter().map(|id| {
260 format!("secret `{id}` not injected: plain HTTP needs `plain_http: true`")
261 }));
262 if !self.record(turn, Some(&request), &decision) {
263 refuse(client, 403, "audit log unavailable");
264 return;
265 }
266 let inspect: tunnel::Inspect<'_> = &mut inspect;
267 let injected = self.broker.inject(host, head);
270 let head = injected.as_deref().map_or(head.as_slice(), Vec::as_slice);
271 let body_bytes = &rest[..body_in_rest];
272 (Zeroizing::new([head, body_bytes].concat()), Some(inspect))
273 }
274 };
275 if (&upstream).write_all(&first_bytes).is_ok() {
276 tunnel::relay(client, &upstream, self.limits.idle_timeout, inspect);
277 }
278 }
279
280 fn admit<'s>(
285 &'s self,
286 client: &TcpStream,
287 request: &Request,
288 served: &'s Served<'_>,
289 started: Instant,
290 ) -> Option<(Turn<'s>, Option<Share<'s>>)> {
291 let mut turn = Turn {
292 session: None,
293 started,
294 };
295 let Some(sessions) = self.sessions else {
296 return Some((turn, None));
297 };
298 let session = match sessions.verify(&request.credentials) {
299 Ok(session) => session,
300 Err(why) => {
301 let decision = Decision::single(Verdict::Deny, RULE_SESSION, why.to_string());
302 self.record(turn, Some(request), &decision);
303 refuse(client, 407, &summary(&decision));
304 return None;
305 }
306 };
307 turn.session = Some(session);
308 if let Some(share) = served.shares.take(session) {
309 return Some((turn, Some(share)));
310 }
311 let decision = Decision::single(
312 Verdict::Deny,
313 RULE_SESSION,
314 format!(
315 "the session already holds {} connections, half of the proxy's",
316 served.shares.cap()
317 ),
318 );
319 self.record(turn, Some(request), &decision);
320 refuse(client, 503, &summary(&decision));
321 None
322 }
323
324 fn tainted(&self, turn: Turn<'_>, request: &Request) -> Option<Decision> {
327 let (Some(sessions), Some(session)) = (self.sessions, turn.session) else {
328 return None;
329 };
330 match sessions.taint(session) {
331 Ok(taint) => decide::tainted(self.policy, request.host(), request.port(), &taint),
332 Err(why) => Some(Decision::single(
333 Verdict::Deny,
334 RULE_SESSION,
335 format!("the session's history cannot be read, so its taint is unknown: {why}"),
336 )),
337 }
338 }
339
340 fn check_tunnel(
345 &self,
346 client: &TcpStream,
347 host: &str,
348 mut hello: Vec<u8>,
349 allowed: &Decision,
350 request: &Request,
351 turn: Turn<'_>,
352 ) -> Option<Vec<u8>> {
353 let answered = (&*client).write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n");
354 let deadline = Instant::now() + self.limits.handshake_timeout;
355 let read = answered
356 .map_err(|e| format!("answering the CONNECT: {e}"))
357 .and_then(|()| read_hello(client, &mut hello, deadline));
358 let refusal = match read {
359 Ok((_, true)) if !self.allow_ech => Some(format!(
362 "TLS ClientHello for CONNECT {host} carries an encrypted server name (ECH), \
363 so the server may serve another site; `sandbox.proxy_allow_ech: true` \
364 in the policy allows it"
365 )),
366 Ok((Some(name), _)) if name == host => None,
367 Ok((Some(name), _)) => Some(format!(
368 "TLS server name {name} does not match CONNECT {host}"
369 )),
370 Ok((None, _)) if host.parse::<std::net::IpAddr>().is_ok() => None,
373 Ok((None, _)) => Some(format!(
374 "TLS ClientHello for CONNECT {host} has no server name"
375 )),
376 Err(why) => Some(why),
377 };
378 if let Some(why) = refusal {
379 let decision = Decision::single(Verdict::Deny, RULE_SNI, why);
380 self.record(turn, Some(request), &decision);
381 return None;
382 }
383 self.record(turn, Some(request), allowed).then_some(hello)
384 }
385
386 fn record(&self, turn: Turn<'_>, request: Option<&Request>, decision: &Decision) -> bool {
389 let connection = Connection {
390 session: turn.session,
391 method: request.map(|r| r.method.as_str()),
392 destination: request.map(|r| (r.host(), r.port())),
393 decision,
394 latency_us: u64::try_from(turn.started.elapsed().as_micros()).unwrap_or(u64::MAX),
395 };
396 match self.recorder.record(&connection) {
397 Ok(()) => true,
398 Err(error) => {
399 self.recorder.unrecorded(&error);
402 false
403 }
404 }
405 }
406}
407
408struct Slot<'a>(&'a AtomicUsize);
411
412impl<'a> Slot<'a> {
413 fn take(active: &'a AtomicUsize, max: usize) -> Option<Self> {
414 let slot = Self(active);
417 (active.fetch_add(1, Ordering::AcqRel) < max).then_some(slot)
418 }
419}
420
421impl Drop for Slot<'_> {
422 fn drop(&mut self) {
423 self.0.fetch_sub(1, Ordering::AcqRel);
424 }
425}
426
427fn is_transient(e: &io::Error) -> bool {
428 matches!(
429 e.kind(),
430 ErrorKind::Interrupted | ErrorKind::ConnectionAborted | ErrorKind::ConnectionReset
431 )
432}
433
434#[cfg(test)]
435mod tests;