{ config, pkgs, lib, ... }: let cfg = config.services.matrix-appservice-irc; pkg = pkgs.matrix-appservice-irc; bin = "${pkg}/bin/matrix-appservice-irc"; jsonType = (pkgs.formats.json {}).type; configFile = pkgs.runCommand "matrix-appservice-irc.yml" { # Because this program will be run at build time, we need `nativeBuildInputs` nativeBuildInputs = [ (pkgs.python3.withPackages (ps: [ ps.jsonschema ])) pkgs.remarshal ]; preferLocalBuild = true; config = builtins.toJSON cfg.settings; passAsFile = [ "config" ]; } '' # The schema is given as yaml, we need to convert it to json remarshal --if yaml --of json -i ${pkg}/config.schema.yml -o config.schema.json python -m jsonschema config.schema.json -i $configPath cp "$configPath" "$out" ''; registrationFile = "/var/lib/matrix-appservice-irc/registration.yml"; in { options.services.matrix-appservice-irc = with lib.types; { enable = lib.mkEnableOption "the Matrix/IRC bridge"; port = lib.mkOption { type = port; description = "The port to listen on"; default = 8009; }; needBindingCap = lib.mkOption { type = bool; description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)"; default = false; }; passwordEncryptionKeyLength = lib.mkOption { type = ints.unsigned; description = "Length of the key to encrypt IRC passwords with"; default = 4096; example = 8192; }; registrationUrl = lib.mkOption { type = str; description = '' The URL where the application service is listening for homeserver requests, from the Matrix homeserver perspective. ''; example = "http://localhost:8009"; }; localpart = lib.mkOption { type = str; description = "The user_id localpart to assign to the appservice"; default = "appservice-irc"; }; settings = lib.mkOption { description = '' Configuration for the appservice, see for supported values ''; default = {}; type = submodule { freeformType = jsonType; options = { homeserver = lib.mkOption { description = "Homeserver configuration"; default = {}; type = submodule { freeformType = jsonType; options = { url = lib.mkOption { type = str; description = "The URL to the home server for client-server API calls"; }; domain = lib.mkOption { type = str; description = '' The 'domain' part for user IDs on this home server. Usually (but not always) is the "domain name" part of the homeserver URL. ''; }; }; }; }; database = lib.mkOption { default = {}; description = "Configuration for the database"; type = submodule { freeformType = jsonType; options = { engine = lib.mkOption { type = str; description = "Which database engine to use"; default = "nedb"; example = "postgres"; }; connectionString = lib.mkOption { type = str; description = "The database connection string"; default = "nedb://var/lib/matrix-appservice-irc/data"; example = "postgres://username:password@host:port/databasename"; }; }; }; }; ircService = lib.mkOption { default = {}; description = "IRC bridge configuration"; type = submodule { freeformType = jsonType; options = { passwordEncryptionKeyPath = lib.mkOption { type = str; description = '' Location of the key with which IRC passwords are encrypted for storage. Will be generated on first run if not present. ''; default = "/var/lib/matrix-appservice-irc/passkey.pem"; }; servers = lib.mkOption { type = submodule { freeformType = jsonType; }; description = "IRC servers to connect to"; }; mediaProxy = { signingKeyPath = lib.mkOption { type = path; default = "/var/lib/matrix-appservice-irc/media-signingkey.jwk"; description = '' Path to the signing key file for authenticated media. ''; }; ttlSeconds = lib.mkOption { type = ints.unsigned; default = 3600; example = 0; description = '' Lifetime in seconds, that generated URLs stay valid. Set the lifetime to 0 to prevent URLs from becoming invalid. ''; }; bindPort = lib.mkOption { type = port; default = 11111; description = '' Port that the media proxy binds to. ''; }; publicUrl = lib.mkOption { type = str; example = "https://matrix.example.com/media"; description = '' URL under which the media proxy is publicly acccessible. ''; }; }; }; }; }; }; }; }; }; config = lib.mkIf cfg.enable { systemd.services.matrix-appservice-irc = { description = "Matrix-IRC bridge"; before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse after = lib.optionals (cfg.settings.database.engine == "postgres") [ "postgresql.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' umask 077 # Generate key for crypting passwords if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then ${pkgs.openssl}/bin/openssl genpkey \ -out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \ -outform PEM \ -algorithm RSA \ -pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}" fi # Generate registration file if ! [ -f "${registrationFile}" ]; then # The easy case: the file has not been generated yet ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart} else # The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to # regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them. # 1. Backup id=$(grep "^id:.*$" ${registrationFile}) hs_token=$(grep "^hs_token:.*$" ${registrationFile}) as_token=$(grep "^as_token:.*$" ${registrationFile}) # 2. Regenerate ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart} # 3. Restore sed -i "s/^id:.*$/$id/g" ${registrationFile} sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile} sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile} fi if ! [ -f "${cfg.settings.ircService.mediaProxy.signingKeyPath}"]; then ${lib.getExe pkgs.nodejs} ${pkg}/lib/generate-signing-key.js > "${cfg.settings.ircService.mediaProxy.signingKeyPath}" fi # Allow synapse access to the registration if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then chgrp matrix-synapse ${registrationFile} chmod g+r ${registrationFile} fi ''; serviceConfig = rec { Type = "simple"; ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}"; ProtectHome = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; StateDirectory = "matrix-appservice-irc"; StateDirectoryMode = "755"; User = "matrix-appservice-irc"; Group = "matrix-appservice-irc"; CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE"; AmbientCapabilities = CapabilityBoundingSet; NoNewPrivileges = true; LockPersonality = true; RestrictRealtime = true; PrivateMounts = true; SystemCallFilter = [ "@system-service @pkey" "~@privileged @resources" "@chown" ]; SystemCallArchitectures = "native"; # AF_UNIX is required to connect to a postgres socket. RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; }; }; users.groups.matrix-appservice-irc = {}; users.users.matrix-appservice-irc = { description = "Service user for the Matrix-IRC bridge"; group = "matrix-appservice-irc"; isSystemUser = true; }; }; # uses attributes of the linked package meta.buildDocsInSandbox = false; }