refactor(3202): matrix declares its own vhost, dns name and SPA map

Moves the matrix sub-domain vhost out of the gateway's vhosts.nix, its
`address=` rule out of dnsmasq.nix, and the Accept-header
`$matrix_spa_target` map out of the gateway's appendHttpConfig — all
three into hive-matrix.nix.

The map is the one that had no business being where it was: it exists
solely for the SPA fallback in the vhost's `/` location, and
`appendHttpConfig` is a `lines` option, so a module can contribute to
it without the gateway assembling it.

The `.well-known/matrix/*` delegation deliberately stays on the hive's
own vhost. The spec requires it at the SERVER NAME, which is the hive
domain: that is the hive answering "where is my homeserver", not the
homeserver answering for itself. Moving it would have been the obvious
symmetric thing and it would have been wrong.
This commit is contained in:
atlas 2026-08-13 14:36:17 +02:00
commit 6caf177416
4 changed files with 82 additions and 82 deletions

View file

@ -347,7 +347,7 @@ in
recommendedTlsSettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
inherit (nginxTree) appendHttpConfig virtualHosts;
inherit (nginxTree) virtualHosts;
};
# ⚠️ NO `SupplementaryGroups = [ "hive-core" ]` on nginx, and its
@ -379,7 +379,6 @@ in
lib
cfg
networkCfg
matrixCfg
uiCfg
hyperhiveDomain
;

View file

@ -9,7 +9,6 @@
lib,
cfg, # services.hyperhive.gateway
networkCfg,
matrixCfg,
uiCfg,
hyperhiveDomain,
}:
@ -54,20 +53,9 @@
# Hive authoritative records — answer queries for the hive domain
# + its sub-domains with the bridge IP, where nginx is reachable
# from every container netns.
#
# The matrix entry is redundant in the common case where
# `matrix.gatewayHost` is a sub-domain of `hyperhive.domain` —
# dnsmasq's `/<domain>/` rule already matches sub-domains. Kept
# explicit because an operator can override it to a cross-domain
# hostname (e.g. `git.example.com` for the forge); listing such a
# name explicitly keeps that case routed without an extra config
# block.
address = [
"/${hyperhiveDomain}/${networkCfg.bridgeIp}"
]
++ lib.optional (
matrixCfg.enable && matrixCfg.gatewayHost != null
) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}"
# The swarm UI's name is the swarm APEX by default — a sibling of
# the three above, not a child of anything this resolver already
# answers for, so the `/<hive domain>/` rule does not cover it.

View file

@ -3,7 +3,7 @@
# and authelia sub-domain vhosts, and the Accept-header SPA map for the
# matrix GUI. Pure function — called from ./default.nix with the
# outer-scope config values as arguments; returns
# `{ virtualHosts, appendHttpConfig }`.
# `{ virtualHosts }`.
{
lib,
cfg, # services.hyperhive.gateway
@ -145,61 +145,6 @@ let
};
};
# Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`.
# `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll
# timeout). `/` serves fluffychat or 404 if GUI off. nginx
# longer-prefix-wins puts `/_matrix/` ahead of `/`. See
# `docs/gateway.md`. Empty attrset when matrix has no gateway host.
matrixVhost = lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
"${matrixCfg.gatewayHost}" = (vhostTlsFor matrixCfg.gatewayHost) // {
listen = vhostListen;
extraConfig = securityHeaders;
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;
${securityHeaders}
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;
'';
};
}
// {
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the
# right delegation endpoint. `domain` is required, so
# this is always present.
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
)
// lib.optionalAttrs (!matrixCfg.gui.enable) {
"/" = {
return = "404";
};
};
};
};
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link
# shim during the fluffychat sub-domain move). See `docs/gateway.md`.
matrixRedirectLocations =
@ -385,17 +330,6 @@ let
};
in
{
# Accept-header SPA map for the matrix GUI only (see docs/gateway.md
# "SPA fallback"): text/html → index.html, else a sentinel so
# try_files falls through to 404. The dashboard doesn't use an
# Accept-header map — it routes by path (see dashboardProxyLocation).
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 = {
# `tlsFor "_"`, not a separate binding: the default server is a
# vhost named `_`, and a name that is not a swarm service domain
@ -435,6 +369,5 @@ in
};
}
// matrixVhost
// swarmUiVhost;
}

View file

@ -9,6 +9,7 @@ let
networkCfg = config.services.hyperhive.network;
tlsCfg = config.services.hyperhive.tls;
gatewayCfg = config.services.hyperhive.gateway;
hyperhiveDomain = config.services.hyperhive.domain;
# Same runtime→build-time bridge hive-ci and hive-forge already cross:
# binds the hive trust bundle (which folds in the swarm root) into the
@ -396,6 +397,85 @@ in
};
config = lib.mkIf cfg.enable {
# Matrix's own gateway surface: the sub-domain vhost, the name the
# hive resolver answers for, and the Accept-header map that vhost's
# SPA fallback reads. All three are matrix knowledge and none of
# them is the gateway's business.
#
# `gatewayHost = null` means matrix is reachable directly rather
# than fronted, so there is no name to claim and no vhost to serve —
# every clause below carries that guard.
services.hyperhive.gateway.localNames = lib.optional (cfg.gatewayHost != null) cfg.gatewayHost;
# Accept-header SPA map, used only by the `/` location below (see
# docs/gateway.md "SPA fallback"): text/html → index.html, else a
# sentinel so `try_files` falls through to 404. `appendHttpConfig`
# is a `lines` option, so this merges with anything else the host
# contributes instead of replacing it.
#
# The dashboard needs no equivalent — it routes by path.
services.nginx.appendHttpConfig = lib.optionalString cfg.gui.enable ''
map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback";
"~*text/html" "/index.html";
}
'';
# `server_name = gatewayHost`. `/_matrix/*` → tuwunel (CORS `*`, 50M
# body cap, 1h long-poll timeout). `/` serves fluffychat, or 404
# with the GUI off. nginx's longest-prefix rule puts `/_matrix/`
# ahead of `/` with no ordering needed.
#
# ⚠️ The `.well-known/matrix/*` delegation is deliberately NOT here.
# It stays on the hive's own vhost because the spec requires it to
# be served at the *server name*, which is the hive domain — it is
# the hive answering "where is my homeserver", not the homeserver
# answering for itself.
services.nginx.virtualHosts = lib.optionalAttrs (cfg.gatewayHost != null) {
"${cfg.gatewayHost}" = (gatewayCfg.lib.tlsFor cfg.gatewayHost) // {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = {
"/_matrix/" = {
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 50M;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
${gatewayCfg.lib.securityHeaders}
add_header Access-Control-Allow-Origin *;
'';
};
}
// lib.optionalAttrs cfg.gui.enable {
# fluffychat at sub-domain root, SPA-fallback via the
# Accept-header `$matrix_spa_target` map above.
"/" = {
alias = "${cfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the right delegation
# endpoint. `domain` is required, so this is always present.
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
// lib.optionalAttrs (!cfg.gui.enable) {
"/" = {
return = "404";
};
};
};
};
# `serverName` is irrevocably embedded in user/room IDs; it derives
# from `services.hyperhive.domain` (required, asserted in
# hive-network.nix) when not set explicitly, so no separate