1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::collections::HashMap;
5use std::io::{BufRead, BufReader, Read, Write};
6use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
7use std::path::PathBuf;
8use std::process::Command;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12use std::time::{Duration, Instant};
13
14const MAX_HEAD: usize = 16 * 1024;
15
16pub const MAX_CONNECTIONS: usize = 256;
19
20const CHUNK: usize = 16 * 1024;
23
24#[derive(Debug, Clone, Copy)]
26pub struct Request<'a> {
27 pub host: &'a str,
29 pub port: u16,
30 pub peer: SocketAddr,
32 pub local: SocketAddr,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Verdict {
38 pub allow: bool,
39 pub allow_private: bool,
41 pub reason: String,
43}
44
45impl Verdict {
46 pub fn allow() -> Verdict {
47 Verdict {
48 allow: true,
49 allow_private: false,
50 reason: String::new(),
51 }
52 }
53
54 pub fn deny(reason: impl Into<String>) -> Verdict {
55 Verdict {
56 allow: false,
57 allow_private: false,
58 reason: reason.into(),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Outcome {
66 Refused,
68 Unresolved,
70 Private,
72 Unreachable,
74 Cut,
76 Tunneled,
78}
79
80pub trait Gate: Send + Sync + 'static {
81 fn decide(&self, request: &Request<'_>) -> Verdict;
83
84 fn finished(&self, _request: &Request<'_>, _outcome: Outcome) {}
86
87 fn carried(&self, _request: &Request<'_>, _bytes: u64) -> bool {
90 true
91 }
92
93 fn relay(&self, _request: &Request<'_>, tunnel: Tunnel) -> Option<Tunnel> {
99 Some(tunnel)
100 }
101
102 fn relay_plain(
110 &self,
111 _request: &Request<'_>,
112 head: String,
113 tunnel: Tunnel,
114 ) -> Option<(String, Tunnel)> {
115 Some((head, tunnel))
116 }
117}
118
119pub struct Tunnel {
122 pub client: TcpStream,
123 pub upstream: TcpStream,
124 pub early: Vec<u8>,
127}
128
129pub fn bind(addr: &str) -> std::io::Result<TcpListener> {
132 let l = TcpListener::bind(addr)?;
133 if !l.local_addr()?.ip().is_loopback() {
134 return Err(std::io::Error::new(
135 std::io::ErrorKind::InvalidInput,
136 "the proxy only listens on loopback",
137 ));
138 }
139 Ok(l)
140}
141
142pub fn serve<G: Gate>(listener: TcpListener, gate: Arc<G>) {
144 accept(listener, gate, &Arc::new(Shared::default()));
145}
146
147pub struct Server {
150 addr: SocketAddr,
151 shared: Arc<Shared>,
152 thread: Option<JoinHandle<()>>,
153}
154
155#[derive(Default)]
156struct Shared {
157 stopping: AtomicBool,
158 open: Mutex<HashMap<u64, Vec<TcpStream>>>,
161 next: AtomicU64,
162}
163
164impl Shared {
165 fn track(&self, id: u64, stream: &TcpStream) {
168 let mut open = self.open.lock().unwrap();
169 if self.stopping.load(Ordering::SeqCst) {
170 let _ = stream.shutdown(std::net::Shutdown::Both);
171 } else if let Ok(clone) = stream.try_clone() {
172 open.entry(id).or_default().push(clone);
173 }
174 }
175}
176
177pub fn spawn<G: Gate>(listener: TcpListener, gate: Arc<G>) -> std::io::Result<Server> {
179 let addr = listener.local_addr()?;
180 let shared = Arc::new(Shared::default());
181 let served = Arc::clone(&shared);
182 let thread = std::thread::spawn(move || accept(listener, gate, &served));
183 Ok(Server {
184 addr,
185 shared,
186 thread: Some(thread),
187 })
188}
189
190impl Server {
191 pub fn local_addr(&self) -> SocketAddr {
192 self.addr
193 }
194
195 pub fn stop(mut self) {
199 self.shut();
200 }
201
202 fn shut(&mut self) {
203 let Some(thread) = self.thread.take() else {
204 return;
205 };
206 {
207 let _open = self.shared.open.lock().unwrap();
210 self.shared.stopping.store(true, Ordering::SeqCst);
211 }
212 let _ = TcpStream::connect_timeout(&self.addr, Duration::from_secs(1));
214 let _ = thread.join();
215 for stream in self.shared.open.lock().unwrap().values().flatten() {
216 let _ = stream.shutdown(std::net::Shutdown::Both);
217 }
218 let deadline = Instant::now() + Duration::from_secs(3);
221 while !self.shared.open.lock().unwrap().is_empty() && Instant::now() < deadline {
222 std::thread::sleep(Duration::from_millis(5));
223 }
224 }
225}
226
227impl Drop for Server {
228 fn drop(&mut self) {
229 self.shut();
230 }
231}
232
233fn accept<G: Gate>(listener: TcpListener, gate: Arc<G>, shared: &Arc<Shared>) {
234 for conn in listener.incoming() {
235 if shared.stopping.load(Ordering::SeqCst) {
236 return;
237 }
238 let Ok(stream) = conn else { continue };
239 if shared.open.lock().unwrap().len() >= MAX_CONNECTIONS {
240 respond(&stream, 503, "too many connections through the proxy");
241 continue;
242 }
243 let id = shared.next.fetch_add(1, Ordering::Relaxed);
244 shared.track(id, &stream);
245 let (gate, shared) = (gate.clone(), Arc::clone(shared));
246 std::thread::spawn(move || {
247 if !shared.stopping.load(Ordering::SeqCst) {
249 let _ = handle(stream, &*gate, &|upstream| shared.track(id, upstream));
250 }
251 shared.open.lock().unwrap().remove(&id);
252 });
253 }
254}
255
256pub fn is_private(ip: IpAddr) -> bool {
261 match ip {
262 IpAddr::V4(v) => private_v4(v),
263 IpAddr::V6(v) => {
264 if let Some(v4) = v.to_ipv4_mapped() {
265 return private_v4(v4);
266 }
267 let s = v.segments();
268 if s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
270 let [a, b] = s[6].to_be_bytes();
271 let [c, d] = s[7].to_be_bytes();
272 return private_v4(std::net::Ipv4Addr::new(a, b, c, d));
273 }
274 v.is_unspecified()
275 || v.is_loopback()
276 || v.is_multicast()
277 || (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || (s[0] & 0xffc0) == 0xfec0 || (s[0] == 0x2001 && s[1] == 0x0db8) || s[..6] == [0, 0, 0, 0, 0, 0] }
283 }
284}
285
286pub fn is_metadata(ip: IpAddr) -> bool {
290 let v4 = |v: std::net::Ipv4Addr| {
291 matches!(
292 v.octets(),
293 [169, 254, 169, 254] | [169, 254, 170, 2] | [169, 254, 170, 23] | [100, 100, 100, 200]
294 )
295 };
296 match ip {
297 IpAddr::V4(v) => v4(v),
298 IpAddr::V6(v) => {
299 v.to_ipv4_mapped().is_some_and(v4)
300 || v.segments() == [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x0254]
301 }
302 }
303}
304
305fn private_v4(ip: std::net::Ipv4Addr) -> bool {
306 let [a, b, c, _] = ip.octets();
307 ip.is_unspecified()
308 || ip.is_loopback()
309 || ip.is_private()
310 || ip.is_link_local()
311 || ip.is_broadcast()
312 || ip.is_documentation()
313 || ip.is_multicast()
314 || a == 0
315 || (a == 100 && (64..128).contains(&b)) || (a == 192 && b == 0 && c == 0) || (a == 198 && (18..20).contains(&b)) || a >= 240 }
320
321fn parse_authority(a: &str, default_port: u16) -> Option<(String, u16)> {
323 let (host, port) = if let Some(rest) = a.strip_prefix('[') {
324 let (h, tail) = rest.split_once(']')?;
325 let port = tail
326 .strip_prefix(':')
327 .map(|p| p.parse().ok())
328 .unwrap_or(Some(default_port))?;
329 (h.to_string(), port)
330 } else {
331 match a.rsplit_once(':') {
332 Some((h, p)) => (h.to_string(), p.parse().ok()?),
333 None => (a.to_string(), default_port),
334 }
335 };
336 let ok = !host.is_empty()
337 && host.len() <= 253
338 && host
339 .chars()
340 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | ':'));
341 ok.then_some((host.to_ascii_lowercase(), port))
342}
343
344pub fn peer_cwd(peer: SocketAddr, local: SocketAddr) -> Option<PathBuf> {
347 let me = std::process::id().to_string();
348 let out = Command::new("lsof")
349 .args([
350 "-nP",
351 &format!("-iTCP@{}:{}", peer.ip(), peer.port()),
352 "-sTCP:ESTABLISHED",
353 "-Fpn",
354 ])
355 .output()
356 .ok()?;
357 let text = String::from_utf8_lossy(&out.stdout);
358 let want = format!(
361 "{}:{}->{}:{}",
362 peer.ip(),
363 peer.port(),
364 local.ip(),
365 local.port()
366 );
367 let mut pid = None;
368 let mut cur = None;
369 for line in text.lines() {
370 if let Some(p) = line.strip_prefix('p') {
371 cur = Some(p.to_string());
372 } else if let Some(n) = line.strip_prefix('n')
373 && n == want
374 && cur.as_deref() != Some(me.as_str())
375 {
376 pid = cur.clone();
377 }
378 }
379 let pid = pid?;
380 let out = Command::new("lsof")
381 .args(["-a", "-p", &pid, "-d", "cwd", "-Fn"])
382 .output()
383 .ok()?;
384 String::from_utf8_lossy(&out.stdout)
385 .lines()
386 .find_map(|l| l.strip_prefix('n').map(PathBuf::from))
387}
388
389fn connect_any(addrs: &[SocketAddr], within: Duration) -> Option<TcpStream> {
394 let deadline = Instant::now() + within;
395 addrs.iter().find_map(|a| {
396 TcpStream::connect_timeout(a, deadline.saturating_duration_since(Instant::now())).ok()
397 })
398}
399
400fn respond(mut s: &TcpStream, code: u16, reason: &str) {
401 let body = format!("proxy: {reason}\n");
402 let _ = write!(
403 s,
404 "HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
405 body.len()
406 );
407}
408
409fn handle(client: TcpStream, gate: &dyn Gate, track: &dyn Fn(&TcpStream)) -> std::io::Result<()> {
412 client.set_read_timeout(Some(Duration::from_secs(30)))?;
413 let peer = client.peer_addr()?;
414 let local = client.local_addr()?;
415 let mut reader = BufReader::new(client.try_clone()?);
416 let mut head = Vec::new();
417 loop {
418 let mut line = Vec::new();
419 let n = reader
420 .by_ref()
421 .take((MAX_HEAD - head.len()) as u64)
422 .read_until(b'\n', &mut line)?;
423 if n == 0 || head.len() + n >= MAX_HEAD {
424 respond(&client, 400, "bad request");
425 return Ok(());
426 }
427 head.extend_from_slice(&line);
428 if line == b"\r\n" || line == b"\n" {
429 break;
430 }
431 }
432 let head_text = String::from_utf8_lossy(&head).into_owned();
433 let first = head_text.lines().next().unwrap_or("");
434 let mut parts = first.split_whitespace();
435 let (method, target) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));
436 let (authority, rest_of_request) = if method.eq_ignore_ascii_case("CONNECT") {
437 (parse_authority(target, 443), None)
438 } else if let Some(url) = target.strip_prefix("http://") {
439 let (auth, path) = url.split_at(url.find('/').unwrap_or(url.len()));
440 let path = if path.is_empty() { "/" } else { path };
441 let rewritten = head_text.replacen(target, path, 1);
442 (parse_authority(auth, 80), Some(rewritten))
443 } else {
444 respond(&client, 400, "only CONNECT and absolute http:// requests");
445 return Ok(());
446 };
447 let Some((host, port)) = authority else {
448 respond(&client, 400, "bad target");
449 return Ok(());
450 };
451 let request = Request {
452 host: &host,
453 port,
454 peer,
455 local,
456 };
457 let verdict = gate.decide(&request);
458 if !verdict.allow {
459 respond(&client, 403, &verdict.reason);
460 gate.finished(&request, Outcome::Refused);
461 return Ok(());
462 }
463 let addrs: Vec<SocketAddr> = match (host.as_str(), port).to_socket_addrs() {
464 Ok(a) => a.collect(),
465 Err(_) => {
466 respond(&client, 502, "cannot resolve host");
467 gate.finished(&request, Outcome::Unresolved);
468 return Ok(());
469 }
470 };
471 if !addrs.is_empty() && addrs.iter().all(|a| is_metadata(a.ip())) {
472 respond(&client, 403, "cloud metadata addresses are never reachable");
473 gate.finished(&request, Outcome::Private);
474 return Ok(());
475 }
476 let allowed: Vec<SocketAddr> = addrs
477 .into_iter()
478 .filter(|a| !is_metadata(a.ip()))
479 .filter(|a| verdict.allow_private || !is_private(a.ip()))
480 .collect();
481 if allowed.is_empty() {
482 respond(
483 &client,
484 403,
485 "private and loopback addresses are not reachable",
486 );
487 gate.finished(&request, Outcome::Private);
488 return Ok(());
489 }
490 let Some(upstream) = connect_any(&allowed, Duration::from_secs(10)) else {
491 respond(&client, 502, "cannot connect");
492 gate.finished(&request, Outcome::Unreachable);
493 return Ok(());
494 };
495 track(&upstream);
496 client.set_read_timeout(None)?;
497 let buffered = reader.buffer().to_vec();
499 drop(reader);
500 let (client, upstream, buffered) = match rest_of_request {
501 None => {
502 let mut c = &client;
503 c.write_all(b"HTTP/1.1 200 Connection established\r\n\r\n")?;
504 let tunnel = Tunnel {
505 client,
506 upstream,
507 early: buffered,
508 };
509 match gate.relay(&request, tunnel) {
510 Some(t) => (t.client, t.upstream, t.early),
511 None => {
512 gate.finished(&request, Outcome::Tunneled);
513 return Ok(());
514 }
515 }
516 }
517 Some(h) => {
518 let tunnel = Tunnel {
519 client,
520 upstream,
521 early: buffered,
522 };
523 match gate.relay_plain(&request, h, tunnel) {
524 Some((h, t)) => {
525 (&t.upstream).write_all(h.as_bytes())?;
526 (t.client, t.upstream, t.early)
527 }
528 None => {
529 gate.finished(&request, Outcome::Tunneled);
530 return Ok(());
531 }
532 }
533 }
534 };
535 let mut up_w = upstream.try_clone()?;
536 if !buffered.is_empty() {
537 if !gate.carried(&request, buffered.len() as u64) {
538 gate.finished(&request, Outcome::Cut);
539 return Ok(());
540 }
541 up_w.write_all(&buffered)?;
542 }
543 let cut = AtomicBool::new(false);
544 let (up_r, client_w) = (upstream.try_clone()?, client.try_clone()?);
545 std::thread::scope(|scope| {
546 scope.spawn(|| {
547 if !carry(&up_r, &client_w, gate, &request) {
548 cut.store(true, Ordering::SeqCst);
549 let _ = up_r.shutdown(std::net::Shutdown::Both);
550 let _ = client_w.shutdown(std::net::Shutdown::Both);
551 }
552 let _ = client_w.shutdown(std::net::Shutdown::Write);
553 });
554 if !carry(&client, &up_w, gate, &request) {
555 cut.store(true, Ordering::SeqCst);
556 let _ = client.shutdown(std::net::Shutdown::Both);
557 let _ = up_w.shutdown(std::net::Shutdown::Both);
558 }
559 let _ = up_w.shutdown(std::net::Shutdown::Write);
560 });
561 let how = if cut.load(Ordering::SeqCst) {
562 Outcome::Cut
563 } else {
564 Outcome::Tunneled
565 };
566 gate.finished(&request, how);
567 Ok(())
568}
569
570fn carry(mut from: &TcpStream, mut to: &TcpStream, gate: &dyn Gate, request: &Request<'_>) -> bool {
573 let mut buf = vec![0u8; CHUNK];
574 loop {
575 let n = match from.read(&mut buf) {
576 Ok(0) | Err(_) => return true,
577 Ok(n) => n,
578 };
579 if !gate.carried(request, n as u64) {
580 return false;
581 }
582 if to.write_all(&buf[..n]).is_err() {
583 return true;
584 }
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn a_connection_goes_to_the_first_address_that_takes_it() {
594 let closed = TcpListener::bind("127.0.0.1:0")
596 .unwrap()
597 .local_addr()
598 .unwrap();
599 let open = TcpListener::bind("127.0.0.1:0").unwrap();
600 let at = open.local_addr().unwrap();
601 let budget = Duration::from_secs(5);
602 let s = connect_any(&[closed, at], budget).expect("the second address");
603 assert_eq!(s.peer_addr().unwrap(), at);
604 assert!(connect_any(&[closed], budget).is_none());
605 assert!(connect_any(&[], budget).is_none());
606 assert!(
607 connect_any(&[at], Duration::ZERO).is_none(),
608 "no budget, no attempt"
609 );
610 }
611
612 #[test]
613 fn metadata_addresses() {
614 for ip in [
615 "169.254.169.254",
616 "169.254.170.2",
617 "100.100.100.200",
618 "fd00:ec2::254",
619 "::ffff:169.254.169.254",
620 ] {
621 assert!(is_metadata(ip.parse().unwrap()), "{ip}");
622 }
623 for ip in ["169.254.169.253", "10.0.0.1", "fd00:ec2::253", "8.8.8.8"] {
624 assert!(!is_metadata(ip.parse().unwrap()), "{ip}");
625 }
626 }
627
628 #[test]
629 fn private_ranges() {
630 for ip in [
631 "127.0.0.1",
632 "10.1.2.3",
633 "192.168.0.1",
634 "172.16.5.4",
635 "169.254.1.1",
636 "100.64.0.1",
637 "0.0.0.0",
638 "::1",
639 "fd00::1",
640 "fe80::1",
641 "::ffff:127.0.0.1",
642 "192.0.0.1",
643 "198.18.0.1",
644 "192.0.2.1",
645 "224.0.0.1",
646 "240.0.0.1",
647 "255.255.255.255",
648 "fec0::1",
649 "ff02::1",
650 "2001:db8::1",
651 "64:ff9b::7f00:1",
652 "::127.0.0.1",
653 ] {
654 assert!(is_private(ip.parse().unwrap()), "{ip}");
655 }
656 for ip in [
657 "93.184.216.34",
658 "1.1.1.1",
659 "2606:4700::1111",
660 "::ffff:8.8.8.8",
661 "64:ff9b::808:808",
662 ] {
663 assert!(!is_private(ip.parse().unwrap()), "{ip}");
664 }
665 }
666
667 #[test]
668 fn authorities() {
669 assert_eq!(
670 parse_authority("Example.com:443", 1),
671 Some(("example.com".into(), 443))
672 );
673 assert_eq!(
674 parse_authority("example.com", 80),
675 Some(("example.com".into(), 80))
676 );
677 assert_eq!(parse_authority("[::1]:8080", 1), Some(("::1".into(), 8080)));
678 assert_eq!(parse_authority("a b:1", 1), None);
679 assert_eq!(parse_authority("x:notaport", 1), None);
680 }
681}