phase 2: promote services/, tests/, patches/, lib/, scripts/
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
From ab57092a60123e361cf0de1c1a314a9888c45219 Mon Sep 17 00:00:00 2001
|
||||
From: Simon Gardling <titaniumtown@proton.me>
|
||||
Date: Sat, 21 Mar 2026 09:24:39 -0400
|
||||
Subject: [PATCH] temp
|
||||
|
||||
---
|
||||
.../services/networking/firefox-syncserver.md | 23 +++
|
||||
.../networking/firefox-syncserver.nix | 140 ++++++++++++++----
|
||||
pkgs/by-name/sy/syncstorage-rs/package.nix | 49 ++++--
|
||||
3 files changed, 174 insertions(+), 38 deletions(-)
|
||||
|
||||
diff --git a/nixos/modules/services/networking/firefox-syncserver.md b/nixos/modules/services/networking/firefox-syncserver.md
|
||||
index 991e97f799d6..3bc45cfa5640 100644
|
||||
--- a/nixos/modules/services/networking/firefox-syncserver.md
|
||||
+++ b/nixos/modules/services/networking/firefox-syncserver.md
|
||||
@@ -32,6 +32,29 @@ This configuration should never be used in production. It is not encrypted and
|
||||
stores its secrets in a world-readable location.
|
||||
:::
|
||||
|
||||
+## Database backends {#module-services-firefox-syncserver-database}
|
||||
+
|
||||
+The sync server supports MySQL/MariaDB (the default) and PostgreSQL as database
|
||||
+backends. Set `database.type` to choose the backend:
|
||||
+
|
||||
+```nix
|
||||
+{
|
||||
+ services.firefox-syncserver = {
|
||||
+ enable = true;
|
||||
+ database.type = "postgresql";
|
||||
+ secrets = "/run/secrets/firefox-syncserver";
|
||||
+ singleNode = {
|
||||
+ enable = true;
|
||||
+ hostname = "localhost";
|
||||
+ url = "http://localhost:5000";
|
||||
+ };
|
||||
+ };
|
||||
+}
|
||||
+```
|
||||
+
|
||||
+When `database.createLocally` is `true` (the default), the module will
|
||||
+automatically enable and configure the corresponding database service.
|
||||
+
|
||||
## More detailed setup {#module-services-firefox-syncserver-configuration}
|
||||
|
||||
The `firefox-syncserver` service provides a number of options to make setting up
|
||||
diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix
|
||||
index 6a50e49fc096..70a56314e323 100644
|
||||
--- a/nixos/modules/services/networking/firefox-syncserver.nix
|
||||
+++ b/nixos/modules/services/networking/firefox-syncserver.nix
|
||||
@@ -13,7 +13,21 @@ let
|
||||
defaultUser = "firefox-syncserver";
|
||||
|
||||
dbIsLocal = cfg.database.host == "localhost";
|
||||
- dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}";
|
||||
+ dbIsMySQL = cfg.database.type == "mysql";
|
||||
+ dbIsPostgreSQL = cfg.database.type == "postgresql";
|
||||
+
|
||||
+ dbURL =
|
||||
+ if dbIsMySQL then
|
||||
+ "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"
|
||||
+ else
|
||||
+ "postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?host=/run/postgresql"}";
|
||||
+
|
||||
+ # postgresql.target waits for postgresql-setup.service (which runs
|
||||
+ # ensureDatabases / ensureUsers) to complete, avoiding race conditions
|
||||
+ # where the syncserver starts before its database and role exist.
|
||||
+ dbService = if dbIsMySQL then "mysql.service" else "postgresql.target";
|
||||
+
|
||||
+ syncserver = cfg.package.override { dbBackend = cfg.database.type; };
|
||||
|
||||
format = pkgs.formats.toml { };
|
||||
settings = {
|
||||
@@ -22,7 +36,7 @@ let
|
||||
database_url = dbURL;
|
||||
};
|
||||
tokenserver = {
|
||||
- node_type = "mysql";
|
||||
+ node_type = if dbIsMySQL then "mysql" else "postgres";
|
||||
database_url = dbURL;
|
||||
fxa_email_domain = "api.accounts.firefox.com";
|
||||
fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1";
|
||||
@@ -41,7 +55,8 @@ let
|
||||
};
|
||||
};
|
||||
configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
|
||||
- setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
|
||||
+
|
||||
+ mysqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
|
||||
set -euo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
@@ -79,6 +94,47 @@ let
|
||||
echo "Single-node setup failed"
|
||||
exit 1
|
||||
'';
|
||||
+
|
||||
+ postgresqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
|
||||
+ set -euo pipefail
|
||||
+ shopt -s inherit_errexit
|
||||
+
|
||||
+ schema_configured() {
|
||||
+ psql -d ${cfg.database.name} -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')" | grep -q t
|
||||
+ }
|
||||
+
|
||||
+ update_config() {
|
||||
+ psql -d ${cfg.database.name} <<'EOF'
|
||||
+ BEGIN;
|
||||
+
|
||||
+ INSERT INTO services (id, service, pattern)
|
||||
+ VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
|
||||
+ ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{node}/1.5/{uid}';
|
||||
+ INSERT INTO nodes (id, service, node, available, current_load,
|
||||
+ capacity, downed, backoff)
|
||||
+ VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
|
||||
+ 0, ${toString cfg.singleNode.capacity}, 0, 0)
|
||||
+ ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity};
|
||||
+
|
||||
+ COMMIT;
|
||||
+ EOF
|
||||
+ }
|
||||
+
|
||||
+
|
||||
+ for (( try = 0; try < 60; try++ )); do
|
||||
+ if ! schema_configured; then
|
||||
+ sleep 2
|
||||
+ else
|
||||
+ update_config
|
||||
+ exit 0
|
||||
+ fi
|
||||
+ done
|
||||
+
|
||||
+ echo "Single-node setup failed"
|
||||
+ exit 1
|
||||
+ '';
|
||||
+
|
||||
+ setupScript = if dbIsMySQL then mysqlSetupScript else postgresqlSetupScript;
|
||||
in
|
||||
|
||||
{
|
||||
@@ -88,25 +144,26 @@ in
|
||||
the Firefox Sync storage service.
|
||||
|
||||
Out of the box this will not be very useful unless you also configure at least
|
||||
- one service and one nodes by inserting them into the mysql database manually, e.g.
|
||||
- by running
|
||||
-
|
||||
- ```
|
||||
- INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}');
|
||||
- INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
|
||||
- `capacity`, `downed`, `backoff`)
|
||||
- VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0');
|
||||
- ```
|
||||
+ one service and one nodes by inserting them into the database manually, e.g.
|
||||
+ by running the equivalent SQL for your database backend.
|
||||
|
||||
{option}`${opt.singleNode.enable}` does this automatically when enabled
|
||||
'';
|
||||
|
||||
package = lib.mkPackageOption pkgs "syncstorage-rs" { };
|
||||
|
||||
+ database.type = lib.mkOption {
|
||||
+ type = lib.types.enum [
|
||||
+ "mysql"
|
||||
+ "postgresql"
|
||||
+ ];
|
||||
+ default = "mysql";
|
||||
+ description = ''
|
||||
+ Which database backend to use for storage.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
database.name = lib.mkOption {
|
||||
- # the mysql module does not allow `-quoting without resorting to shell
|
||||
- # escaping, so we restrict db names for forward compaitiblity should this
|
||||
- # behavior ever change.
|
||||
type = lib.types.strMatching "[a-z_][a-z0-9_]*";
|
||||
default = defaultDatabase;
|
||||
description = ''
|
||||
@@ -117,9 +174,15 @@ in
|
||||
|
||||
database.user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
- default = defaultUser;
|
||||
+ default = if dbIsPostgreSQL then defaultDatabase else defaultUser;
|
||||
+ defaultText = lib.literalExpression ''
|
||||
+ if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}"
|
||||
+ '';
|
||||
description = ''
|
||||
- Username for database connections.
|
||||
+ Username for database connections. When using PostgreSQL with
|
||||
+ `createLocally`, this defaults to the database name so that
|
||||
+ `ensureDBOwnership` works (it requires user and database names
|
||||
+ to match).
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -137,7 +200,8 @@ in
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to create database and user on the local machine if they do not exist.
|
||||
- This includes enabling unix domain socket authentication for the configured user.
|
||||
+ This includes enabling the configured database service and setting up
|
||||
+ authentication for the configured user.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -237,7 +301,7 @@ in
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
- services.mysql = lib.mkIf cfg.database.createLocally {
|
||||
+ services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
@@ -250,16 +314,27 @@ in
|
||||
];
|
||||
};
|
||||
|
||||
+ services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) {
|
||||
+ enable = true;
|
||||
+ ensureDatabases = [ cfg.database.name ];
|
||||
+ ensureUsers = [
|
||||
+ {
|
||||
+ name = cfg.database.user;
|
||||
+ ensureDBOwnership = true;
|
||||
+ }
|
||||
+ ];
|
||||
+ };
|
||||
+
|
||||
systemd.services.firefox-syncserver = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
- requires = lib.mkIf dbIsLocal [ "mysql.service" ];
|
||||
- after = lib.mkIf dbIsLocal [ "mysql.service" ];
|
||||
+ requires = lib.mkIf dbIsLocal [ dbService ];
|
||||
+ after = lib.mkIf dbIsLocal [ dbService ];
|
||||
restartTriggers = lib.optional cfg.singleNode.enable setupScript;
|
||||
environment.RUST_LOG = cfg.logLevel;
|
||||
serviceConfig = {
|
||||
- User = defaultUser;
|
||||
- Group = defaultUser;
|
||||
- ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}";
|
||||
+ User = cfg.database.user;
|
||||
+ Group = cfg.database.user;
|
||||
+ ExecStart = "${syncserver}/bin/syncserver --config ${configFile}";
|
||||
EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
|
||||
|
||||
# hardening
|
||||
@@ -303,10 +378,19 @@ in
|
||||
|
||||
systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
|
||||
wantedBy = [ "firefox-syncserver.service" ];
|
||||
- requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
|
||||
- after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
|
||||
- path = [ config.services.mysql.package ];
|
||||
- serviceConfig.ExecStart = [ "${setupScript}" ];
|
||||
+ requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
|
||||
+ after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
|
||||
+ path =
|
||||
+ if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ];
|
||||
+ serviceConfig = {
|
||||
+ ExecStart = [ "${setupScript}" ];
|
||||
+ }
|
||||
+ // lib.optionalAttrs dbIsPostgreSQL {
|
||||
+ # PostgreSQL peer authentication requires the system user to match the
|
||||
+ # database user. Run as the superuser so we can access all databases.
|
||||
+ User = "postgres";
|
||||
+ Group = "postgres";
|
||||
+ };
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx {
|
||||
diff --git a/pkgs/by-name/sy/syncstorage-rs/package.nix b/pkgs/by-name/sy/syncstorage-rs/package.nix
|
||||
index 39b2b53ab03c..944ed72525af 100644
|
||||
--- a/pkgs/by-name/sy/syncstorage-rs/package.nix
|
||||
+++ b/pkgs/by-name/sy/syncstorage-rs/package.nix
|
||||
@@ -1,14 +1,18 @@
|
||||
{
|
||||
fetchFromGitHub,
|
||||
+ fetchurl,
|
||||
rustPlatform,
|
||||
pkg-config,
|
||||
python3,
|
||||
cmake,
|
||||
libmysqlclient,
|
||||
+ libpq,
|
||||
+ openssl,
|
||||
makeBinaryWrapper,
|
||||
lib,
|
||||
nix-update-script,
|
||||
nixosTests,
|
||||
+ dbBackend ? "mysql",
|
||||
}:
|
||||
|
||||
let
|
||||
@@ -19,17 +23,23 @@ let
|
||||
p.tokenlib
|
||||
p.cryptography
|
||||
]);
|
||||
+ # utoipa-swagger-ui downloads Swagger UI assets at build time.
|
||||
+ # Prefetch the archive for sandboxed builds.
|
||||
+ swaggerUi = fetchurl {
|
||||
+ url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip";
|
||||
+ hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw=";
|
||||
+ };
|
||||
in
|
||||
|
||||
-rustPlatform.buildRustPackage rec {
|
||||
+rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "syncstorage-rs";
|
||||
- version = "0.21.1-unstable-2026-01-26";
|
||||
+ version = "0.21.1-unstable-2026-02-24";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "mozilla-services";
|
||||
repo = "syncstorage-rs";
|
||||
- rev = "11659d98f9c69948a0aab353437ce2036c388711";
|
||||
- hash = "sha256-G37QvxTNh/C3gmKG0UYHI6QBr0F+KLGRNI/Sx33uOsc=";
|
||||
+ rev = "50a739b58dc9ec81995f86e71d992aa14ccc450e";
|
||||
+ hash = "sha256-idq0RGdwoV6GVuq36IVVVCFbyMTe8i/EpVWE59D/dhM=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
@@ -39,16 +49,35 @@ rustPlatform.buildRustPackage rec {
|
||||
python3
|
||||
];
|
||||
|
||||
- buildInputs = [
|
||||
- libmysqlclient
|
||||
- ];
|
||||
+ buildInputs =
|
||||
+ lib.optional (dbBackend == "mysql") libmysqlclient
|
||||
+ ++ lib.optionals (dbBackend == "postgresql") [
|
||||
+ libpq
|
||||
+ openssl
|
||||
+ ];
|
||||
+
|
||||
+ buildNoDefaultFeatures = true;
|
||||
+ # The syncserver "postgres" feature only enables syncstorage-db/postgres.
|
||||
+ # tokenserver-db/postgres must be enabled separately so the tokenserver
|
||||
+ # can also connect to PostgreSQL (it dispatches on the URL scheme at runtime).
|
||||
+ buildFeatures =
|
||||
+ let
|
||||
+ cargoFeature = if dbBackend == "postgresql" then "postgres" else dbBackend;
|
||||
+ in
|
||||
+ [
|
||||
+ cargoFeature
|
||||
+ "tokenserver-db/${cargoFeature}"
|
||||
+ "py_verifier"
|
||||
+ ];
|
||||
+
|
||||
+ SWAGGER_UI_DOWNLOAD_URL = "file://${swaggerUi}";
|
||||
|
||||
preFixup = ''
|
||||
wrapProgram $out/bin/syncserver \
|
||||
--prefix PATH : ${lib.makeBinPath [ pyFxADeps ]}
|
||||
'';
|
||||
|
||||
- cargoHash = "sha256-9Dcf5mDyK/XjsKTlCPXTHoBkIq+FFPDg1zfK24Y9nHQ=";
|
||||
+ cargoHash = "sha256-80EztkSX+SnmqsRWIXbChUB8AeV1Tp9WXoWNbDY8rUE=";
|
||||
|
||||
# almost all tests need a DB to test against
|
||||
doCheck = false;
|
||||
@@ -60,10 +89,10 @@ rustPlatform.buildRustPackage rec {
|
||||
meta = {
|
||||
description = "Mozilla Sync Storage built with Rust";
|
||||
homepage = "https://github.com/mozilla-services/syncstorage-rs";
|
||||
- changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${version}";
|
||||
+ changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${finalAttrs.version}";
|
||||
license = lib.licenses.mpl20;
|
||||
maintainers = [ ];
|
||||
platforms = lib.platforms.linux;
|
||||
mainProgram = "syncserver";
|
||||
};
|
||||
-}
|
||||
+})
|
||||
--
|
||||
2.53.0
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
From f0582558f0a8b0ef543b3251c4a07afab89fde63 Mon Sep 17 00:00:00 2001
|
||||
From: Simon Gardling <titaniumtown@proton.me>
|
||||
Date: Fri, 17 Apr 2026 19:37:11 -0400
|
||||
Subject: [PATCH] nixos/jellyfin: add declarative network.xml options
|
||||
|
||||
Adds services.jellyfin.network.* (baseUrl, ports, IPv4/6, LAN subnets,
|
||||
known proxies, remote IP filter, etc.) and services.jellyfin.forceNetworkConfig,
|
||||
mirroring the existing hardwareAcceleration / forceEncodingConfig pattern.
|
||||
|
||||
Motivation: running Jellyfin behind a reverse proxy requires configuring
|
||||
KnownProxies (so the real client IP is extracted from X-Forwarded-For)
|
||||
and LocalNetworkSubnets (so LAN clients are correctly classified and not
|
||||
subject to RemoteClientBitrateLimit). These settings previously had no
|
||||
declarative option -- they could only be set via the web dashboard or
|
||||
by hand-editing network.xml, with no guarantee they would survive a
|
||||
reinstall or be consistent across deployments.
|
||||
|
||||
Implementation:
|
||||
- Adds a networkXmlText template alongside the existing encodingXmlText.
|
||||
- Factors the force-vs-soft install logic out of preStart into a
|
||||
small 'manage_config_xml' shell helper; encoding.xml and network.xml
|
||||
now share the same install/backup semantics.
|
||||
- Extends the VM test with a machineWithNetworkConfig node and a
|
||||
subtest that verifies the declared values land in network.xml,
|
||||
Jellyfin parses them at startup, and the backup-on-overwrite path
|
||||
works (same shape as the existing 'Force encoding config' subtest).
|
||||
---
|
||||
nixos/modules/services/misc/jellyfin.nix | 303 ++++++++++++++++++++---
|
||||
nixos/tests/jellyfin.nix | 50 ++++
|
||||
2 files changed, 317 insertions(+), 36 deletions(-)
|
||||
|
||||
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
|
||||
index 5c08fc478e45..387da907c652 100644
|
||||
--- a/nixos/modules/services/misc/jellyfin.nix
|
||||
+++ b/nixos/modules/services/misc/jellyfin.nix
|
||||
@@ -26,8 +26,10 @@ let
|
||||
bool
|
||||
enum
|
||||
ints
|
||||
+ listOf
|
||||
nullOr
|
||||
path
|
||||
+ port
|
||||
str
|
||||
submodule
|
||||
;
|
||||
@@ -68,6 +70,41 @@ let
|
||||
</EncodingOptions>
|
||||
'';
|
||||
encodingXmlFile = pkgs.writeText "encoding.xml" encodingXmlText;
|
||||
+ stringListToXml =
|
||||
+ tag: items:
|
||||
+ if items == [ ] then
|
||||
+ "<${tag} />"
|
||||
+ else
|
||||
+ "<${tag}>\n ${
|
||||
+ concatMapStringsSep "\n " (item: "<string>${escapeXML item}</string>") items
|
||||
+ }\n </${tag}>";
|
||||
+ networkXmlText = ''
|
||||
+ <?xml version="1.0" encoding="utf-8"?>
|
||||
+ <NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
+ <BaseUrl>${escapeXML cfg.network.baseUrl}</BaseUrl>
|
||||
+ <EnableHttps>${boolToString cfg.network.enableHttps}</EnableHttps>
|
||||
+ <RequireHttps>${boolToString cfg.network.requireHttps}</RequireHttps>
|
||||
+ <InternalHttpPort>${toString cfg.network.internalHttpPort}</InternalHttpPort>
|
||||
+ <InternalHttpsPort>${toString cfg.network.internalHttpsPort}</InternalHttpsPort>
|
||||
+ <PublicHttpPort>${toString cfg.network.publicHttpPort}</PublicHttpPort>
|
||||
+ <PublicHttpsPort>${toString cfg.network.publicHttpsPort}</PublicHttpsPort>
|
||||
+ <AutoDiscovery>${boolToString cfg.network.autoDiscovery}</AutoDiscovery>
|
||||
+ <EnableUPnP>${boolToString cfg.network.enableUPnP}</EnableUPnP>
|
||||
+ <EnableIPv4>${boolToString cfg.network.enableIPv4}</EnableIPv4>
|
||||
+ <EnableIPv6>${boolToString cfg.network.enableIPv6}</EnableIPv6>
|
||||
+ <EnableRemoteAccess>${boolToString cfg.network.enableRemoteAccess}</EnableRemoteAccess>
|
||||
+ ${stringListToXml "LocalNetworkSubnets" cfg.network.localNetworkSubnets}
|
||||
+ ${stringListToXml "LocalNetworkAddresses" cfg.network.localNetworkAddresses}
|
||||
+ ${stringListToXml "KnownProxies" cfg.network.knownProxies}
|
||||
+ <IgnoreVirtualInterfaces>${boolToString cfg.network.ignoreVirtualInterfaces}</IgnoreVirtualInterfaces>
|
||||
+ ${stringListToXml "VirtualInterfaceNames" cfg.network.virtualInterfaceNames}
|
||||
+ <EnablePublishedServerUriByRequest>${boolToString cfg.network.enablePublishedServerUriByRequest}</EnablePublishedServerUriByRequest>
|
||||
+ ${stringListToXml "PublishedServerUriBySubnet" cfg.network.publishedServerUriBySubnet}
|
||||
+ ${stringListToXml "RemoteIPFilter" cfg.network.remoteIPFilter}
|
||||
+ <IsRemoteIPFilterBlacklist>${boolToString cfg.network.isRemoteIPFilterBlacklist}</IsRemoteIPFilterBlacklist>
|
||||
+ </NetworkConfiguration>
|
||||
+ '';
|
||||
+ networkXmlFile = pkgs.writeText "network.xml" networkXmlText;
|
||||
codecListToType =
|
||||
desc: list:
|
||||
submodule {
|
||||
@@ -205,6 +242,196 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
+ network = {
|
||||
+ baseUrl = mkOption {
|
||||
+ type = str;
|
||||
+ default = "";
|
||||
+ example = "/jellyfin";
|
||||
+ description = ''
|
||||
+ Prefix added to Jellyfin's internal URLs when it sits behind a reverse proxy at a sub-path.
|
||||
+ Leave empty when Jellyfin is served at the root of its host.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ enableHttps = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ Serve HTTPS directly from Jellyfin. Usually unnecessary when terminating TLS in a reverse proxy.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ requireHttps = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ Redirect plaintext HTTP requests to HTTPS. Only meaningful when {option}`enableHttps` is true.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ internalHttpPort = mkOption {
|
||||
+ type = port;
|
||||
+ default = 8096;
|
||||
+ description = "TCP port Jellyfin binds for HTTP.";
|
||||
+ };
|
||||
+
|
||||
+ internalHttpsPort = mkOption {
|
||||
+ type = port;
|
||||
+ default = 8920;
|
||||
+ description = "TCP port Jellyfin binds for HTTPS. Only used when {option}`enableHttps` is true.";
|
||||
+ };
|
||||
+
|
||||
+ publicHttpPort = mkOption {
|
||||
+ type = port;
|
||||
+ default = 8096;
|
||||
+ description = "HTTP port Jellyfin advertises in server discovery responses and published URIs.";
|
||||
+ };
|
||||
+
|
||||
+ publicHttpsPort = mkOption {
|
||||
+ type = port;
|
||||
+ default = 8920;
|
||||
+ description = "HTTPS port Jellyfin advertises in server discovery responses and published URIs.";
|
||||
+ };
|
||||
+
|
||||
+ autoDiscovery = mkOption {
|
||||
+ type = bool;
|
||||
+ default = true;
|
||||
+ description = "Respond to LAN client auto-discovery broadcasts (UDP 7359).";
|
||||
+ };
|
||||
+
|
||||
+ enableUPnP = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = "Attempt to open the public ports on the router via UPnP.";
|
||||
+ };
|
||||
+
|
||||
+ enableIPv4 = mkOption {
|
||||
+ type = bool;
|
||||
+ default = true;
|
||||
+ description = "Listen on IPv4.";
|
||||
+ };
|
||||
+
|
||||
+ enableIPv6 = mkOption {
|
||||
+ type = bool;
|
||||
+ default = true;
|
||||
+ description = "Listen on IPv6.";
|
||||
+ };
|
||||
+
|
||||
+ enableRemoteAccess = mkOption {
|
||||
+ type = bool;
|
||||
+ default = true;
|
||||
+ description = ''
|
||||
+ Allow connections from clients outside the subnets listed in {option}`localNetworkSubnets`.
|
||||
+ When false, Jellyfin rejects non-local requests regardless of reverse proxy configuration.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ localNetworkSubnets = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ ];
|
||||
+ example = [
|
||||
+ "192.168.1.0/24"
|
||||
+ "10.0.0.0/8"
|
||||
+ ];
|
||||
+ description = ''
|
||||
+ CIDR ranges (or bare IPs) that Jellyfin classifies as the local network.
|
||||
+ Clients originating from these ranges -- as seen after {option}`knownProxies` X-Forwarded-For
|
||||
+ unwrapping -- are not subject to {option}`services.jellyfin` remote-client bitrate limits.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ localNetworkAddresses = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ ];
|
||||
+ example = [ "192.168.1.50" ];
|
||||
+ description = ''
|
||||
+ Specific interface addresses Jellyfin binds to. Leave empty to bind all interfaces.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ knownProxies = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ ];
|
||||
+ example = [ "127.0.0.1" ];
|
||||
+ description = ''
|
||||
+ Addresses of reverse proxies trusted to forward the real client IP via `X-Forwarded-For`.
|
||||
+ Without this, Jellyfin sees the proxy's address for every request and cannot apply
|
||||
+ {option}`localNetworkSubnets` classification to the true client.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ ignoreVirtualInterfaces = mkOption {
|
||||
+ type = bool;
|
||||
+ default = true;
|
||||
+ description = "Skip virtual network interfaces (matching {option}`virtualInterfaceNames`) during auto-bind.";
|
||||
+ };
|
||||
+
|
||||
+ virtualInterfaceNames = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ "veth" ];
|
||||
+ description = "Interface name prefixes treated as virtual when {option}`ignoreVirtualInterfaces` is true.";
|
||||
+ };
|
||||
+
|
||||
+ enablePublishedServerUriByRequest = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ Derive the server's public URI from the incoming request's Host header instead of any
|
||||
+ configured {option}`publishedServerUriBySubnet` entry.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ publishedServerUriBySubnet = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ ];
|
||||
+ example = [ "192.168.1.0/24=http://jellyfin.lan:8096" ];
|
||||
+ description = ''
|
||||
+ Per-subnet overrides for the URI Jellyfin advertises to clients, in `subnet=uri` form.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ remoteIPFilter = mkOption {
|
||||
+ type = listOf str;
|
||||
+ default = [ ];
|
||||
+ example = [ "203.0.113.0/24" ];
|
||||
+ description = ''
|
||||
+ IPs or CIDRs used as the allow- or denylist for remote access.
|
||||
+ Behaviour is controlled by {option}`isRemoteIPFilterBlacklist`.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ isRemoteIPFilterBlacklist = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ When true, {option}`remoteIPFilter` is a denylist; when false, it is an allowlist
|
||||
+ (and an empty list allows all remote addresses).
|
||||
+ '';
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ forceNetworkConfig = mkOption {
|
||||
+ type = bool;
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ Whether to overwrite Jellyfin's `network.xml` configuration file on each service start.
|
||||
+
|
||||
+ When enabled, the network configuration specified in {option}`services.jellyfin.network`
|
||||
+ is applied on every service restart. A backup of the existing `network.xml` will be
|
||||
+ created at `network.xml.backup-$timestamp`.
|
||||
+
|
||||
+ ::: {.warning}
|
||||
+ Enabling this option means that any changes made to networking settings through
|
||||
+ Jellyfin's web dashboard will be lost on the next service restart. The NixOS configuration
|
||||
+ becomes the single source of truth for network settings.
|
||||
+ :::
|
||||
+
|
||||
+ When disabled (the default), the network configuration is only written if no `network.xml`
|
||||
+ exists yet. This allows settings to be changed through Jellyfin's web dashboard and persist
|
||||
+ across restarts, but means the NixOS configuration options will be ignored after the initial setup.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
transcoding = {
|
||||
maxConcurrentStreams = mkOption {
|
||||
type = nullOr ints.positive;
|
||||
@@ -384,46 +611,50 @@ in
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
- preStart = mkIf cfg.hardwareAcceleration.enable (
|
||||
- ''
|
||||
- configDir=${escapeShellArg cfg.configDir}
|
||||
- encodingXml="$configDir/encoding.xml"
|
||||
- ''
|
||||
- + (
|
||||
- if cfg.forceEncodingConfig then
|
||||
- ''
|
||||
- if [[ -e $encodingXml ]]; then
|
||||
+ preStart =
|
||||
+ let
|
||||
+ # manage_config_xml <source> <destination> <force> <description>
|
||||
+ #
|
||||
+ # Installs a NixOS-declared XML config at <destination>, preserving
|
||||
+ # any existing file as a timestamped backup when <force> is true.
|
||||
+ # With <force>=false, leaves existing files untouched and warns if
|
||||
+ # the on-disk content differs from the declared content.
|
||||
+ helper = ''
|
||||
+ manage_config_xml() {
|
||||
+ local src="$1" dest="$2" force="$3" desc="$4"
|
||||
+ if [[ -e "$dest" ]]; then
|
||||
# this intentionally removes trailing newlines
|
||||
- currentText="$(<"$encodingXml")"
|
||||
- configuredText="$(<${encodingXmlFile})"
|
||||
- if [[ $currentText == "$configuredText" ]]; then
|
||||
- # don't need to do anything
|
||||
- exit 0
|
||||
- else
|
||||
- encodingXmlBackup="$configDir/encoding.xml.backup-$(date -u +"%FT%H_%M_%SZ")"
|
||||
- mv --update=none-fail -T "$encodingXml" "$encodingXmlBackup"
|
||||
+ local currentText configuredText
|
||||
+ currentText="$(<"$dest")"
|
||||
+ configuredText="$(<"$src")"
|
||||
+ if [[ "$currentText" == "$configuredText" ]]; then
|
||||
+ return 0
|
||||
fi
|
||||
- fi
|
||||
- cp --update=none-fail -T ${encodingXmlFile} "$encodingXml"
|
||||
- chmod u+w "$encodingXml"
|
||||
- ''
|
||||
- else
|
||||
- ''
|
||||
- if [[ -e $encodingXml ]]; then
|
||||
- # this intentionally removes trailing newlines
|
||||
- currentText="$(<"$encodingXml")"
|
||||
- configuredText="$(<${encodingXmlFile})"
|
||||
- if [[ $currentText != "$configuredText" ]]; then
|
||||
- echo "WARN: $encodingXml already exists and is different from the configured settings. transcoding options NOT applied." >&2
|
||||
- echo "WARN: Set config.services.jellyfin.forceEncodingConfig = true to override." >&2
|
||||
+ if [[ "$force" == true ]]; then
|
||||
+ local backup
|
||||
+ backup="$dest.backup-$(date -u +"%FT%H_%M_%SZ")"
|
||||
+ mv --update=none-fail -T "$dest" "$backup"
|
||||
+ else
|
||||
+ echo "WARN: $dest already exists and is different from the configured settings. $desc options NOT applied." >&2
|
||||
+ echo "WARN: Set the corresponding force*Config option to override." >&2
|
||||
+ return 0
|
||||
fi
|
||||
- else
|
||||
- cp --update=none-fail -T ${encodingXmlFile} "$encodingXml"
|
||||
- chmod u+w "$encodingXml"
|
||||
fi
|
||||
- ''
|
||||
- )
|
||||
- );
|
||||
+ cp --update=none-fail -T "$src" "$dest"
|
||||
+ chmod u+w "$dest"
|
||||
+ }
|
||||
+ configDir=${escapeShellArg cfg.configDir}
|
||||
+ '';
|
||||
+ in
|
||||
+ (
|
||||
+ helper
|
||||
+ + optionalString cfg.hardwareAcceleration.enable ''
|
||||
+ manage_config_xml ${encodingXmlFile} "$configDir/encoding.xml" ${boolToString cfg.forceEncodingConfig} transcoding
|
||||
+ ''
|
||||
+ + ''
|
||||
+ manage_config_xml ${networkXmlFile} "$configDir/network.xml" ${boolToString cfg.forceNetworkConfig} network
|
||||
+ ''
|
||||
+ );
|
||||
|
||||
# This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service
|
||||
# Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option
|
||||
diff --git a/nixos/tests/jellyfin.nix b/nixos/tests/jellyfin.nix
|
||||
index 4896c13d4eca..0c9191960f78 100644
|
||||
--- a/nixos/tests/jellyfin.nix
|
||||
+++ b/nixos/tests/jellyfin.nix
|
||||
@@ -63,6 +63,26 @@
|
||||
environment.systemPackages = with pkgs; [ ffmpeg ];
|
||||
virtualisation.diskSize = 3 * 1024;
|
||||
};
|
||||
+
|
||||
+ machineWithNetworkConfig = {
|
||||
+ services.jellyfin = {
|
||||
+ enable = true;
|
||||
+ forceNetworkConfig = true;
|
||||
+ network = {
|
||||
+ localNetworkSubnets = [
|
||||
+ "192.168.1.0/24"
|
||||
+ "10.0.0.0/8"
|
||||
+ ];
|
||||
+ knownProxies = [ "127.0.0.1" ];
|
||||
+ enableUPnP = false;
|
||||
+ enableIPv6 = false;
|
||||
+ remoteIPFilter = [ "203.0.113.5" ];
|
||||
+ isRemoteIPFilterBlacklist = true;
|
||||
+ };
|
||||
+ };
|
||||
+ environment.systemPackages = with pkgs; [ ffmpeg ];
|
||||
+ virtualisation.diskSize = 3 * 1024;
|
||||
+ };
|
||||
};
|
||||
|
||||
# Documentation of the Jellyfin API: https://api.jellyfin.org/
|
||||
@@ -122,6 +142,36 @@
|
||||
# Verify the new encoding.xml does not have the marker (was overwritten)
|
||||
machineWithForceConfig.fail("grep -q 'MARKER' /var/lib/jellyfin/config/encoding.xml")
|
||||
|
||||
+ # Test forceNetworkConfig and network.xml generation
|
||||
+ with subtest("Force network config writes declared values and backs up on overwrite"):
|
||||
+ wait_for_jellyfin(machineWithNetworkConfig)
|
||||
+
|
||||
+ # Verify network.xml exists and contains the declared values
|
||||
+ machineWithNetworkConfig.succeed("test -f /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<string>192.168.1.0/24</string>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<string>10.0.0.0/8</string>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<string>127.0.0.1</string>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<string>203.0.113.5</string>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<IsRemoteIPFilterBlacklist>true</IsRemoteIPFilterBlacklist>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<EnableIPv6>false</EnableIPv6>' /var/lib/jellyfin/config/network.xml")
|
||||
+ machineWithNetworkConfig.succeed("grep -F '<EnableUPnP>false</EnableUPnP>' /var/lib/jellyfin/config/network.xml")
|
||||
+
|
||||
+ # Stop service before modifying config
|
||||
+ machineWithNetworkConfig.succeed("systemctl stop jellyfin.service")
|
||||
+
|
||||
+ # Plant a marker so we can prove the backup-and-overwrite path runs
|
||||
+ machineWithNetworkConfig.succeed("echo '<!-- NETMARKER -->' > /var/lib/jellyfin/config/network.xml")
|
||||
+
|
||||
+ # Restart the service to trigger the backup
|
||||
+ machineWithNetworkConfig.succeed("systemctl restart jellyfin.service")
|
||||
+ wait_for_jellyfin(machineWithNetworkConfig)
|
||||
+
|
||||
+ # Verify the marked content was preserved as a timestamped backup
|
||||
+ machineWithNetworkConfig.succeed("grep -q 'NETMARKER' /var/lib/jellyfin/config/network.xml.backup-*")
|
||||
+
|
||||
+ # Verify the new network.xml does not have the marker (was overwritten)
|
||||
+ machineWithNetworkConfig.fail("grep -q 'NETMARKER' /var/lib/jellyfin/config/network.xml")
|
||||
+
|
||||
auth_header = 'MediaBrowser Client="NixOS Integration Tests", DeviceId="1337", Device="Apple II", Version="20.09"'
|
||||
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
Reference in New Issue
Block a user