1 { lib, pkgs, config, ... }:
4 settingsFormat = pkgs.formats.yaml {};
7 options.services.slskd = with lib; with types; {
8 enable = mkEnableOption "slskd";
10 package = mkPackageOption pkgs "slskd" { };
14 default = defaultUser;
15 description = "User account under which slskd runs.";
20 default = defaultUser;
21 description = "Group under which slskd runs.";
25 type = types.nullOr types.str;
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`.
30 example = "slskd.example.com";
34 type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
36 example = lib.literalExpression ''
43 This option customizes the nginx virtual host set up for slskd.
47 environmentFile = mkOption {
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
59 openFirewall = mkOption {
61 description = "Whether to open the firewall for the soulseek network listen port (not the web interface port).";
67 Application configuration for slskd. See
68 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md).
72 freeformType = settingsFormat.type;
74 remote_file_management = mkEnableOption "modification of share contents through the web ui";
77 force_share_scan = mkOption {
79 description = "Force a rescan of shares on every startup.";
81 no_version_check = mkOption {
85 description = "Don't perform a version check on startup.";
90 incomplete = mkOption {
92 description = "Directory where incomplete downloading files are stored.";
93 defaultText = "/var/lib/slskd/incomplete";
96 downloads = mkOption {
98 description = "Directory where downloaded files are stored.";
99 defaultText = "/var/lib/slskd/downloads";
105 directories = mkOption {
108 Paths to shared directories. See
109 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories)
112 example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]'';
116 example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]'';
117 description = "Regular expressions of files to exclude from sharing.";
123 description = "Chat rooms to join on startup.";
127 description = mkOption {
129 description = "The user description for the Soulseek network.";
130 defaultText = "A slskd user. https://github.com/slskd/slskd";
132 listen_port = mkOption {
134 description = "The port on which to listen for incoming connections.";
143 type = ints.unsigned;
144 description = "Limit of the number of concurrent upload slots.";
146 speed_limit = mkOption {
147 type = ints.unsigned;
148 description = "Total upload speed limit.";
153 type = ints.unsigned;
154 description = "Limit of the number of concurrent download slots.";
156 speed_limit = mkOption {
157 type = ints.unsigned;
158 description = "Total upload download limit";
163 filters.search.request = mkOption {
165 example = lib.literalExpression ''[ "^.{1,2}$" ]'';
166 description = "Incoming search requests which match this filter are ignored.";
173 description = "The HTTP listen port.";
175 url_base = mkOption {
178 description = "The base path in the url for web requests.";
180 # Users should use a reverse proxy instead for https
181 https.disabled = mkOption {
184 description = "Disable the built-in HTTPS server";
191 succeeded = mkOption {
192 type = ints.unsigned;
193 description = "Lifespan of succeeded upload tasks.";
194 defaultText = "(indefinite)";
197 type = ints.unsigned;
198 description = "Lifespan of errored upload tasks.";
199 defaultText = "(indefinite)";
201 cancelled = mkOption {
202 type = ints.unsigned;
203 description = "Lifespan of cancelled upload tasks.";
204 defaultText = "(indefinite)";
208 succeeded = mkOption {
209 type = ints.unsigned;
210 description = "Lifespan of succeeded download tasks.";
211 defaultText = "(indefinite)";
214 type = ints.unsigned;
215 description = "Lifespan of errored download tasks.";
216 defaultText = "(indefinite)";
218 cancelled = mkOption {
219 type = ints.unsigned;
220 description = "Lifespan of cancelled download tasks.";
221 defaultText = "(indefinite)";
226 complete = mkOption {
227 type = ints.unsigned;
228 description = "Lifespan of completely downloaded files in minutes.";
230 defaultText = "(indefinite)";
232 incomplete = mkOption {
233 type = ints.unsigned;
234 description = "Lifespan of incomplete downloading files in minutes.";
235 defaultText = "(indefinite)";
241 # Disable by default, journald already retains as needed
244 description = "Whether to log to the application directory.";
255 cfg = config.services.slskd;
257 confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings);
259 configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues;
261 in lib.mkIf cfg.enable {
263 # Force off, configuration file is in nix store and is immutable
264 services.slskd.settings.remote_configuration = lib.mkForce false;
266 users.users = lib.optionalAttrs (cfg.user == defaultUser) {
273 users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
274 "${defaultUser}" = {};
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" ];
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;
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;
300 ProtectControlGroups = true;
302 ProtectHostname = true;
303 ProtectKernelLogs = true;
304 ProtectKernelModules = true;
305 ProtectKernelTunables = true;
306 ProtectProc = "invisible";
307 ProtectSystem = "strict";
309 RestrictNamespaces = true;
310 RestrictSUIDSGID = true;
314 networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port;
316 services.nginx = lib.mkIf (cfg.domain != null) {
317 enable = lib.mkDefault true;
318 virtualHosts."${cfg.domain}" = lib.mkMerge [
321 locations."${cfg.settings.web.url_base}" = {
322 proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}";
323 proxyWebsockets = true;
331 maintainers = with lib.maintainers; [ ppom melvyn2 ];