nixpkgs/nixos/modules/services/networking/sunshine.nix

164 lines
5.2 KiB
Nix

{ config, lib, pkgs, utils, ... }:
let
inherit (lib) mkEnableOption mkPackageOption mkOption mkIf mkDefault types optionals getExe;
inherit (utils) escapeSystemdExecArgs;
cfg = config.services.sunshine;
# ports used are offset from a single base port, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#port
generatePorts = port: offsets: map (offset: port + offset) offsets;
defaultPort = 47989;
appsFormat = pkgs.formats.json { };
settingsFormat = pkgs.formats.keyValue { };
appsFile = appsFormat.generate "apps.json" cfg.applications;
configFile = settingsFormat.generate "sunshine.conf" cfg.settings;
in
{
options.services.sunshine = with types; {
enable = mkEnableOption "Sunshine, a self-hosted game stream host for Moonlight";
package = mkPackageOption pkgs "sunshine" { };
openFirewall = mkOption {
type = bool;
default = false;
description = ''
Whether to automatically open ports in the firewall.
'';
};
capSysAdmin = mkOption {
type = bool;
default = false;
description = ''
Whether to give the Sunshine binary CAP_SYS_ADMIN, required for DRM/KMS screen capture.
'';
};
autoStart = mkOption {
type = bool;
default = true;
description = ''
Whether sunshine should be started automatically.
'';
};
settings = mkOption {
default = { };
description = ''
Settings to be rendered into the configuration file. If this is set, no configuration is possible from the web UI.
See https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#configuration for syntax.
'';
example = ''
{
sunshine_name = "nixos";
}
'';
type = submodule (settings: {
freeformType = settingsFormat.type;
options.port = mkOption {
type = port;
default = defaultPort;
description = ''
Base port -- others used are offset from this one, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#port for details.
'';
};
});
};
applications = mkOption {
default = { };
description = ''
Configuration for applications to be exposed to Moonlight. If this is set, no configuration is possible from the web UI, and must be by the `settings` option.
'';
example = ''
{
env = {
PATH = "$(PATH):$(HOME)/.local/bin";
};
apps = [
{
name = "1440p Desktop";
prep-cmd = [
{
do = "''${pkgs.kdePackages.libkscreen}/bin/kscreen-doctor output.DP-4.mode.2560x1440@144";
undo = "''${pkgs.kdePackages.libkscreen}/bin/kscreen-doctor output.DP-4.mode.3440x1440@144";
}
];
exclude-global-prep-cmd = "false";
auto-detach = "true";
}
];
}
'';
type = submodule {
options = {
env = mkOption {
default = { };
description = ''
Environment variables to be set for the applications.
'';
type = attrsOf str;
};
apps = mkOption {
default = [ ];
description = ''
Applications to be exposed to Moonlight.
'';
type = listOf attrs;
};
};
};
};
};
config = mkIf cfg.enable {
services.sunshine.settings.file_apps = mkIf (cfg.applications.apps != [ ]) "${appsFile}";
environment.systemPackages = [
cfg.package
];
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = generatePorts cfg.settings.port [ (-5) 0 1 21 ];
allowedUDPPorts = generatePorts cfg.settings.port [ 9 10 11 13 21 ];
};
boot.kernelModules = [ "uinput" ];
services.udev.packages = [ cfg.package ];
services.avahi = {
enable = mkDefault true;
publish = {
enable = mkDefault true;
userServices = mkDefault true;
};
};
security.wrappers.sunshine = mkIf cfg.capSysAdmin {
owner = "root";
group = "root";
capabilities = "cap_sys_admin+p";
source = getExe cfg.package;
};
systemd.user.services.sunshine = {
description = "Self-hosted game stream host for Moonlight";
wantedBy = mkIf cfg.autoStart [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
wants = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
startLimitIntervalSec = 500;
startLimitBurst = 5;
serviceConfig = {
# only add configFile if an application or a setting other than the default port is set to allow configuration from web UI
ExecStart = escapeSystemdExecArgs ([
(if cfg.capSysAdmin then "${config.security.wrapperDir}/sunshine" else "${getExe cfg.package}")
] ++ optionals (cfg.applications.apps != [ ] || (builtins.length (builtins.attrNames cfg.settings) > 1 || cfg.settings.port != defaultPort)) [ "${configFile}" ]);
Restart = "on-failure";
RestartSec = "5s";
};
};
};
}