use std::io::{Read, Write};
use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
use std::sync::OnceLock;
use std::time::Duration;
pub const EGRESS_PORT: u16 = 3128;
pub const FEED_EGRESS: &str = "egress";
pub const TLS_PORTS: [u16; 3] = [443, 465, 587];
pub const MAX_HEAD: usize = 8 * 1024;
pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
pub fn door_url() -> String {
format!("http://{}:{EGRESS_PORT}", crate::OVERLAY_FRONT)
}
pub fn parse_connect(head: &str) -> Result<(String, u16), String> {
let line = head.lines().next().unwrap_or_default();
let mut words = line.split_whitespace();
match (words.next(), words.next(), words.next()) {
(Some("CONNECT"), Some(target), Some(v)) if v.starts_with("HTTP/1.") => {
let (host, port) = target.rsplit_once(':').ok_or_else(|| format!("CONNECT {target}: no port"))?;
let host = host.trim_start_matches('[').trim_end_matches(']');
let port: u16 = port.parse().map_err(|_| format!("CONNECT {target}: the port is not a number"))?;
if host.is_empty() {
return Err(format!("CONNECT {target}: no host"));
}
Ok((host.to_ascii_lowercase(), port))
}
_ => Err(format!("not a CONNECT request: {line:?}")),
}
}
pub fn inside(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v) => crate::fleet::bench_address(ip) || v.is_unspecified() || v.is_broadcast() || v.is_multicast(),
IpAddr::V6(v) => crate::fleet::bench_address(ip) || v.is_unspecified() || v.is_multicast() || v.to_ipv4_mapped().is_some_and(|m| inside(IpAddr::V4(m))),
}
}
pub fn admits(host: &str, port: u16, resolved: &[SocketAddr]) -> Result<(), String> {
if !TLS_PORTS.contains(&port) {
return Err(format!("REFUSED [egress-port] {host}:{port}: the estate leaves on {TLS_PORTS:?} only"));
}
if resolved.is_empty() {
return Err(format!("REFUSED [egress-unresolved] {host}: the resolver named no address"));
}
if let Some(a) = resolved.iter().find(|a| inside(a.ip())) {
return Err(format!("REFUSED [egress-inside] {host} is {}: the door is a way out, never into a private network", a.ip()));
}
Ok(())
}
#[derive(Clone, Copy)]
pub struct Policy {
pub peer: fn(IpAddr) -> bool,
pub admits: fn(&str, u16, &[SocketAddr]) -> Result<(), String>,
}
impl Policy {
pub const OUT: Policy = Policy { peer: on_tunnel, admits };
}
fn on_tunnel(ip: IpAddr) -> bool {
matches!(ip, IpAddr::V4(v) if crate::in_overlay(v))
}
pub const MAX_CONNS: usize = 64;
pub const PER_PEER: usize = 16;
pub const HEAD_DEADLINE: Duration = Duration::from_secs(30);
pub const IDLE: Duration = Duration::from_secs(300);
pub const SAY_EVERY: Duration = Duration::from_secs(60);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
pub total: usize,
pub per_peer: usize,
pub head: Duration,
pub idle: Duration,
}
impl Limits {
pub const DOOR: Limits = Limits { total: MAX_CONNS, per_peer: PER_PEER, head: HEAD_DEADLINE, idle: IDLE };
}
pub struct Slots {
total: usize,
per_peer: usize,
held: std::sync::Mutex<(usize, std::collections::HashMap<IpAddr, usize>)>,
}
impl Slots {
pub fn new(total: usize, per_peer: usize) -> std::sync::Arc<Slots> {
std::sync::Arc::new(Slots { total, per_peer, held: std::sync::Mutex::new((0, std::collections::HashMap::new())) })
}
pub fn take(self: &std::sync::Arc<Self>, peer: IpAddr) -> Result<Slot, String> {
let peer = peer.to_canonical();
let mut held = self.held.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mine = held.1.get(&peer).copied().unwrap_or(0);
if held.0 >= self.total {
return Err(format!("REFUSED [egress-full] {} connections are open, the door's most at once", held.0));
}
if mine >= self.per_peer {
return Err(format!("REFUSED [egress-peer-full] {peer} holds {mine} connections, its most at once"));
}
held.0 += 1;
*held.1.entry(peer).or_insert(0) += 1;
Ok(Slot { slots: std::sync::Arc::clone(self), peer })
}
pub fn held(&self) -> usize {
self.held.lock().unwrap_or_else(std::sync::PoisonError::into_inner).0
}
}
pub struct Slot {
slots: std::sync::Arc<Slots>,
peer: IpAddr,
}
impl Drop for Slot {
fn drop(&mut self) {
let mut held = self.slots.held.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
held.0 = held.0.saturating_sub(1);
if let Some(n) = held.1.get_mut(&self.peer) {
*n -= 1;
if *n == 0 {
held.1.remove(&self.peer);
}
}
}
}
pub struct Said {
every: Duration,
state: std::sync::Mutex<(Option<std::time::Instant>, u64)>,
}
impl Said {
pub fn new(every: Duration) -> Said {
Said { every, state: std::sync::Mutex::new((None, 0)) }
}
pub fn may_say(&self, now: std::time::Instant) -> Option<u64> {
let mut s = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
match s.0 {
Some(last) if now.saturating_duration_since(last) < self.every => {
s.1 += 1;
None
}
_ => {
s.0 = Some(now);
Some(std::mem::take(&mut s.1))
}
}
}
pub fn say(&self, line: &str) {
match self.may_say(std::time::Instant::now()) {
Some(0) => eprintln!("egress: {line}"),
Some(n) => eprintln!("egress: {line} ({n} more lines held back since the last one)"),
None => {}
}
}
}
pub fn serve(bind: SocketAddr, policy: Policy) -> std::io::Result<SocketAddr> {
serve_with(bind, policy, Limits::DOOR)
}
pub fn serve_capped(bind: SocketAddr, policy: Policy, cap: usize) -> std::io::Result<SocketAddr> {
serve_with(bind, policy, Limits { total: cap, per_peer: cap, ..Limits::DOOR })
}
pub fn serve_with(bind: SocketAddr, policy: Policy, limits: Limits) -> std::io::Result<SocketAddr> {
let listener = TcpListener::bind(bind)?;
let at = listener.local_addr()?;
let slots = Slots::new(limits.total, limits.per_peer);
let said = std::sync::Arc::new(Said::new(SAY_EVERY));
std::thread::Builder::new().name("egress".into()).spawn(move || {
for mut conn in listener.incoming().flatten() {
let peer = match conn.peer_addr() {
Ok(p) => p.ip(),
Err(_) => continue,
};
let slot = match slots.take(peer) {
Ok(slot) => slot,
Err(why) => {
let _ = conn.write_all(format!("HTTP/1.1 503 Service Unavailable\r\n\r\n{why}; ask again\r\n").as_bytes());
said.say(&why);
continue;
}
};
let said = std::sync::Arc::clone(&said);
let _ = std::thread::Builder::new().name("egress-conn".into()).spawn(move || {
let _slot = slot;
if let Err(why) = one(conn, policy, limits) {
said.say(&why);
}
});
}
})?;
Ok(at)
}
fn one(mut conn: TcpStream, policy: Policy, limits: Limits) -> Result<(), String> {
let peer = conn.peer_addr().map_err(|e| e.to_string())?;
if !(policy.peer)(peer.ip()) {
let _ = conn.write_all(b"HTTP/1.1 403 Forbidden\r\n\r\n");
return Err(format!("REFUSED [egress-peer] {peer}: not on the estate's tunnel"));
}
let until = std::time::Instant::now() + limits.head;
let mut head = Vec::new();
let mut byte = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") {
if head.len() >= MAX_HEAD {
let _ = conn.write_all(b"HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n");
return Err(format!("{peer}: a request head over {MAX_HEAD} bytes"));
}
let left = until.saturating_duration_since(std::time::Instant::now());
if left.is_zero() {
let _ = conn.write_all(b"HTTP/1.1 408 Request Timeout\r\n\r\n");
return Err(format!("REFUSED [egress-head-slow] {peer}: no whole request head within {:?}", limits.head));
}
conn.set_read_timeout(Some(left)).map_err(|e| e.to_string())?;
match conn.read(&mut byte) {
Ok(1) => head.push(byte[0]),
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => continue,
_ => return Err(format!("{peer}: closed before a whole request head")),
}
}
let (host, port) = match parse_connect(&String::from_utf8_lossy(&head)) {
Ok(d) => d,
Err(why) => {
let _ = conn.write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n");
return Err(format!("{peer}: {why}"));
}
};
let resolved: Vec<SocketAddr> = (host.as_str(), port).to_socket_addrs().map(|a| a.collect()).unwrap_or_default();
if let Err(why) = (policy.admits)(&host, port, &resolved) {
let _ = conn.write_all(b"HTTP/1.1 403 Forbidden\r\n\r\n");
return Err(format!("{peer}: {why}"));
}
let out = resolved.iter().find_map(|a| TcpStream::connect_timeout(a, CONNECT_TIMEOUT).ok());
let Some(out) = out else {
let _ = conn.write_all(b"HTTP/1.1 502 Bad Gateway\r\n\r\n");
return Err(format!("{peer}: {host}:{port} did not answer"));
};
conn.set_read_timeout(None).map_err(|e| e.to_string())?;
conn.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").map_err(|e| e.to_string())?;
splice(conn, out, limits.idle);
Ok(())
}
fn splice(a: TcpStream, b: TcpStream, idle: Duration) {
let (Ok(a2), Ok(b2)) = (a.try_clone(), b.try_clone()) else { return };
let start = std::time::Instant::now();
let last = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
let last2 = std::sync::Arc::clone(&last);
let up = std::thread::spawn(move || pump(a2, b2, idle, start, &last2));
pump(b, a, idle, start, &last);
let _ = up.join();
}
fn pump(mut from: TcpStream, mut to: TcpStream, idle: Duration, start: std::time::Instant, last: &std::sync::atomic::AtomicU64) {
use std::sync::atomic::Ordering::SeqCst;
let _ = from.set_read_timeout(Some(idle));
let _ = to.set_write_timeout(Some(idle));
let mut buf = [0u8; 16 * 1024];
loop {
match from.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if to.write_all(&buf[..n]).is_err() {
break;
}
last.store(start.elapsed().as_millis() as u64, SeqCst);
}
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
let silent = start.elapsed().saturating_sub(Duration::from_millis(last.load(SeqCst)));
if silent >= idle {
let _ = from.shutdown(Shutdown::Both);
let _ = to.shutdown(Shutdown::Both);
return;
}
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => break,
}
}
let _ = to.shutdown(Shutdown::Write);
}
static PROXY: OnceLock<String> = OnceLock::new();
pub fn set_proxy(url: &str) {
let url = url.trim();
if !url.is_empty() {
let _ = PROXY.set(url.to_string());
}
}
pub fn proxy() -> Option<&'static str> {
PROXY.get().map(String::as_str)
}
pub fn connect_head(host: &str, port: u16) -> String {
let h = if host.contains(':') { format!("[{host}]") } else { host.to_string() };
format!("CONNECT {h}:{port} HTTP/1.1\r\nHost: {h}:{port}\r\n\r\n")
}
pub fn door_addr(url: &str) -> Result<SocketAddr, String> {
let rest = url.trim().strip_prefix("http://").ok_or_else(|| format!("the door {url:?} is not an http:// URL"))?;
let hostport = rest.trim_end_matches('/');
hostport.parse::<SocketAddr>().map_err(|_| format!("the door {url:?} names no address and port (a box behind its front resolves no names)"))
}
pub fn established(head: &str) -> Result<(), String> {
let line = head.lines().next().unwrap_or_default().trim();
let mut words = line.split_whitespace();
match (words.next(), words.next()) {
(Some(v), Some("200")) if v.starts_with("HTTP/1.") => Ok(()),
_ => Err(format!("the egress door answered {line:?}")),
}
}
pub fn dial(door: &str, host: &str, port: u16) -> std::io::Result<TcpStream> {
let other = |why: String| std::io::Error::other(why);
let at = door_addr(door).map_err(other)?;
let mut conn = TcpStream::connect_timeout(&at, CONNECT_TIMEOUT).map_err(|e| other(format!("the egress door {at} did not answer: {e}")))?;
conn.set_read_timeout(Some(CONNECT_TIMEOUT + Duration::from_secs(15)))?;
conn.write_all(connect_head(host, port).as_bytes())?;
let mut head = Vec::new();
let mut byte = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") {
if head.len() >= MAX_HEAD {
return Err(other(format!("the egress door {at} answered a head over {MAX_HEAD} bytes")));
}
match conn.read(&mut byte) {
Ok(1) => head.push(byte[0]),
_ => return Err(other(format!("the egress door {at} closed before it answered for {host}:{port}"))),
}
}
established(&String::from_utf8_lossy(&head)).map_err(|why| other(format!("{host}:{port} through {at}: {why}")))?;
conn.set_read_timeout(None)?;
Ok(conn)
}
pub struct Forward {
at: SocketAddr,
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl Forward {
pub fn open(door: &str, host: &str, port: u16) -> std::io::Result<Forward> {
let first = dial(door, host, port)?;
let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))?;
let at = listener.local_addr()?;
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let (door, host, halt) = (door.to_string(), host.to_string(), stop.clone());
std::thread::Builder::new().name("egress-forward".into()).spawn(move || {
let mut first = Some(first);
for conn in listener.incoming() {
if halt.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
let Ok(conn) = conn else { continue };
let out = match first.take() {
Some(t) => Ok(t),
None => dial(&door, &host, port),
};
match out {
Ok(out) => {
let _ = std::thread::Builder::new().name("egress-forward-conn".into()).spawn(move || splice(conn, out, IDLE));
}
Err(why) => eprintln!("egress forward: {why}"),
}
}
})?;
Ok(Forward { at, stop })
}
pub fn addr(&self) -> SocketAddr {
self.at
}
}
impl Drop for Forward {
fn drop(&mut self) {
self.stop.store(true, std::sync::atomic::Ordering::SeqCst);
let _ = TcpStream::connect_timeout(&self.at, Duration::from_secs(1));
}
}
#[cfg(test)]
mod tests {
#[test]
fn the_door_refuses_a_connection_over_its_cap_by_name_and_frees_the_slot() {
let door = serve_capped("127.0.0.1:0".parse().unwrap(), Policy { peer: |_| true, admits }, 2).unwrap();
let held: Vec<TcpStream> = (0..2).map(|_| TcpStream::connect(door).unwrap()).collect();
std::thread::sleep(Duration::from_millis(200));
let mut over = TcpStream::connect(door).unwrap();
over.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut said = String::new();
let _ = over.read_to_string(&mut said);
assert!(said.starts_with("HTTP/1.1 503") && said.contains("egress-full"), "one over the cap is refused by name: {said:?}");
drop(held);
std::thread::sleep(Duration::from_millis(300));
let mut again = TcpStream::connect(door).unwrap();
again.set_read_timeout(Some(Duration::from_millis(500))).unwrap();
let mut b = [0u8; 1];
let r = again.read(&mut b);
assert!(matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut), "a freed slot takes the next connection (it waits for its head): {r:?}");
}
use super::*;
#[test]
fn one_peer_with_64_connections_does_not_block_a_second() {
let slots = Slots::new(MAX_CONNS, PER_PEER);
let a: IpAddr = "10.44.0.2".parse().unwrap();
let b: IpAddr = "10.44.0.3".parse().unwrap();
let tries: Vec<Result<Slot, String>> = (0..64).map(|_| slots.take(a)).collect();
let held = tries.iter().filter(|t| t.is_ok()).count();
assert_eq!(held, PER_PEER, "one peer holds at most its own cap");
assert!(tries.iter().filter_map(|t| t.as_ref().err()).all(|e| e.contains("[egress-peer-full]")), "each refusal names the peer's cap");
let second = slots.take(b);
assert!(second.is_ok(), "a second peer still gets a slot: {:?}", second.err());
drop(second);
let mut all: Vec<Slot> = tries.into_iter().flatten().collect();
for p in 3..6u8 {
all.extend((0..PER_PEER).map(|_| slots.take(IpAddr::from([10, 44, 0, p])).unwrap()));
}
assert_eq!(slots.held(), MAX_CONNS);
let fifth = slots.take("10.44.0.9".parse().unwrap()).err().unwrap();
assert!(fifth.contains("[egress-full]"), "{fifth}");
drop(all);
assert_eq!(slots.held(), 0, "every slot is given back");
assert!(slots.take(a).is_ok(), "a peer's count is given back too");
assert_eq!(Limits::DOOR, Limits { total: 64, per_peer: 16, head: Duration::from_secs(30), idle: Duration::from_secs(300) });
}
#[test]
fn the_door_refuses_a_peer_over_its_own_cap_by_name() {
let limits = Limits { total: 8, per_peer: 2, ..Limits::DOOR };
let door = serve_with("127.0.0.1:0".parse().unwrap(), Policy { peer: |_| true, admits }, limits).unwrap();
let held: Vec<TcpStream> = (0..2).map(|_| TcpStream::connect(door).unwrap()).collect();
std::thread::sleep(Duration::from_millis(200));
let mut over = TcpStream::connect(door).unwrap();
over.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut said = String::new();
let _ = over.read_to_string(&mut said);
assert!(said.starts_with("HTTP/1.1 503") && said.contains("egress-peer-full"), "one over the peer's cap is refused by name: {said:?}");
drop(held);
}
#[test]
fn a_head_dripped_a_byte_at_a_time_is_cut_at_its_deadline() {
let limits = Limits { total: 1, per_peer: 1, head: Duration::from_millis(600), ..Limits::DOOR };
let door = serve_with("127.0.0.1:0".parse().unwrap(), Policy { peer: |_| true, admits }, limits).unwrap();
let mut c = TcpStream::connect(door).unwrap();
let mut w = c.try_clone().unwrap();
std::thread::spawn(move || {
for b in b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n" {
if w.write_all(&[*b]).is_err() {
return;
}
std::thread::sleep(Duration::from_millis(100));
}
});
c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let begun = std::time::Instant::now();
let mut said = String::new();
let _ = c.read_to_string(&mut said);
assert!(said.starts_with("HTTP/1.1 408"), "a head not whole by its deadline is refused: {said:?}");
assert!(begun.elapsed() < Duration::from_secs(3), "cut at the deadline, not after the drip: {:?}", begun.elapsed());
std::thread::sleep(Duration::from_millis(200));
let mut next = TcpStream::connect(door).unwrap();
next.set_read_timeout(Some(Duration::from_millis(300))).unwrap();
let mut b = [0u8; 1];
let r = next.read(&mut b);
assert!(matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut), "its slot was given back: {r:?}");
}
#[test]
fn an_idle_tunnel_is_closed_and_frees_its_slot() {
let echo = TcpListener::bind("127.0.0.1:0").unwrap();
let echo_at = echo.local_addr().unwrap();
std::thread::spawn(move || {
for c in echo.incoming().flatten() {
std::thread::spawn(move || {
let mut r = c.try_clone().unwrap();
let mut w = c;
let _ = std::io::copy(&mut r, &mut w);
});
}
});
let open = Policy { peer: |_| true, admits: |_, _, r| if r.is_empty() { Err("none".into()) } else { Ok(()) } };
let limits = Limits { total: 1, per_peer: 1, idle: Duration::from_millis(400), ..Limits::DOOR };
let door = serve_with("127.0.0.1:0".parse().unwrap(), open, limits).unwrap();
let mut c = TcpStream::connect(door).unwrap();
c.write_all(format!("CONNECT 127.0.0.1:{} HTTP/1.1\r\n\r\n", echo_at.port()).as_bytes()).unwrap();
let mut reply = [0u8; 39];
c.read_exact(&mut reply).unwrap();
for _ in 0..4 {
c.write_all(b"ping!").unwrap();
let mut back = [0u8; 5];
c.read_exact(&mut back).unwrap();
std::thread::sleep(Duration::from_millis(200));
}
c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let begun = std::time::Instant::now();
let mut b = [0u8; 1];
let r = c.read(&mut b);
assert!(matches!(r, Ok(0)) || matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset), "an idle tunnel is closed: {r:?}");
assert!(begun.elapsed() < Duration::from_secs(3), "closed near its idle time: {:?}", begun.elapsed());
std::thread::sleep(Duration::from_millis(200));
let mut next = TcpStream::connect(door).unwrap();
next.set_read_timeout(Some(Duration::from_millis(300))).unwrap();
let r = next.read(&mut b);
assert!(matches!(&r, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut), "its slot was given back: {r:?}");
}
#[test]
fn the_door_says_a_line_once_per_window_and_counts_the_rest() {
let said = Said::new(Duration::from_secs(60));
let t0 = std::time::Instant::now();
assert_eq!(said.may_say(t0), Some(0), "the first line is said");
assert_eq!(said.may_say(t0 + Duration::from_secs(1)), None);
assert_eq!(said.may_say(t0 + Duration::from_secs(59)), None);
assert_eq!(said.may_say(t0 + Duration::from_secs(60)), Some(2), "the next window says one, with the two held back");
assert_eq!(said.may_say(t0 + Duration::from_secs(61)), None);
assert_eq!(SAY_EVERY, Duration::from_secs(60));
}
#[test]
fn a_connect_head_names_its_destination_and_anything_else_is_refused() {
assert_eq!(parse_connect("CONNECT objects.example.com:443 HTTP/1.1\r\nHost: objects.example.com:443\r\n\r\n").unwrap(), ("objects.example.com".into(), 443));
assert_eq!(parse_connect("CONNECT [2001:db8::1]:443 HTTP/1.1\r\n\r\n").unwrap(), ("2001:db8::1".into(), 443));
assert!(parse_connect("GET http://x/ HTTP/1.1\r\n\r\n").unwrap_err().contains("not a CONNECT"));
assert!(parse_connect("CONNECT x HTTP/1.1\r\n\r\n").unwrap_err().contains("no port"));
assert!(parse_connect("CONNECT :443 HTTP/1.1\r\n\r\n").unwrap_err().contains("no host"));
assert_eq!(door_url(), "http://10.44.0.1:3128");
}
#[test]
fn the_door_leaves_on_tls_ports_to_outside_addresses_only() {
let out: Vec<SocketAddr> = vec!["192.0.2.10:443".parse().unwrap()];
assert!(admits("objects.example.com", 443, &out).is_ok());
assert!(admits("smtp.example.com", 465, &["192.0.2.10:465".parse().unwrap()]).is_ok());
assert!(admits("x", 22, &out).unwrap_err().contains("[egress-port]"));
assert!(admits("x", 80, &out).unwrap_err().contains("[egress-port]"));
for inner in ["10.6.0.2:443", "10.44.0.2:443", "127.0.0.1:443", "169.254.169.254:443", "100.64.0.1:443", "[::1]:443", "[fd00::1]:443", "[::ffff:10.0.0.1]:443"] {
assert!(admits("x", 443, &[inner.parse().unwrap()]).unwrap_err().contains("[egress-inside]"), "{inner}");
}
assert!(admits("x", 443, &[]).unwrap_err().contains("[egress-unresolved]"));
assert!(on_tunnel("10.44.0.2".parse().unwrap()) && !on_tunnel("10.6.0.2".parse().unwrap()) && !on_tunnel("192.0.2.1".parse().unwrap()));
}
#[test]
fn the_door_carries_a_tunnel_and_refuses_what_its_policy_refuses() {
let echo = TcpListener::bind("127.0.0.1:0").unwrap();
let echo_at = echo.local_addr().unwrap();
std::thread::spawn(move || {
for mut c in echo.incoming().flatten() {
let mut b = [0u8; 5];
if c.read_exact(&mut b).is_ok() {
let _ = c.write_all(&b);
}
}
});
let open = Policy { peer: |_| true, admits: |_, _, r| if r.is_empty() { Err("none".into()) } else { Ok(()) } };
let door = serve("127.0.0.1:0".parse().unwrap(), open).unwrap();
let mut c = TcpStream::connect(door).unwrap();
c.write_all(format!("CONNECT 127.0.0.1:{} HTTP/1.1\r\n\r\n", echo_at.port()).as_bytes()).unwrap();
let mut reply = [0u8; 39];
c.read_exact(&mut reply).unwrap();
assert!(String::from_utf8_lossy(&reply).starts_with("HTTP/1.1 200"), "{}", String::from_utf8_lossy(&reply));
c.write_all(b"hello").unwrap();
let mut back = [0u8; 5];
c.read_exact(&mut back).unwrap();
assert_eq!(&back, b"hello");
let shut = Policy { peer: |_| false, admits };
let door = serve("127.0.0.1:0".parse().unwrap(), shut).unwrap();
let mut c = TcpStream::connect(door).unwrap();
let mut reply = String::new();
let _ = c.read_to_string(&mut reply);
assert!(reply.starts_with("HTTP/1.1 403"), "a peer off the tunnel: {reply}");
let door = serve("127.0.0.1:0".parse().unwrap(), Policy { peer: |_| true, admits }).unwrap();
let mut c = TcpStream::connect(door).unwrap();
c.write_all(format!("CONNECT 127.0.0.1:{} HTTP/1.1\r\n\r\n", echo_at.port()).as_bytes()).unwrap();
let mut reply = String::new();
let _ = c.read_to_string(&mut reply);
assert!(reply.starts_with("HTTP/1.1 403"), "the estate's policy refuses loopback and the port: {reply}");
}
#[test]
fn a_box_asks_its_door_for_a_named_destination_and_reads_the_answer() {
assert_eq!(connect_head("smtp.example.com", 465), "CONNECT smtp.example.com:465 HTTP/1.1\r\nHost: smtp.example.com:465\r\n\r\n");
assert_eq!(connect_head("2001:db8::1", 587), "CONNECT [2001:db8::1]:587 HTTP/1.1\r\nHost: [2001:db8::1]:587\r\n\r\n");
assert_eq!(parse_connect(&connect_head("smtp.example.com", 587)).unwrap(), ("smtp.example.com".into(), 587), "the door reads what a box asks");
assert_eq!(door_addr(&door_url()).unwrap(), "10.44.0.1:3128".parse::<SocketAddr>().unwrap());
assert_eq!(door_addr("http://10.44.0.1:3128/").unwrap(), "10.44.0.1:3128".parse::<SocketAddr>().unwrap());
assert!(door_addr("socks5://10.44.0.1:1080").unwrap_err().contains("http://"));
assert!(door_addr("http://front:3128").unwrap_err().contains("address"), "a fed door is an address: the box has no DNS");
assert!(established("HTTP/1.1 200 Connection Established\r\n\r\n").is_ok());
let refused = established("HTTP/1.1 403 Forbidden\r\n\r\n").unwrap_err();
assert!(refused.contains("403"), "{refused}");
}
#[test]
fn a_forward_carries_a_client_that_speaks_no_proxy_through_the_door() {
let relay = TcpListener::bind("127.0.0.1:0").unwrap();
let relay_at = relay.local_addr().unwrap();
std::thread::spawn(move || {
for mut c in relay.incoming().flatten() {
std::thread::spawn(move || {
let _ = c.write_all(b"220 relay ESMTP\r\n");
let mut b = [0u8; 6];
if c.read_exact(&mut b).is_ok() {
let _ = c.write_all(&b);
}
});
}
});
let open = Policy { peer: |_| true, admits: |_, _, r| if r.is_empty() { Err("none".into()) } else { Ok(()) } };
let door = serve("127.0.0.1:0".parse().unwrap(), open).unwrap();
let door = format!("http://{door}");
let fwd = Forward::open(&door, "127.0.0.1", relay_at.port()).unwrap();
assert!(fwd.addr().ip().is_loopback(), "the forward listens on loopback only");
for round in 0..2 {
let mut c = TcpStream::connect(fwd.addr()).unwrap();
let mut greet = [0u8; 17];
c.read_exact(&mut greet).unwrap();
assert_eq!(&greet, b"220 relay ESMTP\r\n", "round {round}: the relay's own greeting, through the door");
c.write_all(b"EHLO x").unwrap();
let mut back = [0u8; 6];
c.read_exact(&mut back).unwrap();
assert_eq!(&back, b"EHLO x");
}
drop(fwd);
let shut = serve("127.0.0.1:0".parse().unwrap(), Policy { peer: |_| true, admits }).unwrap();
let why = Forward::open(&format!("http://{shut}"), "127.0.0.1", relay_at.port()).err().expect("the estate's policy refuses loopback");
assert!(why.to_string().contains("403"), "{why}");
}
#[test]
fn a_fed_door_is_kept_and_a_blank_one_is_not() {
set_proxy(" ");
assert_eq!(proxy(), None);
set_proxy(&door_url());
assert_eq!(proxy(), Some("http://10.44.0.1:3128"));
set_proxy("http://elsewhere:1");
assert_eq!(proxy(), Some("http://10.44.0.1:3128"), "the first wins");
}
}