at 24.11-pre 12 kB view raw
1{ lib, pkgs, config, ... }: 2 3let 4 settingsFormat = pkgs.formats.yaml {}; 5 defaultUser = "slskd"; 6in { 7 options.services.slskd = with lib; with types; { 8 enable = mkEnableOption "enable slskd"; 9 10 package = mkPackageOptionMD pkgs "slskd" { }; 11 12 user = mkOption { 13 type = types.str; 14 default = defaultUser; 15 description = "User account under which slskd runs."; 16 }; 17 18 group = mkOption { 19 type = types.str; 20 default = defaultUser; 21 description = "Group under which slskd runs."; 22 }; 23 24 domain = mkOption { 25 type = types.nullOr types.str; 26 description = '' 27 If non-null, enables an nginx reverse proxy virtual host at this FQDN, 28 at the path configurated with `services.slskd.web.url_base`. 29 ''; 30 example = "slskd.example.com"; 31 }; 32 33 nginx = mkOption { 34 type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }); 35 default = {}; 36 example = lib.literalExpression '' 37 { 38 enableACME = true; 39 forceHttps = true; 40 } 41 ''; 42 description = '' 43 This option customizes the nginx virtual host set up for slskd. 44 ''; 45 }; 46 47 environmentFile = mkOption { 48 type = path; 49 description = '' 50 Path to the environment file sourced on startup. 51 It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`. 52 Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`. 53 Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD` 54 should all reside here instead of in the world-readable nix store. 55 Variables are documented at https://github.com/slskd/slskd/blob/master/docs/config.md 56 ''; 57 }; 58 59 openFirewall = mkOption { 60 type = bool; 61 description = "Whether to open the firewall for the soulseek network listen port (not the web interface port)."; 62 default = false; 63 }; 64 65 settings = mkOption { 66 description = '' 67 Application configuration for slskd. See 68 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md). 69 ''; 70 default = {}; 71 type = submodule { 72 freeformType = settingsFormat.type; 73 options = { 74 remote_file_management = mkEnableOption "modification of share contents through the web ui"; 75 76 flags = { 77 force_share_scan = mkOption { 78 type = bool; 79 description = "Force a rescan of shares on every startup."; 80 }; 81 no_version_check = mkOption { 82 type = bool; 83 default = true; 84 visible = false; 85 description = "Don't perform a version check on startup."; 86 }; 87 }; 88 89 directories = { 90 incomplete = mkOption { 91 type = nullOr path; 92 description = "Directory where incomplete downloading files are stored."; 93 defaultText = "/var/lib/slskd/incomplete"; 94 default = null; 95 }; 96 downloads = mkOption { 97 type = nullOr path; 98 description = "Directory where downloaded files are stored."; 99 defaultText = "/var/lib/slskd/downloads"; 100 default = null; 101 }; 102 }; 103 104 shares = { 105 directories = mkOption { 106 type = listOf str; 107 description = '' 108 Paths to shared directories. See 109 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories) 110 for advanced usage. 111 ''; 112 example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]''; 113 }; 114 filters = mkOption { 115 type = listOf str; 116 example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]''; 117 description = "Regular expressions of files to exclude from sharing."; 118 }; 119 }; 120 121 rooms = mkOption { 122 type = listOf str; 123 description = "Chat rooms to join on startup."; 124 }; 125 126 soulseek = { 127 description = mkOption { 128 type = str; 129 description = "The user description for the Soulseek network."; 130 defaultText = "A slskd user. https://github.com/slskd/slskd"; 131 }; 132 listen_port = mkOption { 133 type = port; 134 description = "The port on which to listen for incoming connections."; 135 default = 50300; 136 }; 137 }; 138 139 global = { 140 # TODO speed units 141 upload = { 142 slots = mkOption { 143 type = ints.unsigned; 144 description = "Limit of the number of concurrent upload slots."; 145 }; 146 speed_limit = mkOption { 147 type = ints.unsigned; 148 description = "Total upload speed limit."; 149 }; 150 }; 151 download = { 152 slots = mkOption { 153 type = ints.unsigned; 154 description = "Limit of the number of concurrent download slots."; 155 }; 156 speed_limit = mkOption { 157 type = ints.unsigned; 158 description = "Total upload download limit"; 159 }; 160 }; 161 }; 162 163 filters.search.request = mkOption { 164 type = listOf str; 165 example = lib.literalExpression ''[ "^.{1,2}$" ]''; 166 description = "Incoming search requests which match this filter are ignored."; 167 }; 168 169 web = { 170 port = mkOption { 171 type = port; 172 default = 5030; 173 description = "The HTTP listen port."; 174 }; 175 url_base = mkOption { 176 type = path; 177 default = "/"; 178 description = "The base path in the url for web requests."; 179 }; 180 # Users should use a reverse proxy instead for https 181 https.disabled = mkOption { 182 type = bool; 183 default = true; 184 description = "Disable the built-in HTTPS server"; 185 }; 186 }; 187 188 retention = { 189 transfers = { 190 upload = { 191 succeeded = mkOption { 192 type = ints.unsigned; 193 description = "Lifespan of succeeded upload tasks."; 194 defaultText = "(indefinite)"; 195 }; 196 errored = mkOption { 197 type = ints.unsigned; 198 description = "Lifespan of errored upload tasks."; 199 defaultText = "(indefinite)"; 200 }; 201 cancelled = mkOption { 202 type = ints.unsigned; 203 description = "Lifespan of cancelled upload tasks."; 204 defaultText = "(indefinite)"; 205 }; 206 }; 207 download = { 208 succeeded = mkOption { 209 type = ints.unsigned; 210 description = "Lifespan of succeeded download tasks."; 211 defaultText = "(indefinite)"; 212 }; 213 errored = mkOption { 214 type = ints.unsigned; 215 description = "Lifespan of errored download tasks."; 216 defaultText = "(indefinite)"; 217 }; 218 cancelled = mkOption { 219 type = ints.unsigned; 220 description = "Lifespan of cancelled download tasks."; 221 defaultText = "(indefinite)"; 222 }; 223 }; 224 }; 225 files = { 226 complete = mkOption { 227 type = ints.unsigned; 228 description = "Lifespan of completely downloaded files in minutes."; 229 example = 20160; 230 defaultText = "(indefinite)"; 231 }; 232 incomplete = mkOption { 233 type = ints.unsigned; 234 description = "Lifespan of incomplete downloading files in minutes."; 235 defaultText = "(indefinite)"; 236 }; 237 }; 238 }; 239 240 logger = { 241 # Disable by default, journald already retains as needed 242 disk = mkOption { 243 type = bool; 244 description = "Whether to log to the application directory."; 245 default = false; 246 visible = false; 247 }; 248 }; 249 }; 250 }; 251 }; 252 }; 253 254 config = let 255 cfg = config.services.slskd; 256 257 confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings); 258 259 configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues; 260 261 in lib.mkIf cfg.enable { 262 263 # Force off, configuration file is in nix store and is immutable 264 services.slskd.settings.remote_configuration = lib.mkForce false; 265 266 users.users = lib.optionalAttrs (cfg.user == defaultUser) { 267 "${defaultUser}" = { 268 group = cfg.group; 269 isSystemUser = true; 270 }; 271 }; 272 273 users.groups = lib.optionalAttrs (cfg.group == defaultUser) { 274 "${defaultUser}" = {}; 275 }; 276 277 systemd.services.slskd = { 278 description = "A modern client-server application for the Soulseek file sharing network"; 279 after = [ "network.target" ]; 280 wantedBy = [ "multi-user.target" ]; 281 serviceConfig = { 282 Type = "simple"; 283 User = cfg.user; 284 Group = cfg.group; 285 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; 286 StateDirectory = "slskd"; # Creates /var/lib/slskd and manages permissions 287 ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}"; 288 Restart = "on-failure"; 289 ReadOnlyPaths = map (d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1) cfg.settings.shares.directories; 290 ReadWritePaths = 291 (lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) ++ 292 (lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads); 293 LockPersonality = true; 294 NoNewPrivileges = true; 295 PrivateDevices = true; 296 PrivateMounts = true; 297 PrivateTmp = true; 298 PrivateUsers = true; 299 ProtectClock = true; 300 ProtectControlGroups = true; 301 ProtectHome = true; 302 ProtectHostname = true; 303 ProtectKernelLogs = true; 304 ProtectKernelModules = true; 305 ProtectKernelTunables = true; 306 ProtectProc = "invisible"; 307 ProtectSystem = "strict"; 308 RemoveIPC = true; 309 RestrictNamespaces = true; 310 RestrictSUIDSGID = true; 311 }; 312 }; 313 314 networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port; 315 316 services.nginx = lib.mkIf (cfg.domain != null) { 317 enable = lib.mkDefault true; 318 virtualHosts."${cfg.domain}" = lib.mkMerge [ 319 cfg.nginx 320 { 321 locations."${cfg.settings.web.url_base}" = { 322 proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}"; 323 proxyWebsockets = true; 324 }; 325 } 326 ]; 327 }; 328 }; 329 330 meta = { 331 maintainers = with lib.maintainers; [ ppom melvyn2 ]; 332 }; 333}