From 8119ec6478deeecf9d3920f4c3c33ee5edd34dc6 Mon Sep 17 00:00:00 2001 From: Assistant Date: Sat, 31 Aug 2024 04:11:56 -0400 Subject: [PATCH] nixos/syncplay: add missing options Exposes all currently available command-line arguments that were missing, including some that were impossible to use with the catch-all option `extraArgs` alone, requiring changes to other parts of the system. Those are now all self-contained in the module. The service now uses systemd's `DynamicUsers`. --- .../manual/release-notes/rl-2411.section.md | 3 + .../modules/services/networking/syncplay.nix | 244 +++++++++++++++--- 2 files changed, 216 insertions(+), 31 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 02b34eed060c..1d10bf577e20 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -368,6 +368,9 @@ - The `antennas` package and the `services.antennas` module have been removed as they only work with `tvheadend` (see above). +- The `services.syncplay` module now exposes all currently available command-line arguments for `syncplay-server` as options, as well as a `useACMEHost` option for easy TLS setup. + The systemd service now uses `DynamicUser`/`StateDirectory` and the `user` and `group` options have been deprecated. + ## Other Notable Changes {#sec-release-24.11-notable-changes} diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix index b56754ea3f2e..22808248abbc 100644 --- a/nixos/modules/services/networking/syncplay.nix +++ b/nixos/modules/services/networking/syncplay.nix @@ -7,18 +7,41 @@ let cmdArgs = [ "--port" cfg.port ] + ++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ] + ++ optionals (!cfg.ready) [ "--disable-ready" ] + ++ optionals (!cfg.chat) [ "--disable-chat" ] ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ] + ++ optionals (cfg.motdFile != null) [ "--motd-file" cfg.motdFile ] + ++ optionals (cfg.roomsDBFile != null) [ "--rooms-db-file" cfg.roomsDBFile ] + ++ optionals (cfg.permanentRoomsFile != null) [ "--permanent-rooms-file" cfg.permanentRoomsFile ] + ++ [ "--max-chat-message-length" cfg.maxChatMessageLength ] + ++ [ "--max-username-length" cfg.maxUsernameLength ] + ++ optionals (cfg.statsDBFile != null) [ "--stats-db-file" cfg.statsDBFile ] ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ] + ++ optionals cfg.ipv4Only [ "--ipv4-only" ] + ++ optionals cfg.ipv6Only [ "--ipv6-only" ] + ++ optionals (cfg.interfaceIpv4 != "") [ "--interface-ipv4" cfg.interfaceIpv4 ] + ++ optionals (cfg.interfaceIpv6 != "") [ "--interface-ipv6" cfg.interfaceIpv6 ] ++ cfg.extraArgs; + useACMEHostDir = optionalString (cfg.useACMEHost != null) config.security.acme.certs.${cfg.useACMEHost}.directory; in { + imports = [ + (mkRemovedOptionModule [ "services" "syncplay" "user" ] + "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.") + (mkRemovedOptionModule [ "services" "syncplay" "group" ] + "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.") + ]; + options = { services.syncplay = { enable = mkOption { type = types.bool; default = false; - description = "If enabled, start the Syncplay server."; + description = '' + If enabled, start the Syncplay server. + ''; }; port = mkOption { @@ -29,6 +52,39 @@ in ''; }; + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to the file that contains the server password. If + `null`, the server doesn't require a password. + ''; + }; + + isolateRooms = mkOption { + type = types.bool; + default = false; + description = '' + Enable room isolation. + ''; + }; + + ready = mkOption { + type = types.bool; + default = true; + description = '' + Check readiness of users. + ''; + }; + + chat = mkOption { + type = types.bool; + default = true; + description = '' + Chat with users in the same room. + ''; + }; + salt = mkOption { type = types.nullOr types.str; default = null; @@ -37,7 +93,7 @@ in instance to still work when the server is restarted. The salt will be readable in the nix store and the processlist. If this is not intended use `saltFile` instead. Mutually exclusive with - . + {option}`services.syncplay.saltFile`. ''; }; @@ -49,7 +105,83 @@ in operator passwords generated by this server instance to still work when the server is restarted. `null`, the server doesn't load the salt from a file. Mutually exclusive with - . + {option}`services.syncplay.salt`. + ''; + }; + + motd = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Text to display when users join. The motd will be readable in the nix store + and the processlist. If this is not intended use `motdFile` instead. + Will be overriden by {option}`services.syncplay.motdFile`. + ''; + }; + + motdFile = mkOption { + type = types.nullOr types.str; + default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null; + defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null''; + description = '' + Path to text to display when users join. + Will override {option}`services.syncplay.motd`. + ''; + }; + + roomsDBFile = mkOption { + type = types.nullOr types.str; + default = null; + example = "rooms.db"; + description = '' + Path to SQLite database file to store room states. + Relative to the working directory provided by systemd. + ''; + }; + + permanentRooms = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of rooms that will be listed even if the room is empty. + Will be overriden by {option}`services.syncplay.permanentRoomsFile`. + ''; + }; + + permanentRoomsFile = mkOption { + type = types.nullOr types.str; + default = if cfg.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) else null; + defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null''; + description = '' + File with list of rooms that will be listed even if the room is empty, + newline delimited. + Will override {option}`services.syncplay.permanentRooms`. + ''; + }; + + maxChatMessageLength = mkOption { + type = types.ints.unsigned; + default = 150; + description = '' + Maximum number of characters in a chat message. + ''; + }; + + maxUsernameLength = mkOption { + type = types.ints.unsigned; + default = 16; + description = '' + Maximum number of characters in a username. + ''; + }; + + statsDBFile = mkOption { + type = types.nullOr types.str; + default = null; + example = "stats.db"; + description = '' + Path to SQLite database file to store stats. + Relative to the working directory provided by systemd. ''; }; @@ -62,6 +194,49 @@ in ''; }; + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + example = "syncplay.example.com"; + description = '' + If set, use NixOS-generated ACME certificate with the specified name for TLS. + + Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation. + ''; + }; + + ipv4Only = mkOption { + type = types.bool; + default = false; + description = '' + Listen only on IPv4 when strting the server. + ''; + }; + + ipv6Only = mkOption { + type = types.bool; + default = false; + description = '' + Listen only on IPv6 when strting the server. + ''; + }; + + interfaceIpv4 = mkOption { + type = types.str; + default = ""; + description = '' + The IP address to bind to for IPv4. Leaving it empty defaults to using all. + ''; + }; + + interfaceIpv6 = mkOption { + type = types.str; + default = ""; + description = '' + The IP address to bind to for IPv6. Leaving it empty defaults to using all. + ''; + }; + extraArgs = mkOption { type = types.listOf types.str; default = [ ]; @@ -70,28 +245,12 @@ in ''; }; - user = mkOption { - type = types.str; - default = "nobody"; + package = mkOption { + type = types.package; + default = pkgs.syncplay-nogui; + defaultText = literalExpression "pkgs.syncplay-nogui"; description = '' - User to use when running Syncplay. - ''; - }; - - group = mkOption { - type = types.str; - default = "nogroup"; - description = '' - Group to use when running Syncplay. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Path to the file that contains the server password. If - `null`, the server doesn't require a password. + Package to use for syncplay. ''; }; }; @@ -103,7 +262,24 @@ in assertion = cfg.salt == null || cfg.saltFile == null; message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive."; } + { + assertion = cfg.certDir == null || cfg.useACMEHost == null; + message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive."; + } + { + assertion = !cfg.ipv4Only || !cfg.ipv6Only; + message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive."; + } ]; + + warnings = optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only." + ++ optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only."; + + security.acme.certs = mkIf (cfg.useACMEHost != null) { + "${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ]; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; systemd.services.syncplay = { description = "Syncplay Service"; wantedBy = [ "multi-user.target" ]; @@ -111,20 +287,26 @@ in after = [ "network-online.target" ]; serviceConfig = { - User = cfg.user; - Group = cfg.group; - LoadCredential = lib.optional (cfg.passwordFile != null) "password:${cfg.passwordFile}" - ++ lib.optional (cfg.saltFile != null) "salt:${cfg.saltFile}"; + DynamicUser = true; + StateDirectory = "syncplay"; + WorkingDirectory = "%S/syncplay"; + LoadCredential = optional (cfg.passwordFile != null) "password:${cfg.passwordFile}" + ++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}" + ++ optionals (cfg.useACMEHost != null) [ + "cert.pem:${useACMEHostDir}/cert.pem" + "privkey.pem:${useACMEHostDir}/key.pem" + "chain.pem:${useACMEHostDir}/chain.pem" + ]; }; script = '' - ${lib.optionalString (cfg.passwordFile != null) '' + ${optionalString (cfg.passwordFile != null) '' export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password") ''} - ${lib.optionalString (cfg.saltFile != null) '' + ${optionalString (cfg.saltFile != null) '' export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt") ''} - exec ${pkgs.syncplay-nogui}/bin/syncplay-server ${escapeShellArgs cmdArgs} + exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY"} ''; }; };