Watch
0
0
Fork
You've already forked hyperhive
0

refuse tls hellos that carry encrypted client hello before squid sees them

This commit is contained in:
atlas 2026-11-13 06:23:42 +01:00 • committed by mara
commit dff662c6b3
4 changed files with 1537 additions and 43 deletions

View file

@ -38,7 +38,9 @@ not the whole daemon-shared crate.
`storage` (btrfs subvolumes, quotas, snapshots, disk measurement), `files`
(pause marker, socket dir, legacy tmpfiles) and `infra` (gateway nginx, forge
admin, infra containers). `egress` holds four sub-commands that never
touch the socket, `egress-load`, `egress-pin`, `egress-lease` and `egress-whois`; the
touch the socket, `egress-load`, `egress-pin`, `egress-lease` and `egress-whois`, and
`egress_ech` a fifth, `egress-ech-relay`, the long-running ClientHello check in front of
squid's HTTPS intercept port; the
`hive-egress` unit, udev, a timer and dnsmasq run the first three to fill the sets of
the anti-spoofing table and the interception exemptions, and squid runs the last to
name the agent of an intercepted flow (see

View file

@ -38,6 +38,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::{Context as _, Result, bail};
use crate::egress_ech::same_host;
/// The sub-command squid runs to learn which agent holds a source address.
pub(crate) const WHOIS: &str = "egress-whois";
@ -344,7 +346,9 @@ fn unrestricted_names() -> Vec<String> {
/// The agent whose container holds the IPv4 address `ip`, as dnsmasq's lease
/// file and the bindings of the ports say: the lease names a MAC, the MAC is
/// bound to one port, and that port's machine is `h-<agent>`. `None` when any
/// bound to one port, and that port's machine is `h-<agent>`. An address in the
/// pool of the ECH relay (`egress_ech`) stands for the leased address it was
/// derived from. `None` when any
/// step is missing or ambiguous (two leases for the address under different
/// MACs, a MAC bound twice, a machine that is no agent, a name that is no
/// identifier): a flow whose agent cannot be told is refused, never guessed.
@ -357,7 +361,7 @@ pub(crate) fn agent_of(ip: &str, leases: &str, pins: &[Pin], now: u64) -> Option
0 => true,
t => t > now,
};
(live && addr.parse::<std::net::Ipv4Addr>().ok()? == ip).then(|| Mac::parse(mac))
(live && same_host(ip, addr.parse::<std::net::Ipv4Addr>().ok()?)).then(|| Mac::parse(mac))
});
let mac = macs.next()??;
if macs.any(|other| other != Some(mac)) {
@ -373,20 +377,25 @@ pub(crate) fn agent_of(ip: &str, leases: &str, pins: &[Pin], now: u64) -> Option
Some(agent.to_owned())
}
/// [`agent_of`] on the files as they are now, which are read for every call.
pub(crate) fn agent_of_now(ip: &str) -> Option<String> {
let leasefile = std::env::var("HIVE_EGRESS_LEASEFILE").unwrap_or_else(|_| LEASEFILE.into());
let pins = parse_state(&std::fs::read_to_string(state_path()).unwrap_or_default());
let leases = std::fs::read_to_string(leasefile).unwrap_or_default();
agent_of(ip, &leases, &pins, now_secs())
}
/// `egress-whois`: squid's external ACL helper. Each line on stdin starts with
/// a source address; the answer is `OK user=<agent>` or `ERR`. The two files
/// are read for every line, since squid caches the answers itself.
fn whois() -> Result<()> {
use std::io::{BufRead as _, Write as _};
let leasefile = std::env::var("HIVE_EGRESS_LEASEFILE").unwrap_or_else(|_| LEASEFILE.into());
let stdin = std::io::stdin();
let mut stdout = std::io::stdout().lock();
for line in stdin.lock().lines() {
let line = line.context("read stdin")?;
let ip = line.split_whitespace().next().unwrap_or_default();
let pins = parse_state(&std::fs::read_to_string(state_path()).unwrap_or_default());
let leases = std::fs::read_to_string(&leasefile).unwrap_or_default();
match agent_of(ip, &leases, &pins, now_secs()) {
match agent_of_now(ip) {
Some(agent) => writeln!(stdout, "OK user={agent}"),
None => writeln!(stdout, "ERR"),
}
@ -1117,6 +1126,31 @@ mod tests {
);
}
#[test]
fn a_relay_pool_address_names_the_agent_it_was_derived_from() {
let pins = [
pin("vb-h-atlas", 10, 1, "h-atlas"),
pin("vb-h-uma", 11, 2, "h-uma"),
];
let leases = lease_line(NOW + 60, 1, "10.42.0.5") + &lease_line(NOW + 60, 2, "10.42.0.6");
assert_eq!(
agent_of("198.18.0.5", &leases, &pins, NOW).as_deref(),
Some("atlas")
);
assert_eq!(
agent_of("198.18.0.6", &leases, &pins, NOW).as_deref(),
Some("uma")
);
// No lease behind the pool address, and an address of the pool that
// is itself leased: neither names anyone.
assert_eq!(agent_of("198.18.0.7", &leases, &pins, NOW), None);
let leased_pool = lease_line(NOW + 60, 1, "198.18.0.9");
assert_eq!(agent_of("198.18.0.9", &leased_pool, &pins, NOW), None);
// Two leases that share the pool address are ambiguous.
let twins = lease_line(NOW + 60, 1, "10.42.0.5") + &lease_line(NOW + 60, 2, "10.44.0.5");
assert_eq!(agent_of("198.18.0.5", &twins, &pins, NOW), None);
}
#[test]
fn an_address_nobody_can_be_told_to_hold_has_no_agent() {
let pins = [

1116
hive-priv/src/egress_ech.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,8 @@
//! turns ephemeral, with [`salvage_io`] for its space, reflink and lock calls),
//! [`prestart`] (the check of a container's root that runs before each start,
//! as a sub-command), [`egress`] (the bridge anti-spoofing bindings, also
//! sub-commands), [`storage`] (btrfs subvolumes, quotas, snapshots, disk
//! sub-commands), [`egress_ech`] (the `ClientHello` check in front of squid's
//! HTTPS intercept port), [`storage`] (btrfs subvolumes, quotas, snapshots, disk
//! measurement), [`files`] (pause marker, socket dir, legacy tmpfiles),
//! [`infra`] (gateway nginx, infra containers) and [`closure`] (the swarm-signature
//! check before an apply, and the cache-facts refresh). Helpers more than one family uses are in [`shared`].
@ -32,6 +33,7 @@ mod closure;
mod container;
mod container_config;
mod egress;
mod egress_ech;
mod files;
mod infra;
mod prestart;
@ -60,6 +62,10 @@ async fn main() -> Result<()> {
if let Some(code) = egress::run(&args) {
std::process::exit(code);
}
// `egress-ech-relay` is a long-running unit of its own, not a request either.
if let Some(code) = egress_ech::run(&args).await {
std::process::exit(code);
}
tracing_subscriber::fmt()
.with_env_filter(