hyperhive/nix/modules/hive-gateway.nix
atlas 9eb8012c9b gateway: scope bind-mount to gateway/ subdir, scrub #869 cookie (argus 🟡)
- gateway_nginx.rs: HOST_CONF_PATH → /var/lib/hyperhive/gateway/agents.conf
- hive-gateway.nix: hostPath = /var/lib/hyperhive/gateway (not whole parent
  dir — avoids exposing forge tokens or other credentials to the gateway
  container)
- tmpfiles: add /var/lib/hyperhive/gateway/ dir rule + seed agents.conf there
- scrub "(#869)" from hive-gateway-nginx-reload service description
2026-05-31 20:29:56 +02:00

716 lines
32 KiB
Nix

{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.hyperhive.gateway;
hyperhiveDomain = config.services.hyperhive.domain;
matrixCfg = config.services.hyperhive.matrix;
forgeCfg = config.services.hyperhive.forge;
networkCfg = config.services.hyperhive.network;
# Static error pages for `/agent/<name>/` mishaps (#755). Mara's
# call: useful pages instead of nginx's default 404/502 for routes
# we've already special-cased. See `docs/gateway.md::Per-agent
# error pages` for the design rationale + page-vs-status semantics.
agentErrorPagesDir = pkgs.runCommand "hyperhive-agent-error-pages" { } ''
mkdir -p $out
cat > $out/not-found.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>agent not found hyperhive</title>
<style>
body { background: #1e1e2e; color: #cdd6f4; font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 0; padding: 4rem 1rem; text-align: center; }
h1 { color: #cba6f7; font-size: 1.5rem; margin: 0 0 0.5rem; }
p { max-width: 32rem; margin: 0.5rem auto; color: #a6adc8; }
code { background: #313244; color: #f5c2e7; padding: 0.1rem 0.35rem; border-radius: 0.2rem; }
a { color: #89b4fa; }
</style>
</head>
<body>
<h1> agent not found</h1>
<p>No agent matches the requested <code>/agent/&lt;name&gt;/</code> path on this hive.</p>
<p>Operator: check the agent name in <a href="/">the dashboard</a>.</p>
</body>
</html>
EOF
cat > $out/unreachable.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>agent unreachable hyperhive</title>
<style>
body { background: #1e1e2e; color: #cdd6f4; font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 0; padding: 4rem 1rem; text-align: center; }
h1 { color: #f9e2af; font-size: 1.5rem; margin: 0 0 0.5rem; }
p { max-width: 32rem; margin: 0.5rem auto; color: #a6adc8; }
code { background: #313244; color: #f5c2e7; padding: 0.1rem 0.35rem; border-radius: 0.2rem; }
a { color: #89b4fa; }
</style>
</head>
<body>
<h1> agent unreachable</h1>
<p>The agent's harness web server isn't responding. Container restarting, or the agent crashed.</p>
<p>Operator: <a href="/">dashboard</a> check the container status / journal; the page will recover on retry once the harness is back up.</p>
</body>
</html>
EOF
'';
in
{
# Single nginx in front of every hyperhive web surface — dashboard,
# per-agent UIs (sub-path), forge + matrix (sub-domain), .well-known
# delegations. Container `hive-gateway`, shared host netns,
# state-free. Full vhost map + discovery flow + design rationale in
# `docs/gateway.md`.
options.services.hyperhive.gateway = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Run hive-gateway a single nginx in front of every hyperhive
surface. On by default: the gateway hosts the matrix GUI static
dist (when `services.hyperhive.matrix.gui.enable` is true) and
proxies everything else to hive-c0re's dashboard upstream. Set
`services.hyperhive.gateway.enable = false` to bypass nginx
entirely and reach hive-c0re directly on its dashboard port
(7000 by default).
v0 is HTTP-only; TLS / public-domain shape is tracked
separately.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 80;
example = 8080;
description = ''
TCP port the gateway listens on. Default 80 (canonical web
port). nginx inside the container binds <1024 because the
container's init runs as root; if 80 is already taken on the
host (existing nginx, traefik, etc.) override to an unused
port like 8080 or move the conflicting service.
'';
};
upstreamHost = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Host the gateway proxies non-static requests to. Defaults to
`127.0.0.1` because the gateway container shares the host
netns, so loopback resolves directly to hive-c0re.
'';
};
upstreamPort = lib.mkOption {
type = lib.types.port;
default = 7000;
description = ''
TCP port the gateway proxies non-static requests to. Defaults
to `7000` (hive-c0re's out-of-the-box dashboard port). Operators
who change `services.hyperhive.c0re.dashboardPort` should set
`upstreamPort` to match kept as a hardcoded default rather
than a cross-reference to keep this module's options eval
independent of c0re's option tree shape.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Open `port` in the host firewall. Off by default (#651,
secure-by-default). Flip to `true` to expose the gateway to
the operator's browser / external clients required for any
out-of-host reach, since the agents themselves talk to
hive-c0re via the per-agent unix sockets and don't need the
nginx vhost. Leave off when running behind another reverse
proxy (e.g. caddy / traefik on the host) that handles TLS
termination + forwards to `port`.
**Breaking change as of #651**: this used to default to
`true`. If you relied on the old default for external reach
(the common case the gateway is the operator's primary
entry point), add `services.hyperhive.gateway.openFirewall = true;`
to your host config before rebuilding.
'';
};
localHostsEntry = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Add an `/etc/hosts` entry mapping `services.hyperhive.domain`
to `127.0.0.1` on the host. Useful for local deployments +
tests where there's no real DNS for `services.hyperhive.domain`
but the operator (or browser-based tests) want to hit
`http://''${services.hyperhive.domain}` to exercise the
gateway shape. Off by default operators running with real
DNS shouldn't have a stale `/etc/hosts` entry sticking
around. Requires `services.hyperhive.domain` to be set.
'';
};
selfSignedTls = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Generate a self-signed TLS cert at first gateway boot and
listen on `httpsPort` (default 443) with it on every vhost.
On by default because matrix-dart-sdk (the SDK behind
FluffyChat + several other Matrix clients) hardcodes
`https://<host>/.well-known/matrix/client` for homeserver
discovery and refuses to fall back to plain http without
TLS the browser client just won't connect (#837).
Self-signed means browsers will show a "not secure" warning
on first visit; the operator clicks through once per
browser. For production deployments, set this to `false`
and front the gateway with a reverse proxy (caddy, traefik,
or nginx with ACME) that does proper TLS termination.
The cert is regenerated on demand if the file is missing
but never rotated automatically; delete
`/var/lib/hive-gateway/tls/cert.pem` inside the gateway
container to force a fresh one.
See `docs/gateway.md` ("Self-signed TLS").
'';
};
httpsPort = lib.mkOption {
type = lib.types.port;
default = 443;
example = 8443;
description = ''
TCP port for the TLS-terminated vhosts when `selfSignedTls`
is enabled. Default 443. Setting `selfSignedTls = false`
renders this option inert (the gateway listens on `port`
only).
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.localHostsEntry || hyperhiveDomain != null;
message = ''
services.hyperhive.gateway.localHostsEntry = true requires
services.hyperhive.domain to be set. Either pin a hostname
or leave `localHostsEntry` at its default of false.
'';
}
];
# Ensure bind-mount sources exist at host boot before the gateway
# container's first start. nspawn would auto-create missing dirs
# (argus 🟡 on #829), but tmpfiles rules make the intent explicit
# and cover the fresh-boot window before c0re has run.
#
# /run/hive-agent — per-agent UDS socket dir, written by c0re's
# set_nspawn_flags when agents start.
# /var/lib/hyperhive — hyperhive state dir, created by c0re on
# first run. Also pre-seed agents.conf with an empty-but-valid
# header so nginx can start + include the file before c0re writes
# its first real content (f = create-if-absent, no overwrite).
systemd.tmpfiles.rules = [
"d /run/hive-agent 0755 root root - -"
"d /var/lib/hyperhive 0755 root root - -"
"d /var/lib/hyperhive/gateway 0755 root root - -"
"f /var/lib/hyperhive/gateway/agents.conf 0644 root root - # Generated by hive-c0re do not edit.\n"
];
containers.hive-gateway = {
autoStart = true;
ephemeral = false;
# Share host netns — nginx then binds host-level ports directly,
# `localhost` upstream resolution reaches hive-c0re without any
# port-forward dance, and the firewall config below is the only
# layer that matters.
privateNetwork = false;
# Bind-mount the per-agent socket dir so nginx inside the gateway
# container can `connect(2)` to the UDS upstreams (#784 step 3).
# Read-only (we just connect; harness writes the socket inside
# the agent's own container). Host-side dir is pre-created by a
# tmpfiles rule so nspawn always finds a source at boot.
bindMounts."/run/hive-agent" = {
hostPath = "/run/hive-agent";
isReadOnly = true;
};
# Bind-mount ONLY the gateway-specific subdir of the hyperhive
# state dir. Scoped to /var/lib/hyperhive/gateway/ rather than
# the whole parent so the gateway container can't read forge
# tokens or other files that may live at the parent level (argus
# 🟡 on #872). c0re writes agents.conf under this subdir;
# the systemd path unit inside the container fires nginx -s reload
# on each atomic rename. Pre-created by a tmpfiles rule.
bindMounts."/run/hive-state" = {
hostPath = "/var/lib/hyperhive/gateway";
isReadOnly = true;
};
config =
{ pkgs, ... }:
let
tlsDir = "/var/lib/hive-gateway/tls";
tlsCert = "${tlsDir}/cert.pem";
tlsKey = "${tlsDir}/key.pem";
# Listen addresses every vhost shares. Plain http on `cfg.port`
# always; `cfg.httpsPort` with TLS sits beside it when
# `cfg.selfSignedTls` is on. See `docs/gateway.md`
# ("Self-signed TLS") for the cert lifecycle.
vhostListen = [
{
addr = "0.0.0.0";
port = cfg.port;
}
]
++ lib.optional cfg.selfSignedTls {
addr = "0.0.0.0";
port = cfg.httpsPort;
ssl = true;
};
# nixos `services.nginx.virtualHosts.<name>` ssl attrs to mix
# into each vhost when self-signed TLS is on. `addSSL = true`
# is what gates `ssl_certificate` directive emission in the
# nixos nginx module (`hasSSL` checks addSSL / onlySSL /
# forceSSL). The actual ssl listen is the explicit entry
# with `ssl = true` in `vhostListen` — the nixos module's
# auto-listen-generation only kicks in when `listen` is
# empty, so the explicit listen wins and there's no
# duplicate-listen risk. Empty otherwise so the http-only
# path stays identical.
vhostTls = lib.optionalAttrs cfg.selfSignedTls {
addSSL = true;
sslCertificate = tlsCert;
sslCertificateKey = tlsKey;
};
# Public-facing scheme + port-suffix for URLs the gateway
# mints into responses (well-known JSON, the deprecated
# `<hive>/matrix/*` 301 redirect, future absolute-URL needs).
# When self-signed TLS is on, prefer `https://<host>` (matrix-
# spec compliance) — 443 elides the port. Otherwise fall back
# to the plain-http listen with the bare port. See
# `docs/gateway.md` ("Self-signed TLS"). Shared at this scope
# (was inlined twice, argus 🟡 on #848).
publicScheme = if cfg.selfSignedTls then "https" else "http";
publicPort = if cfg.selfSignedTls then cfg.httpsPort else cfg.port;
publicPortDefault = if cfg.selfSignedTls then 443 else 80;
publicPortSuffix = if publicPort == publicPortDefault then "" else ":${toString publicPort}";
in
{
system.stateVersion = "26.05";
# Ensure a valid self-signed cert exists before nginx starts.
# nginx `Requires=` this via `requiredBy`, so systemd refuses
# to start nginx until the script succeeds. ALWAYS runs (no
# ConditionPathExists) and is idempotent — that's necessary
# to reconcile broken state left over from prior failed
# boots (a 0700 dir from a stale UMask, a truncated cert
# from an interrupted oneshot, etc.) which a guarded-on-
# missing-cert script would silently skip and leave broken.
# Cert covers the bare hive domain plus `*.${hyperhiveDomain}`
# so the matrix + forge sub-domains are valid under the same
# cert. See `docs/gateway.md` ("Self-signed TLS").
systemd.services.hive-gateway-self-signed-cert = lib.mkIf cfg.selfSignedTls {
description = "Ensure self-signed TLS cert for hive-gateway";
wantedBy = [ "multi-user.target" ];
before = [ "nginx.service" ];
requiredBy = [ "nginx.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [
pkgs.openssl
pkgs.coreutils
];
script =
let
subjectCN = if hyperhiveDomain != null then hyperhiveDomain else "hyperhive.local";
# `subjectAltName` covers the bare hive + the canonical
# sub-domains. Includes a wildcard `*.${domain}` so any
# future sub-domain we mint (per-agent UI under a
# sub-domain, etc.) inherits the cert without a rebuild.
sanLines = lib.concatStringsSep "," (
[ "DNS:${subjectCN}" ] ++ lib.optional (hyperhiveDomain != null) "DNS:*.${hyperhiveDomain}"
);
in
''
set -eu
mkdir -p ${tlsDir}
# 0755 on BOTH the cert dir and its parent so the
# nginx user can traverse the full path. The parent
# `/var/lib/hive-gateway` lands at 0700 by default
# (systemd StateDirectory / mkdir umask depending on
# which service created it first), which on its own
# blocks traversal. Re-applied every boot in case a
# prior run left a tighter mode behind.
chmod 0755 ${builtins.dirOf tlsDir}
chmod 0755 ${tlsDir}
# Generate the cert when EITHER the cert or key is
# missing/empty, OR the cert fails an openssl parse
# catches truncated / corrupt leftovers from a previous
# interrupted run AND the "cert clean but key absent"
# edge case (argus 🟡 on the first revision) which
# otherwise tripped `chmod 0600 ${tlsKey}` below with
# ENOENT under `set -eu`. The whole oneshot is safe to
# re-run; a healthy cert+key pair is left alone.
if [ ! -s ${tlsCert} ] || [ ! -s ${tlsKey} ] || ! openssl x509 -in ${tlsCert} -noout >/dev/null 2>&1; then
echo "generating fresh self-signed cert at ${tlsCert}"
openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \
-keyout ${tlsKey} \
-out ${tlsCert} \
-subj "/CN=${subjectCN}" \
-addext "subjectAltName=${sanLines}"
fi
# Key owned by root:nginx, mode 0640 so nginx-pre-start
# (which runs `nginx -t` as the nginx user, not root)
# can read it. A 0600 root:root key passes the master-
# process load (master starts as root) but fails the
# pre-start config test with `BIO_new_file()
# Permission denied`, blocking the unit from starting
# at all. Cert is world-readable.
chown root:nginx ${tlsKey}
chmod 0640 ${tlsKey}
chmod 0644 ${tlsCert}
'';
};
# Watch /run/hive-state/agents.conf (bind-mounted from the
# host's /var/lib/hyperhive/agents.conf) for changes and
# trigger an nginx reload when c0re atomically renames a new
# version into place (#869). PathChanged fires on
# IN_CLOSE_WRITE + IN_MOVED_TO, so the atomic rename c0re
# uses (write .conf.tmp → rename) wakes the path unit.
# The reload is a no-op if the new config is identical —
# gateway_nginx::write skips the rename when content is
# unchanged, so the path unit doesn't fire at all on quiet
# ticks.
systemd.paths.hive-gateway-agents-conf = {
wantedBy = [ "nginx.service" ];
after = [ "nginx.service" ];
pathConfig = {
PathChanged = "/run/hive-state/agents.conf";
Unit = "hive-gateway-nginx-reload.service";
};
};
systemd.services.hive-gateway-nginx-reload = {
description = "Reload nginx after agents.conf change";
# Don't block any target — fires only when the path unit
# triggers it.
serviceConfig = {
Type = "oneshot";
# nginx -s reload sends SIGHUP to the master process via
# the pid file. Runs as root inside the container (pid 1
# is the nspawn init; nginx master starts as root).
ExecStart = "/run/current-system/sw/bin/nginx -s reload";
};
};
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedOptimisation = true;
# Accept-header SPA fallback (#686 / #729): navigations
# (`Accept: text/html,...`) fall to index.html, asset
# fetches (Accept *anything else*) fall to a sentinel
# nonexistent path → `try_files` returns 404. Pattern
# detailed in `docs/gateway.md` ("SPA fallback").
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback";
"~*text/html" "/index.html";
}
'';
virtualHosts = {
"_" = vhostTls // {
listen = vhostListen;
locations =
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1`
# (fluffychat moved to sub-domain root in #772; this
# keeps bookmarks + deep-links working during the
# transition). See `docs/gateway.md` for the vhost
# map.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null) (
let
target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
in
{
"/matrix/" = {
extraConfig = ''
rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
'';
};
}
)
//
# `.well-known/matrix/{client,server}` discovery JSON.
# Points clients at `matrixCfg.gatewayHost` (sub-domain
# vhost) when set; falls back to direct `<hive>:<httpPort>`
# when no gateway target. CORS `*` per matrix spec.
# See `docs/gateway.md` "Discovery flow" for the full
# client-bootstrap sequence.
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
let
clientBaseUrl =
if matrixCfg.gatewayHost != null then
"${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"
else
"${publicScheme}://${hyperhiveDomain}:${toString matrixCfg.httpPort}";
serverHostPort =
if matrixCfg.gatewayHost != null then
"${matrixCfg.gatewayHost}${publicPortSuffix}"
else
"${hyperhiveDomain}:${toString matrixCfg.httpPort}";
in
{
"= /.well-known/matrix/client" = {
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver":{"base_url":"${clientBaseUrl}"}}';
'';
};
"= /.well-known/matrix/server" = {
extraConfig = ''
default_type application/json;
return 200 '{"m.server":"${serverHostPort}"}';
'';
};
}
)
//
# `/agent/` catch-all (#755): hits when an operator
# requests `/agent/<unknown>/...`. Without this the
# request falls through to `/` (c0re dashboard) and
# returns 404 with no useful context. Custom 404
# page instead. Per-agent `location /agent/<name>/`
# blocks live in `/run/hive-state/agents.conf` —
# nginx picks them up via the `include` in
# `extraConfig` below; the catch-all only matches
# names that aren't in that file (nginx longest-
# prefix-match: `/agent/atlas/` beats `/agent/`).
{
"/agent/" = {
extraConfig = ''
error_page 404 = /__hive_agent_not_found;
return 404;
'';
};
# Internal static-file locations the error_page
# directives above point at. `internal` keeps
# operators from hitting the file directly (only
# nginx's error-handling can reach it); `alias`
# serves the exact file regardless of request URI.
"= /__hive_agent_not_found" = {
extraConfig = ''
internal;
alias ${agentErrorPagesDir}/not-found.html;
default_type text/html;
'';
};
"= /__hive_agent_unreachable" = {
extraConfig = ''
internal;
alias ${agentErrorPagesDir}/unreachable.html;
default_type text/html;
'';
};
}
// {
# Everything else proxies to hive-c0re. Upgrade
# headers stay set so SSE (`/dashboard/stream`,
# `/events/stream`) + websocket (`/screen/ws`)
# endpoints keep working transparently.
"/" = {
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
proxy_read_timeout 1d;
'';
};
};
# Per-agent location blocks, generated at runtime by
# hive-c0re and written to /var/lib/hyperhive/agents.conf
# on the host. The bind-mount at /run/hive-state/ exposes
# that file here. nginx parses `include` at config-load
# time so a reload (triggered by the hive-gateway-nginx-
# reload path unit when agents.conf changes) picks up new
# or removed agents without a nixos-rebuild. nginx's
# longest-prefix-match rule ensures `/agent/<name>/` from
# this file beats the `/agent/` catch-all above (#869).
extraConfig = ''
include /run/hive-state/agents.conf;
'';
};
}
//
# Forge sub-domain vhost (#749 / #754). `server_name =
# forge.domain`, proxies all `/` → forgejo. Tuned for
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h`
# (multi-GB clones). SSH stays direct on `forge.sshPort`.
# See `docs/gateway.md`.
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
"${forgeCfg.domain}" = vhostTls // {
listen = vhostListen;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 1G;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
'';
};
};
}
//
# Matrix sub-domain vhost (#747 / #764). `server_name =
# matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *,
# 50M body cap, 1h long-poll timeout). `/` serves
# fluffychat (#772) or 404 if GUI off. nginx
# longer-prefix-wins puts `/_matrix/` ahead of `/`.
# See `docs/gateway.md`.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
"${matrixCfg.gatewayHost}" = vhostTls // {
listen = vhostListen;
locations = {
"/_matrix/" = {
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 50M;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
add_header Access-Control-Allow-Origin *;
'';
};
}
// lib.optionalAttrs (matrixCfg.gui.enable) (
{
# fluffychat at sub-domain root, SPA-fallback via
# the Accept-header `$matrix_spa_target` map.
"/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
}
// lib.optionalAttrs (hyperhiveDomain != null) {
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the
# right delegation endpoint (#736).
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
)
// lib.optionalAttrs (!matrixCfg.gui.enable) {
"/" = {
return = "404";
};
};
};
};
};
# Hive-internal DNS resolver (#805 v1). Co-located in the
# gateway container per mara's call (#805:10957) — single
# front-door for both DNS and HTTP, saves a sibling
# container. Listens on the bridge interface from
# `services.hyperhive.network`; authoritative for the hive
# domain + sub-domains, forwards everything else upstream.
# No-op when `network.enable = false`.
services.dnsmasq = lib.mkIf networkCfg.enable {
enable = true;
# Don't substitute the container's /etc/resolv.conf —
# the gateway uses the host's resolver for its own
# outbound traffic; dnsmasq is purely for incoming
# queries from agent containers.
resolveLocalQueries = false;
settings = {
# Bind only on the bridge interface (and lo for
# health-checks). Outside hosts can't even see the
# listener.
interface = [
networkCfg.bridgeName
"lo"
];
bind-interfaces = true;
port = 53;
# Don't read /etc/resolv.conf — we control upstream
# explicitly to dodge dependency on the gateway
# container's own resolver state.
no-resolv = true;
server = networkCfg.upstreamDns;
# Hive authoritative records — answer queries for the
# hive domain + its sub-domains with the bridge IP
# (where nginx is reachable from container netns once
# #14 lands; today it's the host loopback alias and
# works in either shape).
#
# The forge / matrix entries are redundant in the
# common case where `forge.domain` /
# `matrix.gatewayHost` are sub-domains of
# `hyperhive.domain` — dnsmasq's `/<domain>/` rule
# already matches sub-domains (argus 🟡 on #845).
# Kept explicit because operators can override either
# to a cross-domain hostname (e.g.
# `forge.domain = "git.example.com"`); listing them
# explicitly keeps that case routed without needing
# an extra config block.
address = [
"/${hyperhiveDomain}/${networkCfg.bridgeIp}"
]
++ lib.optional (
(forgeCfg.enable or false) && (forgeCfg.behindGateway or false)
) "/${forgeCfg.domain}/${networkCfg.bridgeIp}"
++ lib.optional (
matrixCfg.enable && matrixCfg.gatewayHost != null
) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}";
};
};
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ] ++ lib.optional cfg.selfSignedTls cfg.httpsPort;
};
# `/etc/hosts` entries for local dev — bare hive domain + any
# sub-domain modules that are on. `lib.unique` dedupes if any
# sub-domain happens to equal another. See `docs/gateway.md`
# ("Local dev").
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
"127.0.0.1" = lib.unique (
[ hyperhiveDomain ]
++ lib.optional (
(config.services.hyperhive.forge.enable or false)
&& (config.services.hyperhive.forge.behindGateway or false)
) config.services.hyperhive.forge.domain
++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost
);
};
};
}