{ config, lib, pkgs, ... }: with lib; let cfg = config.services.rqbit; in { options.services.rqbit = { enable = mkEnableOption "rqbit BitTorrent client"; package = mkOption { type = types.package; default = pkgs.rqbit; defaultText = literalExpression "pkgs.rqbit"; description = "The rqbit package to use."; }; dataDir = mkOption { type = types.path; default = "/var/lib/rqbit"; description = "Directory to store downloaded torrents."; }; # HTTP API Configuration httpApi = { listenAddress = mkOption { type = types.str; default = "127.0.0.1"; description = "IP address to listen on for the web UI and API."; }; listenPort = mkOption { type = types.port; default = 3030; description = "Port for the web UI and API."; }; openFirewall = mkOption { type = types.bool; default = false; description = "Open the firewall for the web UI port."; }; }; # BitTorrent TCP Configuration tcp = { minPort = mkOption { type = types.port; default = 4240; description = "Minimum port for incoming BitTorrent connections."; }; maxPort = mkOption { type = types.port; default = 4260; description = "Maximum port for incoming BitTorrent connections."; }; disable = mkOption { type = types.bool; default = false; description = "Disable listening for incoming TCP connections."; }; openFirewall = mkOption { type = types.bool; default = false; description = "Open firewall ports for incoming BitTorrent connections."; }; }; # DHT Configuration dht = { disable = mkOption { type = types.bool; default = false; description = "Disable DHT (Distributed Hash Table) for peer discovery."; }; disablePersistence = mkOption { type = types.bool; default = false; description = "Disable DHT state persistence (useful for multiple instances)."; }; }; # UPnP Configuration upnp = { disablePortForward = mkOption { type = types.bool; default = false; description = "Disable UPnP port forwarding."; }; enableServer = mkOption { type = types.bool; default = false; description = "Enable UPnP Media Server to stream torrents."; }; serverFriendlyName = mkOption { type = types.nullOr types.str; default = null; description = "Friendly name for the UPnP server."; example = "rqbit Media Server"; }; }; # Rate Limiting rateLimit = { download = mkOption { type = types.nullOr types.int; default = null; description = "Download rate limit in bytes per second."; example = 1048576; # 1 MB/s }; upload = mkOption { type = types.nullOr types.int; default = null; description = "Upload rate limit in bytes per second."; example = 524288; # 512 KB/s }; }; # Logging Configuration logging = { level = mkOption { type = types.nullOr ( types.enum [ "trace" "debug" "info" "warn" "error" ] ); default = null; description = "Console log level."; }; file = mkOption { type = types.nullOr types.path; default = null; description = "Log file path (in addition to console logging)."; example = "/var/log/rqbit/rqbit.log"; }; fileRustLog = mkOption { type = types.str; default = "librqbit=debug,info"; description = "RUST_LOG value for the log file."; }; }; # Performance Configuration performance = { workerThreads = mkOption { type = types.nullOr types.ints.positive; default = null; description = "Number of worker threads for the executor."; }; maxBlockingThreads = mkOption { type = types.ints.positive; default = 8; description = "Maximum blocking threads for disk I/O operations."; }; singleThreadRuntime = mkOption { type = types.bool; default = false; description = "Use tokio's single-threaded runtime (for debugging)."; }; concurrentInitLimit = mkOption { type = types.ints.positive; default = 5; description = "Maximum number of torrents that can initialize simultaneously."; }; }; # Peer Configuration peer = { connectTimeout = mkOption { type = types.str; default = "2s"; description = "Peer connection timeout."; example = "1.5s"; }; readWriteTimeout = mkOption { type = types.str; default = "10s"; description = "Peer read/write timeout."; example = "5s"; }; }; # Tracker Configuration tracker = { refreshInterval = mkOption { type = types.nullOr types.str; default = null; description = "Force a specific tracker refresh interval."; example = "30s"; }; trackersFile = mkOption { type = types.nullOr types.path; default = null; description = "File with tracker URLs to use for all torrents."; }; }; # Advanced Options socksProxy = mkOption { type = types.nullOr types.str; default = null; description = "SOCKS5 proxy URL."; example = "socks5://user:pass@localhost:1080"; }; blocklistUrl = mkOption { type = types.nullOr types.str; default = null; description = "URL to download a P2P blocklist from."; example = "https://example.com/blocklist.txt"; }; umask = mkOption { type = types.nullOr types.str; default = null; description = "Set the process umask for file creation permissions."; example = "022"; }; # User/Group Configuration user = mkOption { type = types.str; default = "rqbit"; description = "User account under which rqbit runs."; }; group = mkOption { type = types.str; default = "rqbit"; description = "Group under which rqbit runs."; }; extraArgs = mkOption { type = types.listOf types.str; default = [ ]; description = "Extra command-line arguments to pass to rqbit."; example = literalExpression ''[ "--experimental-mmap-storage" ]''; }; }; config = mkIf cfg.enable { systemd.services.rqbit = { description = "rqbit BitTorrent Client"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; preStart = mkIf (cfg.logging.file != null) '' mkdir -p $(dirname ${cfg.logging.file}) chown ${cfg.user}:${cfg.group} $(dirname ${cfg.logging.file}) ''; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; Environment = [ "XDG_CACHE_HOME=${cfg.dataDir}/.cache" "XDG_DATA_HOME=${cfg.dataDir}/.local/share" ]; ExecStart = let args = [ "${cfg.package}/bin/rqbit" "--http-api-listen-addr ${cfg.httpApi.listenAddress}:${toString cfg.httpApi.listenPort}" ] ++ optional (cfg.logging.level != null) "-v ${cfg.logging.level}" ++ optional (cfg.logging.file != null) "--log-file ${cfg.logging.file}" ++ optional (cfg.logging.file != null) "--log-file-rust-log ${cfg.logging.fileRustLog}" ++ optional (cfg.tracker.refreshInterval != null) "-i ${cfg.tracker.refreshInterval}" ++ optional cfg.performance.singleThreadRuntime "-s" ++ optional cfg.dht.disable "--disable-dht" ++ optional cfg.dht.disablePersistence "--disable-dht-persistence" ++ optional (cfg.peer.connectTimeout != "2s") "--peer-connect-timeout ${cfg.peer.connectTimeout}" ++ optional ( cfg.peer.readWriteTimeout != "10s" ) "--peer-read-write-timeout ${cfg.peer.readWriteTimeout}" ++ optional (cfg.performance.workerThreads != null) "-t ${toString cfg.performance.workerThreads}" ++ optional cfg.tcp.disable "--disable-tcp-listen" ++ optional (cfg.tcp.minPort != 4240) "--tcp-min-port ${toString cfg.tcp.minPort}" ++ optional (cfg.tcp.maxPort != 4260) "--tcp-max-port ${toString cfg.tcp.maxPort}" ++ optional cfg.upnp.disablePortForward "--disable-upnp-port-forward" ++ optional cfg.upnp.enableServer "--enable-upnp-server" ++ optional ( cfg.upnp.serverFriendlyName != null ) "--upnp-server-friendly-name '${cfg.upnp.serverFriendlyName}'" ++ optional ( cfg.performance.maxBlockingThreads != 8 ) "--max-blocking-threads ${toString cfg.performance.maxBlockingThreads}" ++ optional (cfg.socksProxy != null) "--socks-url ${cfg.socksProxy}" ++ optional ( cfg.performance.concurrentInitLimit != 5 ) "--concurrent-init-limit ${toString cfg.performance.concurrentInitLimit}" ++ optional (cfg.umask != null) "--umask ${cfg.umask}" ++ optional ( cfg.rateLimit.download != null ) "--ratelimit-download ${toString cfg.rateLimit.download}" ++ optional (cfg.rateLimit.upload != null) "--ratelimit-upload ${toString cfg.rateLimit.upload}" ++ optional (cfg.blocklistUrl != null) "--blocklist-url ${cfg.blocklistUrl}" ++ optional (cfg.tracker.trackersFile != null) "--trackers-filename ${cfg.tracker.trackersFile}" ++ cfg.extraArgs ++ [ "server" "start" cfg.dataDir ]; in concatStringsSep " " args; Restart = "on-failure"; StateDirectory = "rqbit"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ReadWritePaths = [ cfg.dataDir ] ++ optional (cfg.logging.file != null) (dirOf cfg.logging.file); }; }; users.users = mkIf (cfg.user == "rqbit") { rqbit = { isSystemUser = true; group = cfg.group; description = "rqbit BitTorrent client user"; }; }; users.groups = mkIf (cfg.group == "rqbit") { rqbit = { }; }; networking.firewall = mkIf (cfg.httpApi.openFirewall || cfg.tcp.openFirewall) { allowedTCPPorts = optional cfg.httpApi.openFirewall cfg.httpApi.listenPort; allowedTCPPortRanges = optional cfg.tcp.openFirewall { from = cfg.tcp.minPort; to = cfg.tcp.maxPort; }; }; }; }