nix/hive-{gateway,matrix}: matrix vhost at matrix.<hive-domain> + .well-known delegation (#747)
mara on #747:9722: "this still seems to be an issue in current version" (after #751 closed without merge). Mirroring the forge sub-domain pattern just merged as #754 for matrix per mara's #749:9609 verdict (sub-domain over sub-path for forge + matrix, "not user-visible for matrix because the .well-known/matrix/{client,server} redirect routes clients through automatically"). ## Mechanics **New `services.hyperhive.matrix.gatewayHost`** — nullable str, defaults to `matrix.<services.hyperhive.domain>` when hive-domain set, else null. Full hostname (`matrix.darkest.space`, `homeserver.internal.lan`) for bespoke shapes per mara's #754:9684 "specify full domain in options instead" pattern. **Gateway:** new `server { server_name = matrixCfg.gatewayHost; }` block proxying `/_matrix/...` → `http://127.0.0.1:<httpPort>/_matrix/...` with matrix-spec CORS + tuned for long-poll `/sync` (1h timeout) + typical media uploads (50M body cap). `/` returns 404 — nothing else lives at the matrix vhost. Matches the forge vhost shape from #754. **`.well-known/matrix/{client,server}`** (already served at bare hive- domain since #660): now points at `matrixCfg.gatewayHost` (no port suffix when gateway is on the canonical port 80) instead of the direct `<hive-domain>:<httpPort>` shape. Falls back to direct shape when `gatewayHost = null` (no hive-domain, or operator nulled it). **`localHostsEntry` extension**: `/etc/hosts` (when set) now adds the matrix sub-domain → 127.0.0.1 alongside hive-domain + forge.domain. `lib.unique` collapses any duplicate (edge case if operator sets gatewayHost equal to hive-domain). ## Verified via `nix eval` ``` vhosts: ["_", "forge.test.local", "matrix.test.local"] gatewayHost: "matrix.test.local" client wellknown: m.homeserver.base_url = "http://matrix.test.local" server wellknown: m.server = "matrix.test.local" /etc/hosts: ["test.local", "forge.test.local", "matrix.test.local"] ``` ## What this fixes for #747 mara's HAR showed `GET /.well-known/matrix/client` and `GET /_matrix/client/versions` both failing on `pr1ma.darkest.space`: 1. **`.well-known/matrix/client`** was advertising `http://pr1ma.darkest.space:8008` — that URL only works if tuwunel's port 8008 is firewall-open to the operator's browser (it isn't by default — `services.hyperhive.matrix.openFirewall` defaults to false since #651). Now advertises `http://matrix.pr1ma.darkest.space/` which goes through the gateway on the (already-open) port 80. 2. **`/_matrix/client/versions`** was hitting the bare-domain `"_"` vhost, which has no `/_matrix/` location — fell through to `/` → c0re's dashboard upstream → 404. Now hits the new `matrix.<hive>` vhost which proxies the request to tuwunel cleanly. server_name + serverName unaffected — matrix identifiers (`@alice:<hive>`) still embed the bare hive-domain per #660; only the wire-level transport URL moves to the sub-domain. ## Risk Medium. Existing matrix tokens / sessions stay valid because: - `serverName` (the identifier domain) doesn't change - tuwunel's `/_matrix/` endpoints serve the same requests, just reached via the new sub-domain instead of the direct port Operators with `services.hyperhive.matrix.openFirewall = true` and external clients reaching `:8008` directly keep working too — the sub-domain vhost is additive, doesn't take away the direct port. ## Sequencing This is a parallel matrix-side mirror of #754 (forge). Both follow the same mara-verdict pattern; once both have soaked, the gateway- behind-everything story is done for v0. Closes #747.
This commit is contained in:
parent
8f9c77df06
commit
01cc664d76
2 changed files with 151 additions and 24 deletions
|
|
@ -326,21 +326,48 @@ in
|
||||||
# (https://spec.matrix.org/v1.15/client-server-api/#getwell-knownmatrixclient).
|
# (https://spec.matrix.org/v1.15/client-server-api/#getwell-knownmatrixclient).
|
||||||
# No-op until the operator turns matrix on; until then
|
# No-op until the operator turns matrix on; until then
|
||||||
# there's no homeserver to advertise.
|
# there's no homeserver to advertise.
|
||||||
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) {
|
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
|
||||||
"= /.well-known/matrix/client" = {
|
let
|
||||||
extraConfig = ''
|
# `.well-known/matrix/{client,server}` advertise where
|
||||||
default_type application/json;
|
# the actual matrix API lives. When `matrixCfg.gatewayHost`
|
||||||
add_header Access-Control-Allow-Origin *;
|
# is set (default `matrix.<hive-domain>`, #747), point
|
||||||
return 200 '{"m.homeserver":{"base_url":"http://${hyperhiveDomain}:${toString matrixCfg.httpPort}"}}';
|
# at the sub-domain — no port suffix when the gateway
|
||||||
'';
|
# is on the canonical port 80, transparent to clients
|
||||||
};
|
# (mara on #749:9609 sub-domain verdict, "not user-
|
||||||
"= /.well-known/matrix/server" = {
|
# visible because the .well-known redirect routes
|
||||||
extraConfig = ''
|
# clients through automatically"). When `gatewayHost`
|
||||||
default_type application/json;
|
# is unset (no hive-domain, or operator nulled it),
|
||||||
return 200 '{"m.server":"${hyperhiveDomain}:${toString matrixCfg.httpPort}"}';
|
# fall back to the direct `host:port` shape — clients
|
||||||
'';
|
# reach tuwunel without going through the gateway,
|
||||||
};
|
# no sub-domain delegation.
|
||||||
}
|
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
|
||||||
|
clientBaseUrl =
|
||||||
|
if matrixCfg.gatewayHost != null then
|
||||||
|
"http://${matrixCfg.gatewayHost}${portSuffix}"
|
||||||
|
else
|
||||||
|
"http://${hyperhiveDomain}:${toString matrixCfg.httpPort}";
|
||||||
|
serverHostPort =
|
||||||
|
if matrixCfg.gatewayHost != null then
|
||||||
|
"${matrixCfg.gatewayHost}${portSuffix}"
|
||||||
|
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}"}';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
//
|
//
|
||||||
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
||||||
# block per `<name>: <port>` entry in
|
# block per `<name>: <port>` entry in
|
||||||
|
|
@ -442,6 +469,61 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
//
|
||||||
|
# Matrix homeserver vhost (#747, mara verdict on #749:9609 —
|
||||||
|
# sub-domain over sub-path for matrix; "not user-visible"
|
||||||
|
# because clients discover the sub-domain via the
|
||||||
|
# `.well-known/matrix/{client,server}` delegation served
|
||||||
|
# above on the bare hive-domain).
|
||||||
|
#
|
||||||
|
# `server { server_name = matrixCfg.gatewayHost; }` proxies
|
||||||
|
# `/_matrix/...` → `http://127.0.0.1:''${matrixCfg.httpPort}/_matrix/...`.
|
||||||
|
# Tuwunel listens on `:''${httpPort}` (default 8008); the
|
||||||
|
# gateway terminates on `:''${cfg.port}` (80) so external
|
||||||
|
# clients speak matrix over the canonical web port without
|
||||||
|
# operators having to open the tuwunel port through firewalls.
|
||||||
|
#
|
||||||
|
# `/` returns 404 — nothing else lives at the matrix vhost;
|
||||||
|
# the matrix client-server API is entirely under `/_matrix/`,
|
||||||
|
# and federation under `/_matrix/federation/...`.
|
||||||
|
#
|
||||||
|
# CORS `*` on the matrix vhost per the matrix spec —
|
||||||
|
# federation + client requests come from any origin.
|
||||||
|
#
|
||||||
|
# `client_max_body_size 50M` covers typical media uploads
|
||||||
|
# (matrix-spec media size cap default); operators with bigger
|
||||||
|
# uploads override via the matrix module's own cap when that
|
||||||
|
# lands.
|
||||||
|
#
|
||||||
|
# `proxy_read_timeout 1h` for long-poll `/sync`; the default
|
||||||
|
# 60s would abort `/sync?timeout=30000` legitimately when
|
||||||
|
# tuwunel's keepalive exceeds that.
|
||||||
|
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
|
||||||
|
"${matrixCfg.gatewayHost}" = {
|
||||||
|
listen = [
|
||||||
|
{
|
||||||
|
addr = "0.0.0.0";
|
||||||
|
port = cfg.port;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
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 *;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"/" = {
|
||||||
|
return = "404";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -452,16 +534,16 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
# `/etc/hosts` entries for local dev: the bare hive domain plus
|
# `/etc/hosts` entries for local dev: the bare hive domain plus
|
||||||
# any sub-domain modules (forge, matrix-via-#751) that are on.
|
# any sub-domain modules (forge via #749/#754, matrix via #747)
|
||||||
# All map to `127.0.0.1` since the gateway shares host netns.
|
# that are on. All map to `127.0.0.1` since the gateway shares
|
||||||
# Operators with real DNS leave `localHostsEntry = false`; this
|
# host netns. Operators with real DNS leave `localHostsEntry =
|
||||||
# is the dev-loop shortcut for `http://<hive-domain>/` +
|
# false`; this is the dev-loop shortcut for `http://<hive-domain>/`
|
||||||
# `http://forge.<hive-domain>/` resolving locally.
|
# + `http://forge.<hive-domain>/` + `http://matrix.<hive-domain>/`
|
||||||
|
# resolving locally.
|
||||||
#
|
#
|
||||||
# Forge's `cfg.domain` may equal `hyperhiveDomain` (e.g. operator
|
# `lib.unique` collapses any duplicate (e.g. if forge.domain
|
||||||
# set `forge.domain = "darkest.space"` matching the hive domain)
|
# happens to equal hyperhiveDomain or matrixCfg.gatewayHost) so
|
||||||
# — `lib.unique` collapses the duplicate so `/etc/hosts` doesn't
|
# `/etc/hosts` doesn't carry the same entry twice.
|
||||||
# carry the same entry twice.
|
|
||||||
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
|
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
|
||||||
"127.0.0.1" = lib.unique (
|
"127.0.0.1" = lib.unique (
|
||||||
[ hyperhiveDomain ]
|
[ hyperhiveDomain ]
|
||||||
|
|
@ -469,6 +551,7 @@ in
|
||||||
(config.services.hyperhive.forge.enable or false)
|
(config.services.hyperhive.forge.enable or false)
|
||||||
&& (config.services.hyperhive.forge.behindGateway or false)
|
&& (config.services.hyperhive.forge.behindGateway or false)
|
||||||
) config.services.hyperhive.forge.domain
|
) config.services.hyperhive.forge.domain
|
||||||
|
++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,50 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
gatewayHost = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = if hyperhiveDomain != null then "matrix.${hyperhiveDomain}" else null;
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
if services.hyperhive.domain != null then
|
||||||
|
"matrix.''${services.hyperhive.domain}"
|
||||||
|
else
|
||||||
|
null
|
||||||
|
'';
|
||||||
|
example = "matrix.example.com";
|
||||||
|
description = ''
|
||||||
|
Public hostname for the matrix homeserver behind the
|
||||||
|
hive-gateway nginx (#747, mara verdict on #749:9609 — sub-domain
|
||||||
|
over sub-path for matrix, but **not user-visible** because the
|
||||||
|
`.well-known/matrix/{client,server}` redirect routes clients
|
||||||
|
through automatically).
|
||||||
|
|
||||||
|
When set + gateway is on, the gateway adds a `server { server_name
|
||||||
|
= gatewayHost; }` block that proxies `/_matrix/...` →
|
||||||
|
`http://127.0.0.1:''${httpPort}/_matrix/...`. The
|
||||||
|
`.well-known/matrix/{client,server}` endpoints (served by the
|
||||||
|
gateway at the bare hive-domain) then point at
|
||||||
|
`http(s)://''${gatewayHost}/` — matrix clients automatically
|
||||||
|
discover + follow that delegation.
|
||||||
|
|
||||||
|
Defaults to `matrix.''${services.hyperhive.domain}` when the
|
||||||
|
hive-domain is set (idiomatic matrix-spec shape — `matrix`
|
||||||
|
labelled under the hive's bare server_name domain). Defaults to
|
||||||
|
`null` when the hive-domain is unset (gateway vhost not added;
|
||||||
|
clients reach tuwunel directly on `httpPort`).
|
||||||
|
|
||||||
|
Set to a full hostname (`matrix.example.com`,
|
||||||
|
`homeserver.internal.lan`) for a bespoke vhost shape. Set to
|
||||||
|
`null` to disable the gateway vhost entirely (tuwunel stays
|
||||||
|
direct on `httpPort`).
|
||||||
|
|
||||||
|
**server_name vs gatewayHost**: `serverName` is the matrix
|
||||||
|
identifier domain embedded in user/room IDs irrevocably (per
|
||||||
|
#660 default = bare hive-domain). `gatewayHost` is just where
|
||||||
|
the API listens behind nginx. The two are different — see the
|
||||||
|
matrix-spec server-discovery flow.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
openFirewall = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue