All jellyfin traffic should actually go through caddy. This port being open caused a lot of confusion for me. As I was getting traffic from typo'd domain names, such as `jellfin.gardling.com`, which made NO SENSE! But since it was going directly via port 8096, it skipped caddy entirely so the traffic went through.
146 lines
4.3 KiB
Nix
146 lines
4.3 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
baseServiceConfigs = import ../service-configs.nix;
|
|
testServiceConfigs = lib.recursiveUpdate baseServiceConfigs {
|
|
zpool_ssds = "";
|
|
https.domain = "test.local";
|
|
jellyfin = {
|
|
dataDir = "/var/lib/jellyfin";
|
|
cacheDir = "/var/cache/jellyfin";
|
|
};
|
|
};
|
|
|
|
testLib = lib.extend (
|
|
final: prev: {
|
|
serviceMountWithZpool =
|
|
serviceName: zpool: dirs:
|
|
{ ... }:
|
|
{ };
|
|
serviceFilePerms = serviceName: tmpfilesRules: { ... }: { };
|
|
optimizePackage = pkg: pkg; # No-op for testing
|
|
}
|
|
);
|
|
|
|
jellyfinModule =
|
|
{ config, pkgs, ... }:
|
|
{
|
|
imports = [
|
|
(import ../services/jellyfin.nix {
|
|
inherit config pkgs;
|
|
lib = testLib;
|
|
service_configs = testServiceConfigs;
|
|
})
|
|
];
|
|
};
|
|
in
|
|
pkgs.testers.runNixOSTest {
|
|
name = "fail2ban-jellyfin";
|
|
|
|
nodes = {
|
|
server =
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
{
|
|
imports = [
|
|
../modules/security.nix
|
|
jellyfinModule
|
|
];
|
|
|
|
# needed for testing
|
|
services.jellyfin.openFirewall = true;
|
|
|
|
# Create the media group
|
|
users.groups.media = { };
|
|
|
|
# Disable ZFS mount dependency
|
|
systemd.services."jellyfin-mounts".enable = lib.mkForce false;
|
|
systemd.services.jellyfin = {
|
|
wants = lib.mkForce [ ];
|
|
after = lib.mkForce [ ];
|
|
requires = lib.mkForce [ ];
|
|
};
|
|
|
|
# Override for faster testing and correct port
|
|
services.fail2ban.jails.jellyfin.settings = {
|
|
maxretry = lib.mkForce 3;
|
|
# In test, we connect directly to Jellyfin port, not via Caddy
|
|
port = lib.mkForce "8096";
|
|
};
|
|
|
|
# Create log directory and placeholder log file for fail2ban
|
|
# Jellyfin logs to files, not systemd journal
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/jellyfin/log 0755 jellyfin jellyfin"
|
|
"f /var/lib/jellyfin/log/log_placeholder.log 0644 jellyfin jellyfin"
|
|
];
|
|
|
|
# Make fail2ban start after Jellyfin
|
|
systemd.services.fail2ban = {
|
|
wants = [ "jellyfin.service" ];
|
|
after = [ "jellyfin.service" ];
|
|
};
|
|
|
|
# Give jellyfin more disk space and memory
|
|
virtualisation.diskSize = 3 * 1024;
|
|
virtualisation.memorySize = 2 * 1024;
|
|
};
|
|
|
|
client = {
|
|
environment.systemPackages = [ pkgs.curl ];
|
|
};
|
|
};
|
|
|
|
testScript = ''
|
|
import time
|
|
import re
|
|
|
|
start_all()
|
|
server.wait_for_unit("jellyfin.service")
|
|
server.wait_for_unit("fail2ban.service")
|
|
server.wait_for_open_port(8096)
|
|
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
|
|
time.sleep(2)
|
|
|
|
# Wait for Jellyfin to create real log files and reload fail2ban
|
|
server.wait_until_succeeds("ls /var/lib/jellyfin/log/log_2*.log", timeout=30)
|
|
server.succeed("fail2ban-client reload jellyfin")
|
|
|
|
with subtest("Verify jellyfin jail is active"):
|
|
status = server.succeed("fail2ban-client status")
|
|
assert "jellyfin" in status, f"jellyfin jail not found in: {status}"
|
|
|
|
with subtest("Generate failed login attempts"):
|
|
# Use -4 to force IPv4 for consistent IP tracking
|
|
for i in range(4):
|
|
client.execute("""
|
|
curl -4 -s -X POST http://server:8096/Users/authenticatebyname \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'X-Emby-Authorization: MediaBrowser Client="test", Device="test", DeviceId="test", Version="1.0"' \
|
|
-d '{"Username":"baduser","Pw":"badpass"}' || true
|
|
""")
|
|
time.sleep(0.5)
|
|
|
|
with subtest("Verify IP is banned"):
|
|
time.sleep(3)
|
|
status = server.succeed("fail2ban-client status jellyfin")
|
|
print(f"jellyfin jail status: {status}")
|
|
# Check that at least 1 IP is banned
|
|
match = re.search(r"Currently banned:\s*(\d+)", status)
|
|
assert match and int(match.group(1)) >= 1, f"Expected at least 1 banned IP, got: {status}"
|
|
|
|
with subtest("Verify banned client cannot connect"):
|
|
# Use -4 to test with same IP that was banned
|
|
exit_code = client.execute("curl -4 -s --max-time 3 http://server:8096/ 2>&1")[0]
|
|
assert exit_code != 0, "Connection should be blocked"
|
|
'';
|
|
}
|