nixos-matrix-modules/synapse-module/default.nix

471 lines
16 KiB
Nix

{ pkgs, lib, config, ... }:
let
matrix-lib = (import ../lib.nix { inherit lib; });
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 // {
listeners = map (lib.filterAttrsRecursive (_: v: v != null)) cfg.settings.listeners;
});
# TODO: Align better with the upstream module
wrapped = cfg.package.override {
inherit (cfg) plugins;
extras = [
"postgres"
"saml2"
"oidc"
"systemd"
"url-preview"
"sentry"
"jwt"
"redis"
"cache-memory"
"user-search"
];
};
inherit (lib)
literalExpression
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
types;
throw' = str: throw ''
matrix-synapse-next error:
${str}
'';
in
{
imports = [
./nginx.nix
(import ./workers.nix {
inherit matrix-lib throw' format matrix-synapse-common-config wrapped;
})
];
options.services.matrix-synapse-next = {
enable = mkEnableOption "matrix-synapse";
package = mkPackageOption pkgs "matrix-synapse" {};
plugins = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with ${cfgText}.package.plugins; [
matrix-synapse-ldap3
matrix-synapse-pam
];
'';
description = ''
List of additional Matrix plugins to make available.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/matrix-synapse";
description = ''
The directory where matrix-synapse stores its stateful data such as
certificates, media and uploads.
'';
};
socketDir = mkOption {
type = types.path;
default = "/run/matrix-synapse";
description = ''
The directory where matrix-synapse by default stores the sockets of
all listeners that bind to UNIX sockets.
'';
};
enableNginx = mkEnableOption "The synapse module managing nginx";
public_baseurl = mkOption {
type = types.str;
default = "matrix.${cfg.settings.server_name}";
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 = mkOption {
type = with types; coercedTo path lib.readFile lines;
default = ./matrix-synapse-log_config.yaml;
description = "A yaml python logging config file";
};
enableSlidingSync = mkEnableOption (lib.mdDoc "automatic Sliding Sync setup at `slidingsync.<domain>`");
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
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";
};
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 = with types; nullOr types.port;
default = null;
description = ''
The TCP port to bind to.
::: {.note}
This option will be ignored if {option}`path` is set to a non-null value.
:::
'';
example = 8448;
};
path = mkOption {
type = with types; nullOr path;
default = null;
description = ''
The UNIX socket to bind to.
::: {.note}
This option will override {option}`bind_addresses` and {option}`port`
if set to a non-null value.
:::
'';
example = literalExpression ''''${${cfgText}.socketDir}/matrix-synapse.sock'';
};
bind_addresses = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of local addresses to listen on.
::: {.note}
This option will be ignored if {option}`path` is set to a non-null value.
:::
'';
};
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;
};
x_forwarded = mkOption {
type = types.bool;
description = ''
Set to true to use the X-Forwarded-For header as the client IP.
Only valid for an 'http' listener.
Useful when Synapse is behind a reverse-proxy.
'';
default = true;
};
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";
};
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 = [
{
path = "${cfg.socketDir}/matrix-synapse.sock";
resources = [
{ names = [ "client" ]; compress = true; }
{ names = [ "federation" ]; compress = false; }
];
}
(mkIf (wcfg.instances != { }) {
path = "${cfg.socketDir}/matrix-synapse-replication.sock";
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";
};
};
};
};
extraConfigFiles = mkOption {
type = types.listOf types.path;
default = [];
description = ''
Extra config files to include.
The configuration files will be included based on the command line
argument --config-path. This allows to configure secrets without
having to go through the Nix store, e.g. based on deployment keys if
NixOPS is in use.
'';
};
};
config = mkIf cfg.enable {
assertions = [ ]
++ (map (l: {
assertion = l.path == null -> (l.bind_addresses != [ ] && l.port != null);
message = "Some listeners are missing either a socket path or a bind_address + port to listen on";
}) cfg.settings.listeners);
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" ];
};
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 = 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}";
serviceConfig = {
Type = "notify";
User = "matrix-synapse";
Group = "matrix-synapse";
Slice = "system-matrix-synapse.slice";
WorkingDirectory = cfg.dataDir;
StateDirectory = "matrix-synapse";
RuntimeDirectory = "matrix-synapse";
ExecStart = let
flags = lib.cli.toGNUCommandLineShell {} {
config-path = [ matrix-synapse-common-config ] ++ cfg.extraConfigFiles;
keys-directory = cfg.dataDir;
};
in "${wrapped}/bin/synapse_homeserver ${flags}";
ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
};
};
};
services.matrix-synapse-next.settings.extra_well_known_client_content."org.matrix.msc3575.proxy" = mkIf cfg.enableSlidingSync {
url = "https://${config.services.matrix-synapse.sliding-sync.publicBaseUrl}";
};
services.matrix-synapse.sliding-sync = mkIf cfg.enableSlidingSync {
enable = true;
enableNginx = lib.mkDefault cfg.enableNginx;
publicBaseUrl = lib.mkDefault "slidingsync.${cfg.settings.server_name}";
settings = {
SYNCV3_SERVER = lib.mkDefault "https://${cfg.public_baseurl}";
SYNCV3_PROM = lib.mkIf cfg.settings.enable_metrics (lib.mkDefault "127.0.0.1:9001");
};
};
};
}