diff options
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2405.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/networking/sunshine.nix | 156 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/sunshine.nix | 70 | ||||
-rw-r--r-- | pkgs/servers/sunshine/default.nix | 6 |
6 files changed, 235 insertions, 1 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index b70050a741109..0fd44f0673315 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -185,6 +185,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [Mealie](https://nightly.mealie.io/), a self-hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in NuxtJS for a pleasant user experience for the whole family. Available as [services.mealie](#opt-services.mealie.enable) +- [Sunshine](https://app.lizardbyte.dev/Sunshine), a self-hosted game stream host for Moonlight. Available as [services.sunshine](#opt-services.sunshine.enable). + - [Uni-Sync](https://github.com/EightB1ts/uni-sync), a synchronization tool for Lian Li Uni Controllers. Available as [hardware.uni-sync](#opt-hardware.uni-sync.enable) - [prometheus-nats-exporter](https://github.com/nats-io/prometheus-nats-exporter), a Prometheus exporter for NATS. Available as [services.prometheus.exporters.nats](#opt-services.prometheus.exporters.nats.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 394451231bce5..29c373788c1fe 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1161,6 +1161,7 @@ ./services/networking/strongswan.nix ./services/networking/stubby.nix ./services/networking/stunnel.nix + ./services/networking/sunshine.nix ./services/networking/supplicant.nix ./services/networking/supybot.nix ./services/networking/syncplay.nix diff --git a/nixos/modules/services/networking/sunshine.nix b/nixos/modules/services/networking/sunshine.nix new file mode 100644 index 0000000000000..c115b9cd5cf99 --- /dev/null +++ b/nixos/modules/services/networking/sunshine.nix @@ -0,0 +1,156 @@ +{ 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. + ''; + }; + 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 = [ "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"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5abcbabf18e11..232f10d7c24dd 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -858,6 +858,7 @@ in { stunnel = handleTest ./stunnel.nix {}; sudo = handleTest ./sudo.nix {}; sudo-rs = handleTest ./sudo-rs.nix {}; + sunshine = handleTest ./sunshine.nix {}; suwayomi-server = handleTest ./suwayomi-server.nix {}; swap-file-btrfs = handleTest ./swap-file-btrfs.nix {}; swap-partition = handleTest ./swap-partition.nix {}; diff --git a/nixos/tests/sunshine.nix b/nixos/tests/sunshine.nix new file mode 100644 index 0000000000000..7c7e86de203a0 --- /dev/null +++ b/nixos/tests/sunshine.nix @@ -0,0 +1,70 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: { + name = "sunshine"; + meta = { + # test is flaky on aarch64 + broken = pkgs.stdenv.isAarch64; + maintainers = [ lib.maintainers.devusb ]; + }; + + nodes.sunshine = { config, pkgs, ... }: { + imports = [ + ./common/x11.nix + ]; + + services.sunshine = { + enable = true; + openFirewall = true; + settings = { + capture = "x11"; + encoder = "software"; + output_name = 0; + }; + }; + + environment.systemPackages = with pkgs; [ + gxmessage + ]; + + }; + + nodes.moonlight = { config, pkgs, ... }: { + imports = [ + ./common/x11.nix + ]; + + environment.systemPackages = with pkgs; [ + moonlight-qt + ]; + + }; + + enableOCR = true; + + testScript = '' + # start the tests, wait for sunshine to be up + start_all() + sunshine.wait_for_open_port(48010,"localhost") + + # set the admin username/password, restart sunshine + sunshine.execute("sunshine --creds sunshine sunshine") + sunshine.systemctl("restart sunshine","root") + sunshine.wait_for_open_port(48010,"localhost") + + # initiate pairing from moonlight + moonlight.execute("moonlight pair sunshine --pin 1234 >&2 & disown") + moonlight.wait_for_console_text("Executing request") + + # respond to pairing request from sunshine + sunshine.succeed("curl --insecure -u sunshine:sunshine -d '{\"pin\": \"1234\"}' https://localhost:47990/api/pin") + + # close moonlight once pairing complete + moonlight.send_key("kp_enter") + + # put words on the sunshine screen for moonlight to see + sunshine.execute("gxmessage 'hello world' -center -font 'sans 75' >&2 & disown") + + # connect to sunshine from moonlight and look for the words + moonlight.execute("moonlight --video-decoder software stream sunshine 'Desktop' >&2 & disown") + moonlight.wait_for_text("hello world") + ''; +}) diff --git a/pkgs/servers/sunshine/default.nix b/pkgs/servers/sunshine/default.nix index 7d43eff483e8c..d4ad28ca4361d 100644 --- a/pkgs/servers/sunshine/default.nix +++ b/pkgs/servers/sunshine/default.nix @@ -5,6 +5,7 @@ , autoAddDriverRunpath , makeWrapper , buildNpmPackage +, nixosTests , cmake , avahi , libevdev @@ -185,7 +186,10 @@ stdenv'.mkDerivation rec { install -Dm644 ../packaging/linux/${pname}.desktop $out/share/applications/${pname}.desktop ''; - passthru.updateScript = ./updater.sh; + passthru = { + tests.sunshine = nixosTests.sunshine; + updateScript = ./updater.sh; + }; meta = with lib; { description = "Sunshine is a Game stream host for Moonlight"; |