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"}
'';
};
};