diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index ce146d0cdedf..4c44cc4e5b08 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -20,6 +20,8 @@ - [LACT](https://github.com/ilya-zlobintsev/LACT), a GPU monitoring and configuration tool, can now be enabled through [services.lact.enable](#opt-services.lact.enable). Note that for LACT to work properly on AMD GPU systems, you need to enable [hardware.amdgpu.overdrive.enable](#opt-hardware.amdgpu.overdrive.enable). +- [Broadcast Box](https://github.com/Glimesh/broadcast-box), a WebRTC broadcast server. Available as [services.broadcast-box](options.html#opt-services.broadcast-box.enable). + - [SuiteNumérique Docs](https://github.com/suitenumerique/docs), a collaborative note taking, wiki and documentation web platform and alternative to Notion or Outline. Available as [services.lasuite-docs](#opt-services.lasuite-docs.enable). ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 955bd480d6c7..f093b10dc188 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1491,6 +1491,7 @@ ./services/ttys/getty.nix ./services/ttys/gpm.nix ./services/ttys/kmscon.nix + ./services/video/broadcast-box.nix ./services/video/epgstation/default.nix ./services/video/frigate.nix ./services/video/go2rtc/default.nix diff --git a/nixos/modules/services/video/broadcast-box.nix b/nixos/modules/services/video/broadcast-box.nix new file mode 100644 index 000000000000..2038ca02ea37 --- /dev/null +++ b/nixos/modules/services/video/broadcast-box.nix @@ -0,0 +1,274 @@ +{ + lib, + pkgs, + config, + ... +}: +let + inherit (lib) + mkIf + mkEnableOption + mkPackageOption + mkOption + attrNames + types + match + optional + optionals + toInt + last + splitString + allUnique + concatStringsSep + all + filter + mapAttrs + any + getExe + maintainers + ; + inherit (cfg) settings; + cfg = config.services.broadcast-box; + + addressToPort = address: toInt (last (splitString ":" address)); + httpPort = cfg.web.port; + tcpMuxPort = addressToPort settings.TCP_MUX_ADDRESS; + httpRedirect = settings.ENABLE_HTTP_REDIRECT or (settings.HTTPS_REDIRECT_PORT != null); + + udpPorts = + optional (settings.UDP_MUX_PORT != null) settings.UDP_MUX_PORT + ++ optional (settings.UDP_WHEP_PORT != null) settings.UDP_WHEP_PORT + ++ optional (settings.UDP_WHIP_PORT != null) settings.UDP_WHIP_PORT; + tcpPorts = optional (settings.TCP_MUX_ADDRESS != null) tcpMuxPort; + webPorts = [ httpPort ] ++ optional httpRedirect settings.HTTPS_REDIRECT_PORT; +in +{ + options.services.broadcast-box = { + enable = mkEnableOption "Broadcast Box"; + package = mkPackageOption pkgs "broadcast-box" { }; + + web = { + host = mkOption { + type = types.str; + default = ""; + example = "127.0.0.1"; + description = '' + Host address the HTTP server listens on. By default the server + listens on all interfaces. + ''; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = '' + Port the HTTP server listens on. + ''; + }; + + openFirewall = mkEnableOption '' + opening the HTTP server port and, if enabled, the HTTPS redirect server + port in the firewall. + ''; + }; + + openFirewall = mkEnableOption '' + opening WebRTC traffic ports in the firewall. Randomly selected ports + will not be opened. + ''; + + settings = mkOption { + visible = "shallow"; + + type = types.submodule { + freeformType = + with types; + attrsOf ( + nullOr (oneOf [ + bool + int + str + ]) + ); + options = { + TCP_MUX_ADDRESS = mkOption { + type = with types; nullOr (strMatching ".*:[0-9]+"); + default = null; + }; + + DISABLE_STATUS = mkOption { + type = types.bool; + default = true; + }; + + UDP_MUX_PORT = mkOption { + type = with types; nullOr port; + default = null; + }; + + UDP_WHEP_PORT = mkOption { + type = with types; nullOr port; + default = null; + }; + + UDP_WHIP_PORT = mkOption { + type = with types; nullOr port; + default = null; + }; + + ENABLE_HTTP_REDIRECT = mkOption { + type = types.bool; + default = false; + }; + + HTTPS_REDIRECT_PORT = mkOption { + type = with types; nullOr port; + default = if settings.ENABLE_HTTP_REDIRECT then 80 else null; + }; + }; + }; + + default = { + DISABLE_STATUS = true; + }; + + example = { + DISABLE_STATUS = true; + INCLUDE_PUBLIC_IP_IN_NAT_1_TO_1_IP = true; + UDP_MUX_PORT = 3000; + }; + + description = '' + Attribute set of environment variables. + + + + :::{.warning} + The status API exposes stream keys so {env}`DISABLE_STATUS` is enabled + by default. + ::: + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !(settings ? HTTP_ADDRESS); + message = '' + The Broadcast Box `HTTP_ADDRESS` variable should not be used. Instead + use the `host` and `port` options. + ''; + } + { + assertion = httpRedirect -> settings ? SSL_CERT && settings ? SSL_KEY; + message = '' + The Broadcast Box `ENABLE_HTTP_REDIRECT` variable requires `SSL_CERT` + and `SSL_KEY` to be configured. + ''; + } + { + assertion = httpRedirect -> httpPort == 443; + message = '' + Broadcast Box HTTP redirect only works if the HTTP server listen port + is 443. + ''; + } + { + assertion = allUnique (tcpPorts ++ webPorts); + message = '' + Broadcast Box configuration contains duplicate TCP ports. + ''; + } + { + assertion = all (name: (match "[A-Z0-9_]+" name) != null) (attrNames settings); + message = + let + offenders = filter (name: (match "[A-Z0-9_]+" name) == null) (attrNames settings); + in + '' + Broadcast Box `settings` attribute names must be in uppercase snake + case. Invalid attribute name(s): `${concatStringsSep ", " offenders}` + ''; + } + ]; + + systemd.services.broadcast-box = { + description = "Broadcast Box"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + startLimitBurst = 3; + startLimitIntervalSec = 180; + + environment = + (mapAttrs ( + _: value: + if (builtins.typeOf value == "bool") then + if !value then null else "true" + else if (builtins.typeOf value == "int") then + toString value + else + value + ) cfg.settings) + // { + APP_ENV = "nixos"; + HTTP_ADDRESS = cfg.web.host + ":" + toString cfg.web.port; + }; + + serviceConfig = + let + priviledgedPort = any (p: p > 0 && p < 1024) (udpPorts ++ tcpPorts ++ webPorts); + in + { + ExecStart = "${getExe cfg.package}"; + Restart = "always"; + RestartSec = "10s"; + + DynamicUser = true; + LockPersonality = true; + NoNewPrivileges = true; + PrivateUsers = !priviledgedPort; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectProc = "invisible"; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProcSubset = "pid"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + CapabilityBoundingSet = if priviledgedPort then [ "CAP_NET_BIND_SERVICE" ] else ""; + AmbientCapabilities = mkIf priviledgedPort [ "CAP_NET_BIND_SERVICE" ]; + DeviceAllow = ""; + MemoryDenyWriteExecute = true; + UMask = "0077"; + }; + }; + + networking.firewall = { + allowedTCPPorts = optionals cfg.openFirewall tcpPorts ++ optionals cfg.web.openFirewall webPorts; + allowedUDPPorts = optionals cfg.openFirewall udpPorts; + }; + }; + + meta.maintainers = with maintainers; [ JManch ]; +}