Watch
0
0
Fork
You've already forked hyperhive
0

squid intercepts restricted and log agents' web traffic, named from the bound source address

This commit is contained in:
atlas 2026-11-13 05:17:01 +01:00 • committed by mara
commit 712dcf2b25
2 changed files with 1005 additions and 154 deletions

View file

@ -1686,6 +1686,104 @@ let
in
refused egressForwardDrop && !(refused bare) && !(refused egressForwardAccept);
}
{
# The redirect of the agents' web traffic: IPv4 TCP 80 and 443 from the
# bridge go to squid's two intercept ports, after the rules that leave
# alone the exempt MACs (service containers, unrestricted agents), the
# host's own addresses, the bridge and the mesh. The chain is read as the
# lines between its header and the log sets that follow it. The host
# without a controller (`egressForwardAccept`) has the set but no chain.
name = "the egress table redirects the bridge's web traffic into squid after its exemptions";
ok =
let
inet = lib.head (
lib.splitString "table bridge hive_egress {" bare.services.hyperhive.network.egress.ruleset
);
chain = lib.head (
lib.splitString "set log_v4" (lib.last (lib.splitString "chain intercept {" inet))
);
rules = lib.filter (l: l != "" && l != "}") (map lib.trim (lib.splitString "\n" chain));
idx = pat: lib.lists.findFirstIndex (lib.hasInfix pat) 1000 rules;
redirects = lib.filter (lib.hasInfix " redirect to ") rules;
without = egressForwardAccept.services.hyperhive.network.egress.ruleset;
in
lib.hasInfix "type nat hook prerouting priority dstnat; policy accept;" chain
&& lib.hasInfix "set direct_mac { type ether_addr; }" inet
&& idx "ether saddr @direct_mac return" < idx "tcp dport 80"
&& idx "fib daddr type local return" < idx "tcp dport 80"
&& idx ''fib daddr oifname { "hive-br0", "wg-hive" } return'' < idx "tcp dport 80"
&& lib.length redirects == 2
&& lib.hasInfix "tcp dport 80 counter redirect to :3129" (lib.head redirects)
&& lib.hasInfix "tcp dport 443 counter redirect to :3130" (lib.last redirects)
# Only IPv4 that comes in on the bridge is looked at.
&& lib.all (l: lib.hasPrefix ''iifname "hive-br0" meta nfproto ipv4 '' l) (lib.drop 1 rules)
&& lib.hasInfix "set direct_mac { type ether_addr; }" without
&& !(lib.hasInfix "chain intercept" without);
}
{
# Squid listens on the two intercept ports, peeks and splices and never
# decrypts, names the agent of a flow with the helper, refuses a source
# it cannot name before any other rule, lets the CONNECT it builds
# before the ClientHello through only after the address denies, and
# leaves the proxy credential rules to the proxy port.
name = "squid's intercept ports splice, name the agent from the bindings and keep the credential rules for the proxy port";
ok =
let
conf = bare.services.squid.configText;
lines = map lib.trim (lib.splitString "\n" conf);
idx = line: lib.lists.findFirstIndex (l: l == line) 1000 lines;
squidCfg = bare.services.hyperhive.deploy.hive-controller.squid;
in
lib.hasInfix "http_port 10.42.0.1:3129 intercept name=icpt_http" conf
&& lib.hasInfix "https_port 10.42.0.1:3130 intercept ssl-bump name=icpt_tls " conf
&& lib.hasInfix "generate-host-certificates=off" conf
&& lib.elem "ssl_bump peek first_hop" lines
&& lib.elem "ssl_bump splice all" lines
&& !(lib.hasInfix "ssl_bump bump" conf)
&& !(lib.hasInfix "ssl_bump stare" conf)
&& !(lib.hasInfix "sslcrtd_program" conf)
&& lib.elem "host_verify_strict on" lines
&& lib.hasInfix "%SRC /run/hive-rootfs-check egress-whois" conf
&& idx "http_access deny icpt !icpt_agent" < idx "http_access deny ip_v4_host"
&& idx "http_access deny icpt !icpt_agent" < idx "include ${squidCfg.denyFile}"
&&
idx "http_access deny icpt !icpt_agent"
< idx "http_access allow icpt first_hop CONNECT !to_localhost !to_linklocal !to_bridge !to_private"
&&
idx "http_access allow icpt first_hop CONNECT !to_localhost !to_linklocal !to_bridge !to_private"
< idx "http_access deny ip_v4_host"
&& lib.elem "http_access deny !icpt !authed" lines
&& !(lib.elem "http_access deny !authed" lines)
&& lib.elem "http_access allow !icpt unrestricted" lines
&& !(lib.elem "http_access allow unrestricted" lines)
&& lib.all (p: lib.elem p bare.services.hyperhive.network.exposeHostPorts) [
3128
3129
3130
];
}
{
# `hive-priv` fills `direct_mac` from the same bindings as the bridge
# table's, so the units that run it are told the list of unrestricted
# agents, and a change of the list runs a pass. A host without squid has
# neither.
name = "the units that fill the egress exemptions read squid's unrestricted list and a change of it runs a pass";
ok =
let
list = bare.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile;
envOf = m: name: m.systemd.services.${name}.environment;
in
list == "/var/lib/hive-squid/unrestricted"
&& lib.all (name: ((envOf bare name).HIVE_EGRESS_UNRESTRICTED or null) == list) [
"hive-egress"
"hive-egress-pin@"
"hive-egress-pin"
]
&& bare.systemd.paths.hive-egress-direct.pathConfig.PathChanged == list
&& bare.systemd.paths.hive-egress-direct.pathConfig.Unit == "hive-egress-pin.service"
&& !((envOf egressForwardAccept "hive-egress") ? HIVE_EGRESS_UNRESTRICTED)
&& !(egressForwardAccept.systemd.paths ? hive-egress-direct);
}
{
# The host that turns `route_localnet` on is refused by an assertion
# naming it; the ordinary host, which leaves it alone, is not.
@ -2189,7 +2287,7 @@ let
"http_access deny to_linklocal"
"http_access deny to_bridge"
"http_access deny to_private"
"http_access deny !authed"
"http_access deny !icpt !authed"
];
in
e.HIVE_SQUID_PROXY == "${withIdentity.services.hyperhive.network.bridgeIp}:3128"

View file

@ -513,17 +513,24 @@ let
}
{
# Control for the absence checks: the bridge listener is there, so the
# absences below are not a misspelled lookup.
name = "squid listens on the bridge IP only, without a second listener or ssl_bump";
# absences below are not a misspelled lookup. The two intercept ports are
# on the bridge IP too, and the only TLS directives are the ones that
# peek and splice.
name = "squid listens on the bridge IP only, with one proxy port, two intercept ports and no decryption";
ok =
let
conf = hiveOtel.services.squid.configText;
lines = lib.splitString "\n" conf;
ip = hiveOtel.services.hyperhive.network.bridgeIp;
listeners = lib.filter (l: lib.hasPrefix "http_port " l || lib.hasPrefix "https_port " l) lines;
in
builtins.elem "http_port ${hiveOtel.services.hyperhive.network.bridgeIp}:3128" lines
&& lib.length (lib.filter (l: lib.hasPrefix "http_port " l) lines) == 1
&& !(lib.hasInfix "ssl_bump" conf)
&& !(lib.hasInfix "ssl-bump" conf)
builtins.elem "http_port ${ip}:3128" lines
&& lib.length listeners == 3
&& lib.all (l: lib.hasPrefix "${lib.head (lib.splitString " " l)} ${ip}:" l) listeners
&& lib.length (lib.filter (lib.hasInfix " intercept ") listeners) == 2
&& lib.elem "ssl_bump peek first_hop" lines
&& lib.elem "ssl_bump splice all" lines
&& lib.length (lib.filter (lib.hasPrefix "ssl_bump ") lines) == 2
&& !(lib.hasInfix "localnet" conf);
}
{
@ -540,8 +547,8 @@ let
at = l: lib.lists.findFirstIndex (x: x == l) null lines;
allow = at "http_access allow allowed_dst";
base = at "http_access allow base_dst";
unrestricted = at "http_access allow unrestricted";
authed = at "http_access deny !authed";
unrestricted = at "http_access allow !icpt unrestricted";
authed = at "http_access deny !icpt !authed";
# The generated per-agent `http_access allow` rules sit in this file.
agentRules = at "include ${hiveOtel.services.hyperhive.deploy.hive-controller.squid.agentAllowFile}";
firstAllow = lib.min agentRules (lib.min base (lib.min unrestricted allow));
@ -631,11 +638,11 @@ let
at = l: lib.lists.findFirstIndex (x: x == l) null lines;
list = hiveOtel.services.hyperhive.deploy.hive-controller.squid.unrestrictedFile;
in
at "http_access allow unrestricted" != null
at "http_access allow !icpt unrestricted" != null
&& at "http_access deny !allowed_dst" != null
&& at "http_access deny !authed" < at "http_access allow base_dst"
&& at "http_access allow base_dst" < at "http_access allow unrestricted"
&& at "http_access allow unrestricted" < at "http_access deny !allowed_dst"
&& at "http_access deny !icpt !authed" < at "http_access allow base_dst"
&& at "http_access allow base_dst" < at "http_access allow !icpt unrestricted"
&& at "http_access allow !icpt unrestricted" < at "http_access deny !allowed_dst"
&& at "http_access deny !allowed_dst" < at "http_access allow allowed_dst"
&& builtins.elem ''acl unrestricted proxy_auth "${list}"'' lines
&& list == "/var/lib/hive-squid/unrestricted";
@ -655,7 +662,7 @@ let
allows = map at [
"http_access allow base_dst"
"include ${sq.agentAllowFile}"
"http_access allow unrestricted"
"http_access allow !icpt unrestricted"
"http_access allow allowed_dst"
];
in
@ -665,7 +672,7 @@ let
&& lib.all (a: deny < a) allows
&& at "http_access deny to_private" < deny
&& at "http_access deny ip_v4_host" < deny
&& deny < at "http_access deny !authed"
&& deny < at "http_access deny !icpt !authed"
# Two includes: this one and the per-agent allow rules.
&& lib.length (lib.filter (l: lib.hasPrefix "include " l) lines) == 2;
}