at master 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 cfg = config.services.syncplay; 12 13 cmdArgs = [ 14 "--port" 15 cfg.port 16 ] 17 ++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ] 18 ++ optionals (!cfg.ready) [ "--disable-ready" ] 19 ++ optionals (!cfg.chat) [ "--disable-chat" ] 20 ++ optionals (cfg.salt != null) [ 21 "--salt" 22 cfg.salt 23 ] 24 ++ optionals (cfg.motdFile != null) [ 25 "--motd-file" 26 cfg.motdFile 27 ] 28 ++ optionals (cfg.roomsDBFile != null) [ 29 "--rooms-db-file" 30 cfg.roomsDBFile 31 ] 32 ++ optionals (cfg.permanentRoomsFile != null) [ 33 "--permanent-rooms-file" 34 cfg.permanentRoomsFile 35 ] 36 ++ [ 37 "--max-chat-message-length" 38 cfg.maxChatMessageLength 39 ] 40 ++ [ 41 "--max-username-length" 42 cfg.maxUsernameLength 43 ] 44 ++ optionals (cfg.statsDBFile != null) [ 45 "--stats-db-file" 46 cfg.statsDBFile 47 ] 48 ++ optionals (cfg.certDir != null) [ 49 "--tls" 50 cfg.certDir 51 ] 52 ++ optionals cfg.ipv4Only [ "--ipv4-only" ] 53 ++ optionals cfg.ipv6Only [ "--ipv6-only" ] 54 ++ optionals (cfg.interfaceIpv4 != "") [ 55 "--interface-ipv4" 56 cfg.interfaceIpv4 57 ] 58 ++ optionals (cfg.interfaceIpv6 != "") [ 59 "--interface-ipv6" 60 cfg.interfaceIpv6 61 ] 62 ++ cfg.extraArgs; 63 64 useACMEHostDir = optionalString ( 65 cfg.useACMEHost != null 66 ) config.security.acme.certs.${cfg.useACMEHost}.directory; 67in 68{ 69 imports = [ 70 (mkRemovedOptionModule [ "services" "syncplay" "user" ] 71 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality." 72 ) 73 (mkRemovedOptionModule [ "services" "syncplay" "group" ] 74 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality." 75 ) 76 ]; 77 78 options = { 79 services.syncplay = { 80 enable = mkOption { 81 type = types.bool; 82 default = false; 83 description = '' 84 If enabled, start the Syncplay server. 85 ''; 86 }; 87 88 port = mkOption { 89 type = types.port; 90 default = 8999; 91 description = '' 92 TCP port to bind to. 93 ''; 94 }; 95 96 passwordFile = mkOption { 97 type = types.nullOr types.path; 98 default = null; 99 description = '' 100 Path to the file that contains the server password. If 101 `null`, the server doesn't require a password. 102 ''; 103 }; 104 105 isolateRooms = mkOption { 106 type = types.bool; 107 default = false; 108 description = '' 109 Enable room isolation. 110 ''; 111 }; 112 113 ready = mkOption { 114 type = types.bool; 115 default = true; 116 description = '' 117 Check readiness of users. 118 ''; 119 }; 120 121 chat = mkOption { 122 type = types.bool; 123 default = true; 124 description = '' 125 Chat with users in the same room. 126 ''; 127 }; 128 129 salt = mkOption { 130 type = types.nullOr types.str; 131 default = null; 132 description = '' 133 Salt to allow room operator passwords generated by this server 134 instance to still work when the server is restarted. The salt will be 135 readable in the nix store and the processlist. If this is not 136 intended use `saltFile` instead. Mutually exclusive with 137 {option}`services.syncplay.saltFile`. 138 ''; 139 }; 140 141 saltFile = mkOption { 142 type = types.nullOr types.path; 143 default = null; 144 description = '' 145 Path to the file that contains the server salt. This allows room 146 operator passwords generated by this server instance to still work 147 when the server is restarted. `null`, the server doesn't load the 148 salt from a file. Mutually exclusive with 149 {option}`services.syncplay.salt`. 150 ''; 151 }; 152 153 motd = mkOption { 154 type = types.nullOr types.str; 155 default = null; 156 description = '' 157 Text to display when users join. The motd will be readable in the nix store 158 and the processlist. If this is not intended use `motdFile` instead. 159 Will be overriden by {option}`services.syncplay.motdFile`. 160 ''; 161 }; 162 163 motdFile = mkOption { 164 type = types.nullOr types.str; 165 default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null; 166 defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null''; 167 description = '' 168 Path to text to display when users join. 169 Will override {option}`services.syncplay.motd`. 170 ''; 171 }; 172 173 roomsDBFile = mkOption { 174 type = types.nullOr types.str; 175 default = null; 176 example = "rooms.db"; 177 description = '' 178 Path to SQLite database file to store room states. 179 Relative to the working directory provided by systemd. 180 ''; 181 }; 182 183 permanentRooms = mkOption { 184 type = types.listOf types.str; 185 default = [ ]; 186 description = '' 187 List of rooms that will be listed even if the room is empty. 188 Will be overriden by {option}`services.syncplay.permanentRoomsFile`. 189 ''; 190 }; 191 192 permanentRoomsFile = mkOption { 193 type = types.nullOr types.str; 194 default = 195 if cfg.permanentRooms != [ ] then 196 (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) 197 else 198 null; 199 defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null''; 200 description = '' 201 File with list of rooms that will be listed even if the room is empty, 202 newline delimited. 203 Will override {option}`services.syncplay.permanentRooms`. 204 ''; 205 }; 206 207 maxChatMessageLength = mkOption { 208 type = types.ints.unsigned; 209 default = 150; 210 description = '' 211 Maximum number of characters in a chat message. 212 ''; 213 }; 214 215 maxUsernameLength = mkOption { 216 type = types.ints.unsigned; 217 default = 16; 218 description = '' 219 Maximum number of characters in a username. 220 ''; 221 }; 222 223 statsDBFile = mkOption { 224 type = types.nullOr types.str; 225 default = null; 226 example = "stats.db"; 227 description = '' 228 Path to SQLite database file to store stats. 229 Relative to the working directory provided by systemd. 230 ''; 231 }; 232 233 certDir = mkOption { 234 type = types.nullOr types.path; 235 default = null; 236 description = '' 237 TLS certificates directory to use for encryption. See 238 <https://github.com/Syncplay/syncplay/wiki/TLS-support>. 239 ''; 240 }; 241 242 useACMEHost = mkOption { 243 type = types.nullOr types.str; 244 default = null; 245 example = "syncplay.example.com"; 246 description = '' 247 If set, use NixOS-generated ACME certificate with the specified name for TLS. 248 249 Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation. 250 ''; 251 }; 252 253 ipv4Only = mkOption { 254 type = types.bool; 255 default = false; 256 description = '' 257 Listen only on IPv4 when strting the server. 258 ''; 259 }; 260 261 ipv6Only = mkOption { 262 type = types.bool; 263 default = false; 264 description = '' 265 Listen only on IPv6 when strting the server. 266 ''; 267 }; 268 269 interfaceIpv4 = mkOption { 270 type = types.str; 271 default = ""; 272 description = '' 273 The IP address to bind to for IPv4. Leaving it empty defaults to using all. 274 ''; 275 }; 276 277 interfaceIpv6 = mkOption { 278 type = types.str; 279 default = ""; 280 description = '' 281 The IP address to bind to for IPv6. Leaving it empty defaults to using all. 282 ''; 283 }; 284 285 extraArgs = mkOption { 286 type = types.listOf types.str; 287 default = [ ]; 288 description = '' 289 Additional arguments to be passed to the service. 290 ''; 291 }; 292 293 package = mkPackageOption pkgs "syncplay-nogui" { }; 294 }; 295 }; 296 297 config = mkIf cfg.enable { 298 assertions = [ 299 { 300 assertion = cfg.salt == null || cfg.saltFile == null; 301 message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive."; 302 } 303 { 304 assertion = cfg.certDir == null || cfg.useACMEHost == null; 305 message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive."; 306 } 307 { 308 assertion = !cfg.ipv4Only || !cfg.ipv6Only; 309 message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive."; 310 } 311 ]; 312 313 warnings = 314 optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) 315 "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only." 316 ++ 317 optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) 318 "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only."; 319 320 security.acme.certs = mkIf (cfg.useACMEHost != null) { 321 "${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ]; 322 }; 323 324 networking.firewall.allowedTCPPorts = [ cfg.port ]; 325 systemd.services.syncplay = { 326 description = "Syncplay Service"; 327 wantedBy = [ "multi-user.target" ]; 328 wants = [ "network-online.target" ]; 329 after = [ "network-online.target" ]; 330 331 serviceConfig = { 332 DynamicUser = true; 333 StateDirectory = "syncplay"; 334 WorkingDirectory = "%S/syncplay"; 335 LoadCredential = 336 optional (cfg.passwordFile != null) "password:${cfg.passwordFile}" 337 ++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}" 338 ++ optionals (cfg.useACMEHost != null) [ 339 "cert.pem:${useACMEHostDir}/cert.pem" 340 "privkey.pem:${useACMEHostDir}/key.pem" 341 "chain.pem:${useACMEHostDir}/chain.pem" 342 ]; 343 }; 344 345 script = '' 346 ${optionalString (cfg.passwordFile != null) '' 347 export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password") 348 ''} 349 ${optionalString (cfg.saltFile != null) '' 350 export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt") 351 ''} 352 exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${ 353 optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY" 354 } 355 ''; 356 }; 357 }; 358}