From 078aed1335eea5ceef16528673faf9aca1458989 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 27 Oct 2026 18:56:15 +0100 Subject: [PATCH] refuse unlisted squid names before any destination lookup --- docs/networking/network.md | 1 + nix/host-modules/hive-squid.nix | 10 +++++++--- nix/module-eval/hive-otel.nix | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/networking/network.md b/docs/networking/network.md index 3e183a4f..e12748dc 100644 --- a/docs/networking/network.md +++ b/docs/networking/network.md @@ -222,6 +222,7 @@ host. It never decrypts TLS, so it holds no signing key. It caches nothing. - **Allow-list.** `services.hyperhive.squid.allowedDomains` defaults to the swarm's own service names (`services.hyperhive.swarm.serviceDomains`). Each entry matches that exact name. Everything else gets a 403. +- **Unlisted names.** Refused first, without a DNS lookup. - **Denied before any allow.** Destinations that resolve to loopback, link-local or the bridge subnet, and IP-literal hosts. Squid resolves names on the host, so without these a listed name pointing at `127.0.0.1` would diff --git a/nix/host-modules/hive-squid.nix b/nix/host-modules/hive-squid.nix index e6026294..41f2556b 100644 --- a/nix/host-modules/hive-squid.nix +++ b/nix/host-modules/hive-squid.nix @@ -24,7 +24,10 @@ let # Evaluated top to bottom, first match wins. The three destination denies # sit before the only allow: squid resolves names on the host, so a name # that answers 127.0.0.1, 169.254.169.254 or a bridge address would - # otherwise reach services the bridge firewall keeps agents out of. + # otherwise reach services the bridge firewall keeps agents out of. The + # unlisted-name deny comes first because `dstdomain` needs no lookup while + # the `dst` ACLs do: an unlisted name is refused at once instead of waiting + # on the host resolver, and costs a client no DNS query. squidConf = '' http_port ${netCfg.bridgeIp}:${toString port} pid_filename /run/squid/squid.pid @@ -60,11 +63,12 @@ let http_access deny !Safe_ports http_access deny CONNECT !SSL_ports + http_access deny ip_v4_host + http_access deny ip_v6_host + ${lib.optionalString (allowed != [ ]) "http_access deny !allowed_dst"} http_access deny to_localhost http_access deny to_linklocal http_access deny to_bridge - http_access deny ip_v4_host - http_access deny ip_v6_host ${lib.optionalString (allowed != [ ]) "http_access allow allowed_dst"} http_access deny all ''; diff --git a/nix/module-eval/hive-otel.nix b/nix/module-eval/hive-otel.nix index f2434b59..3db70378 100644 --- a/nix/module-eval/hive-otel.nix +++ b/nix/module-eval/hive-otel.nix @@ -461,7 +461,7 @@ let } { # First match wins, so a destination deny after the allow is dead. - name = "squid denies loopback, link-local and bridge destinations before any destination allow"; + name = "squid refuses unlisted names, then loopback, link-local and bridge destinations, before any allow"; ok = let lines = lib.splitString "\n" hiveOtel.services.squid.configText; @@ -475,6 +475,8 @@ let && at "http_access deny to_linklocal" < allow && at "http_access deny to_bridge" != null && at "http_access deny to_bridge" < allow + && at "http_access deny !allowed_dst" != null + && at "http_access deny !allowed_dst" < at "http_access deny to_localhost" && builtins.elem "cache deny all" lines && lib.last (lib.filter (l: l != "") lines) == "http_access deny all"; }