diff --git a/synapse-module/default.nix b/synapse-module/default.nix index ec5f3d8..14405c3 100644 --- a/synapse-module/default.nix +++ b/synapse-module/default.nix @@ -1,35 +1,50 @@ -{ lib, pkgs, config, ... }: - +{ pkgs, lib, config, ... }: let cfg = config.services.matrix-synapse-next; wcfg = cfg.workers; + # Used to generate proper defaultTexts. + cfgText = "config.services.matrix-synapse-next"; + wcfgText = "config.services.matrix-synapse-next.workers"; + format = pkgs.formats.yaml {}; matrix-synapse-common-config = format.generate "matrix-synapse-common-config.yaml" cfg.settings; pluginsEnv = cfg.package.python.buildEnv.override { extraLibs = cfg.plugins; }; - genAttrs' = items: f: g: builtins.listToAttrs (builtins.map (i: lib.attrsets.nameValuePair (f i) (g i)) items); + inherit (lib) + literalExpression + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + types; - isListenerType = type: listener: lib.lists.any (r: lib.lists.any (n: n == type) r.names) listener.resources; + throw' = str: throw '' + matrix-synapse-next error: + ${str} + ''; in { - imports = [ ./nginx.nix ]; + imports = [ + ./nginx.nix + (import ./workers.nix { + inherit throw' format matrix-synapse-common-config pluginsEnv; + }) + ]; options.services.matrix-synapse-next = { - enable = lib.mkEnableOption "matrix-synapse"; + enable = mkEnableOption "matrix-synapse"; - package = lib.mkOption { - type = lib.types.package; - default = pkgs.matrix-synapse; - }; - - plugins = lib.mkOption { - type = lib.types.listOf lib.types.package; + package = mkPackageOption pkgs "matrix-synapse" {}; + + plugins = mkOption { + type = types.listOf types.package; default = [ ]; - example = lib.literalExample '' - with config.services.matrix-synapse-advanced.package.plugins; [ + example = literalExpression '' + with ${cfgText}.package.plugins; [ matrix-synapse-ldap3 matrix-synapse-pam ]; @@ -38,9 +53,9 @@ in List of additional Matrix plugins to make available. ''; }; - - dataDir = lib.mkOption { - type = lib.types.str; + + dataDir = mkOption { + type = types.path; default = "/var/lib/matrix-synapse"; description = '' The directory where matrix-synapse stores its stateful data such as @@ -48,449 +63,265 @@ in ''; }; - enableNginx = lib.mkEnableOption "Enable the synapse module managing nginx"; + enableNginx = mkEnableOption "The synapse module managing nginx"; - public_baseurl = lib.mkOption { - type = lib.types.str; + public_baseurl = mkOption { + type = types.str; default = "matrix.${cfg.settings.server_name}"; - description = "The domain where clients and such will connect (May be different from server_name if using delegation)"; + defaultText = + literalExpression ''matrix.''${${cfgText}.settings.server_name}''; + description = '' + The domain where clients and such will connect. + This may be different from server_name if using delegation. + ''; }; - mainLogConfig = lib.mkOption { - type = lib.types.lines; + mainLogConfig = mkOption { + type = with types; coercedTo path lib.readFile lines; + default = ./matrix-synapse-log_config.yaml; description = "A yaml python logging config file"; - default = lib.readFile ./matrix-synapse-log_config.yaml; }; - workers = let - isReplication = l: lib.lists.any (r: lib.lists.any (n: n == "replication") r.names) l.resources; - - dMRL = lib.lists.findFirst isReplication - (throw "No replication listener configured!") - cfg.settings.listeners; - - dMRH = lib.findFirst (x: true) (throw "Replication listener had no addresses") - dMRL.bind_addresses; - dMRP = dMRL.port; - in { - mainReplicationHost = lib.mkOption { - type = lib.types.str; - default = if builtins.elem dMRH [ "0.0.0.0" "::" ] then "127.0.0.1" else dMRH; - description = "Host of the main synapse instance's replication listener"; - }; - - mainReplicationPort = lib.mkOption { - type = lib.types.port; - default = dMRP; - description = "Port for the main synapse instance's replication listener"; - }; - - defaultListenerAddress = lib.mkOption { - type = lib.types.str; - default = "127.0.0.1"; - description = "The default listener address for the worker"; - }; - - workerStartingPort = lib.mkOption { - type = lib.types.port; - description = "What port should the automatically configured workers start enumerating from"; - default = 8083; - }; - - enableMetrics = lib.mkOption { - type = lib.types.bool; - default = cfg.settings.enable_metrics; - }; - - metricsStartingPort = lib.mkOption { - type = lib.types.port; - default = 18083; - }; - - federationSenders = lib.mkOption { - type = lib.types.ints.unsigned; - description = "How many automatically configured federation senders to set up"; - default = 0; - }; - - federationReceivers = lib.mkOption { - type = lib.types.ints.unsigned; - description = "How many automatically configured federation recievers to set up"; - default = 0; - }; - - initialSyncers = lib.mkOption { - type = lib.types.ints.unsigned; - description = "How many automatically configured intial syncers to set up"; - default = 0; - }; - - normalSyncers = lib.mkOption { - type = lib.types.ints.unsigned; - description = "How many automatically configured sync workers to set up"; - default = 0; - }; - - eventPersisters = lib.mkOption { - type = lib.types.ints.unsigned; - description = "How many automatically configured event-persisters to set up"; - default = 0; - }; - - useUserDirectoryWorker = lib.mkEnableOption "user directory worker"; - - instances = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({config, ...}: { - - options.isAuto = lib.mkOption { - type = lib.types.bool; - internal = true; - default = false; - }; - - options.index = lib.mkOption { - internal = true; - type = lib.types.ints.positive; - }; - - # The custom string type here is mainly for the name to use for the metrics of custom worker types - options.type = lib.mkOption { - type = lib.types.either (lib.types.str) (lib.types.enum [ "fed-sender" "fed-receiver" ]); - }; - - options.settings = let - instanceCfg = config; - inherit (instanceCfg) type isAuto; - in lib.mkOption { - type = lib.types.submodule ({config, ...}: { - freeformType = format.type; - - options.worker_app = let - mapTypeApp = t: { - "fed-sender" = "synapse.app.generic_worker"; - "fed-receiver" = "synapse.app.generic_worker"; - "initial-sync" = "synapse.app.generic_worker"; - "normal-sync" = "synapse.app.generic_worker"; - "event-persist" = "synapse.app.generic_worker"; - "user-dir" = "synapse.app.generic_worker"; - }.${t}; - defaultApp = if (!isAuto) - then "synapse.app.generic_worker" - else mapTypeApp type; - in lib.mkOption { - type = lib.types.enum [ - "synapse.app.generic_worker" - "synapse.app.appservice" - "synapse.app.media_repository" - "synapse.app.user_dir" - ]; - description = "The type of worker application"; - default = defaultApp; - }; - options.worker_replication_host = lib.mkOption { - type = lib.types.str; - default = cfg.workers.mainReplicationHost; - description = "The replication listeners ip on the main synapse process"; - }; - options.worker_replication_http_port = lib.mkOption { - type = lib.types.port; - default = cfg.workers.mainReplicationPort; - description = "The replication listeners port on the main synapse process"; - }; - options.worker_listeners = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options.type = lib.mkOption { - type = lib.types.enum [ "http" "metrics" ]; - description = "The type of the listener"; - default = "http"; - }; - options.port = lib.mkOption { - type = lib.types.port; - description = "the TCP port to bind to"; - }; - options.bind_addresses = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "A list of local addresses to listen on"; - default = [ cfg.workers.defaultListenerAddress ]; - }; - options.tls = lib.mkOption { - type = lib.types.bool; - description = "set to true to enable TLS for this listener. Will use the TLS key/cert specified in tls_private_key_path / tls_certificate_path."; - default = false; - }; - options.x_forwarded = lib.mkOption { - type = lib.types.bool; - description = '' - Only valid for an 'http' listener. Set to true to use the X-Forwarded-For header as the client IP. - Useful when Synapse is behind a reverse-proxy. - ''; - default = true; - }; - options.resources = let - typeToResources = t: { - "fed-receiver" = [ "federation" ]; - "fed-sender" = [ ]; - "initial-sync" = [ "client" ]; - "normal-sync" = [ "client" ]; - "event-persist" = [ "replication" ]; - "user-dir" = [ "client" ]; - }.${t}; - in lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options.names = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ "client" "consent" "federation" "keys" "media" "metrics" "openid" "replication" "static" "webclient" ]); - description = "A list of resources to host on this port"; - default = lib.optionals isAuto (typeToResources type); - }; - options.compress = lib.mkOption { - type = lib.types.bool; - description = "enable HTTP compression for this resource"; - default = false; - }; - }); - default = [{ }]; - }; - }); - description = "Listener configuration for the worker, similar to the main synapse listener"; - default = [ ]; - }; - }); - default = { }; - }; - })); - default = { }; - description = "Worker configuration"; - example = { - "federation_sender1" = { - settings = { - worker_name = "federation_sender1"; - worker_app = "synapse.app.generic_worker"; - - worker_replication_host = "127.0.0.1"; - worker_replication_http_port = 9093; - worker_listeners = [ ]; - }; - }; - }; - }; - }; - - settings = lib.mkOption { - type = lib.types.submodule { + settings = mkOption { + type = types.submodule { freeformType = format.type; + options = { + server_name = mkOption { + type = types.str; + description = '' + The server_name name will appear at the end of usernames and room addresses + created on this server. For example if the server_name was example.com, + usernames on this server would be in the format @user:example.com - options.server_name = lib.mkOption { - type = lib.types.str; - description = '' - The server_name name will appear at the end of usernames and room addresses - created on this server. For example if the server_name was example.com, - usernames on this server would be in the format @user:example.com - - In most cases you should avoid using a matrix specific subdomain such as - matrix.example.com or synapse.example.com as the server_name for the same - reasons you wouldn't use user@email.example.com as your email address. - See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md - for information on how to host Synapse on a subdomain while preserving - a clean server_name. - - The server_name cannot be changed later so it is important to - configure this correctly before you start Synapse. It should be all - lowercase and may contain an explicit port. - ''; - example = "matrix.org"; - }; + In most cases you should avoid using a matrix specific subdomain such as + matrix.example.com or synapse.example.com as the server_name for the same + reasons you wouldn't use user@email.example.com as your email address. + See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md + for information on how to host Synapse on a subdomain while preserving + a clean server_name. - options.use_presence = lib.mkOption { - type = lib.types.bool; - description = "disable presence tracking, if you're having perfomance issues this can have a big impact"; - default = true; - }; - options.listeners = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options.port = lib.mkOption { - type = lib.types.port; - description = "the TCP port to bind to"; - example = 8448; - }; - options.bind_addresses = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "A list of local addresses to listen on"; - }; - options.type = lib.mkOption { - type = lib.types.enum [ "http" "manhole" "metrics" "replication" ]; - description = "The type of the listener"; - default = "http"; - }; - options.tls = lib.mkOption { - type = lib.types.bool; - description = "set to true to enable TLS for this listener. Will use the TLS key/cert specified in tls_private_key_path / tls_certificate_path."; - default = false; - }; - options.x_forwarded = lib.mkOption { - type = lib.types.bool; - description = '' - Only valid for an 'http' listener. Set to true to use the X-Forwarded-For header as the client IP. - Useful when Synapse is behind a reverse-proxy. - ''; - default = true; - }; - options.resources = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options.names = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ "client" "consent" "federation" "keys" "media" "metrics" "openid" "replication" "static" "webclient" ]); - description = "A list of resources to host on this port"; + The server_name cannot be changed later so it is important to + configure this correctly before you start Synapse. It should be all + lowercase and may contain an explicit port. + ''; + example = "matrix.org"; + }; + + use_presence = mkOption { + type = types.bool; + description = "Disable presence tracking, if you're having perfomance issues this can have a big impact"; + default = true; + }; + + listeners = mkOption { + type = types.listOf (types.submodule { + options = { + port = mkOption { + type = types.port; + description = "The TCP port to bind to"; + example = 8448; }; - options.compress = lib.mkOption { - type = lib.types.bool; - description = "enable HTTP compression for this resource"; + + bind_addresses = mkOption { + type = types.listOf types.str; + description = "A list of local addresses to listen on"; + }; + + type = mkOption { + type = types.enum [ "http" "manhole" "metrics" "replication" ]; + description = "The type of the listener"; + default = "http"; + }; + + tls = mkOption { + type = types.bool; + description = '' + Set to true to enable TLS for this listener. + + Will use the TLS key/cert specified in tls_private_key_path / tls_certificate_path. + ''; default = false; }; - }); - }; - }); - description = "List of ports that Synapse should listen on, their purpose and their configuration"; - default = [ - { - port = 8008; - bind_addresses = [ "127.0.0.1" ]; - resources = [ - { names = [ "client" ]; compress = true; } - { names = [ "federation" ]; compress = false; } - ]; - } - (lib.mkIf (wcfg.instances != { }) { - port = 9093; - bind_addresses = [ "127.0.0.1" ]; - resources = [ - { names = [ "replication" ]; } - ]; - }) - (lib.mkIf cfg.settings.enable_metrics { - port = 9000; - bind_addresses = [ "127.0.0.1" ]; - resources = [ - { names = [ "metrics" ]; } - ]; - }) - ]; - }; - options.federation_ip_range_blacklist = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - Prevent federation requests from being sent to the following - blacklist IP address CIDR ranges. If this option is not specified, or - specified with an empty list, no ip range blacklist will be enforced. - ''; - default = [ - "127.0.0.0/8" - "10.0.0.0/8" - "172.16.0.0/12" - "192.168.0.0/16" - "100.64.0.0/10" - "169.254.0.0/16" - "::1/128" - "fe80::/64" - "fc00::/7" - ]; - }; - options.log_config = lib.mkOption { - type = lib.types.path; - description = '' - A yaml python logging config file as described by - https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema - ''; - default = pkgs.writeText "log_config.yaml" cfg.mainLogConfig; - }; + x_forwarded = mkOption { + type = types.bool; + description = '' + Set to true to use the X-Forwarded-For header as the client IP. - options.media_store_path = lib.mkOption { - type = lib.types.path; - description = "Directory where uploaded images and attachments are stored"; - default = "${cfg.dataDir}/media_store"; - }; - options.max_upload_size = lib.mkOption { - type = lib.types.str; - description = "The largest allowed upload size in bytes"; - default = "50M"; - }; + Only valid for an 'http' listener. + Useful when Synapse is behind a reverse-proxy. + ''; + default = true; + }; - options.enable_registration = lib.mkOption { - type = lib.types.bool; - description = "Enable registration for new users"; - default = false; - }; + resources = mkOption { + type = types.listOf (types.submodule { + options = { + names = mkOption { + type = with types; listOf (enum [ + "client" + "consent" + "federation" + "keys" + "media" + "metrics" + "openid" + "replication" + "static" + "webclient" + ]); + description = "A list of resources to host on this port"; + }; - options.enable_metrics = lib.mkOption { - type = lib.types.bool; - description = "Enable collection and rendering of performance metrics"; - default = false; - }; - options.report_stats = lib.mkOption { - type = lib.types.bool; - description = "TODO: Enable and Disable reporting usage stats"; - default = false; - }; - - options.app_service_config_files = lib.mkOption { - type = lib.types.listOf lib.types.path; - description = "A list of application service config files to use"; - default = []; - }; - - options.signing_key_path = lib.mkOption { - type = lib.types.path; - description = "Path to the signing key to sign messages with"; - default = "${cfg.dataDir}/homeserver.signing.key"; - }; - - options.trusted_key_servers = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - freeformType = format.type; - - options.server_name = lib.mkOption { - type = lib.types.str; - description = "the name of the server. required"; - }; - }); - description = "The trusted servers to download signing keys from"; - default = [ - { - server_name = "matrix.org"; - verify_keys."ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"; - } - ]; - }; - - options.federation_sender_instances = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - This configuration must be shared between all federation sender workers, and if - changed all federation sender workers must be stopped at the same time and then - started, to ensure that all instances are running with the same config (otherwise - events may be dropped) - ''; - default = [ ]; - }; - - options.redis = lib.mkOption { - type = lib.types.submodule { - freeformType = format.type; - - options.enabled = lib.mkOption { - type = lib.types.bool; - description = "Enables using redis, required for worker support"; - default = (lib.lists.count (x: true) - (lib.attrsets.attrValues cfg.workers.instances)) > 0; - }; + compress = mkEnableOption "HTTP compression for this resource"; + }; + }); + }; + }; + }); + description = "List of ports that Synapse should listen on, their purpose and their configuration"; + # TODO: add defaultText + default = [ + { + port = 8008; + bind_addresses = [ "127.0.0.1" ]; + resources = [ + { names = [ "client" ]; compress = true; } + { names = [ "federation" ]; compress = false; } + ]; + } + (mkIf (wcfg.instances != { }) { + port = 9093; + bind_addresses = [ "127.0.0.1" ]; + resources = [ + { names = [ "replication" ]; } + ]; + }) + (mkIf cfg.settings.enable_metrics { + port = 9000; + bind_addresses = [ "127.0.0.1" ]; + resources = [ + { names = [ "metrics" ]; } + ]; + }) + ]; + }; + + federation_ip_range_blacklist = mkOption { + type = types.listOf types.str; + description = '' + Prevent federation requests from being sent to the following + blacklist IP address CIDR ranges. If this option is not specified, or + specified with an empty list, no ip range blacklist will be enforced. + ''; + default = [ + "127.0.0.0/8" + "10.0.0.0/8" + "172.16.0.0/12" + "192.168.0.0/16" + "100.64.0.0/10" + "169.254.0.0/16" + "::1/128" + "fe80::/64" + "fc00::/7" + ]; + }; + + log_config = mkOption { + type = types.path; + description = '' + A yaml python logging config file as described by + https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema + ''; + default = pkgs.writeText "log_config.yaml" cfg.mainLogConfig; + defaultText = "A config file generated from ${cfgText}.mainLogConfig"; + }; + + media_store_path = mkOption { + type = types.path; + description = "Directory where uploaded images and attachments are stored"; + default = "${cfg.dataDir}/media_store"; + defaultText = literalExpression ''''${${cfgText}.dataDir}/media_store''; + }; + + max_upload_size = mkOption { + type = types.str; + description = "The largest allowed upload size in bytes"; + default = "50M"; + example = "800K"; + }; + + enable_registration = mkEnableOption "registration for new users"; + enable_metrics = mkEnableOption "collection and rendering of performance metrics"; + report_stats = mkEnableOption "reporting usage stats"; + + app_service_config_files = mkOption { + type = types.listOf types.path; + description = "A list of application service config files to use"; + default = []; + }; + + signing_key_path = mkOption { + type = types.path; + description = "Path to the signing key to sign messages with"; + default = "${cfg.dataDir}/homeserver.signing.key"; + defaultText = literalExpression ''''${${cfgText}.dataDir}/homeserver.signing.key''; + }; + + trusted_key_servers = mkOption { + type = types.listOf (types.submodule { + freeformType = format.type; + + options.server_name = mkOption { + type = types.str; + description = "The name of the server. This is required."; + }; + }); + description = "The trusted servers to download signing keys from"; + default = [ + { + server_name = "matrix.org"; + verify_keys."ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"; + } + ]; + }; + + federation_sender_instances = mkOption { + type = types.listOf types.str; + description = '' + This configuration must be shared between all federation sender workers. + + When changed, all federation sender workers must be stopped at the same time and + restarted, to ensure that all instances are running with the same config. + Otherwise, events may be dropped. + ''; + default = [ ]; + }; + + redis = mkOption { + type = types.submodule { + freeformType = format.type; + + options.enabled = mkOption { + type = types.bool; + description = '' + Whether to enable redis within synapse. + + This is required for worker support. + ''; + default = wcfg.instances != { }; + defaultText = literalExpression "${wcfgText}.instances != { }"; + }; + }; + default = { }; + description = "Redis configuration for synapse and workers"; }; - default = { }; - description = "configuration of redis for synapse and workers"; }; }; }; - extraConfigFiles = lib.mkOption { - type = lib.types.listOf lib.types.path; + extraConfigFiles = mkOption { + type = types.listOf types.path; default = []; description = '' Extra config files to include. @@ -500,186 +331,66 @@ in NixOPS is in use. ''; }; - }; - config = lib.mkIf cfg.enable (lib.mkMerge [ - ({ - users.users.matrix-synapse = { - group = "matrix-synapse"; - home = cfg.dataDir; - createHome = true; - shell = "${pkgs.bash}/bin/bash"; - uid = config.ids.uids.matrix-synapse; - }; - users.groups.matrix-synapse = { - gid = config.ids.gids.matrix-synapse; - }; - systemd.targets.matrix-synapse = { - description = "Synapse parent target"; + config = mkIf cfg.enable { + users.users.matrix-synapse = { + group = "matrix-synapse"; + home = cfg.dataDir; + createHome = true; + shell = "${pkgs.bash}/bin/bash"; + uid = config.ids.uids.matrix-synapse; + }; + + users.groups.matrix-synapse = { + gid = config.ids.gids.matrix-synapse; + }; + + systemd = { + targets.matrix-synapse = { + description = "Matrix synapse parent target"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; }; - }) - ({ - systemd.services.matrix-synapse = { + slices.system-matrix-synapse = { + description = "Matrix synapse slice"; + requires= [ "system.slice" ]; + after= [ "system.slice" ]; + }; + + services.matrix-synapse = { description = "Synapse Matrix homeserver"; partOf = [ "matrix-synapse.target" ]; wantedBy = [ "matrix-synapse.target" ]; - preStart = '' - ${cfg.package}/bin/synapse_homeserver \ - ${ lib.concatMapStringsSep "\n " (x: "--config-path ${x} \\") ([ matrix-synapse-common-config ] ++ cfg.extraConfigFiles) } - --keys-directory ${cfg.dataDir} \ - --generate-keys - ''; - environment.PYTHONPATH = lib.makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ]; + + preStart = let + flags = lib.cli.toGNUCommandLineShell {} { + config-path = [ matrix-synapse-common-config ] ++ cfg.extraConfigFiles; + keys-directory = cfg.dataDir; + generate-keys = true; + }; + in "${cfg.package}/bin/synapse_homeserver ${flags}"; + + environment.PYTHONPATH = + lib.makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ]; + serviceConfig = { Type = "notify"; User = "matrix-synapse"; Group = "matrix-synapse"; + Slice = "system-matrix-synapse.slice"; WorkingDirectory = cfg.dataDir; - ExecStart = '' - ${cfg.package}/bin/synapse_homeserver \ - ${ lib.concatMapStringsSep "\n " (x: "--config-path ${x} \\") ([ matrix-synapse-common-config ] ++ cfg.extraConfigFiles) } - --keys-directory ${cfg.dataDir} - ''; + ExecStart = let + flags = lib.cli.toGNUCommandLineShell {} { + config-path = [ matrix-synapse-common-config ] ++ cfg.extraConfigFiles; + keys-directory = cfg.dataDir; + }; + in "${cfg.package}/bin/synapse_homeserver ${flags}"; ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID"; Restart = "on-failure"; }; }; - }) - - (lib.mkMerge [ - ({ - services.matrix-synapse-next.settings.federation_sender_instances = lib.genList (i: "auto-fed-sender${toString (i + 1)}") cfg.workers.federationSenders; - services.matrix-synapse-next.workers.instances = genAttrs' (lib.lists.range 1 cfg.workers.federationSenders) - (i: "auto-fed-sender${toString i}") - (i: { - isAuto = true; type = "fed-sender"; index = i; - settings.worker_listeners = lib.mkIf wcfg.enableMetrics [ - { port = cfg.workers.metricsStartingPort + i - 1; - resources = [ { names = [ "metrics" ]; } ]; - } - ]; - }); - }) - - ({ - services.matrix-synapse-next.workers.instances = genAttrs' (lib.lists.range 1 cfg.workers.federationReceivers) - (i: "auto-fed-receiver${toString i}") - (i: { - isAuto = true; type = "fed-receiver"; index = i; - settings.worker_listeners = [{ port = cfg.workers.workerStartingPort + i - 1; }] - ++ lib.optional wcfg.enableMetrics { port = cfg.workers.metricsStartingPort + cfg.workers.federationSenders + i; - resources = [ { names = [ "metrics" ]; } ]; - }; - }); - }) - - - ({ - services.matrix-synapse-next.workers.instances = genAttrs' (lib.lists.range 1 cfg.workers.initialSyncers) - (i: "auto-initial-sync${toString i}") - (i: { - isAuto = true; type = "initial-sync"; index = i; - settings.worker_listeners = [{ port = cfg.workers.workerStartingPort + cfg.workers.federationReceivers + i - 1; }] - ++ lib.optional wcfg.enableMetrics { port = cfg.workers.metricsStartingPort + cfg.workers.federationSenders + cfg.workers.federationReceivers + i; - resources = [ { names = [ "metrics" ]; } ]; - }; - }); - }) - - ({ - services.matrix-synapse-next.workers.instances = genAttrs' (lib.lists.range 1 cfg.workers.normalSyncers) - (i: "auto-normal-sync${toString i}") - (i: { - isAuto = true; type = "normal-sync"; index = i; - settings.worker_listeners = [{ port = cfg.workers.workerStartingPort + cfg.workers.federationReceivers + cfg.workers.initialSyncers + i - 1; }] - ++ lib.optional wcfg.enableMetrics { port = cfg.workers.metricsStartingPort + cfg.workers.federationSenders + cfg.workers.federationReceivers + cfg.workers.initialSyncers + i; - resources = [ { names = [ "metrics" ]; } ]; - }; - }); - }) - - ({ - services.matrix-synapse-next.settings.instance_map = genAttrs' (lib.lists.range 1 cfg.workers.eventPersisters) - (i: "auto-event-persist${toString i}") - (i: let - isReplication = l: lib.lists.any (r: lib.lists.any (n: n == "replication") r.names) l.resources; - wRL = lib.lists.findFirst isReplication - (throw "No replication listener configured!") - cfg.workers.instances."auto-event-persist${toString i}".settings.worker_listeners; - wRH = lib.findFirst (x: true) (throw "Replication listener had no addresses") - wRL.bind_addresses; - wRP = wRL.port; - in { - host = wRH; - port = wRP; - } - ); - services.matrix-synapse-next.settings.stream_writers.events = lib.mkIf (cfg.workers.eventPersisters > 0) (lib.genList (i: "auto-event-persist${toString (i + 1)}") cfg.workers.eventPersisters); - services.matrix-synapse-next.workers.instances = genAttrs' (lib.lists.range 1 cfg.workers.eventPersisters) - (i: "auto-event-persist${toString i}") - (i: { - isAuto = true; type = "event-persist"; index = i; - settings.worker_listeners = [{ port = cfg.workers.workerStartingPort + cfg.workers.federationReceivers + cfg.workers.initialSyncers + cfg.workers.normalSyncers + i - 1;}] - ++ lib.optional wcfg.enableMetrics { port = cfg.workers.metricsStartingPort + cfg.workers.federationSenders + cfg.workers.federationReceivers + cfg.workers.initialSyncers + cfg.workers.normalSyncers + i; - resources = [ { names = [ "metrics" ]; } ]; - }; - }); - }) - - (lib.mkIf cfg.workers.useUserDirectoryWorker { - services.matrix-synapse-next.workers.instances."auto-user-dir" = { - isAuto = true; type = "user-dir"; index = 1; - settings.worker_listeners = [{ port = cfg.workers.workerStartingPort + cfg.workers.federationReceivers + cfg.workers.initialSyncers + cfg.workers.normalSyncers + cfg.workers.eventPersisters + 1 - 1;}] - ++ lib.optional wcfg.enableMetrics { port = cfg.workers.metricsStartingPort + cfg.workers.federationSenders + cfg.workers.federationReceivers + cfg.workers.initialSyncers + cfg.workers.normalSyncers + cfg.workers.eventPersisters + 1; - resources = [ { names = [ "metrics"]; } ]; - }; - }; - services.matrix-synapse-next.settings.update_user_directory_from_worker = "auto-user-dir"; - }) - ]) - - ({ - systemd.services = let - workerList = lib.mapAttrsToList (name: value: lib.nameValuePair name value ) cfg.workers.instances; - workerName = worker: worker.name; - workerSettings = worker: (worker.value.settings // {worker_name = (workerName worker);}); - workerConfig = worker: format.generate "matrix-synapse-worker-${workerName worker}-config.yaml" (workerSettings worker); - in builtins.listToAttrs (map (worker: - { - name = "matrix-synapse-worker-${workerName worker}"; - value = { - description = "Synapse Matrix Worker"; - partOf = [ "matrix-synapse.target" ]; - wantedBy = [ "matrix-synapse.target" ]; - after = [ "matrix-synapse.service" ]; - requires = [ "matrix-synapse.service" ]; - environment.PYTHONPATH = lib.makeSearchPathOutput "lib" cfg.package.python.sitePackages [ - pluginsEnv - ]; - serviceConfig = { - Type = "notify"; - User = "matrix-synapse"; - Group = "matrix-synapse"; - WorkingDirectory = cfg.dataDir; - ExecStartPre = pkgs.writers.writeBash "wait-for-synapse" '' - # From https://md.darmstadt.ccc.de/synapse-at-work - while ! systemctl is-active -q matrix-synapse.service; do - sleep 1 - done - ''; - ExecStart = '' - ${cfg.package}/bin/synapse_worker \ - ${ lib.concatMapStringsSep "\n " (x: "--config-path ${x} \\") ([ matrix-synapse-common-config (workerConfig worker) ] ++ cfg.extraConfigFiles) } - --keys-directory ${cfg.dataDir} - ''; - }; - }; - } - ) workerList); - }) - ]); -} \ No newline at end of file + }; + }; +} diff --git a/synapse-module/nginx.nix b/synapse-module/nginx.nix index e6ced1d..dee922a 100644 --- a/synapse-module/nginx.nix +++ b/synapse-module/nginx.nix @@ -1,4 +1,4 @@ -{ lib, pkgs, config, ...}: +{ pkgs, lib, config, ... }: let cfg = config.services.matrix-synapse-next; @@ -12,224 +12,224 @@ let in { config = lib.mkIf cfg.enableNginx { - services.nginx.commonHttpConfig = '' - # No since argument means its initialSync - map $arg_since $synapse_unknown_sync { - default synapse_normal_sync; - ''' synapse_initial_sync; - } + services.nginx.commonHttpConfig = '' + # No since argument means its initialSync + map $arg_since $synapse_unknown_sync { + default synapse_normal_sync; + ''' synapse_initial_sync; + } - map $uri $synapse_uri_group { - # Sync requests - ~^/_matrix/client/(r0|v3)/sync$ $synapse_unknown_sync; - ~^/_matrix/client/(api/v1|r0|v3)/event$ synapse_normal_sync; - ~^/_matrix/client/(api/v1|r0|v3)/initialSync$ synapse_initial_sync; - ~^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$ synapse_initial_sync; + map $uri $synapse_uri_group { + # Sync requests + ~^/_matrix/client/(r0|v3)/sync$ $synapse_unknown_sync; + ~^/_matrix/client/(api/v1|r0|v3)/event$ synapse_normal_sync; + ~^/_matrix/client/(api/v1|r0|v3)/initialSync$ synapse_initial_sync; + ~^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$ synapse_initial_sync; - # Federation requests - ~^/_matrix/federation/v1/event/ synapse_federation; - ~^/_matrix/federation/v1/state/ synapse_federation; - ~^/_matrix/federation/v1/state_ids/ synapse_federation; - ~^/_matrix/federation/v1/backfill/ synapse_federation; - ~^/_matrix/federation/v1/get_missing_events/ synapse_federation; - ~^/_matrix/federation/v1/publicRooms synapse_federation; - ~^/_matrix/federation/v1/query/ synapse_federation; - ~^/_matrix/federation/v1/make_join/ synapse_federation; - ~^/_matrix/federation/v1/make_leave/ synapse_federation; - ~^/_matrix/federation/(v1|v2)/send_join/ synapse_federation; - ~^/_matrix/federation/(v1|v2)/send_leave/ synapse_federation; - ~^/_matrix/federation/(v1|v2)/invite/ synapse_federation; - ~^/_matrix/federation/v1/event_auth/ synapse_federation; - ~^/_matrix/federation/v1/timestamp_to_event/ synapse_federation; - ~^/_matrix/federation/v1/exchange_third_party_invite/ synapse_federation; - ~^/_matrix/federation/v1/user/devices/ synapse_federation; - ~^/_matrix/key/v2/query synapse_federation; - ~^/_matrix/federation/v1/hierarchy/ synapse_federation; + # Federation requests + ~^/_matrix/federation/v1/event/ synapse_federation; + ~^/_matrix/federation/v1/state/ synapse_federation; + ~^/_matrix/federation/v1/state_ids/ synapse_federation; + ~^/_matrix/federation/v1/backfill/ synapse_federation; + ~^/_matrix/federation/v1/get_missing_events/ synapse_federation; + ~^/_matrix/federation/v1/publicRooms synapse_federation; + ~^/_matrix/federation/v1/query/ synapse_federation; + ~^/_matrix/federation/v1/make_join/ synapse_federation; + ~^/_matrix/federation/v1/make_leave/ synapse_federation; + ~^/_matrix/federation/(v1|v2)/send_join/ synapse_federation; + ~^/_matrix/federation/(v1|v2)/send_leave/ synapse_federation; + ~^/_matrix/federation/(v1|v2)/invite/ synapse_federation; + ~^/_matrix/federation/v1/event_auth/ synapse_federation; + ~^/_matrix/federation/v1/timestamp_to_event/ synapse_federation; + ~^/_matrix/federation/v1/exchange_third_party_invite/ synapse_federation; + ~^/_matrix/federation/v1/user/devices/ synapse_federation; + ~^/_matrix/key/v2/query synapse_federation; + ~^/_matrix/federation/v1/hierarchy/ synapse_federation; - # Inbound federation transaction request - ~^/_matrix/federation/v1/send/ synapse_federation_transaction; + # Inbound federation transaction request + ~^/_matrix/federation/v1/send/ synapse_federation_transaction; - # Client API requests - ~^/_matrix/client/(api/v1|r0|v3|unstable)/createRoom$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/publicRooms$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/joined_members$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/context/.*$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/members$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state$ synapse_client_interaction; - ~^/_matrix/client/v1/rooms/.*/hierarchy$ synapse_client_interaction; - ~^/_matrix/client/(v1|unstable)/rooms/.*/relations/ synapse_client_interaction; - ~^/_matrix/client/v1/rooms/.*/threads$ synapse_client_interaction; - ~^/_matrix/client/unstable/org.matrix.msc2716/rooms/.*/batch_send$ synapse_client_interaction; - ~^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary$ synapse_client_interaction; - ~^/_matrix/client/(r0|v3|unstable)/account/3pid$ synapse_client_interaction; - ~^/_matrix/client/(r0|v3|unstable)/account/whoami$ synapse_client_interaction; - ~^/_matrix/client/(r0|v3|unstable)/devices$ synapse_client_interaction; - ~^/_matrix/client/versions$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/voip/turnServer$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/event/ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/joined_rooms$ synapse_client_interaction; - ~^/_matrix/client/v1/rooms/.*/timestamp_to_event$ synapse_client_interaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/search$ synapse_client_interaction; + # Client API requests + ~^/_matrix/client/(api/v1|r0|v3|unstable)/createRoom$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/publicRooms$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/joined_members$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/context/.*$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/members$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state$ synapse_client_interaction; + ~^/_matrix/client/v1/rooms/.*/hierarchy$ synapse_client_interaction; + ~^/_matrix/client/(v1|unstable)/rooms/.*/relations/ synapse_client_interaction; + ~^/_matrix/client/v1/rooms/.*/threads$ synapse_client_interaction; + ~^/_matrix/client/unstable/org.matrix.msc2716/rooms/.*/batch_send$ synapse_client_interaction; + ~^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary$ synapse_client_interaction; + ~^/_matrix/client/(r0|v3|unstable)/account/3pid$ synapse_client_interaction; + ~^/_matrix/client/(r0|v3|unstable)/account/whoami$ synapse_client_interaction; + ~^/_matrix/client/(r0|v3|unstable)/devices$ synapse_client_interaction; + ~^/_matrix/client/versions$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/voip/turnServer$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/event/ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/joined_rooms$ synapse_client_interaction; + ~^/_matrix/client/v1/rooms/.*/timestamp_to_event$ synapse_client_interaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/search$ synapse_client_interaction; - # Encryption requests - ~^/_matrix/client/(r0|v3|unstable)/keys/query$ synapse_client_encryption; - ~^/_matrix/client/(r0|v3|unstable)/keys/changes$ synapse_client_encryption; - ~^/_matrix/client/(r0|v3|unstable)/keys/claim$ synapse_client_encryption; - ~^/_matrix/client/(r0|v3|unstable)/room_keys/ synapse_client_encryption; - ~^/_matrix/client/(r0|v3|unstable)/keys/upload/ synapse_client_encryption; + # Encryption requests + ~^/_matrix/client/(r0|v3|unstable)/keys/query$ synapse_client_encryption; + ~^/_matrix/client/(r0|v3|unstable)/keys/changes$ synapse_client_encryption; + ~^/_matrix/client/(r0|v3|unstable)/keys/claim$ synapse_client_encryption; + ~^/_matrix/client/(r0|v3|unstable)/room_keys/ synapse_client_encryption; + ~^/_matrix/client/(r0|v3|unstable)/keys/upload/ synapse_client_encryption; - # Registration/login requests - ~^/_matrix/client/(api/v1|r0|v3|unstable)/login$ synapse_client_login; - ~^/_matrix/client/(r0|v3|unstable)/register$ synapse_client_login; - ~^/_matrix/client/v1/register/m.login.registration_token/validity$ synapse_client_login; + # Registration/login requests + ~^/_matrix/client/(api/v1|r0|v3|unstable)/login$ synapse_client_login; + ~^/_matrix/client/(r0|v3|unstable)/register$ synapse_client_login; + ~^/_matrix/client/v1/register/m.login.registration_token/validity$ synapse_client_login; - # Event sending requests - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/redact synapse_client_transaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/send synapse_client_transaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state/ synapse_client_transaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$ synapse_client_transaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/join/ synapse_client_transaction; - ~^/_matrix/client/(api/v1|r0|v3|unstable)/profile/ synapse_client_transaction; + # Event sending requests + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/redact synapse_client_transaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/send synapse_client_transaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state/ synapse_client_transaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$ synapse_client_transaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/join/ synapse_client_transaction; + ~^/_matrix/client/(api/v1|r0|v3|unstable)/profile/ synapse_client_transaction; - # Account data requests - ~^/_matrix/client/(r0|v3|unstable)/.*/tags synapse_client_data; - ~^/_matrix/client/(r0|v3|unstable)/.*/account_data synapse_client_data; + # Account data requests + ~^/_matrix/client/(r0|v3|unstable)/.*/tags synapse_client_data; + ~^/_matrix/client/(r0|v3|unstable)/.*/account_data synapse_client_data; - # Receipts requests - ~^/_matrix/client/(r0|v3|unstable)/rooms/.*/receipt synapse_client_interaction; - ~^/_matrix/client/(r0|v3|unstable)/rooms/.*/read_markers synapse_client_interaction; + # Receipts requests + ~^/_matrix/client/(r0|v3|unstable)/rooms/.*/receipt synapse_client_interaction; + ~^/_matrix/client/(r0|v3|unstable)/rooms/.*/read_markers synapse_client_interaction; - # Presence requests - ~^/_matrix/client/(api/v1|r0|v3|unstable)/presence/ synapse_client_presence; + # Presence requests + ~^/_matrix/client/(api/v1|r0|v3|unstable)/presence/ synapse_client_presence; - # User directory search requests; - ~^/_matrix/client/(r0|v3|unstable)/user_directory/search$ synapse_client_user-dir; - } + # User directory search requests; + ~^/_matrix/client/(r0|v3|unstable)/user_directory/search$ synapse_client_user-dir; + } - #Plugboard for url -> workers - map $synapse_uri_group $synapse_backend { - default synapse_master; + #Plugboard for url -> workers + map $synapse_uri_group $synapse_backend { + default synapse_master; - synapse_initial_sync synapse_worker_initial_sync; - synapse_normal_sync synapse_worker_normal_sync; + synapse_initial_sync synapse_worker_initial_sync; + synapse_normal_sync synapse_worker_normal_sync; - synapse_federation synapse_worker_federation; - synapse_federation_transaction synapse_worker_federation; + synapse_federation synapse_worker_federation; + synapse_federation_transaction synapse_worker_federation; - synapse_client_user-dir synapse_worker_user-dir; - } + synapse_client_user-dir synapse_worker_user-dir; + } - # from https://github.com/tswfi/synapse/commit/b3704b936663cc692241e978dce4ac623276b1a6 - map $arg_access_token $accesstoken_from_urlparam { - # Defaults to just passing back the whole accesstoken - default $arg_access_token; - # Try to extract username part from accesstoken URL parameter - "~syt_(?.*?)_.*" $username; - } + # from https://github.com/tswfi/synapse/commit/b3704b936663cc692241e978dce4ac623276b1a6 + map $arg_access_token $accesstoken_from_urlparam { + # Defaults to just passing back the whole accesstoken + default $arg_access_token; + # Try to extract username part from accesstoken URL parameter + "~syt_(?.*?)_.*" $username; + } - map $http_authorization $mxid_localpart { - # Defaults to just passing back the whole accesstoken - default $http_authorization; - # Try to extract username part from accesstoken header - "~Bearer syt_(?.*?)_.*" $username; - # if no authorization-header exist, try mapper for URL parameter "access_token" - "" $accesstoken_from_urlparam; - } - ''; + map $http_authorization $mxid_localpart { + # Defaults to just passing back the whole accesstoken + default $http_authorization; + # Try to extract username part from accesstoken header + "~Bearer syt_(?.*?)_.*" $username; + # if no authorization-header exist, try mapper for URL parameter "access_token" + "" $accesstoken_from_urlparam; + } + ''; - services.nginx.upstreams.synapse_master.servers = let - isMainListener = l: isListenerType "client" l && isListenerType "federation" l; - firstMainListener = lib.findFirst isMainListener - (throw "No cartch-all listener configured") cfg.settings.listeners; - address = lib.findFirst (_: true) (throw "No address in main listener") firstMainListener.bind_addresses; - port = firstMainListener.port; - socketAddress = "${address}:${builtins.toString port}"; - in { - "${socketAddress}" = { }; - }; + services.nginx.upstreams.synapse_master.servers = let + isMainListener = l: isListenerType "client" l && isListenerType "federation" l; + firstMainListener = lib.findFirst isMainListener + (throw "No catch-all listener configured") cfg.settings.listeners; + address = lib.findFirst (_: true) (throw "No address in main listener") firstMainListener.bind_addresses; + port = firstMainListener.port; + socketAddress = "${address}:${builtins.toString port}"; + in { + "${socketAddress}" = { }; + }; - - services.nginx.upstreams.synapse_worker_federation = { - servers = let - fedReceivers = getWorkersOfType "fed-receiver"; - socketAddresses = generateSocketAddresses "federation" fedReceivers; - in if fedReceivers != { } then + + services.nginx.upstreams.synapse_worker_federation = { + servers = let + fedReceivers = getWorkersOfType "fed-receiver"; + socketAddresses = generateSocketAddresses "federation" fedReceivers; + in if fedReceivers != { } then + lib.genAttrs socketAddresses (_: { }) + else config.services.nginx.upstreams.synapse_master.servers; + extraConfig = '' + ip_hash; + ''; + }; + + + services.nginx.upstreams.synapse_worker_initial_sync = { + servers = let + initialSyncers = getWorkersOfType "initial-sync"; + socketAddresses = generateSocketAddresses "client" initialSyncers; + in if initialSyncers != { } then lib.genAttrs socketAddresses (_: { }) - else config.services.nginx.upstreams.synapse_master.servers; - extraConfig = '' - ip_hash; - ''; - }; - - - services.nginx.upstreams.synapse_worker_initial_sync = { - servers = let - initialSyncers = getWorkersOfType "initial-sync"; - socketAddresses = generateSocketAddresses "client" initialSyncers; - in if initialSyncers != { } then - lib.genAttrs socketAddresses (_: { }) - else config.services.nginx.upstreams.synapse_master.servers; - extraConfig = '' - hash $mxid_localpart consistent; - ''; - }; - - - services.nginx.upstreams.synapse_worker_normal_sync = { - servers = let - normalSyncers = getWorkersOfType "normal-sync"; - socketAddresses = generateSocketAddresses "client" normalSyncers; - in if normalSyncers != { } then - lib.genAttrs socketAddresses (_: { }) - else config.services.nginx.upstreams.synapse_master.servers; - extraConfig = '' - hash $mxid_localpart consistent; - ''; - }; - - - services.nginx.upstreams.synapse_worker_user-dir = { - servers = let - workers = getWorkersOfType "user-dir"; - socketAddresses = generateSocketAddresses "client" workers; - in if workers != { } then - lib.genAttrs socketAddresses (_: { }) - else config.services.nginx.upstreams.synapse_master.servers; - }; - - services.nginx.virtualHosts."${cfg.public_baseurl}" = { - enableACME = true; - forceSSL = true; - locations."/_matrix" = { - proxyPass = "http://$synapse_backend"; + else config.services.nginx.upstreams.synapse_master.servers; extraConfig = '' - add_header X-debug-backend $synapse_backend; - add_header X-debug-group $synapse_uri_group; - client_max_body_size ${cfg.settings.max_upload_size}; - proxy_read_timeout 10m; + hash $mxid_localpart consistent; ''; }; - locations."~ ^/_matrix/client/(r0|v3)/sync$" = { - proxyPass = "http://$synapse_backend"; + + + services.nginx.upstreams.synapse_worker_normal_sync = { + servers = let + normalSyncers = getWorkersOfType "normal-sync"; + socketAddresses = generateSocketAddresses "client" normalSyncers; + in if normalSyncers != { } then + lib.genAttrs socketAddresses (_: { }) + else config.services.nginx.upstreams.synapse_master.servers; extraConfig = '' - proxy_read_timeout 1h; + hash $mxid_localpart consistent; ''; }; - locations."~ ^/_matrix/client/(api/v1|r0|v3)/initialSync$" = { - proxyPass = "http://synapse_worker_initial_sync"; - extraConfig = '' - proxy_read_timeout 1h; - ''; + + + services.nginx.upstreams.synapse_worker_user-dir = { + servers = let + workers = getWorkersOfType "user-dir"; + socketAddresses = generateSocketAddresses "client" workers; + in if workers != { } then + lib.genAttrs socketAddresses (_: { }) + else config.services.nginx.upstreams.synapse_master.servers; }; - locations."~ ^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$" = { - proxyPass = "http://synapse_worker_initial_sync"; - extraConfig = '' - proxy_read_timeout 1h; - ''; - }; - locations."/_synapse/client" = { - proxyPass = "http://$synapse_backend"; + + services.nginx.virtualHosts."${cfg.public_baseurl}" = { + enableACME = true; + forceSSL = true; + locations."/_matrix" = { + proxyPass = "http://$synapse_backend"; + extraConfig = '' + add_header X-debug-backend $synapse_backend; + add_header X-debug-group $synapse_uri_group; + client_max_body_size ${cfg.settings.max_upload_size}; + proxy_read_timeout 10m; + ''; + }; + locations."~ ^/_matrix/client/(r0|v3)/sync$" = { + proxyPass = "http://$synapse_backend"; + extraConfig = '' + proxy_read_timeout 1h; + ''; + }; + locations."~ ^/_matrix/client/(api/v1|r0|v3)/initialSync$" = { + proxyPass = "http://synapse_worker_initial_sync"; + extraConfig = '' + proxy_read_timeout 1h; + ''; + }; + locations."~ ^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$" = { + proxyPass = "http://synapse_worker_initial_sync"; + extraConfig = '' + proxy_read_timeout 1h; + ''; + }; + locations."/_synapse/client" = { + proxyPass = "http://$synapse_backend"; + }; }; }; -}; -} \ No newline at end of file +} diff --git a/synapse-module/workers.nix b/synapse-module/workers.nix new file mode 100644 index 0000000..2e7fc26 --- /dev/null +++ b/synapse-module/workers.nix @@ -0,0 +1,382 @@ +{ matrix-synapse-common-config, + pluginsEnv, + throw', + format +}: +{ pkgs, lib, config, ... }: let + + cfg = config.services.matrix-synapse-next; + wcfg = config.services.matrix-synapse-next.workers; + + # Used to generate proper defaultTexts. + cfgText = "config.services.matrix-synapse-next"; + wcfgText = "config.services.matrix-synapse-next.workers"; + + inherit (lib) types mkOption mkEnableOption mkIf mkMerge literalExpression; + + mkWorkerCountOption = workerType: mkOption { + type = types.ints.unsigned; + description = "How many automatically configured ${workerType} workers to set up"; + default = 0; + }; + + genAttrs' = items: f: g: builtins.listToAttrs (map (i: lib.nameValuePair (f i) (g i)) items); + + isReplicationListener = + l: lib.any (r: lib.any (n: n == "replication") r.names) l.resources; + + mainReplicationListener = lib.lists.findFirst isReplicationListener + (throw' "No replication listener configured!") + cfg.settings.listeners; + mainReplicationListenerHost = + if mainReplicationListener.bind_addresses == [] + then throw' "Replication listener had no addresses" + else builtins.head mainReplicationListener.bind_addresses; + mainReplicationListenerPort = mainReplicationListener.port; +in { + # See https://github.com/matrix-org/synapse/blob/develop/docs/workers.md for more info + options.services.matrix-synapse-next.workers = let + workerInstanceType = types.submodule ({ config, ... }: { + options = { + isAuto = mkOption { + type = types.bool; + internal = true; + default = false; + }; + + index = mkOption { + internal = true; + type = types.ints.positive; + }; + + # The custom string type here is mainly for the name to use + # for the metrics of custom worker types + type = mkOption { + type = types.str; + # TODO: add description and possibly default value? + }; + + settings = mkOption { + type = workerSettingsType config; + default = { }; + }; + }; + }); + + workerSettingsType = instanceCfg: types.submodule { + freeformType = format.type; + + options = { + worker_app = mkOption { + type = types.enum [ + "synapse.app.generic_worker" + "synapse.app.appservice" + "synapse.app.media_repository" + "synapse.app.user_dir" + ]; + description = "The type of worker application"; + default = "synapse.app.generic_worker"; + }; + + worker_replication_host = mkOption { + type = types.str; + default = wcfg.mainReplicationHost; + defaultText = literalExpression "${wcfgText}.mainReplicationHost"; + description = "The replication listeners IP on the main synapse process"; + }; + + worker_replication_http_port = mkOption { + type = types.port; + default = wcfg.mainReplicationPort; + defaultText = literalExpression "${wcfgText}.mainReplicationPort"; + description = "The replication listeners port on the main synapse process"; + }; + + worker_listeners = mkOption { + type = types.listOf (workerListenerType instanceCfg); + description = "Listener configuration for the worker, similar to the main synapse listener"; + default = [ ]; + }; + }; + }; + + workerListenerType = instanceCfg: types.submodule { + options = { + type = mkOption { + type = types.enum [ "http" "metrics" ]; + description = "The type of the listener"; + default = "http"; + }; + + port = mkOption { + type = types.port; + description = "The TCP port to bind to"; + }; + + bind_addresses = mkOption { + type = with types; listOf str; + description = "A list of local addresses to listen on"; + default = [ wcfg.defaultListenerAddress ]; + defaultText = literalExpression "[ ${wcfgText}.defaultListenerAddress ]"; + }; + + tls = mkOption { + type = types.bool; + description = '' + Whether to enable TLS for this listener. + Will use the TLS key/cert specified in tls_private_key_path / tls_certificate_path. + ''; + default = false; + example = true; + }; + + x_forwarded = mkOption { + type = types.bool; + description = '' + Whether to use the X-Forwarded-For HTTP header as the client IP. + + This option is only valid for an 'http' listener. + It is useful when Synapse is running behind a reverse-proxy. + ''; + default = true; + example = false; + }; + + resources = let + typeToResources = t: { + "fed-receiver" = [ "federation" ]; + "fed-sender" = [ ]; + "initial-sync" = [ "client" ]; + "normal-sync" = [ "client" ]; + "event-persist" = [ "replication" ]; + "user-dir" = [ "client" ]; + }.${t}; + in mkOption { + type = types.listOf (types.submodule { + options = { + names = mkOption { + type = with types; listOf (enum [ + "client" + "consent" + "federation" + "keys" + "media" + "metrics" + "openid" + "replication" + "static" + "webclient" + ]); + description = "A list of resources to host on this port"; + default = lib.optionals instanceCfg.isAuto (typeToResources instanceCfg.type); + defaultText = '' + If the worker is generated from other config, the resource type will + be determined automatically. + ''; + }; + + compress = mkEnableOption "HTTP compression for this resource"; + }; + }); + default = [{ }]; + }; + }; + }; + in { + mainReplicationHost = mkOption { + type = types.str; + default = + if builtins.elem mainReplicationListenerHost [ "0.0.0.0" "::" ] + then "127.0.0.1" + else mainReplicationListenerHost; + # TODO: add defaultText + description = "Host of the main synapse instance's replication listener"; + }; + + mainReplicationPort = mkOption { + type = types.port; + default = mainReplicationListenerPort; + # TODO: add defaultText + description = "Port for the main synapse instance's replication listener"; + }; + + defaultListenerAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The default listener address for the worker"; + }; + + workerStartingPort = mkOption { + type = types.port; + description = "What port should the automatically configured workers start enumerating from"; + default = 8083; + }; + + enableMetrics = mkOption { + type = types.bool; + default = cfg.settings.enable_metrics; + defaultText = literalExpression "${cfgText}.settings.enable_metrics"; + # TODO: add description + }; + + metricsStartingPort = mkOption { + type = types.port; + default = 18083; + # TODO: add description + }; + + federationSenders = mkWorkerCountOption "federation-sender"; + federationReceivers = mkWorkerCountOption "federation-reciever"; + initialSyncers = mkWorkerCountOption "initial-syncer"; + normalSyncers = mkWorkerCountOption "sync"; + eventPersisters = mkWorkerCountOption "event-persister"; + + useUserDirectoryWorker = mkEnableOption "user directory worker"; + + instances = mkOption { + type = types.attrsOf workerInstanceType; + default = { }; + description = "Worker configuration"; + example = { + "federation_sender1" = { + settings = { + worker_name = "federation_sender1"; + worker_app = "synapse.app.generic_worker"; + + worker_replication_host = "127.0.0.1"; + worker_replication_http_port = 9093; + worker_listeners = [ ]; + }; + }; + }; + }; + }; + + config = { + services.matrix-synapse-next.settings = { + federation_sender_instances = + lib.genList (i: "auto-fed-sender${toString (i + 1)}") wcfg.federationSenders; + + instance_map = genAttrs' (lib.lists.range 1 wcfg.eventPersisters) + (i: "auto-event-persist${toString i}") + (i: let + wRL = lib.lists.findFirst isReplicationListener + (throw' "No replication listener configured!") + wcfg.instances."auto-event-persist${toString i}".settings.worker_listeners; + wRH = lib.findFirst (x: true) (throw' "Replication listener had no addresses") + wRL.bind_addresses; + wRP = wRL.port; + in { + host = wRH; + port = wRP; + }); + + stream_writers.events = + mkIf (wcfg.eventPersisters > 0) + (lib.genList (i: "auto-event-persist${toString (i + 1)}") wcfg.eventPersisters); + + update_user_directory_from_worker = + mkIf wcfg.useUserDirectoryWorker "auto-user-dir"; + }; + + services.matrix-synapse-next.workers.instances = let + sum = lib.foldl lib.add 0; + workerListenersWithMetrics = portOffset: + lib.singleton ({ + port = wcfg.workerStartingPort + portOffset - 1; + }) + ++ lib.optional wcfg.enableMetrics { + port = wcfg.metricsStartingPort + portOffset; + resources = [ { names = [ "metrics" ]; } ]; + }; + + makeWorkerInstances = { + type, + numberOfWorkers, + portOffset ? 0, + nameFn ? i: "auto-${type}${toString i}", + workerListenerFn ? i: workerListenersWithMetrics (portOffset + i) + }: genAttrs' + (lib.lists.range 1 numberOfWorkers) + nameFn + (i: { + isAuto = true; + inherit type; + index = i; + settings.worker_listeners = workerListenerFn i; + }); + + workerInstances = { + "fed-sender" = wcfg.federationSenders; + "fed-receiver" = wcfg.federationReceivers; + "initial-sync" = wcfg.initialSyncers; + "normal-sync" = wcfg.normalSyncers; + "event-persist" = wcfg.eventPersisters; + } // (lib.optionalAttrs wcfg.useUserDirectoryWorker { + "user-dir" = { + numberOfWorkers = 1; + nameFn = _: "auto-user-dir"; + }; + }); + + coerceWorker = { name, value }: if builtins.isInt value then { + type = name; + numberOfWorkers = value; + } else { type = name; } // value; + + # Like foldl, but keeps all intermediate values + # + # (b -> a -> b) -> b -> [a] -> [b] + scanl = f: x1: list: let + x2 = lib.head list; + x1' = f x1 x2; + in if list == [] then [] else [x1'] ++ (scanl f x1' (lib.tail list)); + + f = { portOffset, numberOfWorkers, ... }: x: x // { portOffset = portOffset + numberOfWorkers; }; + init = { portOffset = 0; numberOfWorkers = 0; }; + in lib.pipe workerInstances [ + (lib.mapAttrsToList lib.nameValuePair) + (map coerceWorker) + (scanl f init) + (map makeWorkerInstances) + mkMerge + ]; + + systemd.services = let + workerList = lib.mapAttrsToList lib.nameValuePair wcfg.instances; + workerConfig = worker: format.generate "matrix-synapse-worker-${worker.name}-config.yaml" + (worker.value.settings // { worker_name = worker.name; }); + in builtins.listToAttrs (lib.flip map workerList (worker: { + name = "matrix-synapse-worker-${worker.name}"; + value = { + description = "Synapse Matrix Worker"; + partOf = [ "matrix-synapse.target" ]; + wantedBy = [ "matrix-synapse.target" ]; + after = [ "matrix-synapse.service" ]; + requires = [ "matrix-synapse.service" ]; + environment.PYTHONPATH = lib.makeSearchPathOutput "lib" cfg.package.python.sitePackages [ + pluginsEnv + ]; + serviceConfig = { + Type = "notify"; + User = "matrix-synapse"; + Group = "matrix-synapse"; + Slice = "system-matrix-synapse.slice"; + WorkingDirectory = cfg.dataDir; + ExecStartPre = pkgs.writers.writeBash "wait-for-synapse" '' + # From https://md.darmstadt.ccc.de/synapse-at-work + while ! systemctl is-active -q matrix-synapse.service; do + sleep 1 + done + ''; + ExecStart = let + flags = lib.cli.toGNUCommandLineShell {} { + config-path = [ matrix-synapse-common-config (workerConfig worker) ] ++ cfg.extraConfigFiles; + keys-directory = cfg.dataDir; + }; + in "${cfg.package}/bin/synapse_worker ${flags}"; + }; + }; + })); + }; +}