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).
|
||||
# No-op until the operator turns matrix on; until then
|
||||
# there's no homeserver to advertise.
|
||||
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) {
|
||||
"= /.well-known/matrix/client" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
return 200 '{"m.homeserver":{"base_url":"http://${hyperhiveDomain}:${toString matrixCfg.httpPort}"}}';
|
||||
'';
|
||||
};
|
||||
"= /.well-known/matrix/server" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
return 200 '{"m.server":"${hyperhiveDomain}:${toString matrixCfg.httpPort}"}';
|
||||
'';
|
||||
};
|
||||
}
|
||||
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
|
||||
let
|
||||
# `.well-known/matrix/{client,server}` advertise where
|
||||
# the actual matrix API lives. When `matrixCfg.gatewayHost`
|
||||
# is set (default `matrix.<hive-domain>`, #747), point
|
||||
# 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-
|
||||
# visible because the .well-known redirect routes
|
||||
# clients through automatically"). When `gatewayHost`
|
||||
# is unset (no hive-domain, or operator nulled it),
|
||||
# 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>/`
|
||||
# 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
|
||||
# any sub-domain modules (forge, matrix-via-#751) that are on.
|
||||
# All map to `127.0.0.1` since the gateway shares host netns.
|
||||
# Operators with real DNS leave `localHostsEntry = false`; this
|
||||
# is the dev-loop shortcut for `http://<hive-domain>/` +
|
||||
# `http://forge.<hive-domain>/` resolving locally.
|
||||
# any sub-domain modules (forge via #749/#754, matrix via #747)
|
||||
# that are on. All map to `127.0.0.1` since the gateway shares
|
||||
# host netns. Operators with real DNS leave `localHostsEntry =
|
||||
# false`; this is the dev-loop shortcut for `http://<hive-domain>/`
|
||||
# + `http://forge.<hive-domain>/` + `http://matrix.<hive-domain>/`
|
||||
# resolving locally.
|
||||
#
|
||||
# Forge's `cfg.domain` may equal `hyperhiveDomain` (e.g. operator
|
||||
# set `forge.domain = "darkest.space"` matching the hive domain)
|
||||
# — `lib.unique` collapses the duplicate so `/etc/hosts` doesn't
|
||||
# carry the same entry twice.
|
||||
# `lib.unique` collapses any duplicate (e.g. if forge.domain
|
||||
# happens to equal hyperhiveDomain or matrixCfg.gatewayHost) so
|
||||
# `/etc/hosts` doesn't carry the same entry twice.
|
||||
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
|
||||
"127.0.0.1" = lib.unique (
|
||||
[ hyperhiveDomain ]
|
||||
|
|
@ -469,6 +551,7 @@ in
|
|||
(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
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
|
|
|
|||
Loading…
Reference in a new issue