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

View file

@ -517,10 +517,11 @@ credential=present|absent unrestricted_canary=… unrestricted_status=…` line
A program that ignores the proxy variables still gets squid's policy for its
web traffic. In the `inet hive_egress` table (`nix/host-modules/hive-egress.nix`)
the `intercept` chain, at `prerouting`, redirects IPv4 TCP port 80 to
`<bridgeIp>:3129` and port 443 to `<bridgeIp>:3130`, the two intercept listeners
that `hive-squid.nix` opens. Squid reads the original destination from
conntrack. The chain exists where the host runs squid
(`services.hyperhive.network.egress.intercept.enable`).
`<bridgeIp>:3129`, an intercept listener of squid, and port 443 to
`<bridgeIp>:3131`, the ClientHello relay (see _ECH_ below), which sends the
connection on to squid's other intercept listener, `<bridgeIp>:3130`. Squid reads
the original destination from conntrack. The chain exists where the host runs
squid (`services.hyperhive.network.egress.intercept.enable`).
- **Who gets redirected.** Every flow from the bridge to port 80 or 443, except
a flow to an address that stays on the bridge, to one of the host's own
@ -570,25 +571,79 @@ conntrack. The chain exists where the host runs squid
- a ClientHello with no SNI, and a URL with an IP literal, which sends none:
the host of the `CONNECT` stays an address, and the IP-literal denies refuse
it;
- bytes on port 443 that aren't a ClientHello, and on port 80 that aren't an
HTTP request.
- bytes on port 80 that aren't an HTTP request. Bytes on port 443 that aren't
a ClientHello never reach squid (see _ECH_ below).
- **ECH.** Squid 7.6 can't see the `encrypted_client_hello` extension, only the
public name in the outer hello. An agent that holds an ECH configuration could
send a hello with an allowed outer name, a destination address that name
resolves to, and a hidden inner name that the same address serves. Port 443
therefore doesn't go to squid directly. `hive-priv egress-ech-relay`
(`hive-egress-ech-relay.service`, `egress.intercept.refuseEch`, on by default)
listens on `<bridgeIp>:3131`. It reads the first TLS records of each redirected
connection, up to four records and 16 KiB of ClientHello within 10 seconds, and
closes the connection when the ClientHello of a `restricted` agent carries the
extension (type `0xfe0d`, and the draft types `0xfe08` to `0xfe0c`, `0xff02`
and `0xffce`). The relay only observes a `log` agent, as everywhere in `log`
mode: its hello goes on like a clean one, and the relay logs it (see below).
It closes a connection too, for any agent, when the bytes aren't a well-formed
ClientHello, when they don't arrive in time, or when the connection wasn't
redirected. It also closes a connection from a source that already has 256
open, and any connection beyond 2048 in all, so one agent can't use up the
relay. Otherwise it connects to the destination the agent dialed and sends the bytes
on, and squid sees an ordinary intercepted connection:
- the relay connects from an address in `198.18.0.0/15`, which the unit routes
to `lo`, and an `output` rule DNATs its marked connections (mark
`0x45434800`) to squid's port 3130, so squid reads the original destination
from conntrack as before;
- the source address is `198.18.0.0` plus the low 17 bits of the agent's
address, and `egress-whois` maps it back to the lease it came from, so the
pool needs a bridge subnet of /15 or smaller (an assertion checks) and
squid's log shows `198.18.x.y` as the client address of an intercepted HTTPS
flow. The user column still names the agent. The `input` chain drops a pool
address that arrives from the bridge;
- the relay logs to the journal of its unit, at most one line a second per
source address, with the agent named the way squid names it (`-` when no
lease and binding says). A refusal is
`hive-egress-ech-refused agent=<name> dst=<address>`; an ECH hello of a `log`
agent, which goes on, is
`hive-egress-ech-would-refuse agent=<name> dst=<address>`; a hello that isn't
well formed is
`hive-egress-hello-refused agent=<name> dst=<address> reason=<why>`. Squid logs
nothing for a refused flow;
- the mode comes from `agent-allow.conf`, the include `hive-c0re` renders: an
agent is in `log` mode when it holds the line
`acl agent_log_<name> proxy_auth <name>`. Squid applies the same file, so the
relay and squid can't disagree about an agent's mode, and the file changes
when the operator changes the mode, with no second list to keep in step. A
missing or unreadable file, or an agent with no lease, counts as
`restricted`, and the relay refuses the hello;
- while the relay is down the redirect reaches a closed port, so HTTPS from
`restricted` and `log` agents fails. The unit restarts after 5 seconds.
Squid 7.6 can't see the `encrypted_client_hello` extension, only the public
name in the outer hello. An agent that holds an ECH configuration can send a
hello with an allowed outer name, a destination address that name resolves
to, and a hidden inner name that the same address serves. Squid lets that
flow through, so ECH isn't refused here.
GREASE: a client with no ECH configuration, Chrome and Firefox by default,
sends the extension anyway with random contents, and the protocol makes the two
look the same on the wire. The relay can't tell them apart, so it treats both
as ECH: a `restricted` agent's HTTPS fails when the client ignores the proxy
variables, and a `log` agent's goes on with a would-refuse line. Through the
proxy port (`HTTPS_PROXY`, a `CONNECT`) it works as before: that path has no
interception. A host where that costs more than the gap does can set
`egress.intercept.refuseEch = false`, which sends port 443 straight to squid
and leaves ECH unchecked. The relay only sees the hello of a connection that
the redirect catches. It doesn't touch UDP (QUIC), IPv6 or other ports.
- **Reading the log.** Each intercepted HTTPS connection logs twice: first
`NONE_NONE/000 … CONNECT <address>:443 <agent>`, before the ClientHello, then
the `CONNECT <name>:443 <agent>` line with `TCP_TUNNEL/200` or `TCP_DENIED/403`
(`NONE_NONE/409` when the SNI and the address disagree). An intercepted HTTP
request logs `GET http://<name>/ <agent>`. A tunnel to an intercepted
(`NONE_NONE/409` when the SNI and the address disagree). The client address of
an intercepted HTTPS line is the relay's `198.18.x.y` (see _ECH_ above), and an
intercepted HTTP request logs `GET http://<name>/ <agent>` with the agent's own
address. A tunnel to an intercepted
destination ends in `ORIGINAL_DST/<address>`. `cache.log` notes an ACL that met
a flow squid refused before its request.
- **Rollout.** The redirect, squid's configuration and `hive-priv` ship in one
build. A hive that still runs an older build has no redirect, and its agents go
direct as before. The agents that use the proxy variables see no change.
- **Rollout.** The redirect, the relay, squid's configuration and `hive-priv` ship
in one build. A hive that still runs an older build has no redirect, and its
agents go direct as before. The agents that use the proxy variables see no
change.
## Container isolation