nixpkgs/nixos/modules/services/networking/syncplay.nix
Assistant 8119ec6478 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`.
2024-09-02 22:26:11 -04:00

314 lines
10 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.syncplay;
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.
'';
};
port = mkOption {
type = types.port;
default = 8999;
description = ''
TCP port to bind to.
'';
};
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;
description = ''
Salt to allow room operator passwords generated by this server
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`.
'';
};
saltFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the file that contains the server salt. This allows room
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.
'';
};
certDir = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
TLS certificates directory to use for encryption. See
<https://github.com/Syncplay/syncplay/wiki/TLS-support>.
'';
};
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 = [ ];
description = ''
Additional arguments to be passed to the service.
'';
};
package = mkOption {
type = types.package;
default = pkgs.syncplay-nogui;
defaultText = literalExpression "pkgs.syncplay-nogui";
description = ''
Package to use for syncplay.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
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" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
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 = ''
${optionalString (cfg.passwordFile != null) ''
export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
''}
${optionalString (cfg.saltFile != null) ''
export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt")
''}
exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY"}
'';
};
};
}