refuse tls hellos that carry encrypted client hello before squid sees them
This commit is contained in:
parent
de43dac33c
commit
dff662c6b3
1 changed files with 1537 additions and 43 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue