Merge s4/egress-bind
This commit is contained in:
commit
34c2d58c32
100
src/egress.rs
100
src/egress.rs
|
|
@ -22,27 +22,84 @@ pub fn start() -> Result<u16, String> {
|
||||||
return Ok(BOUND.load(Ordering::SeqCst));
|
return Ok(BOUND.load(Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
// macOS (Docker Desktop / OrbStack) forwards host.docker.internal to this machine's
|
// macOS (Docker Desktop / OrbStack) forwards host.docker.internal to this machine's
|
||||||
// 127.0.0.1. Linux Docker reaches the host through the bridge (172.17.0.1…), so listen on all
|
// 127.0.0.1. Linux Docker reaches the host through the bridge: bind loopback plus the
|
||||||
// addresses there and only serve this machine and its containers (see `allowed_peer`).
|
// bridge's own gateway address (detected, `bridge_gateways`), never 0.0.0.0 — an
|
||||||
let host = if cfg!(target_os = "linux") { "0.0.0.0" } else { "127.0.0.1" };
|
// accidentally-open proxy on the LAN is an open door, and `allowed_peer` stays as the
|
||||||
let listener = (PORT..PORT + 20).find_map(|p| TcpListener::bind((host, p)).ok()).ok_or("找不到可用的連接埠")?;
|
// second wall (a container's address changes with the network, a bound socket does not).
|
||||||
let port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
let addrs = bind_addrs();
|
||||||
|
let mut listeners = Vec::new();
|
||||||
|
for p in PORT..PORT + 20 {
|
||||||
|
listeners = addrs.iter().filter_map(|a| TcpListener::bind(((*a).to_string(), p)).ok()).collect();
|
||||||
|
if listeners.len() == addrs.len() && !listeners.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
listeners.clear();
|
||||||
|
}
|
||||||
|
if listeners.is_empty() {
|
||||||
|
return Err("找不到可用的連接埠".into());
|
||||||
|
}
|
||||||
|
let port = listeners[0].local_addr().map_err(|e| e.to_string())?.port();
|
||||||
BOUND.store(port, Ordering::SeqCst);
|
BOUND.store(port, Ordering::SeqCst);
|
||||||
RUNNING.store(true, Ordering::SeqCst);
|
RUNNING.store(true, Ordering::SeqCst);
|
||||||
std::thread::spawn(move || {
|
for l in listeners {
|
||||||
for conn in listener.incoming().flatten() {
|
std::thread::spawn(move || {
|
||||||
if !conn.peer_addr().map(|a| allowed_peer(a.ip())).unwrap_or(false) {
|
for conn in l.incoming().flatten() {
|
||||||
let _ = conn.shutdown(Shutdown::Both);
|
if !conn.peer_addr().map(|a| allowed_peer(a.ip())).unwrap_or(false) {
|
||||||
continue;
|
let _ = conn.shutdown(Shutdown::Both);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = handle(conn);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
std::thread::spawn(move || {
|
});
|
||||||
let _ = handle(conn);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(port)
|
Ok(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where to listen: loopback, and on Linux each Docker bridge's gateway address (the address
|
||||||
|
/// containers reach us on). A host with no `ip` command keeps only loopback — the box's
|
||||||
|
// commands then fail the proxy and the person is told to turn the setting off, which beats
|
||||||
|
/// silently serving the whole LAN.
|
||||||
|
pub fn bind_addrs() -> Vec<std::net::IpAddr> {
|
||||||
|
let mut v: Vec<std::net::IpAddr> = vec!["127.0.0.1".parse().unwrap()];
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
let out = std::process::Command::new("ip").args(["-o", "-4", "addr", "show"]).output().map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default();
|
||||||
|
for (iface, ip) in parse_ipv4_addrs(&out) {
|
||||||
|
if bridge_iface(&iface) && !v.iter().any(|a| a.to_string() == ip.to_string()) {
|
||||||
|
v.push(std::net::IpAddr::V4(ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Docker's own bridges only: `docker0`, `docker_gwbridge`, and the per-network
|
||||||
|
/// `br-<12 hex>` — not user bridges like a router's `br-lan`.
|
||||||
|
pub fn bridge_iface(name: &str) -> bool {
|
||||||
|
name == "docker0" || name == "docker_gwbridge"
|
||||||
|
|| (name.len() == 15 && name.starts_with("br-") && name[3..].bytes().all(|b| b.is_ascii_hexdigit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ip -o -4 addr show` → (interface, address) pairs. Lines look like
|
||||||
|
/// `3: docker0 INET 172.17.0.1/16 brd 172.17.0.255 scope global docker0`.
|
||||||
|
pub fn parse_ipv4_addrs(out: &str) -> Vec<(String, std::net::Ipv4Addr)> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for line in out.lines() {
|
||||||
|
let mut w = line.split_whitespace();
|
||||||
|
let _idx = w.next();
|
||||||
|
let Some(iface) = w.next() else { continue };
|
||||||
|
if w.next() != Some("INET") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(cidr) = w.next() else { continue };
|
||||||
|
if let Ok(ip) = cidr.split('/').next().unwrap_or("").parse::<std::net::Ipv4Addr>() {
|
||||||
|
v.push((iface.trim_matches(':').to_string(), ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
/// Who may use the proxy: this machine, and Docker's default bridge (172.17.0.0/16, where the
|
/// Who may use the proxy: this machine, and Docker's default bridge (172.17.0.0/16, where the
|
||||||
/// boxes run). The rest of 172.16.0.0/12 is often the corporate LAN — an open proxy there is
|
/// boxes run). The rest of 172.16.0.0/12 is often the corporate LAN — an open proxy there is
|
||||||
/// an open door, so it is not served.
|
/// an open door, so it is not served.
|
||||||
|
|
@ -168,6 +225,19 @@ fn handle(mut client: TcpStream) -> Result<(), String> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binds_bridge_gateways_not_the_whole_lan() {
|
||||||
|
let out = "1: lo INET 127.0.0.1/8 scope host lo\n3: docker0 INET 172.17.0.1/16 brd 172.17.0.255 scope global docker0\n8: br-5f3a11c2d4e6 INET 172.18.0.1/16 scope global br-5f3a11c2d4e6\n2: eth0 INET 192.168.1.30/24 brd 192.168.1.255 scope global eth0\n5: virbr0 INET 192.168.122.1/24 scope global virbr0\n";
|
||||||
|
let addrs: Vec<_> = parse_ipv4_addrs(out).into_iter().filter(|(i, _)| bridge_iface(i)).map(|(_, ip)| ip.to_string()).collect();
|
||||||
|
assert_eq!(addrs, ["172.17.0.1", "172.18.0.1"]); // eth0 and virbr0 are not Docker's
|
||||||
|
assert!(bridge_iface("docker_gwbridge") && !bridge_iface("br-lan") && !bridge_iface("docker0x"));
|
||||||
|
// The proxy's second wall matches what it binds: loopback and 172.17/16 only.
|
||||||
|
assert!(allowed_peer("127.0.0.1".parse().unwrap()));
|
||||||
|
assert!(allowed_peer("172.17.0.5".parse().unwrap()));
|
||||||
|
assert!(!allowed_peer("172.18.0.5".parse().unwrap())); // a user-defined network is not served
|
||||||
|
assert!(!allowed_peer("192.168.1.20".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn routes_connect_and_plain_http() {
|
fn routes_connect_and_plain_http() {
|
||||||
let r = route(b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n").unwrap();
|
let r = route(b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n").unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue