# docs: # - # example configs: # - # - # - # - enables STUN and TURN # - only over UDP 3478, not firewall-forwarding any TURN port range # - uses stun_disco module (but with no options) # - # - # - # - 2013: # # compliance tests: # - # # administration: # - `sudo -u ejabberd ejabberdctl help` # # federation/support matrix: # - avatars # - nixnet.services + dino: works in MUCs but not DMs (as of 2023 H1) # - movim.eu + dino: works in DMs, MUCs untested (as of 2023/08/29) # - calls # - local + dino: audio, video, works in DMs (as of 2023/08/29) # - movim.eu + dino: audio, video, works in DMs, no matter which side initiates (as of 2023/08/30) # - +native-cell-number@cheogram.com + dino: audio works in DMs, no matter which side initiates (as of 2023/09/01) # - can receive calls even if sender isn't in my roster # - this is presumably using JMP.chat's SIP servers, which then convert it to XMPP call # # bugs: # - 2023/09/01: will randomly stop federating. `systemctl restart ejabberd` fixes, but takes 10 minutes. { config, lib, pkgs, ... }: let # TODO: this range could be larger, but right now that's costly because each element is its own UPnP forward # TURN port range (inclusive) turnPortLow = 49152; turnPortHigh = 49167; turnPortRange = lib.range turnPortLow turnPortHigh; in { sane.persist.sys.plaintext = [ { user = "ejabberd"; group = "ejabberd"; path = "/var/lib/ejabberd"; } ]; sane.ports.ports = lib.mkMerge ([ { "3478" = { protocol = [ "tcp" "udp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-stun-turn"; }; "5222" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-client-to-server"; }; "5223" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpps-client-to-server"; # XMPP over TLS }; "5269" = { protocol = [ "tcp" ]; visibleTo.wan = true; description = "colin-xmpp-server-to-server"; }; "5270" = { protocol = [ "tcp" ]; visibleTo.wan = true; description = "colin-xmpps-server-to-server"; # XMPP over TLS }; "5280" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-bosh"; }; "5281" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-bosh-https"; }; "5349" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-stun-turn-over-tls"; }; "5443" = { protocol = [ "tcp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-web-services"; # file uploads, websockets, admin }; } ] ++ (builtins.map (port: { "${builtins.toString port}" = let count = port - turnPortLow + 1; numPorts = turnPortHigh - turnPortLow + 1; in { protocol = [ "tcp" "udp" ]; visibleTo.lan = true; visibleTo.wan = true; description = "colin-xmpp-turn-${builtins.toString count}-of-${builtins.toString numPorts}"; }; }) turnPortRange )); # provide access to certs # TODO: this should just be `acme`. then we also add nginx to the `acme` group. # why is /var/lib/acme/* owned by `nginx` group?? users.users.ejabberd.extraGroups = [ "nginx" ]; security.acme.certs."uninsane.org".extraDomainNames = [ "xmpp.uninsane.org" "muc.xmpp.uninsane.org" "pubsub.xmpp.uninsane.org" "upload.xmpp.uninsane.org" "vjid.xmpp.uninsane.org" ]; # exists so the XMPP server's cert can obtain altNames for all its resources services.nginx.virtualHosts."xmpp.uninsane.org" = { useACMEHost = "uninsane.org"; }; services.nginx.virtualHosts."muc.xmpp.uninsane.org" = { useACMEHost = "uninsane.org"; }; services.nginx.virtualHosts."pubsub.xmpp.uninsane.org" = { useACMEHost = "uninsane.org"; }; services.nginx.virtualHosts."upload.xmpp.uninsane.org" = { useACMEHost = "uninsane.org"; }; services.nginx.virtualHosts."vjid.xmpp.uninsane.org" = { useACMEHost = "uninsane.org"; }; sane.dns.zones."uninsane.org".inet = { # XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs A."xmpp" = "%ANATIVE%"; CNAME."muc.xmpp" = "xmpp"; CNAME."pubsub.xmpp" = "xmpp"; CNAME."upload.xmpp" = "xmpp"; CNAME."vjid.xmpp" = "xmpp"; # _Service._Proto.Name TTL Class SRV Priority Weight Port Target # - # something's requesting the SRV records for muc.xmpp, so let's include it # nothing seems to request XMPP SRVs for the other records (except @) # lower numerical priority field tells clients to prefer this method SRV."_xmpps-client._tcp.muc.xmpp" = "3 50 5223 xmpp"; SRV."_xmpps-server._tcp.muc.xmpp" = "3 50 5270 xmpp"; SRV."_xmpp-client._tcp.muc.xmpp" = "5 50 5222 xmpp"; SRV."_xmpp-server._tcp.muc.xmpp" = "5 50 5269 xmpp"; SRV."_xmpps-client._tcp" = "3 50 5223 xmpp"; SRV."_xmpps-server._tcp" = "3 50 5270 xmpp"; SRV."_xmpp-client._tcp" = "5 50 5222 xmpp"; SRV."_xmpp-server._tcp" = "5 50 5269 xmpp"; SRV."_stun._udp" = "5 50 3478 xmpp"; SRV."_stun._tcp" = "5 50 3478 xmpp"; SRV."_stuns._tcp" = "5 50 5349 xmpp"; SRV."_turn._udp" = "5 50 3478 xmpp"; SRV."_turn._tcp" = "5 50 3478 xmpp"; SRV."_turns._tcp" = "5 50 5349 xmpp"; }; # TODO: allocate UIDs/GIDs ? services.ejabberd.enable = true; services.ejabberd.configFile = "/var/lib/ejabberd/ejabberd.yaml"; systemd.services.ejabberd.preStart = let config-in = pkgs.writeText "ejabberd.yaml.in" (lib.generators.toYAML {} { hosts = [ "uninsane.org" ]; # none | emergency | alert | critical | error | warning | notice | info | debug loglevel = "debug"; acme.auto = false; certfiles = [ "/var/lib/acme/uninsane.org/full.pem" ]; # ca_file = "${pkgs.cacert.unbundled}/etc/ssl/certs/"; # ca_file = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; pam_userinfotype = "jid"; acl = { admin.user = [ "colin@uninsane.org" ]; local.user_regexp = ""; loopback.ip = [ "127.0.0.0/8" "::1/128" ]; }; access_rules = { local.allow = "local"; c2s_access.allow = "all"; announce.allow = "admin"; configure.allow = "admin"; muc_create.allow = "local"; pubsub_createnode_access.allow = "all"; trusted_network.allow = "loopback"; }; # docs: shaper_rules = { # setting this to above 1 may break outgoing messages # - maybe some servers rate limit? or just don't understand simultaneous connections? max_s2s_connections = 1; max_user_sessions = 10; max_user_offline_messages = 5000; c2s_shaper.fast = "all"; s2s_shaper.med = "all"; }; # docs: # this limits the bytes/sec. # for example, burst: 3_000_000 and rate: 100_000 means: # - each client has a BW budget that accumulates 100kB/sec and is capped at 3 MB shaper.fast = 1000000; shaper.med = 500000; # shaper.fast.rate = 1000000; # shaper.fast.burst_size = 10000000; # shaper.med.rate = 500000; # shaper.med.burst_size = 5000000; # see: # s2s_use_starttls = true; s2s_use_starttls = "optional"; # lessens 504: remote-server-timeout errors # see: negotiation_timeout = 60; listen = [ { port = 5222; module = "ejabberd_c2s"; shaper = "c2s_shaper"; starttls = true; access = "c2s_access"; } { port = 5223; module = "ejabberd_c2s"; shaper = "c2s_shaper"; tls = true; access = "c2s_access"; } { port = 5269; module = "ejabberd_s2s_in"; shaper = "s2s_shaper"; } { port = 5270; module = "ejabberd_s2s_in"; shaper = "s2s_shaper"; tls = true; } { port = 5443; module = "ejabberd_http"; tls = true; request_handlers = { "/admin" = "ejabberd_web_admin"; # TODO: ensure this actually works "/api" = "mod_http_api"; # ejabberd API endpoint (to control server) "/bosh" = "mod_bosh"; "/upload" = "mod_http_upload"; "/ws" = "ejabberd_http_ws"; # "/.well-known/host-meta" = "mod_host_meta"; # "/.well-known/host-meta.json" = "mod_host_meta"; }; } { # STUN+TURN TCP # note that the full port range should be forwarded ("not NAT'd") # `use_turn=true` enables both TURN *and* STUN port = 3478; module = "ejabberd_stun"; transport = "tcp"; use_turn = true; turn_min_port = turnPortLow; turn_max_port = turnPortHigh; turn_ipv4_address = "%ANATIVE%"; } { # STUN+TURN UDP port = 3478; module = "ejabberd_stun"; transport = "udp"; use_turn = true; turn_min_port = turnPortLow; turn_max_port = turnPortHigh; turn_ipv4_address = "%ANATIVE%"; } { # STUN+TURN TLS over TCP port = 5349; module = "ejabberd_stun"; transport = "tcp"; tls = true; certfile = "/var/lib/acme/uninsane.org/full.pem"; use_turn = true; turn_min_port = turnPortLow; turn_max_port = turnPortHigh; turn_ipv4_address = "%ANATIVE%"; } ]; # TODO: enable mod_fail2ban # TODO(low): look into mod_http_fileserver for serving macros? modules = { # mod_adhoc = {}; # mod_announce = { # access = "admin"; # }; # allows users to set avatars in vCard # - mod_avatar = {}; mod_caps = {}; # for mod_pubsub mod_carboncopy = {}; # allows multiple clients to receive a user's message # queues messages when recipient is offline, including PEP and presence messages. # compliance test suggests this be enabled mod_client_state = {}; # mod_conversejs: TODO: enable once on 21.12 # allows clients like Dino to discover where to upload files mod_disco.server_info = [ { modules = "all"; name = "abuse-addresses"; urls = [ "mailto:admin.xmpp@uninsane.org" "xmpp:colin@uninsane.org" ]; } { modules = "all"; name = "admin-addresses"; urls = [ "mailto:admin.xmpp@uninsane.org" "xmpp:colin@uninsane.org" ]; } ]; mod_http_upload = { host = "upload.xmpp.uninsane.org"; hosts = [ "upload.xmpp.uninsane.org" ]; put_url = "https://@HOST@:5443/upload"; dir_mode = "0750"; file_mode = "0750"; rm_on_unregister = false; }; # allow discoverability of BOSH and websocket endpoints # TODO: enable once on ejabberd 22.05 (presently 21.04) # mod_host_meta = {}; mod_jidprep = {}; # probably not needed: lets clients normalize jids mod_last = {}; # allow other users to know when i was last online mod_mam = { # Mnesia is limited to 2GB, better to use an SQL backend # For small servers SQLite is a good fit and is very easy # to configure. Uncomment this when you have SQL configured: # db_type: sql assume_mam_usage = true; default = "always"; }; mod_muc = { access = [ "allow" ]; access_admin = { allow = "admin"; }; access_create = "muc_create"; access_persistent = "muc_create"; access_mam = [ "allow" ]; history_size = 100; # messages to show new participants host = "muc.xmpp.uninsane.org"; hosts = [ "muc.xmpp.uninsane.org" ]; default_room_options = { anonymous = false; lang = "en"; persistent = true; mam = true; }; }; mod_muc_admin = {}; mod_offline = { # store messages for a user when they're offline (TODO: understand multi-client workflow?) access_max_user_messages = "max_user_offline_messages"; store_groupchat = true; }; mod_ping = {}; mod_privacy = {}; # deprecated, but required for `ejabberctl export_piefxis` mod_private = {}; # allow local clients to persist arbitrary data on my server # push notifications to services integrated with e.g. Apple/Android. # default is for a maximum amount of PII to be withheld, since these push notifs # generally traverse 3rd party services. can opt to include message body, etc, though. mod_push = {}; # i don't fully understand what this does, but it seems aimed at making push notifs more reliable. mod_push_keepalive = {}; mod_roster = { versioning = true; }; # docs: # s2s dialback to verify inbound messages # unclear to what degree the XMPP network requires this mod_s2s_dialback = {}; mod_shared_roster = {}; # creates groups for @all, @online, and anything manually administered? mod_stream_mgmt = { # resend undelivered messages if the origin client is offline resend_on_timeout = "if_offline"; }; # fallback for when DNS-based STUN discovery is unsupported. # - see: # docs: # people say to just keep this defaulted (i guess ejabberd knows to return its `host` option of uninsane.org?) mod_stun_disco = {}; # docs: mod_vcard = { allow_return_all = true; # all users are discoverable (?) host = "vjid.xmpp.uninsane.org"; hosts = [ "vjid.xmpp.uninsane.org" ]; search = true; }; mod_vcard_xupdate = {}; # needed for avatars # docs: mod_pubsub = { #^ needed for avatars access_createnode = "pubsub_createnode_access"; host = "pubsub.xmpp.uninsane.org"; hosts = [ "pubsub.xmpp.uninsane.org" ]; ignore_pep_from_offline = false; last_item_cache = true; plugins = [ "pep" "flat" ]; force_node_config = { # ensure client bookmarks are private "storage:bookmarks:" = { "access_model" = "whitelist"; }; "urn:xmpp:avatar:data" = { "access_model" = "open"; }; "urn:xmpp:avatar:metadata" = { "access_model" = "open"; }; }; }; mod_version = {}; }; }); sed = "${pkgs.gnused}/bin/sed"; in '' ip=$(cat '${config.sane.services.dyn-dns.ipPath}') # config is 444 (not 644), so we want to write out-of-place and then atomically move # TODO: factor this out into `sane-woop` helper? rm -f /var/lib/ejabberd/ejabberd.yaml.new ${sed} "s/%ANATIVE%/$ip/g" ${config-in} > /var/lib/ejabberd/ejabberd.yaml.new mv /var/lib/ejabberd/ejabberd.yaml{.new,} ''; sane.services.dyn-dns.restartOnChange = [ "ejabberd.service" ]; }