{ config, pkgs, lib, ... }: let cfg = config.services.ejabberd; inherit (config.security.acme) certs; runtimeDir = "/run/ejabberd"; # wouldn’t it be cool if we could attach extra data? biboumiCfg = rec { user = "biboumi"; group = "biboumi"; database = { user = user; name = "biboumi"; }; } // config.services.biboumi; fqdn = "toastal.in.th"; database = { name = "ejabberd"; user = cfg.user; }; ports = { mqtt = 1883; c2s = 5222; c2ss = 5223; s2s = 5269; s2ss = 5270; http = 5280; https = 5443; irc = 6667; ircs = 6697; proxy65 = 7777; matrix = 8448; }; pow = lib.fix ( self: base: power: if power != 0 then base * (self base (power - 1)) else 1 ); ejabberd_config = { # Use systemd EnvironmentFile + EJABBERD_MACRO_* to define define_macro = { BIBOUMI_SECRET = null; MATRIX_SECRET = null; TURN_SECRET = null; }; loglevel = "notice"; log_rotate_size = 1 * (pow 2 30); log_rotate_count = 1; hide_sensitive_log_data = true; hosts = [ fqdn ]; language = "en"; default_db = "mnesia"; acme.auto = false; ca_file = "${config.environment.etc."ssl/certs/ca-certificates.crt".source}"; certfiles = [ "${certs.${fqdn}.directory}/*.pem" ]; c2s_tls_compression = true; s2s_access = "s2s"; s2s_tls_compression = true; s2s_use_starttls = true; new_sql_schema = true; captcha_cmd = "${cfg.package.out}/lib/ejabberd-${cfg.package.version}/priv/bin/captcha.sh"; captcha_url = "https://xmpp.@HOST@/captcha"; acl = { admin = [ { user = "admin@${fqdn}"; } { user = "toastal@${fqdn}"; } ]; local.user_regexp = ""; loopback.ip = [ "127.0.0.1/8" "::1/128" ]; }; access_rules = { c2s = { deny = "blocked"; allow = "all"; }; s2s = { allow = "all"; }; local.allow = "local"; announce.allow = "admin"; configure.allow = "admin"; muc_create.allow = "local"; pubsub_createnode.allow = "local"; trusted_network.allow = "loopback"; }; api_permissions = { "console commands" = { from = [ "ejabberd_ctl" ]; who = "all"; what = "*"; }; "admin access" = { who = { access.allow = [ { acl = "local"; } { acl = "admin"; } ]; oauth = { scope = "ejabberd:admin"; access.allow = [ { acl = "local"; } { acl = "admin"; } ]; }; }; what = [ "*" "!stop" "!start" ]; }; "public commands" = { who.ip = "127.0.0.1/8"; what = [ "status" "connected_users_number" ]; }; }; shaper = { normal = { rate = 3000; burst_size = 20000; }; fast = 100000; }; modules = { mod_adhoc = { }; mod_admin_extra = { }; mod_announce = { access = "announce"; }; mod_avatar = { }; mod_blocking = { }; mod_bosh = { }; mod_caps = { }; mod_carboncopy = { }; mod_client_state = { }; mod_configure = { }; mod_disco = { server_info = [ { modules = "all"; name = "abuse-addresses"; urls = [ "mailto:toastal+abuse@posteo.net" ]; } ]; }; mod_host_meta = { bosh_service_url = "https://xmpp.@HOST@/bosh"; websocket_url = "wss://xmpp.@HOST@/ws"; }; mod_http_api = { }; #mod_http_fileserver.docroot = "${cfg.spoolDir}/http"; mod_http_upload = { docroot = "${cfg.spoolDir}/uploads"; dir_mode = "0755"; file_mode = "0644"; get_url = "https://xmpp.@HOST@/upload"; put_url = "https://xmpp.@HOST@/upload"; max_size = 4 * (pow 2 30); custom_headers = { Access-Control-Allow-Origin = "https://@HOST@,https://xmpp.@HOST@,https://social.@HOST@"; Access-Control-Allow-Methods = "GET,HEAD,PUT,OPTIONS"; Access-Control-Allow-Headers = "Content-Type"; }; }; mod_http_upload_quota = { max_days = 2 * 365; }; mod_last = { }; mod_matrix_gw = { host = "matrix.@HOST@"; key_name = "use_xmpp"; key = "MATRIX_SECRET"; }; mod_mam = { assume_mam_usage = true; default = "always"; db_type = "sql"; compress_xml = true; }; mod_mqtt = { }; mod_muc = { hosts = [ "chat.@HOST@" ]; access = [ "allow" ]; access_admin = [ { allow = "admin"; } ]; access_create = "muc_create"; access_persistent = "muc_create"; access_mam = [ "allow" ]; default_room_options = { allow_change_subj = true; allow_private_messages_from_visitors = "moderators"; allow_subscription = true; allow_user_invites = true; allowpm = "participants"; lang = "en"; mam = true; max_users = 512; moderated = true; }; }; mod_muc_admin = { }; mod_offline = { access_max_user_messages = "max_user_offline_messages"; use_mam_for_storage = true; }; mod_ping = { }; mod_private = { db_type = "sql"; }; mod_privacy = { db_type = "sql"; }; mod_proxy65 = { hosts = [ "proxy.@HOST@" ]; port = ports.proxy65; access = "local"; max_connections = 8; }; mod_pubsub = { hosts = [ "tidings.@HOST@" ]; access_createnode = "pubsub_createnode"; ignore_pep_from_offline = false; last_item_cache = false; max_items_node = 2048; default_node_config = { max_items = 2048; }; plugins = [ "flat" "pep" ]; force_node_config = { "storage:bookmarks".access_model = "whitelist"; "eu.siacs.conversations.axolotl.*".access_model = "open"; "urn:xmpp:bookmarks:0" = { access_model = "whitelist"; send_last_published_item = "never"; max_items = "infinity"; persist_items = true; }; "urn:xmpp:bookmarks:1" = { access_model = "whitelist"; send_last_published_item = "never"; max_items = "infinity"; persist_items = true; }; "urn:xmpp:pubsub:movim-public-subscription" = { access_model = "whitelist"; max_items = "infinity"; persist_items = true; }; "urn:xmpp:microblog:0" = { notify_retract = true; max_items = "infinity"; persist_items = true; }; "urn:xmpp:microblog:0:comments*" = { access_model = "open"; notify_retract = true; max_items = "infinity"; persist_items = true; }; }; }; mod_push = { }; mod_push_keepalive = { }; mod_register = { ip_access = "trusted_network"; }; mod_roster = { versioning = true; }; mod_s2s_dialback = { }; mod_shared_roster = { }; mod_stream_mgmt = { }; mod_stun_disco = { services = map (type: { inherit type; host = "turn.${fqdn}"; port = 3478; }) [ "turn" "turns" ]; secret = "TURN_SECRET"; }; mod_time = { }; mod_vcard = { db_type = "sql"; }; mod_vcard_xupdate = { }; }; listen = [ { module = "ejabberd_c2s"; port = ports.c2s; max_stanza_size = 262144; #shaper = "c2s_shaper"; access = "c2s"; starttls_required = true; } { module = "ejabberd_c2s"; port = ports.c2ss; max_stanza_size = 262144; #shaper = "c2s_shaper"; access = "c2s"; tls = true; starttls_required = true; } { module = "ejabberd_s2s_in"; port = ports.s2s; max_stanza_size = 524288; shaper = "fast"; } { module = "ejabberd_s2s_in"; port = ports.s2ss; tls = true; max_stanza_size = 524288; shaper = "fast"; } { module = "ejabberd_http"; port = ports.http; tls = false; request_handlers = { }; } { module = "ejabberd_http"; port = ports.https; tls = true; request_handlers = { "/admin" = "ejabberd_web_admin"; "/api" = "mod_http_api"; "/bosh" = "mod_bosh"; "/captcha" = "ejabberd_captcha"; "/upload" = "mod_http_upload"; "/ws" = "ejabberd_http_ws"; "/.well-known/host-meta" = "mod_host_meta"; "/.well-known/host-meta.json" = "mod_host_meta"; }; } { module = "mod_mqtt"; port = ports.mqtt; backlog = 1024; } { module = "ejabberd_service"; port = biboumiCfg.settings.port; hosts = { "${biboumiCfg.settings.hostname}" = { password = "BIBOUMI_SECRET"; }; }; } { module = "ejabberd_http"; port = ports.matrix; tls = true; request_handlers = { "/_matrix" = "mod_matrix_gw"; }; } ]; host_config = { "${fqdn}" = { auth_method = "sql"; auth_password_format = "scram"; sql_type = "pgsql"; sql_server = "localhost"; sql_port = config.services.postgresql.settings.port; sql_database = database.name; sql_username = database.user; }; }; }; ejabberd_config_file = let settingsFormat = pkgs.formats.yaml { }; in settingsFormat.generate "ejabberd.yml" ejabberd_config; in { users = { groups = { ${biboumiCfg.group} = { }; xmpp = { }; }; users = { ${cfg.user} = { extraGroups = [ "xmpp" certs.${fqdn}.group ]; }; ${biboumiCfg.user} = { isSystemUser = true; group = biboumiCfg.group; extraGroups = [ "xmpp" ]; }; }; }; security.acme.certs.${fqdn} = { postRun = lib.mkAfter "systemctl restart ejabberd.service"; }; services.ejabberd = { enable = true; package = pkgs.ejabberd.override (old: { withImagemagick = true; withPgsql = true; withTools = true; withZlib = true; }); configFile = ejabberd_config_file; ctlConfig = '' CONFIG_DIR=${runtimeDir} ERL_CRASH_DUMP=${cfg.logsDir}/erl_crash.dump ''; imagemagick = true; }; systemd.services.ejabberd-data-setup = { description = "Ejabberd Setup: creates EnvironmentFile & so on"; wantedBy = [ "multi-user.target" ]; before = [ "ejabberd.service" ]; requiredBy = [ "ejabberd.service" ]; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; UMask = "077"; RuntimeDirectory = lib.removePrefix "/run/" runtimeDir; RuntimeDirectoryMode = "700"; RemainAfterExit = true; ProtectHome = true; PrivateTmp = true; }; script = /* sh */ '' mkdir -p "${runtimeDir}" touch "${runtimeDir}/.env" "${runtimeDir}/inetrc" chmod 600 "${runtimeDir}/.env" old_umask=$(umask) umask 0177 cat << EOF > "${runtimeDir}/.env" EJABBERD_MACRO_BIBOUMI_SECRET="$(${lib.getExe pkgs.gawk} -F '=' '{a[$1]=$2} END {print(a["password"])}' "${biboumiCfg.credentialsFile}")" EJABBERD_MACRO_MATRIX_SECRET="$(cat "/var/secrets/ejabberd/matrix.key")" EJABBERD_MACRO_TURN_SECRET="$(cat "/var/secrets/turn-server/static-auth-secret.txt")" EOF ''; }; systemd.services.ejabberd = { requires = [ "ejabberd-data-setup.service" "postgresql.service" ]; wantedBy = [ "biboumi.service" ]; after = [ "ejabberd-data-setup.service" "postgresql.service" ]; serviceConfig = { StartupMemoryMax = "12G"; MemoryMax = "8G"; RuntimeDirectory = lib.removePrefix "/run/" runtimeDir; RuntimeDirectoryMode = "700"; RuntimeDirectoryPreserve = "yes"; EnvironmentFile = "${runtimeDir}/.env"; ProtectHome = true; PrivateTmp = true; }; }; services.biboumi = { enable = true; package = pkgs.biboumi.override { withPostgreSQL = true; withSQLite = false; }; settings = { admin = map (u: u.user) ejabberd_config.acl.admin; hostname = "irc.${fqdn}"; db_name = "postgresql://${biboumiCfg.database.user}@localhost:${builtins.toString config.services.postgresql.settings.port}/"; password = null; ca_file = "${config.environment.etc."ssl/certs/ca-certificates.crt".source}"; }; credentialsFile = "/var/secrets/biboumi/biboumi.cfg"; openFirewall = true; }; systemd.services.biboumi = { partOf = [ "ip-change@enp2s0.target" ]; before = [ "ip-change@enp2s0.target" ]; requires = [ "ejabberd.service" "postgresql.service" ]; after = [ "ejabberd.service" ]; serviceConfig = { MemoryMax = "256M"; }; }; services.postgresql = { enable = true; ensureDatabases = [ database.name biboumiCfg.database.name ]; ensureUsers = [ { name = database.user; ensureDBOwnership = true; } { name = biboumiCfg.database.user; ensureDBOwnership = true; } ]; authentication = '' host ${database.name} ${database.user} localhost trust host ${biboumiCfg.database.name} ${biboumiCfg.database.user} localhost trust ''; }; services.h2o = { enable = true; hosts = { "matrix.${fqdn}" = { tls.policy = "only"; acme.useHost = fqdn; settings = { paths."/" = { "proxy.reverse.url" = "http://matrix.${fqdn}:${builtins.toString ports.matrix}"; "proxy.ssl.verify-peer" = "OFF"; "proxy.tunnel" = "ON"; }; }; }; "proxy.${fqdn}" = { tls.policy = "only"; acme.useHost = fqdn; settings = { paths."/" = { "proxy.reverse.url" = "http://proxy.${fqdn}:${builtins.toString ports.proxy65}"; "proxy.ssl.verify-peer" = "OFF"; "proxy.tunnel" = "ON"; }; }; }; "http.xmpp.${fqdn}" = { serverName = "xmpp.${fqdn}"; settings = { paths."/" = { "proxy.reverse.url" = "https://xmpp.${fqdn}:${builtins.toString ports.http}"; "proxy.tunnel" = "ON"; }; }; }; "tls.xmpp.${fqdn}" = { serverName = "xmpp.${fqdn}"; tls.policy = "only"; acme.useHost = fqdn; settings = { paths."/" = { "proxy.reverse.url" = "https://xmpp.${fqdn}:${builtins.toString ports.https}"; "proxy.ssl.verify-peer" = "OFF"; "proxy.tunnel" = "ON"; }; }; }; }; }; systemd.services.h2o.wants = [ "ejabberd.service" ]; systemd.tmpfiles.settings."10-ejabberd" = { "${runtimeDir}".d = { inherit (cfg) user group; mode = "0700"; }; "${runtimeDir}/.env".f = { inherit (cfg) user group; mode = "0600"; }; "${runtimeDir}/inetrc".f = { inherit (cfg) user group; mode = "0600"; }; "${runtimeDir}/vm.args".f = { inherit (cfg) user group; mode = "0600"; }; "/run/biboumi".d = { inherit (biboumiCfg) user group; mode = "0740"; }; }; networking = { firewall.allowedTCPPorts = with ports; [ c2s c2ss s2s c2ss http https irc ircs matrix mqtt proxy65 ]; nftables.ruleset = '' add rule inet filter output meta skuid biboumi tcp accept ''; }; }