diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index d38e05de..aeaf6e72 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -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.`, #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//` # block per `: ` 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:///` + - # `http://forge./` 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:///` + # + `http://forge./` + `http://matrix./` + # 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 ); }; }; diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix index 907312aa..0b3b612f 100644 --- a/nix/modules/hive-matrix.nix +++ b/nix/modules/hive-matrix.nix @@ -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;