Compare commits

...

532 Commits

Author SHA1 Message Date
9f949f13d1 minecraft: update mods + add modernfix + debugify 2026-02-28 02:30:19 -05:00
59080fe1b3 fmt 2026-02-28 02:25:38 -05:00
12fca8840d update 2026-02-28 01:57:42 -05:00
49f06fc26c arr-init: extract to standalone flake repo 2026-02-27 15:39:19 -05:00
2c0811cfe9 minecraft: make more responsive 2026-02-27 00:04:07 -05:00
9692fe5f08 update 2026-02-25 19:01:56 -05:00
c142b5d045 ntfy-alerts: suppress notifications for sanoid 2026-02-25 02:10:37 -05:00
16c84fdcb6 zfs: fix sanoid dataset name for jellyfin cache 2026-02-24 21:21:24 -05:00
196f06e41f flake: expose tests as checks output 2026-02-24 14:51:11 -05:00
8013435d99 ntfy-alerts: init 2026-02-24 14:44:00 -05:00
28e3090c72 matrix: update 2026-02-24 13:05:00 -05:00
a22c5b30fe update 2026-02-24 13:04:55 -05:00
c2908f594c matrix: update 2026-02-23 15:24:31 -05:00
9df3f3cae9 update 2026-02-23 15:13:47 -05:00
ea75dad5ba matrix: update 2026-02-21 22:47:33 -05:00
1e25d86d44 update 2026-02-21 22:19:36 -05:00
23475927a1 qbt: increase ConnectionSpeed 2026-02-20 15:27:56 -05:00
fe4040bf3b matrix: update 2026-02-20 15:25:44 -05:00
d91b651152 formating 2026-02-20 15:19:46 -05:00
0a3f93c98d qbt: fix permissions 2026-02-20 14:12:13 -05:00
304ad7f308 qbt: tweak 2026-02-20 11:05:53 -05:00
4fe33b9b32 update 2026-02-19 23:15:26 -05:00
0a0c14993d qbt: Coalesce Read Write 2026-02-19 23:14:27 -05:00
155ebbafcd qbittorrent: enable queueing and AutoTMM 2026-02-19 23:12:18 -05:00
2fed80cdb2 firewall: trust wg-br interface 2026-02-19 23:12:18 -05:00
318908d8ca arr-init: add module for API-based configuration 2026-02-19 23:12:16 -05:00
c35a65e1bf recyclarr: init 2026-02-19 19:12:19 -05:00
af3a3d738e jellyseerr: init 2026-02-19 19:12:19 -05:00
879a3278ee bazarr: init 2026-02-19 19:12:19 -05:00
89d939d37f radarr: init 2026-02-19 19:12:19 -05:00
c290671b52 sonarr: init 2026-02-19 19:12:19 -05:00
ba09476295 prowlarr: init 2026-02-19 19:12:01 -05:00
9b715ba110 qbt: GlobalMaxRatio 6.0 -> 7.0 2026-02-17 22:56:48 -05:00
f6628b9302 jellyfin-qbittorrent-monitor: add stream headroom 2026-02-17 14:31:34 -05:00
7484a11535 jellyfin-qbittorrent-monitor: fix upload 2026-02-17 14:00:05 -05:00
d46ccc8245 update 2026-02-17 00:27:56 -05:00
1988f1a28d minecraft: update mods 2026-02-16 21:57:49 -05:00
9a9ecc6556 jellyfin-qbittorrent-monitor: dynamic bandwidth management 2026-02-15 23:33:45 -05:00
cf3e876f27 matrix: update 2026-02-15 11:51:38 -05:00
935ca6361b update 2026-02-14 22:50:45 -05:00
aa219dcfff matrix: update 2026-02-14 22:49:50 -05:00
62a91a8615 fmt 2026-02-13 15:26:27 -05:00
c01b2336a7 matrix: fix elementx calls
Applies patch from: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1370
That I am working on. Also updates version to latest (at this time) git
2026-02-13 15:26:17 -05:00
f5abfd5bf6 fix(no-rgb): handle transient hardware unavailability during deploy 2026-02-12 18:48:41 -05:00
82add97a80 feat(tmpfiles): defer per-service file permissions to reduce boot time 2026-02-12 18:48:29 -05:00
84cbe82cb0 update 2026-02-12 12:45:28 -05:00
4e9e3f627b matrix: setup livekit
Needed for element X calls.
2026-02-11 22:14:12 -05:00
9cc63fcfb8 impermanence: fix /etc permissions after re-deploy 2026-02-11 15:41:30 -05:00
35f0c08ee2 ntfy: fix directory 2026-02-10 18:47:17 -05:00
0f1e249127 ntfy 2026-02-10 17:39:01 -05:00
f3e972b3a4 matrix: fix registration 2026-02-10 14:49:58 -05:00
e28f8a70df matrix: add coturn 2026-02-10 14:49:50 -05:00
f27068a974 matrix: fix private folder 2026-02-10 14:22:53 -05:00
795c5b3d41 Revert "matrix: disable"
This reverts commit a887edf510.
2026-02-10 14:08:43 -05:00
a887edf510 matrix: disable 2026-02-10 13:55:45 -05:00
4f71f61c4b matrix: fix continuwuity module 2026-02-10 13:54:22 -05:00
3187130cd3 update 2026-02-10 12:56:12 -05:00
11ab6de305 re-add matrix 2026-02-10 12:49:56 -05:00
b67416a74b syncthing: add grayjay backups 2026-02-06 14:43:08 -05:00
954e124b49 potentially fix fail2ban 2026-02-05 15:11:17 -05:00
a7d6018592 update 2026-02-05 01:33:55 -05:00
37fdf13a3f update 2026-02-03 12:25:24 -05:00
8176376f48 update 2026-02-01 21:30:50 -05:00
58c804ea41 update 2026-01-30 00:43:28 -05:00
a61fedb015 fail2ban: ignoreip from local network 2026-01-27 18:51:08 -05:00
2183ea8363 update 2026-01-26 23:09:22 -05:00
27ffe38ed3 xmrig: 12 threads 2026-01-26 17:51:16 -05:00
a0e6b8428e xmrig: 1gb pages 2026-01-26 14:25:25 -05:00
0b01fc3f28 xmrig 2026-01-26 14:15:27 -05:00
016520c579 update 2026-01-23 12:56:54 -05:00
47cc12f4ed cleanup 2026-01-23 00:29:24 -05:00
a766e67fec cleanup minecraft test 2026-01-22 22:40:40 -05:00
fdb1b559bc wg: don't hardcode namespaceAddress 2026-01-22 14:56:36 -05:00
3026897113 Revert "minecraft: fail2ban"
This reverts commit a23b3d8c5f.
2026-01-22 14:25:52 -05:00
a23b3d8c5f minecraft: fail2ban 2026-01-21 20:21:23 -05:00
4bf05f8b51 hostPlatform -> targetPlatform 2026-01-21 15:25:25 -05:00
d15ec9fe0b fix squaremap 2026-01-21 14:26:39 -05:00
89627e1299 update 2026-01-20 23:08:55 -05:00
897f9b2642 flake: impermanence nixpkgs follow nixpkgs 2026-01-20 23:08:41 -05:00
f87e395225 jellyfin-qbittorrent-monitor: don't use mock qbittorrent 2026-01-20 23:05:15 -05:00
9770e6d667 jellyfin-qbittorrent-monitor: fix mock qbittorrent 2026-01-20 22:38:18 -05:00
8ed67464d0 fmt 2026-01-20 19:48:20 -05:00
da6b4d1915 tests: fix all fail2ban NixOS VM tests
- Add explicit iptables banaction in security.nix for test compatibility
- Force IPv4 in all curl requests to prevent IPv4/IPv6 mismatch issues
- Fix caddy test: use basic_auth directive (not basicauth)
- Override service ports in tests to match direct connections (not via Caddy)
- Vaultwarden: override ROCKET_ADDRESS and ROCKET_LOG for external access
- Immich: increase VM memory to 4GB for stability
- Jellyfin: create placeholder log file and reload fail2ban after startup
- Add tests.nix entries for all 6 fail2ban tests

All tests now pass: ssh, caddy, gitea, vaultwarden, immich, jellyfin
2026-01-20 18:41:01 -05:00
f2ef562724 fail2ban: implement for jellyfin 2026-01-20 14:46:49 -05:00
d9236152aa fail2ban: implement for immich 2026-01-20 14:39:38 -05:00
ba45743ea0 fail2ban: implement for gitea 2026-01-20 14:39:29 -05:00
0214621a58 fail2ban: implement for bitwarden 2026-01-20 14:39:23 -05:00
aa2c61dcd3 fail2ban: implement for caddy basic auth 2026-01-20 14:35:20 -05:00
b550e495c8 nit: move fail2ban to security module 2026-01-20 14:11:15 -05:00
5ad5aff5e8 ssh: add fail2ban 2026-01-20 14:05:02 -05:00
d9a1a01f7f jellyfin-qbittorrent-monitor: handle qbittorrent going down state 2026-01-19 02:42:18 -05:00
eb5d0bb093 security things 2026-01-18 02:36:00 -05:00
c6b39a98cd update 2026-01-18 01:03:18 -05:00
11cacffe7d update 2026-01-15 14:01:27 -05:00
4881780186 monero: move back to hdds 2026-01-15 13:51:25 -05:00
f83e1170af syncthing 2026-01-13 16:55:19 -05:00
a93c789278 jellyfin-qbittorrent-monitor: don't mock out jellyfin for testing 2026-01-13 14:15:11 -05:00
df1d983b63 rework qbittorrent jellyfin monitor test 2026-01-13 13:41:23 -05:00
de89e70a05 impermanence: fix /etc/zfs cache 2026-01-13 13:13:49 -05:00
56fe61011a impermanence: fix persistant ssh host keys 2026-01-13 13:10:19 -05:00
528782ae32 update 2026-01-13 12:39:29 -05:00
8e32b73985 update webpage 2026-01-12 20:08:03 -05:00
b5a63da11e fix pkgs.system deprecation 2026-01-12 15:28:38 -05:00
aeab0a6f5b nixfmt-rfc-style -> nixfmt-tree 2026-01-12 15:23:28 -05:00
28623c3d97 update 2026-01-12 13:07:25 -05:00
513e426f89 nit: cleanup imports 2026-01-09 12:52:16 -05:00
aaef39d31a ytbn: use own nixpkgs 2026-01-08 21:50:48 -05:00
5138c2da80 impermanence: fix home directory declaration 2026-01-08 21:47:22 -05:00
6557a81167 update 2026-01-08 21:46:01 -05:00
68f1f6bbc4 cleanup flake deps 2026-01-08 06:24:58 -05:00
1048f261d4 vaapiVdpau -> libva-vdpau-driver 2026-01-08 06:17:48 -05:00
16d3050eb8 fully remove llama-cpp 2026-01-08 05:41:10 -05:00
d4172a5886 25.05 -> 25.11 2025-12-30 16:38:30 -05:00
a549b01111 organize 2025-12-28 15:49:18 -05:00
b5d2e3188d update 2025-12-20 01:17:09 -05:00
4e76882106 Revert "wg.conf: us-mia-wg-002 -> us-mia-wg-001"
This reverts commit 507ee6d57a.
2025-12-18 01:31:19 -05:00
507ee6d57a wg.conf: us-mia-wg-002 -> us-mia-wg-001
There are issues with mullvad's us-mia-wg-002 node
I emailed then about it. For now, moving to
us-mia-wg-001.
2025-12-17 23:44:20 -05:00
afa8981d91 update 2025-12-16 02:37:12 -05:00
6c617ef56b minecraft: update to 1.21.11 2025-12-13 21:35:37 -05:00
c7d884aca0 list-usb-drives: remove (never worked) 2025-12-13 02:24:46 -05:00
74d0620334 ssh: fix ssh_host_key perms 2025-12-12 21:18:51 -05:00
a5112e322e ssh: move to seperate file 2025-12-12 21:09:39 -05:00
5ae54b8981 update 2025-12-12 15:53:53 -05:00
ca4d0c414f monero: move to ssds 2025-12-08 23:19:25 -05:00
66b9c6472e Pin lanzaboote version to fix upstream issue
See: https://github.com/nix-community/lanzaboote/issues/518
2025-12-08 22:20:37 -05:00
e22558ac06 update 2025-12-08 22:11:30 -05:00
ea9cb09550 update 2025-12-05 23:55:58 -05:00
e8b4bc6b81 nix: add gc 2025-12-05 23:22:11 -05:00
3386fd9716 update 2025-12-05 14:13:40 -05:00
1950bcf6f6 update 2025-12-04 18:26:06 -05:00
32eac71ba0 update 2025-12-03 20:33:34 -05:00
78c92f1ae7 update 2025-12-03 18:19:57 -05:00
4a12643817 graphing-calculator: init 2025-12-03 14:10:50 -05:00
3914a29e0c persistent: streamline installation process with persistent.tar 2025-12-02 00:56:44 -05:00
7897d44bfd update + senior project website 2025-12-01 10:53:11 -05:00
a428a7163c update 2025-11-29 23:35:46 -05:00
fc39655e01 update 2025-11-25 13:08:08 -05:00
2656b8db19 zfs: expand testing to include a failing multi case 2025-11-24 16:19:25 -05:00
089fac3623 update (again) 2025-11-24 13:18:35 -05:00
039fa960f3 update + senior project website 2025-11-24 11:38:40 -05:00
31a9feb98c update 2025-11-21 12:06:43 -05:00
05520cc177 minecraft: update lithium 2025-11-21 12:06:35 -05:00
670430a223 secrets: delete old file 2025-11-20 21:59:09 -05:00
bc55d4203f install: cleanup key and secrets handling 2025-11-20 21:02:33 -05:00
8d420ea86b update 2025-11-20 19:12:40 -05:00
0c4baab0ef move to generic /services 2025-11-20 16:57:38 -05:00
363bff8c40 fix: disable serial-getty
keeps spamming dmesg with stupid messages.
2025-11-20 16:34:47 -05:00
223910744a zfs: fix qbittorrent 2025-11-20 16:30:37 -05:00
ae5189b6c6 zfs: HEAVILY REFACTOR subvolume handling 2025-11-20 16:10:35 -05:00
dd9042ae95 add monero service 2025-11-20 00:57:02 -05:00
86753581f1 update 2025-11-19 11:13:59 -05:00
39418b1bb3 update 2025-11-18 13:12:09 -05:00
90fb711115 zfs: fix zfs escaped spaces test 2025-11-17 10:37:45 -05:00
3408aab609 update 2025-11-17 08:30:36 -05:00
f514d5f653 enable kmscon 2025-11-14 12:11:13 -05:00
935252d8c3 update 2025-11-14 12:03:45 -05:00
ba6f47dde9 jellyfin-qbittorrent-monitor: fix jellyfin api key file perms 2025-11-13 02:43:42 -05:00
097b89a14a Revert "openrgb: override mbedtls_2 with mbedtls"
This reverts commit b1b9a3755f.
2025-11-11 00:46:39 -05:00
50d70e8569 update 2025-11-11 00:12:49 -05:00
3f89ee0147 update 2025-11-09 14:35:35 -05:00
98b2490840 update 2025-11-07 13:14:51 -05:00
65f903c20b update 2025-11-07 02:07:01 -05:00
acc4677982 minecraft: update mods 2025-11-07 00:42:01 -05:00
a528317e08 set gpu module to "xe"
Possibly could fix i915 driver issues I'm having
with my arc a380?

Panic:
```
Unexpected send: action=0x1000
WARNING: CPU: 7 PID: 62977 at drivers/gpu/drm/i915/gt/uc/intel_guc_ct.c:844 intel_guc_ct_send+0x67a/0x7d0 [i915]
Modules linked in: bluetooth ecdh_generic ecc crc16 xt_nat nft_chain_nat nf_nat veth wireguard curve25519_x86_64 libchacha20poly1305 chacha_x86_64 poly1305
_x86_64 libcurve25519_generic libchacha ip6_udp_tunnel udp_tunnel msr af_packet mei_hdcp mei_pxp cfg80211 mei_gsc mei_me rfkill mei xe snd_hda_codec_hdmi e
dac_mce_amd edac_core intel_rapl_msr amd_atl intel_rapl_common snd_hda_intel crct10dif_pclmul polyval_clmulni polyval_generic snd_intel_dspcfg ghash_clmuln
i_intel snd_intel_sdw_acpi r8169 sha512_ssse3 sha256_ssse3 snd_hda_codec sha1_ssse3 snd_hda_core aesni_intel drm_gpuvm snd_hwdep gf128mul drm_exec realtek
gpu_sched snd_pcm crypto_simd drm_suballoc_helper cryptd mdio_devres drm_ttm_helper wmi_bmof snd_timer of_mdio snd fixed_phy soundcore fwnode_mdio rapl sp5
100_tco libphy watchdog xt_conntrack input_leds joydev led_class evdev mac_hid tiny_power_button rtc_cmos nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 gpio_a
mdpt onboard_usb_dev gpio_generic xt_tcpudp button uas ipt_rpfilter xt_pkttype nft_compat
 nf_tables libcrc32c crc32c_generic crc32c_intel sch_fq_codel ee1004 i2c_piix4 i2c_smbus i2c_dev atkbd libps2 serio vivaldi_fmap loop cpufreq_powersave tun
 tap macvlan bridge stp llc zenpower(O) kvm_amd ccp rng_core kvm irqbypass fuse efi_pstore configfs nfnetlink dmi_sysfs ip_tables x_tables nls_iso8859_1 nl
s_cp437 vfat f2fs fat dm_snapshot dm_bufio hid_generic dm_mod dax crc32_generic lz4hc_compress sd_mod usbhid hid usb_storage lz4_compress i915 ahci i2c_alg
o_bit drm_buddy libahci video libata ttm intel_gtt nvme scsi_mod drm_display_helper nvme_core xhci_pci nvme_auth cec xhci_hcd crc32_pclmul scsi_common wmi
zfs(PO) spl(O) efivarfs autofs4
CPU: 7 UID: 995 PID: 62977 Comm: av:hevc:df0 Tainted: P        W  O       6.12.50-hardened1 #1-NixOS
Tainted: [P]=PROPRIETARY_MODULE, [W]=WARN, [O]=OOT_MODULE
Hardware name: To Be Filled By O.E.M. B550M Pro4/B550M Pro4, BIOS P3.40 01/18/2024
RIP: 0010:intel_guc_ct_send+0x67a/0x7d0 [i915]
Code: 87 d0 06 00 00 3c 01 0f 87 d7 2f 17 00 a8 01 0f 85 42 ff ff ff 90 48 8b 44 24 18 48 c7 c7 50 43 34 c1 8b 30 e8 07 bf 89 d7 90 <0f> 0b 90 90 e9 24 ff
ff ff 48 8b 7c 24 20 e8 63 45 5d d8 48 8d 7c
RSP: 0018:ffffd57551c770c0 EFLAGS: 00010046
RAX: 0000000000000000 RBX: ffff8e019b6b8508 RCX: 0000000000000000
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
RBP: ffffd57551c77158 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffff8e01a5680b80
R13: ffff8e019b6b82a0 R14: ffff8e018b0c7004 R15: ffff8e019b6b82a0
FS:  00006b26fb1596c0(0000) GS:ffff8e107ef80000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00006b26cc2b8438 CR3: 00000003c0dde000 CR4: 0000000000f50ef0
PKRU: 55555554
Call Trace:
 <TASK>
 ? srso_alias_return_thunk+0x5/0xfbef5
 __guc_add_request+0xd2/0x2c0 [i915]
 guc_submit_request+0x1bc/0x210 [i915]
 submit_notify+0xfd/0x150 [i915]
 __i915_sw_fence_complete+0x3a/0x210 [i915]
 __i915_request_queue+0x51/0x70 [i915]
 i915_request_add+0x64/0xe0 [i915]
 intel_context_migrate_copy+0x39e/0xac0 [i915]
 __i915_ttm_move+0x821/0xa00 [i915]
 i915_ttm_move+0x348/0x470 [i915]
 ? unmap_mapping_range+0x85/0x150
 ttm_bo_handle_move_mem+0xe1/0x1d0 [ttm]
 ttm_bo_validate+0xde/0x190 [ttm]
 ? srso_alias_return_thunk+0x5/0xfbef5
 __i915_ttm_get_pages+0x9f/0x1b0 [i915]
 i915_ttm_get_pages+0xca/0x180 [i915]
 ? srso_alias_return_thunk+0x5/0xfbef5
 ? srso_alias_return_thunk+0x5/0xfbef5
 __i915_gem_object_get_pages+0x3a/0x50 [i915]
 i915_vma_pin_ww+0x718/0x9c0 [i915]
 eb_validate_vmas+0x192/0xaa0 [i915]
 ? srso_alias_return_thunk+0x5/0xfbef5
 i915_gem_do_execbuffer+0xfc9/0x2890 [i915]
 i915_gem_execbuffer2_ioctl+0x16b/0x290 [i915]
 ? __pfx_i915_gem_execbuffer2_ioctl+0x10/0x10 [i915]
 drm_ioctl_kernel+0xb8/0x110
 drm_ioctl+0x2c6/0x550
 ? __pfx_i915_gem_execbuffer2_ioctl+0x10/0x10 [i915]
 __x64_sys_ioctl+0x9c/0xe0
 do_syscall_64+0xd5/0x210
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x6b270a88cf0f
Code: 00 48 89 44 24 18 31 c0 48 8d 44 24 60 c7 04 24 10 00 00 00 48 89 44 24 08 48 8d 44 24 20 48 89 44 24 10 b8 10 00 00 00 0f 05 <89> c2 3d 00 f0 ff ff 77 28 48 8b 44 24 18 64 48 2b 04 25 28 00 00
RSP: 002b:00006b26fb13f560 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00006b26cc2b4540 RCX: 00006b270a88cf0f
RDX: 00006b26fb13f650 RSI: 00000000c0406469 RDI: 0000000000000003
RBP: 00006b26fb13f650 R08: 0000000000000000 R09: 0000000000000000
R10: 00006b26cc263d10 R11: 0000000000000246 R12: 00000000c0406469
R13: 0000000000000003 R14: 000000003c959460 R15: 000000003c948860
 </TASK>
---[ end trace 0000000000000000 ]---
```
2025-11-06 16:41:51 -05:00
94a98349c5 update 2025-11-05 13:20:42 -05:00
ad5ff98841 update 2025-11-05 00:42:23 -05:00
312db92676 update 2025-11-03 13:21:05 -05:00
1fb72c2674 update 2025-11-02 19:50:05 -05:00
de8bec0353 update 2025-10-31 14:50:49 -04:00
0364bd5aeb update 2025-10-30 14:48:00 -04:00
83b3f4de85 secureboot fixes I think 2025-10-30 00:23:32 -04:00
e2ba51580b networking: temporarily use 192 address 2025-10-29 22:05:12 -04:00
ee628b296c update senior project website 2025-10-27 15:05:03 -04:00
0128b4c104 update 2025-10-27 00:54:00 -04:00
a910a30c01 minecraft: speedup test 2025-10-26 21:49:00 -04:00
376ea182cb minecraft: update mods 2025-10-26 15:50:35 -04:00
6ecd228a58 jellyfin-qbittorrent-monitor: nit with test 2025-10-24 18:14:40 -04:00
f7c2c441ac minecraft: fix nix test 2025-10-24 14:40:28 -04:00
a455d592b4 zfs_ensure_mounted: cleanup test 2025-10-24 13:46:22 -04:00
8aabd1466e jellyfin-qbittorrent-monitor: cleanup 2025-10-24 13:16:40 -04:00
f40f9748a4 jellyfin-qbittorrent-monitor: improve testing infra 2025-10-24 12:35:15 -04:00
73379efe40 update 2025-10-24 10:06:51 -04:00
e9c1df44e8 jellyfin-qbittorrent-monitor: write proper test 2025-10-24 00:12:42 -04:00
b1b9a3755f openrgb: override mbedtls_2 with mbedtls 2025-10-23 16:47:37 -04:00
eedf2fa8ed update 2025-10-23 15:40:47 -04:00
f58fd08e43 jellyfin-qbittorrent-monitor: only count external networks 2025-10-21 23:39:44 -04:00
fb98627a58 update 2025-10-21 21:07:02 -04:00
d0f16a3e93 update 2025-10-19 18:03:14 -04:00
c8cc19b698 llama.cpp: disable 2025-10-19 17:48:17 -04:00
386cf266d5 fix jellyfin api key 2025-10-18 03:10:49 -04:00
46bb9734b7 disable flakes (not needed) 2025-10-18 00:27:42 -04:00
1c904907d6 split up no-rgb and secureboot 2025-10-18 00:27:37 -04:00
1ae9fc29bd fix caddy_auth perms 2025-10-17 23:27:19 -04:00
e8aafda386 fix various agenix things 2025-10-17 23:13:25 -04:00
1ddcccd1c2 use filesystems logic 2025-10-17 22:55:02 -04:00
dd18bd1e6d remove various references to ${username} 2025-10-17 22:34:52 -04:00
31b4d7e80d remove service that doesn't exist 2025-10-17 22:34:35 -04:00
003cf474ff fix script 2025-10-17 22:28:02 -04:00
f9515dd160 claude'd better security things 2025-10-17 20:30:56 -04:00
9e35448f04 zfs_ensure_mounted: cleanup echo grep pattern 2025-10-17 17:25:30 -04:00
852ec18c7b update 2025-10-17 12:03:11 -04:00
b8759218ec llama.cpp: ngl 8-> 12 2025-10-16 20:16:03 -04:00
ea5996dc9e qbt: TimeoutStopSec = 10 2025-10-16 18:44:54 -04:00
d96035120f llama.cpp: reenable + Apriel-1.5-15b-Thinker 2025-10-16 18:44:05 -04:00
3811193739 zfs_ensure_mounted: cleanup sed awk call 2025-10-14 23:00:55 -04:00
d7d84848bb update 2025-10-14 22:32:03 -04:00
85fa1bb3ab update 2025-10-14 02:42:01 -04:00
a3d54e82d1 qbt: improve tracker list parsing 2025-10-14 02:37:20 -04:00
f80e1cf7c7 update 2025-10-11 02:50:18 -04:00
ef44aebe20 minecraft: disable scalable lux 2025-10-10 12:59:39 -04:00
a600a0936e update 2025-10-10 12:35:49 -04:00
e53b510e9b minecraft: update to 1.21.10 2025-10-10 12:35:15 -04:00
8da3470934 minecraft: fix caddy user group 2025-10-09 11:51:26 -04:00
f6c0178421 qbt: use trackerlist repo instead of managing my own trackerlist 2025-10-08 12:29:38 -04:00
7956d18daf minecraft 10gb -> 4gb 2025-10-07 14:12:50 -04:00
83a639a20e impermanence 2025-10-07 14:12:47 -04:00
a4bf2a0ea9 llama.cpp: testing 2025-10-06 01:42:37 -04:00
03729c90c1 update 2025-10-05 16:11:22 -04:00
c0eb03f30e update 2025-10-04 20:05:21 -04:00
7ed529128d minecraft: update lithium 2025-10-03 14:05:56 -04:00
c83d34108e Revert "llama-cpp: re-enable"
This reverts commit e98a23934a.
2025-10-02 22:26:30 -04:00
72d950007b llama-cpp: fix postPatch phase 2025-10-02 22:26:25 -04:00
e98a23934a llama-cpp: re-enable 2025-10-02 21:30:02 -04:00
a75f34e113 llama-cpp: change model 2025-10-02 21:29:43 -04:00
eff5b3b8aa update 2025-10-01 23:45:37 -04:00
c986abb9d3 update 2025-09-30 12:23:30 -04:00
b1d92c3825 senior project website: update 2025-09-28 22:28:54 -04:00
5b3332dd7f senior project website: update 2025-09-28 22:20:10 -04:00
13341094d4 update 2025-09-26 23:48:04 -04:00
2f0fb9b2c0 update 2025-09-23 20:01:48 -04:00
abecf4a723 qbt: disable port forwarding 2025-09-23 11:05:31 -04:00
d2b6348085 update 2025-09-21 01:11:58 -04:00
8ed2b9e80c update 2025-09-19 19:51:13 -04:00
0c7e0e0b67 minecraft: add disconnect-packet-fix and packet-fixer 2025-09-19 14:17:57 -04:00
70f8b99dec update 2025-09-19 00:21:19 -04:00
7db414efe1 update 2025-09-17 10:05:24 -04:00
c9068a8b50 update 2025-09-15 12:30:48 -04:00
13a0344db0 jellyfin-monitor: cleanup 2025-09-15 12:30:28 -04:00
1aec911e72 jellyfin-monitor: only print active streams on change 2025-09-12 14:57:53 -04:00
9d8b8ad33f jellyfin-monitor: only trigger for video 2025-09-12 11:34:58 -04:00
0e90fff70d jellyfin-monitor: remove datetime 2025-09-12 10:12:39 -04:00
3f08fb4729 remove nload 2025-09-12 01:53:32 -04:00
f60c31578f jellyfin-monitor: cleanup + remove qbt auth 2025-09-12 01:30:10 -04:00
4729bd2cc4 fix disabling alternate limits 2025-09-12 01:27:17 -04:00
43317044f2 qbt: remove limits 2025-09-11 21:51:12 -04:00
7274b86ec1 claude'd jellyfin auto limit qbt upload 2025-09-11 17:46:25 -04:00
36a00bedc5 update 2025-09-11 16:05:27 -04:00
098e033a4c fix minecraft test 2025-09-10 16:03:23 -04:00
29b45a8386 update 2025-09-09 15:48:28 -04:00
3d5b0dea54 update 2025-09-08 22:39:53 -04:00
626ac61124 update 2025-09-08 10:19:52 -04:00
53cdb6b3b7 update 2025-09-07 00:28:38 -04:00
a2a4111326 update 2025-09-05 10:49:01 -04:00
feb59fd78e update 2025-09-03 10:09:29 -04:00
2e94a29ece caddy: generate from hugo instead 2025-09-02 23:48:04 -04:00
f12ebcb9ca update senior project website 2025-09-02 19:52:30 -04:00
c61a0c07ae caddy: remove orphaned options 2025-09-02 19:51:01 -04:00
9f62ba4d4d caddy: actually fix senior project webpage 2025-09-02 19:49:38 -04:00
54668635e9 caddy: fix senior project site 2025-09-02 19:19:45 -04:00
b2665758ee caddy: TODO fix conflicting def values 2025-09-02 18:01:54 -04:00
b5e6fc022b caddy: add senior project things 2025-09-02 17:59:27 -04:00
a9002155bd caddy: move root 2025-09-02 17:55:28 -04:00
d3faf45c6a qbt: change limits 2025-09-01 21:59:46 -04:00
8ad94948a7 update 2025-09-01 02:53:45 -04:00
1004feba42 qbt: disable queueing 2025-08-30 22:58:55 -04:00
12744a49b6 update 2025-08-30 19:49:55 -04:00
db4969f2b9 qbt: adjust settings 2025-08-30 02:33:43 -04:00
3aed416f40 minecraft: restrict permissions more 2025-08-29 00:43:14 -04:00
e3d59889f9 update 2025-08-28 21:54:44 -04:00
7a199f9176 qbt: fix pkgs.vuetorrent path 2025-08-25 13:29:20 -04:00
b8c5a66cdc zfs: change arc size setting 2025-08-25 13:20:11 -04:00
9e39ce41d0 qbt: use pkgs.vuetorrent 2025-08-25 13:08:27 -04:00
32e1f6771a qbt: restrict permissions around TempPath 2025-08-25 11:13:27 -04:00
a62e71b99c update 2025-08-25 00:46:59 -04:00
62a5a2b984 zfs: add comments about secrets 2025-08-25 00:39:01 -04:00
8d5ee69e55 zfs_unstable -> zfs 2025-08-25 00:37:38 -04:00
a79c09f11e update 2025-08-21 18:24:34 -04:00
006652da36 fix minecraft test 2025-08-21 05:12:31 -04:00
3557a2e6c8 use lib.serviceDependZpool 2025-08-21 05:06:03 -04:00
df8a22760c add lib.serviceDependZpool 2025-08-21 05:02:05 -04:00
a827438a4c expand vpnNamespaceOpenPort 2025-08-20 12:35:19 -04:00
87a5466411 format 2025-08-20 12:31:31 -04:00
306e320a3a create 'lib.vpnNamespaceOpenPort' 2025-08-20 12:28:48 -04:00
c272eb9d5b qbt: cleanup 2025-08-20 12:23:30 -04:00
2ccf55e92b rm NOTES.md 2025-08-20 11:53:47 -04:00
162be1bcac heavily simplify list-usb-drives 2025-08-20 11:53:29 -04:00
4865e0276b cleanup caddy 2025-08-20 10:41:31 -04:00
40729a2597 cleanup 2025-08-20 10:28:42 -04:00
4b850af15a bitwarden: fix backup 2025-08-20 10:11:57 -04:00
d5c2a01ce1 add bitwarden 2025-08-20 05:41:37 -04:00
501510183c soulseek: change limits 2025-08-20 00:35:36 -04:00
c07aa0c406 delete list-usb-drives test 2025-08-19 23:48:04 -04:00
170e124dd2 update 2025-08-19 23:25:15 -04:00
d3c823355a add list-usb-drivers 2025-08-19 01:49:23 -04:00
eecef04065 initial testing for list-usb-drives 2025-08-19 01:47:09 -04:00
65760006ba cleanup + fix minecraft test 2025-08-18 10:24:29 -04:00
13bd5e300d update 2025-08-18 10:14:09 -04:00
0b2d28d617 ups: init 2025-08-18 10:14:06 -04:00
ef8ba6aca3 no-rgb: use lib.getExe 2025-08-18 04:55:48 -04:00
e9a9e9d8a8 add libatasmart 2025-08-18 04:53:58 -04:00
cae94a40f2 delete smart-smart-test 2025-08-18 04:50:43 -04:00
125c8a685e move reflac and disk-smart-test to overlays.nix 2025-08-18 04:39:26 -04:00
01a6ebcba6 satisfy shellcheck for disk-smart-test 2025-08-18 04:29:52 -04:00
91ff16f698 update 2025-08-16 01:40:24 -04:00
4209b7cd91 update 2025-08-15 01:44:21 -07:00
181e940faf minecraft: bump better-fabric-console 2025-08-14 01:30:59 -07:00
fc2766c44f Revert "minecraft: lazymc"
This reverts commit a081e6c6ee.
2025-08-14 01:27:51 -07:00
a081e6c6ee minecraft: lazymc 2025-08-14 01:14:27 -07:00
6683ca0e36 update 2025-08-12 11:56:36 -07:00
948882d2a7 fix zfs test 2025-08-12 02:36:10 -07:00
a2d622613d improve ensureZfsMounted script 2025-08-12 02:26:29 -07:00
ff305c8c4c minecraftTest: edit syntax 2025-08-12 00:25:07 -07:00
30421d96f0 move ensureZfsMounts 2025-08-11 16:18:21 -07:00
80df89e9a1 add minecraft test 2025-08-11 15:39:01 -07:00
14c4aed363 re-disable llama-cpp 2025-08-11 12:34:03 -07:00
7f4552ac90 update 2025-08-11 12:30:32 -07:00
8c92854118 update 2025-08-10 20:20:27 -07:00
ca3ea3166f thing 2025-08-07 21:23:07 -07:00
1242ba2274 improve testing infra 2025-08-07 21:19:22 -07:00
80d9e1029d refactor 2025-08-07 20:53:17 -07:00
003418b27b create handleTest function for future tests 2025-08-07 20:25:07 -07:00
2875d29293 add testing infra 2025-08-07 19:22:05 -07:00
207722acb2 llama-cpp: update 2025-08-05 20:26:17 -07:00
80afe19a43 llama-cpp: re-enable 2025-08-05 19:56:59 -07:00
ffc079fb21 llama-cpp: use gpt-oss-20b-mxfp4 2025-08-05 19:53:20 -07:00
d1bf20f03f update 2025-08-04 23:43:52 -07:00
83c93ca023 qbt: unlimited connections 2025-08-02 01:07:17 -07:00
936386cf96 wg: change endpoint 2025-08-02 01:02:59 -07:00
36fd25ae8d update 2025-08-02 00:06:19 -07:00
cbf2d06e51 update 2025-07-30 09:58:15 -07:00
0b67ca54e5 update 2025-07-28 20:09:28 -07:00
791e31bdeb qbt: add a bunch of trackers 2025-07-28 00:04:41 -07:00
3978c1e904 format 2025-07-27 00:22:43 -07:00
f989ac6e1e minecraft: 1.21.7 -> 1.21.8 2025-07-27 00:12:16 -07:00
dfc3f50f6d qbt: add other settings 2025-07-26 22:15:26 -07:00
a300449f78 qbt: GlobalMaxRatio 4.0 -> 6.0 2025-07-26 12:03:00 -07:00
d2c448191e traffic shaping: we got a upload speed upgrade! 2025-07-25 16:18:46 -07:00
7024edd870 soulseek: re-rely on hdd zpool mountpoints 2025-07-24 23:23:23 -07:00
31a288583f update 2025-07-24 20:02:48 -07:00
2de85c12ff zfs hdds are BACK 2025-07-24 20:02:39 -07:00
1bc361e49c update 2025-07-19 23:33:54 -07:00
5b51c8c3d5 update 2025-07-17 08:44:27 -07:00
f4669f37ff update 2025-07-15 18:02:47 -07:00
b78beb7f71 update 2025-07-15 00:12:29 -07:00
d4a5eb5694 update 2025-07-13 02:54:45 -07:00
786a2d4132 improve zfs mounted script EVEN MORE (EVEN MORE MORE MORE) 2025-07-13 02:53:35 -07:00
17e23895d9 nit 2025-07-11 22:14:28 -07:00
13024f42bd avoid using unitConfig 2025-07-11 20:44:18 -07:00
83c77740b3 update 2025-07-11 20:43:24 -07:00
7f7dc03a20 extend nixpkgs's lib instead 2025-07-11 20:40:27 -07:00
3ba8c1a5a6 improve zfs mounted script EVEN MORE 2025-07-11 20:19:45 -07:00
8e829637ac simplify script 2025-07-11 00:11:34 -07:00
5597fd9c3b this is awful i hate it 2025-07-10 23:07:35 -07:00
71d0c3e7e6 caddy: serviceMountDeps 2025-07-10 18:52:33 -07:00
cc181d1332 simplify mountpoint script 2025-07-10 09:19:35 -07:00
888fbc3649 proper mountpoint testing 2025-07-10 01:31:52 -07:00
265d5ff5fb disable hdd array (broken) 2025-07-10 00:11:01 -07:00
255a9e3781 update 2025-07-09 18:43:42 -07:00
fd9667265e update 2025-07-07 03:43:46 -07:00
9142f7b8a3 minecraft: 1.21.6 -> 1.21.7 2025-07-07 03:43:31 -07:00
39a9540f78 qbt: add udp://tracker.openbittorrent.com:80 tracker 2025-07-01 16:52:07 -07:00
3a814ee6c2 minecraft: update squaremap 2025-06-30 18:40:09 -07:00
f43809cb2d update 2025-06-30 13:33:58 -07:00
9a1fc89488 re-disable ipv6 2025-06-30 13:31:56 -07:00
ce8f0693ea update 2025-06-28 14:57:48 -07:00
05828806fa update minecraft mods to 1.21.6 2025-06-28 14:57:20 -07:00
8d3e64dc01 update 2025-06-26 23:56:55 -07:00
22dfb7f6ae update 2025-06-26 09:14:17 -07:00
96a057c3e6 only open port 8448 for matrix 2025-06-25 23:30:16 -07:00
ab86d39ef0 update 2025-06-24 19:42:59 -07:00
77db147643 update 2025-06-23 18:43:35 -07:00
bb324ecb9a zfs: arc 12000 -> 20000 mb 2025-06-22 22:10:56 -07:00
9175620c35 update 2025-06-22 02:12:05 -07:00
b56187d58d minecraft: 1.21.5 -> 1.21.6 2025-06-19 21:29:37 -07:00
788abf1515 add more terminfos 2025-06-19 18:11:12 -07:00
eaa84d60e8 update 2025-06-19 18:09:15 -07:00
74abae8363 kernel: use hardened kernel 2025-06-19 18:09:11 -07:00
e65645c023 use deploy-rs 2025-06-18 20:54:39 -07:00
b17526a212 use srvos 2025-06-18 20:18:16 -07:00
af7f2bba73 update 2025-06-18 20:11:00 -07:00
c173fee0b0 update 2025-06-17 21:42:58 -07:00
79207224e2 minecraft: remove alternatecurrent 2025-06-16 21:06:24 -07:00
665cc5e473 update 2025-06-15 14:11:21 -07:00
ef9ba83249 qbt settings adjust 2025-06-15 00:29:33 -07:00
eebccdaf08 update 2025-06-12 16:25:10 -07:00
6c94af3972 update 2025-06-11 15:25:55 -07:00
2900c569ca update 2025-06-10 19:06:29 -07:00
42979f9e13 update 2025-06-08 18:46:43 -07:00
1477b2d1b0 disable llama 2025-06-06 20:57:54 -07:00
4a2fe66558 improve deploy script 2025-06-06 20:06:15 -07:00
ce7801795f DeepSeek-R1-0528-Qwen3-8B 2025-06-06 20:05:42 -07:00
9ae5f12b6c update 2025-06-06 20:05:11 -07:00
270d911836 update 2025-06-04 21:07:56 -07:00
987202661c update 2025-06-02 19:42:46 -07:00
8e604caa9b minecraft: update fabricapi 2025-06-01 14:45:29 -07:00
7052818ffa add NOTES.md 2025-05-28 23:32:21 -07:00
7c217d6ead llama-cpp: use q8 quantization instead of q4 2025-05-28 21:20:42 -07:00
4dc577fdcb llama-cpp: disable gpu 2025-05-28 21:09:45 -07:00
109e132497 llama-cpp: vulkan broken 2025-05-28 21:04:34 -07:00
2788984117 re-enable llm 2025-05-28 21:00:28 -07:00
d0da2591a3 llama-cpp: disable flash attn 2025-05-28 21:00:08 -07:00
a292c2fc75 llama-cpp: nvidia-acereason-7b 2025-05-28 20:59:45 -07:00
dccad122f9 minecraft: add better-fabric-console 2025-05-28 20:16:11 -07:00
3883df473e update 2025-05-28 20:14:49 -07:00
2d486931e9 qbt: max ratio 2 -> 4 2025-05-28 20:03:48 -07:00
283b2df9b9 update 2025-05-26 21:28:48 -07:00
887917308a minecraft: reduce render distance 2025-05-26 20:12:18 -07:00
b3aeeab244 minecraft: 4gb -> 10gb ram + install spark 2025-05-26 19:23:17 -07:00
661b7d534f minecraft: update c2me 2025-05-25 23:10:00 -07:00
006e918936 qbt: update vuetorrent 2025-05-25 12:56:42 -07:00
265fff81cb update 2025-05-25 12:55:14 -07:00
0998b4b69d nixos 24.11 -> 25.05 2025-05-23 23:24:19 -07:00
109ae67191 minecraft: update squaremap 2025-05-23 22:01:17 -07:00
b46c6a3983 update 2025-05-23 21:39:44 -07:00
ee206eec98 minecraft whitelist update 2025-05-18 00:22:12 -07:00
4d30ea464a update 2025-05-18 00:22:06 -07:00
bb3064b319 update 2025-05-15 21:40:42 -07:00
a74f52158e update 2025-05-14 00:51:13 -07:00
e222bb30ea disable llama 2025-05-14 00:16:41 -07:00
1b1505b6fd minecraft: update fabricapi 2025-05-12 19:36:02 -07:00
15ac426097 update 2025-05-12 19:34:33 -07:00
ed3c507bf1 minecraft whitelist update 2025-05-12 17:36:08 -07:00
48cd6a9d74 update 2025-05-11 20:15:30 -07:00
72747811cc minecraft disable spawn protection 2025-05-11 16:26:12 -07:00
b5b5f1b00f minecraft whitelist update 2025-05-11 16:26:01 -07:00
bd42ec6bfa update 2025-05-10 20:20:45 -04:00
66f4e3c7c3 update 2025-05-08 14:39:04 -04:00
4481e2d509 update 2025-05-07 12:28:19 -04:00
9b089d630c update 2025-05-07 00:17:27 -04:00
b6cd7016d1 minecraft whitelist update 2025-05-07 00:14:25 -04:00
b3011b215d secureboot directory stuff 2025-05-06 02:30:40 -04:00
abd1021c37 kernel: 6.6 -> 6.12 2025-05-06 01:32:10 -04:00
2766973eef update 2025-05-05 15:18:45 -04:00
a7aa160513 update 2025-05-04 18:37:39 -04:00
d80fdee5ac qbt: config changes 2025-05-04 01:31:14 -04:00
1e5674c7ee update + qbt changes 2025-05-03 12:13:50 -04:00
09b87c2517 update 2025-05-02 21:50:13 -04:00
4e1834cec5 update 2025-05-01 13:03:19 -04:00
1736bb13f6 minecraft: update squaremap + add c2me and scalablelux 2025-04-30 19:19:30 -04:00
140b8405cf update 2025-04-30 18:39:26 -04:00
fb4043712e llm: use vulkan 2025-04-30 12:59:07 -04:00
870093686a qbt: max ratio 1 -> 2 2025-04-30 11:33:53 -04:00
bcb670ecaa re-enable llm 2025-04-30 11:26:31 -04:00
843b64a644 llm: use xiomo model 2025-04-30 11:22:52 -04:00
5d95e305ab update 2025-04-30 00:46:23 -04:00
8893f452f4 update 2025-04-28 01:14:51 -04:00
ce1ef7bc23 minecraft: update squaremap 2025-04-27 00:53:26 -04:00
e2175597f3 update 2025-04-26 20:24:34 -04:00
5c692e5956 update 2025-04-25 00:30:35 -04:00
970d1756b5 disable llama 2025-04-24 13:42:57 -04:00
5bc81322a8 update 2025-04-24 11:45:33 -04:00
ef0e91633a minecraft: update squaremap 2025-04-23 18:37:52 -04:00
157f2bcbf0 update 2025-04-23 18:31:41 -04:00
806d8263c8 update 2025-04-23 01:58:31 -04:00
63be2a68df update 2025-04-21 23:54:29 -04:00
5aa8abce8c minecraft: update squaremap 2025-04-21 22:44:51 -04:00
1e4b2bfeca qbt: update vuetorrent 2025-04-18 20:43:39 -04:00
6d5683aaba format 2025-04-18 20:10:26 -04:00
d83cc6415e minecraft: fabric-api update 2025-04-18 20:10:16 -04:00
4214ac143d update 2025-04-18 17:47:03 -04:00
5d78c2d6e9 traffic shaping improvements 2025-04-17 18:36:52 -04:00
ba5b778c1a throttle torrenting when needed 2025-04-16 23:12:28 -04:00
491807c030 update 2025-04-16 21:34:01 -04:00
34a3502ee9 nixos-24.11-small -> nixos-24.11 2025-04-15 09:32:51 -04:00
7fd2a52c42 update 2025-04-15 09:29:43 -04:00
5480fd03e5 pam: fix file access error 2025-04-15 09:29:43 -04:00
b00ad9a33a deepcoder 14b 2025-04-14 13:11:40 -04:00
80b66beb54 add test for port uniqueness 2025-04-10 14:52:28 -04:00
2aa78bfc48 remove unused port 2025-04-10 14:45:38 -04:00
4358d1cf11 update 2025-04-10 14:41:48 -04:00
db78740db3 change llm model 2025-04-10 11:15:18 -04:00
4c212d7c06 zen 2 -> zen 3 2025-04-09 18:48:32 -04:00
5429dfb4ee update 2025-04-09 17:34:16 -04:00
bfe810827c jellyfin: remove commented out section 2025-04-09 17:34:11 -04:00
2452c544b7 minecraft: lithium update 2025-04-09 17:33:56 -04:00
983c204f05 minecraft: chmod 750 2025-04-08 14:12:22 -04:00
ab05092839 caddy: chmod 750 2025-04-08 14:12:03 -04:00
6e36a0c159 qbt: chmod 750 2025-04-08 14:11:36 -04:00
c8adc27fbe update 2025-04-08 09:48:07 -04:00
fe85083810 llm: model stuff 2025-04-08 00:22:12 -04:00
af1862fcea update 2025-04-07 23:10:33 -04:00
87aeee1903 simplify gitattributes 2025-04-07 16:53:41 -04:00
71c517116c jellyfin: remove packages from systemPackages 2025-04-07 14:41:58 -04:00
e3d38168aa minecraft: update lithium 2025-04-07 14:37:46 -04:00
75359b264b compile jellyfin-ffmpeg with compiler optimizations 2025-04-07 14:35:16 -04:00
3653e06c7d create single function to optimize for system 2025-04-07 14:33:34 -04:00
b764d2de45 move optimizeWithFlags 2025-04-07 14:31:56 -04:00
aa5c015099 kernel: 6.12 -> 6.6 (because of kworker issues) 2025-04-07 10:45:13 -04:00
5739adab1c update 2025-04-07 10:14:50 -04:00
018d11374b update 2025-04-06 14:45:43 -04:00
6cbdcc7a4f fix soulseek folder permissions 2025-04-06 14:45:39 -04:00
afd5310308 minecraft: add squaremap PR in comment 2025-04-06 14:45:30 -04:00
69a4d9b253 slskd: properly integrate into zfs volumes and permissions 2025-04-06 14:24:35 -04:00
b2af5954cc update 2025-04-05 13:54:03 -04:00
18743852c8 update 2025-04-03 14:55:26 -04:00
a688d9e264 fmt 2025-04-02 23:19:06 -04:00
f7356a8940 minecraft: 1.21.4 -> 1.21.5 2025-04-02 23:18:58 -04:00
e375de4fc7 update 2025-04-02 23:06:55 -04:00
2bbc2421d8 update 2025-04-02 10:11:48 -04:00
11164f0859 llm: use finetuned model 2025-04-02 10:11:41 -04:00
06feb4e1e2 gemma-3 27b 2025-03-31 21:52:14 -04:00
2d47c441fe llm: use Q4_0 quants (faster) 2025-03-31 18:33:24 -04:00
c31635bdd7 format 2025-03-31 17:04:41 -04:00
d8ab4ef59c update 2025-03-31 16:02:37 -04:00
1482429a00 llm: enable AVX2 2025-03-31 12:02:38 -04:00
6cc3d96362 llama-cpp: compiler optimizations 2025-03-31 11:17:56 -04:00
d5ac5c8cd8 gemma-3 12b 2025-03-31 10:31:29 -04:00
d774568e01 auth for llm 2025-03-31 10:29:36 -04:00
d34793c18f add llama-server 2025-03-31 03:59:54 -04:00
7d2bb541c3 update 2025-03-31 01:39:21 -04:00
bfb4202f61 update 2025-03-30 15:36:54 -04:00
0aa06ea4c6 update 2025-03-29 12:23:31 -04:00
17ea266501 update + zfs arc increase 2025-03-28 15:02:26 -04:00
a166e3a2c0 update 2025-03-28 00:02:17 -04:00
763e495d2d 0700 -> 0500 secureboot keys 2025-03-26 00:54:11 -04:00
eacd0d6c44 fix slskd env 2025-03-25 12:34:51 -04:00
5034f91bad fix string conversion 2025-03-25 11:41:20 -04:00
f04213dff4 zfs: arc 2048 -> 4096 mb 2025-03-25 11:40:38 -04:00
3447478847 secureboot: restrictive file permissions 2025-03-25 11:33:11 -04:00
eaec89e698 qbt: settings 2025-03-25 11:27:08 -04:00
a0dc72b3b0 deploy: target public ip 2025-03-25 11:26:49 -04:00
9970e33bfb flake update 2025-03-25 11:26:33 -04:00
d7c2b46ae3 update 2025-03-23 17:50:35 -04:00
e7731c3f67 fix qbittorrent mount requirements 2025-03-22 21:21:26 -04:00
7ba8e8b3e7 immich: fix serverMountDeps 2025-03-22 20:50:31 -04:00
f3fe629666 add serviceMountDeps 2025-03-22 20:46:55 -04:00
9a45c808fd add comment about deploying configs to server 2025-03-22 20:06:58 -04:00
235bb1da1a minecraft: move let..in statement 2025-03-22 20:01:17 -04:00
8da2e21c1d move some more caddy stuff to minecraft.nix 2025-03-22 18:34:50 -04:00
6f7e40dcde add nix formatter 2025-03-22 17:37:54 -04:00
b9fab67b53 formatting and nits 2025-03-22 17:20:56 -04:00
e573a1b8ed matrix: fix port 443 with delegation 2025-03-22 14:56:00 -04:00
90 changed files with 5421 additions and 622 deletions

13
.gitattributes vendored
View File

@@ -1,10 +1,3 @@
secrets/murmur_password filter=git-crypt diff=git-crypt
secrets/hashedPass filter=git-crypt diff=git-crypt
secrets/minecraft-whitelist.nix filter=git-crypt diff=git-crypt
secrets/wg0.conf filter=git-crypt diff=git-crypt
secrets/caddy_auth filter=git-crypt diff=git-crypt
secrets/matrix_reg_token filter=git-crypt diff=git-crypt
secrets/owntracks_caddy_auth filter=git-crypt diff=git-crypt
secrets/secureboot.tar filter=git-crypt diff=git-crypt
secrets/zfs-key filter=git-crypt diff=git-crypt
secrets/slskd_env filter=git-crypt diff=git-crypt
secrets/** filter=git-crypt diff=git-crypt
usb-secrets/usb-secrets-key* filter=git-crypt diff=git-crypt

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/result

View File

@@ -6,12 +6,20 @@
username,
eth_interface,
service_configs,
options,
...
}:
{
imports = [
./hardware.nix
./zfs.nix
./modules/hardware.nix
./modules/zfs.nix
./modules/impermanence.nix
./modules/usb-secrets.nix
./modules/age-secrets.nix
./modules/secureboot.nix
./modules/no-rgb.nix
./modules/security.nix
./modules/ntfy-alerts.nix
./services/postgresql.nix
./services/jellyfin.nix
@@ -19,14 +27,48 @@
./services/immich.nix
./services/gitea.nix
./services/minecraft.nix
./services/wg.nix
./services/qbittorrent.nix
./services/jellyfin-qbittorrent-monitor.nix
./services/bitmagnet.nix
./services/matrix.nix
./services/owntracks.nix
./services/arr/prowlarr.nix
./services/arr/sonarr.nix
./services/arr/radarr.nix
./services/arr/bazarr.nix
./services/arr/jellyseerr.nix
./services/arr/recyclarr.nix
./services/arr/init.nix
./services/soulseek.nix
./services/ups.nix
./services/bitwarden.nix
./services/matrix.nix
./services/coturn.nix
./services/livekit.nix
./services/monero.nix
./services/xmrig.nix
# KEEP UNTIL 2028
./services/caddy_senior_project.nix
./services/graphing-calculator.nix
./services/ssh.nix
./services/syncthing.nix
./services/ntfy.nix
./services/ntfy-alerts.nix
];
services.kmscon.enable = true;
systemd.targets = {
sleep.enable = false;
suspend.enable = false;
@@ -34,28 +76,50 @@
hybrid-sleep.enable = false;
};
# Disable serial getty on ttyS0 to prevent dmesg warnings
systemd.services."serial-getty@ttyS0".enable = false;
# srvos enables vim, i don't want to use vim, disable it here:
programs.vim = {
defaultEditor = false;
}
// lib.optionalAttrs (options.programs.vim ? enable) {
enable = false;
};
powerManagement = {
powertop.enable = true;
enable = true;
cpuFreqGovernor = "powersave";
};
# https://github.com/NixOS/nixpkgs/issues/101459#issuecomment-758306434
security.pam.loginLimits = [
{
domain = "*";
type = "soft";
item = "nofile";
value = "4096";
}
];
nix = {
# optimize the store
optimise.automatic = true;
# enable flakes!
settings = {
experimental-features = [
"nix-command"
"flakes"
];
# garbage collection
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
};
hardware.intelgpu.driver = "xe";
boot = {
# 6.12 LTS until 2027
kernelPackages = pkgs.linuxPackages_6_12;
# 6.12 LTS until 2026
kernelPackages = pkgs.linuxPackages_6_12_hardened;
loader = {
# Use the systemd-boot EFI boot loader.
@@ -67,26 +131,41 @@
initrd = {
compressor = "zstd";
supportedFilesystems = [ "f2fs" ];
};
loader.systemd-boot.enable = lib.mkForce false;
# BBR congestion control handles variable-latency VPN connections much
# better than CUBIC by probing bandwidth continuously rather than
# reacting to packet loss.
kernelModules = [ "tcp_bbr" ];
lanzaboote = {
enable = true;
pkiBundle = "/var/lib/sbctl";
kernel.sysctl = {
# Use BBR + fair queuing for smooth throughput through the WireGuard VPN
"net.core.default_qdisc" = "fq";
"net.ipv4.tcp_congestion_control" = "bbr";
# Disable slow-start after idle: prevents TCP from resetting window
# size on each burst cycle (the primary cause of the 0 -> 40 MB/s spikes)
"net.ipv4.tcp_slow_start_after_idle" = 0;
# Larger socket buffers to accommodate the VPN bandwidth-delay product
# (22ms RTT * target throughput). Current 2.5MB max is too small.
"net.core.rmem_max" = 16777216;
"net.core.wmem_max" = 16777216;
"net.ipv4.tcp_rmem" = "4096 87380 16777216";
"net.ipv4.tcp_wmem" = "4096 65536 16777216";
# Higher backlog for the large number of concurrent torrent connections
"net.core.netdev_max_backlog" = 5000;
# Minecraft server optimizations
# Disable autogroup for better scheduling of game server threads
"kernel.sched_autogroup_enabled" = 0;
# Huge pages for Minecraft JVM (4000MB heap / 2MB per page + ~200 overhead)
"vm.nr_hugepages" = 2200;
};
};
system.activationScripts = {
# extract all my secureboot keys
"secureboot-keys".text = ''
#!/bin/sh
rm -fr ${config.boot.lanzaboote.pkiBundle} || true
mkdir -p ${config.boot.lanzaboote.pkiBundle}
${pkgs.gnutar}/bin/tar xf ${./secrets/secureboot.tar} -C ${config.boot.lanzaboote.pkiBundle}
'';
};
environment.etc = {
"issue".text = "";
};
@@ -94,20 +173,10 @@
# Set your time zone.
time.timeZone = "America/New_York";
# Enable the OpenSSH daemon.
services.openssh = {
enable = true;
settings = {
AllowUsers = [ username "root" ];
PasswordAuthentication = false;
PermitRootLogin = "yes"; # for deploying configs
};
};
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
vaapiVdpau
libva-vdpau-driver
intel-compute-runtime # OpenCL filter support (hardware tonemapping and subtitle burn-in)
vpl-gpu-rt # QSV on 11th gen or newer
];
@@ -145,93 +214,16 @@
lsof
(pkgs.writeShellApplication {
name = "disk-smart-test";
runtimeInputs = with pkgs; [
gnugrep
coreutils
smartmontools
];
# i gotta fix that
excludeShellChecks = [ "SC2010" ];
text = ''
#!/bin/sh
set -e
if [[ $EUID -ne 0 ]]; then
echo "This command requires root."
exit 2
fi
DISKS=$(ls /dev/sd* | grep -v "[0-9]$")
for i in $DISKS; do
echo -n "$i "
smartctl -a "$i" | grep "SMART overall-health self-assessment test result:" | cut -d' ' -f6
done
'';
})
(pkgs.writeShellApplication {
name = "reflac";
runtimeInputs = with pkgs; [ flac ];
excludeShellChecks = [ "2086" ];
text = builtins.readFile (
pkgs.fetchurl {
url = "https://raw.githubusercontent.com/chungy/reflac/refs/heads/master/reflac";
sha256 = "61c6cc8be3d276c6714e68b55e5de0e6491f50bbf195233073dbce14a1e278a7";
}
);
})
reflac
pfetch-rs
sbctl
# add `skdump`
libatasmart
];
systemd.services.no-rgb =
let
no-rgb = (
pkgs.writeShellApplication {
name = "no-rgb";
runtimeInputs = with pkgs; [
openrgb
coreutils
gnugrep
];
text = ''
#!/bin/sh
set -e
NUM_DEVICES=$(openrgb --noautoconnect --list-devices | grep -cE '^[0-9]+: ')
for i in $(seq 0 $((NUM_DEVICES - 1))); do
openrgb --noautoconnect --device "$i" --mode direct --color 000000
done
'';
}
);
in
{
description = "disable rgb";
serviceConfig = {
ExecStart = "${no-rgb}/bin/${no-rgb.name}";
Type = "oneshot";
};
wantedBy = [ "multi-user.target" ];
};
services.hardware.openrgb = {
enable = true;
package = pkgs.openrgb-with-all-plugins;
motherboard = "amd";
};
services.udev.packages = [ pkgs.openrgb-with-all-plugins ];
hardware.i2c.enable = true;
networking = {
nameservers = [
"1.1.1.1"
@@ -241,13 +233,15 @@
hostName = hostname;
hostId = "0f712d56";
firewall.enable = true;
firewall.trustedInterfaces = [ "wg-br" ];
useDHCP = false;
# enableIPv6 = false;
enableIPv6 = false;
interfaces.${eth_interface} = {
ipv4.addresses = [
{
address = "10.1.1.102";
address = "192.168.1.50";
# address = "10.1.1.102";
prefixLength = 24;
}
];
@@ -259,7 +253,8 @@
];
};
defaultGateway = {
address = "10.1.1.1";
#address = "10.1.1.1";
address = "192.168.1.1";
interface = eth_interface;
};
# TODO! fix this
@@ -269,7 +264,7 @@
# };
};
users.groups.${service_configs.torrent_group} = { };
users.groups.${service_configs.media_group} = { };
users.users.${username} = {
isNormalUser = true;
@@ -277,19 +272,11 @@
"wheel"
"video"
"render"
service_configs.torrent_group
];
hashedPasswordFile = builtins.toString ./secrets/hashedPass;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO4jL6gYOunUlUtPvGdML0cpbKSsPNqQ1jit4E7U1RyH" # laptop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBJjT5QZ3zRDb+V6Em20EYpSEgPW5e/U+06uQGJdraxi" # desktop
service_configs.media_group
];
hashedPasswordFile = config.age.secrets.hashedPass.path;
};
users.users.root.openssh.authorizedKeys.keys = config.users.users.${username}.openssh.authorizedKeys.keys;
# https://nixos.wiki/wiki/Fish#Setting_fish_as_your_shell
programs.fish.enable = true;
programs.bash = {
@@ -333,7 +320,7 @@
# };
# systemd.tmpfiles.rules = [
# "d /tank/music 775 ${username} users"
# "Z /tank/music 775 ${username} users"
# ];
system.stateVersion = "24.11";

View File

@@ -1,2 +0,0 @@
#!/bin/sh
nixos-rebuild switch --flake .#muffin --target-host root@server --build-host root@server --verbose

View File

@@ -1,4 +1,9 @@
{ inputs, ... }:
{
imports = [
inputs.disko.nixosModules.disko
];
disko.devices = {
disk = {
main = {
@@ -15,17 +20,40 @@
mountpoint = "/boot";
};
};
root = {
persistent = {
size = "20G";
content = {
type = "filesystem";
format = "f2fs";
mountpoint = "/persistent";
};
};
nix = {
size = "100%";
content = {
type = "filesystem";
format = "f2fs";
mountpoint = "/";
mountpoint = "/nix";
};
};
};
};
};
};
nodev = {
"/" = {
fsType = "tmpfs";
mountOptions = [
"defaults"
"size=2G"
"mode=755"
];
};
};
};
fileSystems."/persistent".neededForBoot = true;
fileSystems."/nix".neededForBoot = true;
}

421
flake.lock generated
View File

@@ -1,12 +1,57 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": [],
"home-manager": [
"home-manager"
],
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1770165109,
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
"owner": "ryantm",
"repo": "agenix",
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"arr-init": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772249948,
"narHash": "sha256-v68tO12mTCET68eZG583U+OlBL4f6kAoHS9iKA/xLzQ=",
"ref": "refs/heads/main",
"rev": "d21eb9f5b0a30bb487de7c0afbbbaf19324eaa49",
"revCount": 1,
"type": "git",
"url": "ssh://gitea@git.gardling.com/titaniumtown/arr-init"
},
"original": {
"type": "git",
"url": "ssh://gitea@git.gardling.com/titaniumtown/arr-init"
}
},
"crane": {
"locked": {
"lastModified": 1741148495,
"narHash": "sha256-EV8KUaIZ2/CdBXlutXrHoZYbWPeB65p5kKZk71gvDRI=",
"lastModified": 1771796463,
"narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=",
"owner": "ipetkov",
"repo": "crane",
"rev": "75390a36cd0c2cdd5f1aafd8a9f827d7107f2e53",
"rev": "3d3de3313e263e04894f284ac18177bd26169bad",
"type": "github"
},
"original": {
@@ -15,6 +60,28 @@
"type": "github"
}
},
"deploy-rs": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
],
"utils": "utils"
},
"locked": {
"lastModified": 1770019181,
"narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=",
"owner": "serokell",
"repo": "deploy-rs",
"rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171",
"type": "github"
},
"original": {
"owner": "serokell",
"repo": "deploy-rs",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
@@ -22,11 +89,11 @@
]
},
"locked": {
"lastModified": 1741786315,
"narHash": "sha256-VT65AE2syHVj6v/DGB496bqBnu1PXrrzwlw07/Zpllc=",
"lastModified": 1771881364,
"narHash": "sha256-A5uE/hMium5of/QGC6JwF5TGoDAfpNtW00T0s9u/PN8=",
"owner": "nix-community",
"repo": "disko",
"rev": "0d8c6ad4a43906d14abd5c60e0ffe7b587b213de",
"rev": "a4cb7bf73f264d40560ba527f9280469f1f081c6",
"type": "github"
},
"original": {
@@ -54,43 +121,38 @@
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"lanzaboote",
"nixpkgs"
]
},
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1740872218,
"narHash": "sha256-ZaMw0pdoUKigLpv9HiNDH2Pjnosg7NBYMJlHTIsHEUo=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3876f6b87db82f33775b1ef5ea343986105db764",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
"systems": "systems_4"
},
"locked": {
"lastModified": 1731533236,
@@ -110,7 +172,7 @@
"inputs": {
"nixpkgs": [
"lanzaboote",
"pre-commit-hooks-nix",
"pre-commit",
"nixpkgs"
]
},
@@ -135,37 +197,77 @@
]
},
"locked": {
"lastModified": 1742234739,
"narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
"lastModified": 1772020340,
"narHash": "sha256-aqBl3GNpCadMoJ/hVkWTijM1Aeilc278MjM+LA3jK6g=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
"rev": "36e38ca0d9afe4c55405fdf22179a5212243eecc",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"impermanence",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768598210,
"narHash": "sha256-kkgA32s/f4jaa4UG+2f8C225Qvclxnqs76mf8zvTVPg=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "c47b2cc64a629f8e075de52e4742de688f930dc6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"impermanence": {
"inputs": {
"home-manager": "home-manager_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1769548169,
"narHash": "sha256-03+JxvzmfwRu+5JafM0DLbxgHttOQZkUtDWBmeUkN8Y=",
"owner": "nix-community",
"repo": "impermanence",
"rev": "7b1d382faf603b6d264f58627330f9faa5cba149",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "impermanence",
"type": "github"
}
},
"lanzaboote": {
"inputs": {
"crane": "crane",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"nixpkgs": [
"nixpkgs"
],
"pre-commit-hooks-nix": "pre-commit-hooks-nix",
"pre-commit": "pre-commit",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1741442524,
"narHash": "sha256-tVcxLDLLho8dWcO81Xj/3/ANLdVs0bGyCPyKjp70JWk=",
"lastModified": 1772216104,
"narHash": "sha256-1TnGN26vnCEQk5m4AavJZxGZTb/6aZyphemRPRwFUfs=",
"owner": "nix-community",
"repo": "lanzaboote",
"rev": "d8099586d9a84308ffedac07880e7f07a0180ff4",
"rev": "dbe5112de965bbbbff9f0729a9789c20a65ab047",
"type": "github"
},
"original": {
@@ -176,18 +278,18 @@
},
"nix-minecraft": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils",
"flake-compat": "flake-compat_3",
"nixpkgs": [
"nixpkgs"
]
],
"systems": "systems_3"
},
"locked": {
"lastModified": 1742522051,
"narHash": "sha256-uDlj+5J7eTuFkDaNl9cYf++gJdEW23Z4zSuDcNANIQc=",
"lastModified": 1772160153,
"narHash": "sha256-lk5IxQzY9ZeeEyjKNT7P6dFnlRpQgkus4Ekc/+slypY=",
"owner": "Infinidoge",
"repo": "nix-minecraft",
"rev": "57464e795fd31ceef845d7ce454d3b83e80e283e",
"rev": "deca3fb710b502ba10cd5cdc8f66c2cc184b92df",
"type": "github"
},
"original": {
@@ -198,11 +300,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1742376361,
"narHash": "sha256-VFMgJkp/COvkt5dnkZB4D2szVdmF6DGm5ZdVvTUy61c=",
"lastModified": 1771969195,
"narHash": "sha256-qwcDBtrRvJbrrnv1lf/pREQi8t2hWZxVAyeMo7/E9sw=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "daaae13dff0ecc692509a1332ff9003d9952d7a9",
"rev": "41c6b421bdc301b2624486e11905c9af7b8ec68e",
"type": "github"
},
"original": {
@@ -214,42 +316,39 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1742562948,
"narHash": "sha256-QUnzAW7CW0sCkFN1Kez/8UVq8EbBGNKOfHZHIZON0XQ=",
"lastModified": 1772047000,
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7a04ccc42104e0554f0a2325930fe98db9a5325",
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11-small",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-qbt": {
"nixpkgs_2": {
"locked": {
"lastModified": 1738103934,
"narHash": "sha256-MhDdcDDdK2uscLU370r3V9PQcejx+2LVbMG8bjCXMb0=",
"lastModified": 1764517877,
"narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4f4706686c921ef202712a00da1c96f0100f6921",
"rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "pull/287923/head",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks-nix": {
"pre-commit": {
"inputs": {
"flake-compat": [
"lanzaboote",
"flake-compat"
],
"flake-compat": "flake-compat_2",
"gitignore": "gitignore",
"nixpkgs": [
"lanzaboote",
@@ -257,11 +356,11 @@
]
},
"locked": {
"lastModified": 1740915799,
"narHash": "sha256-JvQvtaphZNmeeV+IpHgNdiNePsIpHD5U/7QN5AeY44A=",
"lastModified": 1771858127,
"narHash": "sha256-Gtre9YoYl3n25tJH2AoSdjuwcqij5CPxL3U3xysYD08=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "42b1ba089d2034d910566bf6b40830af6b8ec732",
"rev": "49bbbfc218bf3856dfa631cead3b052d78248b83",
"type": "github"
},
"original": {
@@ -272,14 +371,22 @@
},
"root": {
"inputs": {
"agenix": "agenix",
"arr-init": "arr-init",
"deploy-rs": "deploy-rs",
"disko": "disko",
"home-manager": "home-manager",
"impermanence": "impermanence",
"lanzaboote": "lanzaboote",
"nix-minecraft": "nix-minecraft",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixpkgs-qbt": "nixpkgs-qbt",
"vpn-confinement": "vpn-confinement"
"senior_project-website": "senior_project-website",
"srvos": "srvos",
"trackerlist": "trackerlist",
"vpn-confinement": "vpn-confinement",
"website": "website",
"ytbn-graphing-software": "ytbn-graphing-software"
}
},
"rust-overlay": {
@@ -290,11 +397,11 @@
]
},
"locked": {
"lastModified": 1741228283,
"narHash": "sha256-VzqI+k/eoijLQ5am6rDFDAtFAbw8nltXfLBC6SIEJAE=",
"lastModified": 1771988922,
"narHash": "sha256-Fc6FHXtfEkLtuVJzd0B6tFYMhmcPLuxr90rWfb/2jtQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "38e9826bc4296c9daf18bc1e6aa299f3e932a403",
"rev": "f4443dc3f0b6c5e6b77d923156943ce816d1fcb9",
"type": "github"
},
"original": {
@@ -303,6 +410,63 @@
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"ytbn-graphing-software",
"nixpkgs"
]
},
"locked": {
"lastModified": 1764729618,
"narHash": "sha256-z4RA80HCWv2los1KD346c+PwNPzMl79qgl7bCVgz8X0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "52764074a85145d5001bf0aa30cb71936e9ad5b8",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"senior_project-website": {
"flake": false,
"locked": {
"lastModified": 1771869552,
"narHash": "sha256-veaVrRWCSy7HYAAjUFLw8HASKcj+3f0W+sCwS3QiaM4=",
"owner": "Titaniumtown",
"repo": "senior-project-website",
"rev": "28a2b93492dac877dce0b38f078eacf74fce26e7",
"type": "github"
},
"original": {
"owner": "Titaniumtown",
"repo": "senior-project-website",
"type": "github"
}
},
"srvos": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772071250,
"narHash": "sha256-LDWvJDR1J8xE8TBJjzWnOA0oVP/l9xBFC4npQPJDHN4=",
"owner": "nix-community",
"repo": "srvos",
"rev": "5cd73bcf984b72d8046e1175d13753de255adfb9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "srvos",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
@@ -318,13 +482,92 @@
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"trackerlist": {
"flake": false,
"locked": {
"lastModified": 1772233783,
"narHash": "sha256-2jPUBKpPuT4dCXwVFuZvTH3QyURixsfJZD7Zqs0atPY=",
"owner": "ngosang",
"repo": "trackerslist",
"rev": "85c4f103f130b070a192343c334f50c2f56b61a9",
"type": "github"
},
"original": {
"owner": "ngosang",
"repo": "trackerslist",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"vpn-confinement": {
"locked": {
"lastModified": 1742138327,
"narHash": "sha256-Y71Mjej98CjaUKa1ecAIOo0eJ1B3ZVQl2ng6xl7/s9Y=",
"lastModified": 1767604552,
"narHash": "sha256-FddhMxnc99KYOZ/S3YNqtDSoxisIhVtJ7L4s8XD2u0A=",
"owner": "Maroka-chan",
"repo": "VPN-Confinement",
"rev": "38eeb3bc501900b48d1caf8c52a5b7f2fb7a52c5",
"rev": "a6b2da727853886876fd1081d6bb2880752937f3",
"type": "github"
},
"original": {
@@ -332,6 +575,42 @@
"repo": "VPN-Confinement",
"type": "github"
}
},
"website": {
"flake": false,
"locked": {
"lastModified": 1768266466,
"narHash": "sha256-d4dZzEcIKuq4DhNtXczaflpRifAtcOgNr45W2Bexnps=",
"ref": "refs/heads/main",
"rev": "06011a27456b3b9f983ef1aa142b5773bcb52b6e",
"revCount": 23,
"type": "git",
"url": "https://git.gardling.com/titaniumtown/website"
},
"original": {
"type": "git",
"url": "https://git.gardling.com/titaniumtown/website"
}
},
"ytbn-graphing-software": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay_2"
},
"locked": {
"lastModified": 1765615270,
"narHash": "sha256-12C6LccKRe5ys0iRd+ob+BliswUSmqOKWhMTI8fNpr0=",
"ref": "refs/heads/main",
"rev": "ac6265eae734363f95909df9a3739bf6360fa721",
"revCount": 1130,
"type": "git",
"url": "https://git.gardling.com/titaniumtown/YTBN-Graphing-Software"
},
"original": {
"type": "git",
"url": "https://git.gardling.com/titaniumtown/YTBN-Graphing-Software"
}
}
},
"root": "root",

288
flake.nix
View File

@@ -2,7 +2,7 @@
description = "Flake for server muffin";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
lanzaboote = {
url = "github:nix-community/lanzaboote";
@@ -18,10 +18,8 @@
vpn-confinement.url = "github:Maroka-chan/VPN-Confinement";
nixpkgs-qbt.url = "github:NixOS/nixpkgs/pull/287923/head";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
@@ -29,54 +27,119 @@
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
srvos = {
url = "github:nix-community/srvos";
inputs.nixpkgs.follows = "nixpkgs";
};
deploy-rs = {
url = "github:serokell/deploy-rs";
inputs.nixpkgs.follows = "nixpkgs";
};
impermanence = {
url = "github:nix-community/impermanence";
inputs.nixpkgs.follows = "nixpkgs";
};
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
inputs.darwin.follows = "";
};
senior_project-website = {
url = "github:Titaniumtown/senior-project-website";
flake = false;
};
website = {
url = "git+https://git.gardling.com/titaniumtown/website";
flake = false;
};
trackerlist = {
url = "github:ngosang/trackerslist";
flake = false;
};
ytbn-graphing-software = {
url = "git+https://git.gardling.com/titaniumtown/YTBN-Graphing-Software";
};
arr-init = {
url = "git+ssh://gitea@git.gardling.com/titaniumtown/arr-init";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
nix-minecraft,
nixos-hardware,
vpn-confinement,
nixpkgs-qbt,
home-manager,
lanzaboote,
disko,
srvos,
deploy-rs,
impermanence,
arr-init,
...
}@inputs:
let
username = "primary";
hostname = "muffin";
eth_interface = "enp4s0";
system = "x86_64-linux";
service_configs = rec {
zpool_ssds = "tank";
zpool_hdds = "hdds";
torrents_path = "/torrents";
services_dir = "/${zpool_ssds}/services";
services_dir = "/services";
music_dir = "/${zpool_ssds}/music";
torrent_group = "media";
media_group = "media";
cpu_arch = "znver3";
# TODO: add checks to make sure none of these collide
ports = {
http = 80;
https = 443;
jellyfin = 8096; # no services.jellyfin option for this
torrent = 6011;
ollama = 11434;
bitmagnet = 3333;
owntracks = 3825;
gitea = 2283;
immich = 2284;
soulseek_web = 5030;
soulseek_listen = 50300;
llama_cpp = 8991;
vaultwarden = 8222;
syncthing_gui = 8384;
syncthing_protocol = 22000;
syncthing_discovery = 21027;
minecraft = 25565;
matrix = 6167;
matrix_federation = 8448;
coturn = 3478;
coturn_tls = 5349;
ntfy = 2586;
livekit = 7880;
lk_jwt = 8081;
prowlarr = 9696;
sonarr = 8989;
radarr = 7878;
bazarr = 6767;
jellyseerr = 5055;
};
https = {
certs = services_dir + "/http_certs";
# TODO! generate website from repo directly using hugo
data_dir = services_dir + "/http";
domain = "gardling.com";
wg_ip = "192.168.15.1";
matrix_hostname = "matrix.${service_configs.https.domain}";
};
gitea = {
@@ -86,6 +149,7 @@
postgres = {
socket = "/run/postgresql";
dataDir = services_dir + "/sql";
};
immich = {
@@ -103,16 +167,88 @@
};
jellyfin = {
dir = services_dir + "/jellyfin";
dataDir = services_dir + "/jellyfin";
cacheDir = services_dir + "/jellyfin_cache";
};
owntracks = {
data_dir = services_dir + "/owntracks";
slskd = rec {
base = "/var/lib/slskd";
downloads = base + "/downloads";
incomplete = base + "/incomplete";
};
vaultwarden = {
path = "/var/lib/vaultwarden";
};
monero = {
dataDir = services_dir + "/monero";
};
matrix = {
dataDir = "/var/lib/continuwuity";
domain = "matrix.${https.domain}";
};
ntfy = {
domain = "ntfy.${https.domain}";
};
livekit = {
domain = "livekit.${https.domain}";
};
syncthing = {
dataDir = services_dir + "/syncthing";
signalBackupDir = "/${zpool_ssds}/bak/signal";
grayjayBackupDir = "/${zpool_ssds}/bak/grayjay";
};
prowlarr = {
dataDir = services_dir + "/prowlarr";
};
sonarr = {
dataDir = services_dir + "/sonarr";
};
radarr = {
dataDir = services_dir + "/radarr";
};
bazarr = {
dataDir = services_dir + "/bazarr";
};
jellyseerr = {
configDir = services_dir + "/jellyseerr";
};
recyclarr = {
dataDir = services_dir + "/recyclarr";
};
media = {
moviesDir = torrents_path + "/media/movies";
tvDir = torrents_path + "/media/tv";
};
};
pkgs = import nixpkgs {
inherit system;
targetPlatform = system;
buildPlatform = builtins.currentSystem;
};
lib = import ./modules/lib.nix { inherit inputs pkgs service_configs; };
testSuite = import ./tests/tests.nix {
inherit pkgs lib inputs;
config = self.nixosConfigurations.muffin.config;
};
in
{
nixosConfigurations.${hostname} = nixpkgs.lib.nixosSystem {
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-tree;
nixosConfigurations.${hostname} = lib.nixosSystem {
inherit system;
specialArgs = {
inherit
username
@@ -122,45 +258,91 @@
inputs
;
};
modules =
[
./disk-config.nix
disko.nixosModules.disko
./configuration.nix
vpn-confinement.nixosModules.default
# import the `services.qbittorrent` module
(nixpkgs-qbt + "/nixos/modules/services/torrent/qbittorrent.nix")
# get nix-minecraft working!
nix-minecraft.nixosModules.minecraft-servers
modules = [
# SAFETY! make sure no ports collide
(
{ lib, ... }:
{
nixpkgs.overlays = [ nix-minecraft.overlay ];
config.assertions = [
{
assertion =
let
ports = lib.attrValues service_configs.ports;
uniquePorts = lib.unique ports;
in
(lib.length ports) == (lib.length uniquePorts);
message = "Duplicate ports detected in 'ports' configuration";
}
];
}
)
lanzaboote.nixosModules.lanzaboote
# sets up things like the watchdog
srvos.nixosModules.server
home-manager.nixosModules.home-manager
(
{
pkgs,
username,
home-manager,
stateVersion,
...
}:
{
home-manager.users.${username} = import ./home.nix;
}
)
]
++ (with nixos-hardware.nixosModules; [
common-cpu-amd-pstate
common-cpu-amd-zenpower
common-pc-ssd
common-gpu-intel
]);
# diff terminal support
srvos.nixosModules.mixins-terminfo
./disk-config.nix
./configuration.nix
{
nixpkgs.overlays = [
nix-minecraft.overlay
(import ./modules/overlays.nix)
];
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (nixpkgs.lib.getName pkg) [
"minecraft-server"
];
}
lanzaboote.nixosModules.lanzaboote
arr-init.nixosModules.default
home-manager.nixosModules.home-manager
(
{
home-manager,
...
}:
{
home-manager.users.${username} = import ./modules/home.nix;
}
)
]
++ (with nixos-hardware.nixosModules; [
common-cpu-amd-pstate
common-cpu-amd-zenpower
common-pc-ssd
common-gpu-intel
]);
};
deploy.nodes.muffin = {
hostname = "server-public";
profiles.system = {
sshUser = "root";
user = "root";
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.muffin;
};
};
checks.${system} = testSuite;
packages.${system} = {
tests = pkgs.linkFarm "all-tests" (
pkgs.lib.mapAttrsToList (name: test: {
name = name;
path = test;
}) testSuite
);
}
// (pkgs.lib.mapAttrs' (name: test: {
name = "test-${name}";
value = test;
}) testSuite);
};
}

View File

@@ -1,36 +0,0 @@
{
pkgs,
username,
stateVersion,
...
}:
{
home.stateVersion = "24.11";
programs.fish =
let
eza = "${pkgs.eza}/bin/eza --color=always --group-directories-first";
coreutils = "${pkgs.coreutils}/bin";
in
{
enable = true;
interactiveShellInit = ''
#disable greeting
set fish_greeting
#fixes gnupg password entry
export GPG_TTY=(${coreutils}/tty)
#pfetch on shell start (disable pkgs because of execution time)
PF_INFO="ascii title os host kernel uptime memory editor wm" ${pkgs.pfetch-rs}/bin/pfetch
'';
shellAliases = {
# from DistroTube's dot files: Changing "ls" to "eza"
ls = "${eza} -al";
la = "${eza} -a";
ll = "${eza} -l";
lt = "${eza} -aT";
};
};
}

84
modules/age-secrets.nix Normal file
View File

@@ -0,0 +1,84 @@
{
config,
lib,
pkgs,
inputs,
...
}:
{
imports = [
inputs.agenix.nixosModules.default
];
# Configure all agenix secrets
age.secrets = {
# ZFS encryption key
zfs-key = {
file = ../secrets/zfs-key.age;
mode = "0400";
owner = "root";
group = "root";
};
# Secureboot keys archive
secureboot-tar = {
file = ../secrets/secureboot.tar.age;
mode = "0400";
owner = "root";
group = "root";
};
# System passwords
hashedPass = {
file = ../secrets/hashedPass.age;
mode = "0400";
owner = "root";
group = "root";
};
# Service authentication
caddy_auth = {
file = ../secrets/caddy_auth.age;
mode = "0400";
owner = "caddy";
group = "caddy";
};
jellyfin-api-key = {
file = ../secrets/jellyfin-api-key.age;
mode = "0400";
owner = "root";
group = "root";
};
slskd_env = {
file = ../secrets/slskd_env.age;
mode = "0400";
owner = "root";
group = "root";
};
# Network configuration
wg0-conf = {
file = ../secrets/wg0.conf.age;
mode = "0400";
owner = "root";
group = "root";
};
# ntfy-alerts secrets
ntfy-alerts-topic = {
file = ../secrets/ntfy-alerts-topic.age;
mode = "0400";
owner = "root";
group = "root";
};
ntfy-alerts-token = {
file = ../secrets/ntfy-alerts-token.age;
mode = "0400";
owner = "root";
group = "root";
};
};
}

View File

@@ -19,7 +19,6 @@
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = true;
hardware.enableRedistributableFirmware = true;
}

31
modules/home.nix Normal file
View File

@@ -0,0 +1,31 @@
{
pkgs,
lib,
...
}:
{
home.stateVersion = "24.11";
programs.fish = {
enable = true;
interactiveShellInit = ''
# disable greeting
set fish_greeting
# pfetch on shell start (disable pkgs because of execution time)
PF_INFO="ascii title os host kernel uptime memory editor wm" ${lib.getExe pkgs.pfetch-rs}
'';
shellAliases =
let
eza = "${lib.getExe pkgs.eza} --color=always --group-directories-first";
in
{
# from DistroTube's dot files: Changing "ls" to "eza"
ls = "${eza} -al";
la = "${eza} -a";
ll = "${eza} -l";
lt = "${eza} -aT";
};
};
}

70
modules/impermanence.nix Normal file
View File

@@ -0,0 +1,70 @@
{
config,
lib,
pkgs,
username,
service_configs,
inputs,
...
}:
{
imports = [
inputs.impermanence.nixosModules.impermanence
];
environment.persistence."/persistent" = {
hideMounts = true;
directories = [
"/var/log"
"/var/lib/systemd/coredump"
"/var/lib/nixos"
"/var/lib/systemd/timers"
# ZFS cache directory - persisting the directory instead of the file
# avoids "device busy" errors when ZFS atomically updates the cache
"/etc/zfs"
];
files = [
# Machine ID
"/etc/machine-id"
];
users.${username} = {
files = [
".local/share/fish/fish_history"
];
};
users.root = {
files = [
".local/share/fish/fish_history"
];
};
};
# Store SSH host keys directly in /persistent to survive tmpfs root wipes.
# This is more reliable than bind mounts for service-generated files.
services.openssh.hostKeys = [
{
path = "/persistent/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
{
path = "/persistent/etc/ssh/ssh_host_rsa_key";
type = "rsa";
bits = 4096;
}
];
# Enforce root ownership on /persistent/etc. The impermanence activation
# script copies ownership from /persistent/etc to /etc via
# `chown --reference`. If /persistent/etc ever gets non-root ownership,
# sshd StrictModes rejects /etc/ssh/authorized_keys.d/root and root SSH
# breaks while non-root users still work.
# Use "z" (set ownership, non-recursive) not "d" (create only, no-op on existing).
systemd.tmpfiles.rules = [
"z /persistent/etc 0755 root root"
];
}

184
modules/lib.nix Normal file
View File

@@ -0,0 +1,184 @@
{
inputs,
pkgs,
service_configs,
...
}:
inputs.nixpkgs.lib.extend (
final: prev:
let
lib = prev;
in
{
# stolen from: https://stackoverflow.com/a/42398526
optimizeWithFlags =
pkg: flags:
lib.overrideDerivation pkg (
old:
let
newflags = lib.foldl' (acc: x: "${acc} ${x}") "" flags;
oldflags = if (lib.hasAttr "NIX_CFLAGS_COMPILE" old) then "${old.NIX_CFLAGS_COMPILE}" else "";
in
{
NIX_CFLAGS_COMPILE = "${oldflags} ${newflags}";
# stdenv = pkgs.clang19Stdenv;
}
);
optimizePackage =
pkg:
final.optimizeWithFlags pkg [
"-O3"
"-march=${service_configs.cpu_arch}"
"-mtune=${service_configs.cpu_arch}"
];
vpnNamespaceOpenPort =
port: service:
{ ... }:
{
vpnNamespaces.wg = {
portMappings = [
{
from = port;
to = port;
}
];
openVPNPorts = [
{
port = port;
protocol = "both";
}
];
};
systemd.services.${service}.vpnConfinement = {
enable = true;
vpnNamespace = "wg";
};
};
serviceMountWithZpool =
serviceName: zpool: dirs:
{ pkgs, config, ... }:
{
systemd.services."${serviceName}-mounts" = {
wants = [ "zfs.target" ] ++ lib.optionals (zpool != "") [ "zfs-import-${zpool}.service" ];
after = lib.optionals (zpool != "") [ "zfs-import-${zpool}.service" ];
before = [ "${serviceName}.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = [
(lib.getExe (
pkgs.writeShellApplication {
name = "ensure-zfs-mounts-with-pool-${serviceName}-${zpool}";
runtimeInputs = with pkgs; [
gawk
coreutils
config.boot.zfs.package
];
text = ''
set -euo pipefail
echo "Ensuring ZFS mounts for service: ${serviceName} (pool: ${zpool})"
echo "Directories: ${lib.strings.concatStringsSep ", " dirs}"
# Validate mounts exist (ensureZfsMounts already has proper PATH)
${lib.getExe pkgs.ensureZfsMounts} ${lib.strings.concatStringsSep " " dirs}
# Additional runtime check: verify paths are on correct zpool
${lib.optionalString (zpool != "") ''
echo "Verifying ZFS mountpoints are on pool '${zpool}'..."
if ! zfs_list_output=$(zfs list -H -o name,mountpoint 2>&1); then
echo "ERROR: Failed to query ZFS datasets: $zfs_list_output" >&2
exit 1
fi
# shellcheck disable=SC2043
for target in ${lib.strings.concatStringsSep " " dirs}; do
echo "Checking: $target"
# Find dataset that has this mountpoint
dataset=$(echo "$zfs_list_output" | awk -v target="$target" '$2 == target {print $1; exit}')
if [ -z "$dataset" ]; then
echo "ERROR: No ZFS dataset found for mountpoint: $target" >&2
exit 1
fi
# Extract pool name from dataset (first part before /)
actual_pool=$(echo "$dataset" | cut -d'/' -f1)
if [ "$actual_pool" != "${zpool}" ]; then
echo "ERROR: ZFS pool mismatch for $target" >&2
echo " Expected pool: ${zpool}" >&2
echo " Actual pool: $actual_pool" >&2
echo " Dataset: $dataset" >&2
exit 1
fi
echo "$target is on $dataset (pool: $actual_pool)"
done
echo "All paths verified successfully on pool '${zpool}'"
''}
echo "Mount validation completed for ${serviceName} (pool: ${zpool})"
'';
}
))
];
};
};
systemd.services.${serviceName} = {
wants = [
"${serviceName}-mounts.service"
];
after = [
"${serviceName}-mounts.service"
];
requires = [
"${serviceName}-mounts.service"
];
};
# assert that the pool is even enabled
#assertions = lib.optionals (zpool != "") [
# {
# assertion = builtins.elem zpool config.boot.zfs.extraPools;
# message = "${zpool} is not enabled in `boot.zfs.extraPools`";
# }
#];
};
serviceFilePerms =
serviceName: tmpfilesRules:
{ pkgs, ... }:
let
confFile = pkgs.writeText "${serviceName}-file-perms.conf" (
lib.concatStringsSep "\n" tmpfilesRules
);
in
{
systemd.services."${serviceName}-file-perms" = {
after = [ "${serviceName}-mounts.service" ];
before = [ "${serviceName}.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.systemd}/bin/systemd-tmpfiles --create ${confFile}";
};
};
systemd.services.${serviceName} = {
wants = [ "${serviceName}-file-perms.service" ];
after = [ "${serviceName}-file-perms.service" ];
};
};
}
)

66
modules/no-rgb.nix Normal file
View File

@@ -0,0 +1,66 @@
{
config,
lib,
pkgs,
...
}:
{
systemd.services.no-rgb =
let
no-rgb = (
pkgs.writeShellApplication {
name = "no-rgb";
runtimeInputs = with pkgs; [
openrgb
coreutils
gnugrep
];
text = ''
# Retry loop to wait for hardware to be ready
NUM_DEVICES=0
for attempt in 1 2 3 4 5; do
DEVICE_LIST=$(openrgb --noautoconnect --list-devices 2>/dev/null) || DEVICE_LIST=""
NUM_DEVICES=$(echo "$DEVICE_LIST" | grep -cE '^[0-9]+: ') || NUM_DEVICES=0
if [ "$NUM_DEVICES" -gt 0 ]; then
break
fi
if [ "$attempt" -lt 5 ]; then
sleep 2
fi
done
# If no devices found after retries, exit gracefully
if [ "$NUM_DEVICES" -eq 0 ]; then
exit 0
fi
# Disable RGB on each device
for i in $(seq 0 $((NUM_DEVICES - 1))); do
openrgb --noautoconnect --device "$i" --mode direct --color 000000 || true
done
'';
}
);
in
{
description = "disable rgb";
after = [ "systemd-udev-settle.service" ];
serviceConfig = {
ExecStart = lib.getExe no-rgb;
Type = "oneshot";
Restart = "on-failure";
RestartSec = 5;
};
wantedBy = [ "multi-user.target" ];
};
services.hardware.openrgb = {
enable = true;
package = pkgs.openrgb-with-all-plugins;
motherboard = "amd";
};
services.udev.packages = [ pkgs.openrgb-with-all-plugins ];
hardware.i2c.enable = true;
}

132
modules/ntfy-alerts.nix Normal file
View File

@@ -0,0 +1,132 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ntfyAlerts;
curl = "${pkgs.curl}/bin/curl";
hostname = config.networking.hostName;
# Build the curl auth args as a proper bash array fragment
authCurlArgs =
if cfg.tokenFile != null then
''
if [ -f "${cfg.tokenFile}" ]; then
TOKEN=$(cat "${cfg.tokenFile}" 2>/dev/null || echo "")
if [ -n "$TOKEN" ]; then
AUTH_ARGS=(-H "Authorization: Bearer $TOKEN")
fi
fi
''
else
"";
# Systemd failure alert script
systemdAlertScript = pkgs.writeShellScript "ntfy-systemd-alert" ''
set -euo pipefail
UNIT_NAME="$1"
SERVER_URL="${cfg.serverUrl}"
TOPIC=$(cat "${cfg.topicFile}" 2>/dev/null | tr -d '[:space:]')
if [ -z "$TOPIC" ]; then
echo "ERROR: Could not read topic from ${cfg.topicFile}"
exit 1
fi
# Get journal output for context
JOURNAL_OUTPUT=$(${pkgs.systemd}/bin/journalctl -u "$UNIT_NAME" -n 15 --no-pager 2>/dev/null || echo "No journal output available")
# Build auth args
AUTH_ARGS=()
${authCurlArgs}
# Send notification
${curl} -sf --max-time 15 -X POST \
"$SERVER_URL/$TOPIC" \
-H "Title: [${hostname}] Service failed: $UNIT_NAME" \
-H "Priority: high" \
-H "Tags: warning" \
"''${AUTH_ARGS[@]}" \
-d "$JOURNAL_OUTPUT" || true
'';
in
{
options.services.ntfyAlerts = {
enable = lib.mkEnableOption "ntfy push notifications for system alerts";
serverUrl = lib.mkOption {
type = lib.types.str;
description = "The ntfy server URL (e.g. https://ntfy.example.com)";
example = "https://ntfy.example.com";
};
topicFile = lib.mkOption {
type = lib.types.path;
description = "Path to a file containing the ntfy topic name to publish alerts to.";
example = "/run/agenix/ntfy-alerts-topic";
};
tokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the ntfy auth token.
If set, uses Authorization: Bearer header for authentication.
'';
example = "/run/secrets/ntfy-token";
};
};
config = lib.mkIf cfg.enable {
# Per-service OnFailure for monitored services
systemd.services = {
"ntfy-alert@" = {
description = "Send ntfy notification for failed service %i";
unitConfig.OnFailure = lib.mkForce "";
serviceConfig = {
Type = "oneshot";
ExecStart = "${systemdAlertScript} %i";
TimeoutSec = 30;
};
};
# TODO: sanoid's ExecStartPre runs `zfs allow` which blocks on TXG sync;
# on the hdds pool (slow spinning disks + large async frees) this causes
# 30+ minute hangs and guaranteed timeouts. Suppress until we fix sanoid
# to run as root without `zfs allow`. See: nixpkgs#72060, openzfs/zfs#14180
"sanoid".unitConfig.OnFailure = lib.mkForce "";
};
# Global OnFailure drop-in for all services
systemd.packages = [
(pkgs.writeTextDir "etc/systemd/system/service.d/onfailure.conf" ''
[Unit]
OnFailure=ntfy-alert@%p.service
'')
# Sanoid-specific drop-in to override the global OnFailure (see TODO above)
(pkgs.writeTextDir "etc/systemd/system/sanoid.service.d/onfailure.conf" ''
[Unit]
OnFailure=
'')
];
# ZED (ZFS Event Daemon) ntfy notification settings
services.zfs.zed = {
enableMail = false;
settings = {
ZED_NTFY_URL = cfg.serverUrl;
ZED_NTFY_TOPIC = "$(cat ${cfg.topicFile} | tr -d '[:space:]')";
ZED_NTFY_ACCESS_TOKEN = lib.mkIf (cfg.tokenFile != null) "$(cat ${cfg.tokenFile})";
ZED_NOTIFY_VERBOSE = true;
};
};
};
}

46
modules/overlays.nix Normal file
View File

@@ -0,0 +1,46 @@
final: prev: {
ensureZfsMounts = prev.writeShellApplication {
name = "zfsEnsureMounted";
runtimeInputs = with prev; [
zfs
gawk
coreutils
];
text = ''
#!/bin/sh
if [[ "$#" -eq "0" ]]; then
echo "no arguments passed"
exit 1
fi
MOUNTED=$(zfs list -o mountpoint,mounted -H | awk '$NF == "yes" {NF--; print}')
MISSING=""
for target in "$@"; do
if ! grep -Fxq "$target" <<< "$MOUNTED"; then
MISSING="$MISSING $target"
fi
done
if [[ -n "$MISSING" ]]; then
echo "FAILURE, missing:$MISSING" 1>&2
exit 1
fi
'';
};
reflac = prev.writeShellApplication {
name = "reflac";
runtimeInputs = with prev; [ flac ];
excludeShellChecks = [ "2086" ];
text = builtins.readFile (
prev.fetchurl {
url = "https://raw.githubusercontent.com/chungy/reflac/refs/heads/master/reflac";
sha256 = "61c6cc8be3d276c6714e68b55e5de0e6491f50bbf195233073dbce14a1e278a7";
}
);
};
}

41
modules/secureboot.nix Normal file
View File

@@ -0,0 +1,41 @@
{
config,
lib,
pkgs,
...
}:
{
boot = {
loader.systemd-boot.enable = lib.mkForce false;
lanzaboote = {
enable = true;
# needed to be in `/etc/secureboot` for sbctl to work
pkiBundle = "/etc/secureboot";
};
};
system.activationScripts = {
# extract secureboot keys from agenix-decrypted tar
"secureboot-keys" = {
deps = [ "agenix" ];
text = ''
#!/bin/sh
# Check if keys already exist (e.g., from disko-install)
if [[ -d ${config.boot.lanzaboote.pkiBundle} && -f ${config.boot.lanzaboote.pkiBundle}/db.key ]]; then
echo "Secureboot keys already present, skipping extraction"
chown -R root:wheel ${config.boot.lanzaboote.pkiBundle}
chmod -R 500 ${config.boot.lanzaboote.pkiBundle}
else
echo "Extracting secureboot keys from agenix"
rm -fr ${config.boot.lanzaboote.pkiBundle} || true
mkdir -p ${config.boot.lanzaboote.pkiBundle}
${pkgs.gnutar}/bin/tar xf ${config.age.secrets.secureboot-tar.path} -C ${config.boot.lanzaboote.pkiBundle}
chown -R root:wheel ${config.boot.lanzaboote.pkiBundle}
chmod -R 500 ${config.boot.lanzaboote.pkiBundle}
fi
'';
};
};
}

37
modules/security.nix Normal file
View File

@@ -0,0 +1,37 @@
{
config,
lib,
pkgs,
...
}:
{
# memory allocator
# BREAKS REDIS-IMMICH
# environment.memoryAllocator.provider = "graphene-hardened";
# disable coredumps
systemd.coredump.enable = false;
services = {
dbus.implementation = "broker";
/*
logrotate.enable = true;
journald = {
storage = "volatile"; # Store logs in memory
upload.enable = false; # Disable remote log upload (the default)
extraConfig = ''
SystemMaxUse=500M
SystemMaxFileSize=50M
'';
};
*/
};
services.fail2ban = {
enable = true;
# Use iptables actions for compatibility
banaction = "iptables-multiport";
banaction-allports = "iptables-allports";
};
}

22
modules/usb-secrets.nix Normal file
View File

@@ -0,0 +1,22 @@
{
config,
lib,
pkgs,
...
}:
{
# Mount USB secrets drive via fileSystems
fileSystems."/mnt/usb-secrets" = {
device = "/dev/disk/by-label/SECRETS";
fsType = "vfat";
options = [
"ro"
"uid=root"
"gid=root"
"umask=377"
];
neededForBoot = true;
};
age.identityPaths = [ "/mnt/usb-secrets/usb-secrets-key" ];
}

View File

@@ -1,29 +1,40 @@
{
config,
service_configs,
pkgs,
...
}:
let
# DO NOT CHANGE
# path is set via a zfs property
zfs-key = "/etc/zfs-key";
in
{
system.activationScripts = {
# Copy decrypted ZFS key from agenix to expected location
# /etc is on tmpfs due to impermanence, so no persistent storage risk
"zfs-key".text = ''
#!/bin/sh
rm -fr ${zfs-key} || true
cp ${./secrets/zfs-key} ${zfs-key}
chmod 0500 ${zfs-key}
chown root:wheel ${zfs-key}
rm -f ${zfs-key} || true
cp ${config.age.secrets.zfs-key.path} ${zfs-key}
chmod 0400 ${zfs-key}
chown root:root ${zfs-key}
'';
};
boot.zfs.package = pkgs.zfs_unstable;
boot.zfs.package = pkgs.zfs;
boot.initrd.kernelModules = [ "zfs" ];
boot.kernelParams = [
# 2048MB
"zfs.zfs_arc_max=2048000000"
];
boot.kernelParams =
let
gb = 20;
mb = gb * 1000;
kb = mb * 1000;
b = kb * 1000;
in
[
"zfs.zfs_arc_max=${builtins.toString b}"
];
boot.supportedFilesystems = [ "zfs" ];
boot.zfs.extraPools = [
@@ -53,7 +64,7 @@ in
yearly = 0;
};
datasets."${service_configs.zpool_ssds}/services/jellyfin_cache" = {
datasets."${service_configs.zpool_ssds}/services/jellyfin/cache" = {
recursive = true;
autoprune = true;
autosnap = true;

88
scripts/install.sh Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
DISK="${1:-}"
FLAKE_DIR="$(dirname "$(realpath "$0")")"
if [[ -z "$DISK" ]]; then
echo "Usage: $0 <disk_device>"
echo "Example: $0 /dev/nvme0n1"
echo " $0 /dev/sda"
exit 1
fi
if [[ ! -b "$DISK" ]]; then
echo "Error: $DISK is not a block device"
exit 1
fi
echo "Installing NixOS to $DISK using flake at $FLAKE_DIR"
# Create temporary directories
mkdir -p /tmp/secureboot
mkdir -p /tmp/persistent
# Function to cleanup on exit
cleanup() {
echo "Cleaning up..."
rm -rf /tmp/secureboot 2>/dev/null || true
rm -rf /tmp/persistent 2>/dev/null || true
}
trap cleanup EXIT
# Decrypt secureboot keys using the key in the repo
echo "Decrypting secureboot keys..."
if [[ ! -f "$FLAKE_DIR/usb-secrets/usb-secrets-key" ]]; then
echo "Error: usb-secrets-key not found at $FLAKE_DIR/usb-secrets/usb-secrets-key"
exit 1
fi
nix-shell -p age --run "age -d -i '$FLAKE_DIR/usb-secrets/usb-secrets-key' '$FLAKE_DIR/secrets/secureboot.tar.age'" | \
tar -x -C /tmp/secureboot
echo "Secureboot keys extracted"
# Extract persistent partition secrets
echo "Extracting persistent partition contents..."
if [[ -f "$FLAKE_DIR/secrets/persistent.tar" ]]; then
tar -xzf "$FLAKE_DIR/secrets/persistent.tar" -C /tmp/persistent
echo "Persistent partition contents extracted"
else
echo "Warning: persistent.tar not found, skipping persistent secrets"
fi
# Check if disko-install is available
if ! command -v disko-install >/dev/null 2>&1; then
echo "Running disko-install via nix..."
DISKO_INSTALL="nix run github:nix-community/disko#disko-install --"
else
DISKO_INSTALL="disko-install"
fi
echo "Running disko-install to partition, format, and install NixOS..."
# Build the extra-files arguments
EXTRA_FILES_ARGS=(
--extra-files /tmp/secureboot /etc/secureboot
--extra-files "$FLAKE_DIR/usb-secrets/usb-secrets-key" /mnt/usb-secrets/usb-secrets-key
)
# Add each top-level item from persistent separately to avoid nesting
# cp -ar creates /dst/src when copying directories, so we need to copy each item
#
# Also disko-install actually copies the files from extra-files, so we are good here
if [[ -d /tmp/persistent ]] && [[ -n "$(ls -A /tmp/persistent 2>/dev/null)" ]]; then
for item in /tmp/persistent/*; do
if [[ -e "$item" ]]; then
basename=$(basename "$item")
EXTRA_FILES_ARGS+=(--extra-files "$item" "/persistent/$basename")
fi
done
fi
# Run disko-install with secureboot keys available
sudo $DISKO_INSTALL \
--mode format \
--flake "$FLAKE_DIR#muffin" \
--disk main "$DISK" \
"${EXTRA_FILES_ARGS[@]}"

Binary file not shown.

BIN
secrets/caddy_auth.age Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
secrets/hashedPass.age Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/livekit_keys Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
secrets/persistent.tar Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/secureboot.tar.age Normal file

Binary file not shown.

BIN
secrets/slskd_env.age Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/wg0.conf.age Normal file

Binary file not shown.

BIN
secrets/xmrig-wallet Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/zfs-key.age Normal file

Binary file not shown.

34
services/arr/bazarr.nix Normal file
View File

@@ -0,0 +1,34 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "bazarr" service_configs.zpool_ssds [
service_configs.bazarr.dataDir
])
(lib.serviceMountWithZpool "bazarr" service_configs.zpool_hdds [
service_configs.torrents_path
])
(lib.serviceFilePerms "bazarr" [
"Z ${service_configs.bazarr.dataDir} 0700 ${config.services.bazarr.user} ${config.services.bazarr.group}"
])
];
services.bazarr = {
enable = true;
listenPort = service_configs.ports.bazarr;
};
services.caddy.virtualHosts."bazarr.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${builtins.toString service_configs.ports.bazarr}
'';
users.users.${config.services.bazarr.user}.extraGroups = [
service_configs.media_group
];
}

115
services/arr/init.nix Normal file
View File

@@ -0,0 +1,115 @@
{ config, service_configs, ... }:
{
services.arrInit = {
prowlarr = {
enable = true;
serviceName = "prowlarr";
port = service_configs.ports.prowlarr;
dataDir = service_configs.prowlarr.dataDir;
apiVersion = "v1";
networkNamespacePath = "/run/netns/wg";
syncedApps = [
{
name = "Sonarr";
implementation = "Sonarr";
configContract = "SonarrSettings";
prowlarrUrl = "http://localhost:${builtins.toString service_configs.ports.prowlarr}";
baseUrl = "http://${config.vpnNamespaces.wg.bridgeAddress}:${builtins.toString service_configs.ports.sonarr}";
apiKeyFrom = "${service_configs.sonarr.dataDir}/config.xml";
syncCategories = [
5000
5010
5020
5030
5040
5045
5050
5090
];
serviceName = "sonarr";
}
{
name = "Radarr";
implementation = "Radarr";
configContract = "RadarrSettings";
prowlarrUrl = "http://localhost:${builtins.toString service_configs.ports.prowlarr}";
baseUrl = "http://${config.vpnNamespaces.wg.bridgeAddress}:${builtins.toString service_configs.ports.radarr}";
apiKeyFrom = "${service_configs.radarr.dataDir}/config.xml";
syncCategories = [
2000
2010
2020
2030
2040
2045
2050
2060
2070
2080
];
serviceName = "radarr";
}
];
};
sonarr = {
enable = true;
serviceName = "sonarr";
port = service_configs.ports.sonarr;
dataDir = service_configs.sonarr.dataDir;
rootFolders = [ service_configs.media.tvDir ];
downloadClients = [
{
name = "qBittorrent";
implementation = "QBittorrent";
configContract = "QBittorrentSettings";
fields = {
host = config.vpnNamespaces.wg.namespaceAddress;
port = service_configs.ports.torrent;
useSsl = false;
tvCategory = "tvshows";
};
}
];
};
radarr = {
enable = true;
serviceName = "radarr";
port = service_configs.ports.radarr;
dataDir = service_configs.radarr.dataDir;
rootFolders = [ service_configs.media.moviesDir ];
downloadClients = [
{
name = "qBittorrent";
implementation = "QBittorrent";
configContract = "QBittorrentSettings";
fields = {
host = config.vpnNamespaces.wg.namespaceAddress;
port = service_configs.ports.torrent;
useSsl = false;
movieCategory = "movies";
};
}
];
};
};
services.bazarrInit = {
enable = true;
dataDir = "/var/lib/bazarr";
port = service_configs.ports.bazarr;
sonarr = {
enable = true;
dataDir = service_configs.sonarr.dataDir;
port = service_configs.ports.sonarr;
serviceName = "sonarr";
};
radarr = {
enable = true;
dataDir = service_configs.radarr.dataDir;
port = service_configs.ports.radarr;
serviceName = "radarr";
};
};
}

View File

@@ -0,0 +1,43 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "jellyseerr" service_configs.zpool_ssds [
service_configs.jellyseerr.configDir
])
(lib.serviceFilePerms "jellyseerr" [
"Z ${service_configs.jellyseerr.configDir} 0700 jellyseerr jellyseerr"
])
];
services.jellyseerr = {
enable = true;
port = service_configs.ports.jellyseerr;
configDir = service_configs.jellyseerr.configDir;
};
systemd.services.jellyseerr.serviceConfig = {
DynamicUser = lib.mkForce false;
User = "jellyseerr";
Group = "jellyseerr";
ReadWritePaths = [ service_configs.jellyseerr.configDir ];
};
users.users.jellyseerr = {
isSystemUser = true;
group = "jellyseerr";
home = service_configs.jellyseerr.configDir;
};
users.groups.jellyseerr = { };
services.caddy.virtualHosts."jellyseerr.${service_configs.https.domain}".extraConfig = ''
# import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${builtins.toString service_configs.ports.jellyseerr}
'';
}

30
services/arr/prowlarr.nix Normal file
View File

@@ -0,0 +1,30 @@
{
pkgs,
service_configs,
config,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "prowlarr" service_configs.zpool_ssds [
service_configs.prowlarr.dataDir
])
(lib.vpnNamespaceOpenPort service_configs.ports.prowlarr "prowlarr")
];
services.prowlarr = {
enable = true;
dataDir = service_configs.prowlarr.dataDir;
settings.server.port = service_configs.ports.prowlarr;
};
systemd.services.prowlarr.serviceConfig = {
ExecStartPre = "+${pkgs.coreutils}/bin/chown -R prowlarr /var/lib/prowlarr";
};
services.caddy.virtualHosts."prowlarr.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString service_configs.ports.prowlarr}
'';
}

36
services/arr/radarr.nix Normal file
View File

@@ -0,0 +1,36 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "radarr" service_configs.zpool_ssds [
service_configs.radarr.dataDir
])
(lib.serviceMountWithZpool "radarr" service_configs.zpool_hdds [
service_configs.torrents_path
])
(lib.serviceFilePerms "radarr" [
"Z ${service_configs.radarr.dataDir} 0700 ${config.services.radarr.user} ${config.services.radarr.group}"
])
];
services.radarr = {
enable = true;
dataDir = service_configs.radarr.dataDir;
settings.server.port = service_configs.ports.radarr;
settings.update.mechanism = "external";
};
services.caddy.virtualHosts."radarr.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${builtins.toString service_configs.ports.radarr}
'';
users.users.${config.services.radarr.user}.extraGroups = [
service_configs.media_group
];
}

202
services/arr/recyclarr.nix Normal file
View File

@@ -0,0 +1,202 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
let
radarrConfig = "${service_configs.radarr.dataDir}/config.xml";
sonarrConfig = "${service_configs.sonarr.dataDir}/config.xml";
appDataDir = "${service_configs.recyclarr.dataDir}/data";
# Runs as root (via + prefix) to read API keys, writes secrets.yml for recyclarr
generateSecrets = pkgs.writeShellScript "recyclarr-generate-secrets" ''
RADARR_KEY=$(${pkgs.gnugrep}/bin/grep -oP '(?<=<ApiKey>)[^<]+' ${radarrConfig})
SONARR_KEY=$(${pkgs.gnugrep}/bin/grep -oP '(?<=<ApiKey>)[^<]+' ${sonarrConfig})
cat > ${appDataDir}/secrets.yml <<EOF
movies_api_key: $RADARR_KEY
series_api_key: $SONARR_KEY
EOF
chown recyclarr:recyclarr ${appDataDir}/secrets.yml
chmod 600 ${appDataDir}/secrets.yml
'';
in
{
imports = [
(lib.serviceMountWithZpool "recyclarr" service_configs.zpool_ssds [
service_configs.recyclarr.dataDir
])
];
systemd.tmpfiles.rules = [
"d ${service_configs.recyclarr.dataDir} 0755 recyclarr recyclarr -"
"d ${appDataDir} 0755 recyclarr recyclarr -"
];
services.recyclarr = {
enable = true;
command = "sync";
schedule = "daily";
user = "recyclarr";
group = "recyclarr";
configuration = {
radarr.movies = {
base_url = "http://localhost:${builtins.toString service_configs.ports.radarr}";
include = [
{ template = "radarr-quality-definition-movie"; }
{ template = "radarr-quality-profile-remux-web-2160p"; }
{ template = "radarr-custom-formats-remux-web-2160p"; }
];
quality_profiles = [
{
name = "Remux + WEB 2160p";
upgrade = {
allowed = true;
until_quality = "Remux-2160p";
};
qualities = [
{ name = "Remux-2160p"; }
{
name = "WEB 2160p";
qualities = [
"WEBDL-2160p"
"WEBRip-2160p"
];
}
{ name = "Remux-1080p"; }
{ name = "Bluray-1080p"; }
{
name = "WEB 1080p";
qualities = [
"WEBDL-1080p"
"WEBRip-1080p"
];
}
{ name = "HDTV-1080p"; }
];
}
];
custom_formats = [
# Upscaled
{
trash_ids = [ "bfd8eb01832d646a0a89c4deb46f8564" ];
assign_scores_to = [
{
name = "Remux + WEB 2160p";
score = -10000;
}
];
}
# x265 (HD) - override template -10000 penalty
{
trash_ids = [ "dc98083864ea246d05a42df0d05f81cc" ];
assign_scores_to = [
{
name = "Remux + WEB 2160p";
score = 0;
}
];
}
# x265 (no HDR/DV) - override template -10000 penalty
{
trash_ids = [ "839bea857ed2c0a8e084f3cbdbd65ecb" ];
assign_scores_to = [
{
name = "Remux + WEB 2160p";
score = 0;
}
];
}
];
};
sonarr.series = {
base_url = "http://localhost:${builtins.toString service_configs.ports.sonarr}";
include = [
{ template = "sonarr-quality-definition-series"; }
{ template = "sonarr-v4-quality-profile-web-2160p"; }
{ template = "sonarr-v4-custom-formats-web-2160p"; }
];
quality_profiles = [
{
name = "WEB-2160p";
upgrade = {
allowed = true;
until_quality = "WEB 2160p";
};
qualities = [
{
name = "WEB 2160p";
qualities = [
"WEBDL-2160p"
"WEBRip-2160p"
];
}
{ name = "Bluray-1080p Remux"; }
{ name = "Bluray-1080p"; }
{
name = "WEB 1080p";
qualities = [
"WEBDL-1080p"
"WEBRip-1080p"
];
}
{ name = "HDTV-1080p"; }
];
}
];
custom_formats = [
# Upscaled
{
trash_ids = [ "23297a736ca77c0fc8e70f8edd7ee56c" ];
assign_scores_to = [
{
name = "WEB-2160p";
score = -10000;
}
];
}
# x265 (HD) - override template -10000 penalty
{
trash_ids = [ "47435ece6b99a0b477caf360e79ba0bb" ];
assign_scores_to = [
{
name = "WEB-2160p";
score = 0;
}
];
}
# x265 (no HDR/DV) - override template -10000 penalty
{
trash_ids = [ "9b64dff695c2115facf1b6ea59c9bd07" ];
assign_scores_to = [
{
name = "WEB-2160p";
score = 0;
}
];
}
];
};
};
};
# Add secrets generation before recyclarr runs
systemd.services.recyclarr = {
after = [
"network-online.target"
"radarr.service"
"sonarr.service"
];
wants = [ "network-online.target" ];
serviceConfig.ExecStartPre = "+${generateSecrets}";
};
}

42
services/arr/sonarr.nix Normal file
View File

@@ -0,0 +1,42 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "sonarr" service_configs.zpool_ssds [
service_configs.sonarr.dataDir
])
(lib.serviceMountWithZpool "sonarr" service_configs.zpool_hdds [
service_configs.torrents_path
])
(lib.serviceFilePerms "sonarr" [
"Z ${service_configs.sonarr.dataDir} 0700 ${config.services.sonarr.user} ${config.services.sonarr.group}"
])
];
systemd.tmpfiles.rules = [
"d /torrents/media 2775 root ${service_configs.media_group} -"
"d ${service_configs.media.tvDir} 2775 root ${service_configs.media_group} -"
"d ${service_configs.media.moviesDir} 2775 root ${service_configs.media_group} -"
];
services.sonarr = {
enable = true;
dataDir = service_configs.sonarr.dataDir;
settings.server.port = service_configs.ports.sonarr;
settings.update.mechanism = "external";
};
services.caddy.virtualHosts."sonarr.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${builtins.toString service_configs.ports.sonarr}
'';
users.users.${config.services.sonarr.user}.extraGroups = [
service_configs.media_group
];
}

View File

@@ -2,24 +2,13 @@
pkgs,
service_configs,
config,
lib,
...
}:
{
vpnNamespaces.wg = {
portMappings = [
{
from = service_configs.ports.bitmagnet;
to = service_configs.ports.bitmagnet;
}
];
openVPNPorts = [
{
port = service_configs.ports.bitmagnet;
protocol = "both";
}
];
};
imports = [
(lib.vpnNamespaceOpenPort service_configs.ports.bitmagnet "bitmagnet")
];
services.bitmagnet = {
enable = true;
@@ -36,13 +25,7 @@
};
services.caddy.virtualHosts."bitmagnet.${service_configs.https.domain}".extraConfig = ''
# tls internal
${builtins.readFile ../secrets/caddy_auth}
reverse_proxy ${service_configs.https.wg_ip}:${builtins.toString service_configs.ports.bitmagnet}
import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString service_configs.ports.bitmagnet}
'';
systemd.services.bitmagnet.vpnConfinement = {
enable = true;
vpnNamespace = "wg";
};
}

60
services/bitwarden.nix Normal file
View File

@@ -0,0 +1,60 @@
{
config,
lib,
pkgs,
service_configs,
...
}:
{
imports = [
(lib.serviceMountWithZpool "vaultwarden" service_configs.zpool_ssds [
service_configs.vaultwarden.path
config.services.vaultwarden.backupDir
])
(lib.serviceMountWithZpool "backup-vaultwarden" service_configs.zpool_ssds [
service_configs.vaultwarden.path
config.services.vaultwarden.backupDir
])
(lib.serviceFilePerms "vaultwarden" [
"Z ${service_configs.vaultwarden.path} 0700 vaultwarden vaultwarden"
"Z ${config.services.vaultwarden.backupDir} 0700 vaultwarden vaultwarden"
])
];
services.vaultwarden = {
enable = true;
backupDir = "/${service_configs.zpool_ssds}/bak/vaultwarden";
config = {
# Refer to https://github.com/dani-garcia/vaultwarden/blob/main/.env.template
DOMAIN = "https://bitwarden.${service_configs.https.domain}";
SIGNUPS_ALLOWED = false;
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = service_configs.ports.vaultwarden;
ROCKET_LOG = "critical";
};
};
services.caddy.virtualHosts."bitwarden.${service_configs.https.domain}".extraConfig = ''
encode zstd gzip
reverse_proxy :${toString config.services.vaultwarden.config.ROCKET_PORT} {
header_up X-Real-IP {remote_host}
}
'';
# Protect Vaultwarden login from brute force attacks
services.fail2ban.jails.vaultwarden = {
enabled = true;
settings = {
backend = "systemd";
port = "http,https";
# defaults: maxretry=5, findtime=10m, bantime=10m
};
filter.Definition = {
failregex = ''^.*Username or password is incorrect\. Try again\. IP: <HOST>\..*$'';
ignoreregex = "";
journalmatch = "_SYSTEMD_UNIT=vaultwarden.service";
};
};
}

View File

@@ -1,18 +1,61 @@
{
config,
service_configs,
username,
pkgs,
lib,
inputs,
...
}:
let
theme = pkgs.fetchFromGitHub {
owner = "kaiiiz";
repo = "hugo-theme-monochrome";
rev = "d17e05715e91f41a842f2656e6bdd70cba73de91";
sha256 = "h9I2ukugVrldIC3SXefS0L3R245oa+TuRChOCJJgF24=";
};
hugo-neko = pkgs.fetchFromGitHub {
owner = "ystepanoff";
repo = "hugo-neko";
rev = "5a50034acbb1ae0cec19775af64e7167ca22725e";
sha256 = "VLwr4zEeFQU/b+vj0XTLSuEiosuNFu2du4uud7m8bnw=";
};
hugoWebsite = pkgs.stdenv.mkDerivation {
pname = "hugo-site";
version = "0.1";
src = inputs.website;
nativeBuildInputs = with pkgs; [
hugo
go
git
];
installPhase = ''
rm -fr themes/theme modules/hugo-neko
cp -r ${theme} themes/theme
cp -r ${hugo-neko} modules/hugo-neko
hugo --minify -d $out;
'';
};
in
{
imports = [
(lib.serviceMountWithZpool "caddy" service_configs.zpool_ssds [
config.services.caddy.dataDir
])
];
services.caddy = {
enable = true;
email = "titaniumtown@proton.me";
virtualHosts = {
${service_configs.https.domain} = {
extraConfig = ''
root * ${service_configs.https.data_dir}
root * ${hugoWebsite}
file_server browse
'';
@@ -22,7 +65,7 @@
};
systemd.tmpfiles.rules = [
"d ${service_configs.https.data_dir} 770 ${config.services.caddy.user} ${config.services.caddy.group}"
"d ${config.services.caddy.dataDir} 700 ${config.services.caddy.user} ${config.services.caddy.group}"
];
systemd.packages = with pkgs; [ nssTools ];
@@ -31,25 +74,34 @@
service_configs.ports.https
# http (but really acmeCA challenges)
80
# for matrix federation
8448
service_configs.ports.http
];
networking.firewall.allowedUDPPorts = [
service_configs.ports.https
# for matrix federation
8448
];
users.users.${config.services.caddy.user}.extraGroups = [
# for `map.gardling.com`
"minecraft"
];
# Protect Caddy basic auth endpoints from brute force attacks
services.fail2ban.jails.caddy-auth = {
enabled = true;
settings = {
backend = "auto";
port = "http,https";
logpath = "/var/log/caddy/access-*.log";
# defaults: maxretry=5, findtime=10m, bantime=10m
users.users.${username}.extraGroups = [
config.services.caddy.group
];
# Ignore local network IPs - NAT hairpinning causes all LAN traffic to
# appear from the router IP (192.168.1.1). Banning it blocks all internal access.
ignoreip = "127.0.0.1/8 ::1 192.168.1.0/24";
};
filter.Definition = {
# Only match 401s where an Authorization header was actually sent.
# Without this, the normal HTTP Basic Auth challenge-response flow
# (browser probes without credentials, gets 401, then resends with
# credentials) counts every page visit as a "failure."
failregex = ''^.*"remote_ip":"<HOST>".*"Authorization":\["REDACTED"\].*"status":401.*$'';
ignoreregex = "";
datepattern = ''"ts":{Epoch}\.'';
};
};
}

View File

@@ -0,0 +1,39 @@
{
config,
lib,
pkgs,
service_configs,
inputs,
...
}:
let
theme = pkgs.fetchFromGitHub {
owner = "kaiiiz";
repo = "hugo-theme-monochrome";
rev = "d17e05715e91f41a842f2656e6bdd70cba73de91";
sha256 = "h9I2ukugVrldIC3SXefS0L3R245oa+TuRChOCJJgF24=";
};
hugoWebsite = pkgs.stdenv.mkDerivation {
pname = "hugo-site";
version = "0.1";
src = inputs.senior_project-website;
nativeBuildInputs = with pkgs; [
hugo
];
installPhase = ''
rm -fr themes/theme
cp -rv ${theme} themes/theme
hugo --minify -d $out;
'';
};
in
{
services.caddy.virtualHosts."senior-project.${service_configs.https.domain}".extraConfig = ''
root * ${hugoWebsite}
file_server browse
'';
}

59
services/coturn.nix Normal file
View File

@@ -0,0 +1,59 @@
{
config,
lib,
service_configs,
...
}:
{
services.coturn = {
enable = true;
realm = service_configs.https.domain;
use-auth-secret = true;
static-auth-secret = lib.strings.trim (builtins.readFile ../secrets/coturn_static_auth_secret);
listening-port = service_configs.ports.coturn;
tls-listening-port = service_configs.ports.coturn_tls;
no-cli = true;
# recommended security settings from Synapse's coturn docs
extraConfig = ''
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
'';
};
# coturn needs these ports open
networking.firewall = {
allowedTCPPorts = [
service_configs.ports.coturn
service_configs.ports.coturn_tls
];
allowedUDPPorts = [
service_configs.ports.coturn
service_configs.ports.coturn_tls
];
# relay port range
allowedUDPPortRanges = [
{
from = config.services.coturn.min-port;
to = config.services.coturn.max-port;
}
];
};
}

View File

@@ -1,10 +1,18 @@
{
pkgs,
lib,
config,
service_configs,
username,
...
}:
{
imports = [
(lib.serviceMountWithZpool "gitea" service_configs.zpool_ssds [ config.services.gitea.stateDir ])
(lib.serviceFilePerms "gitea" [
"Z ${config.services.gitea.stateDir} 0700 ${config.services.gitea.user} ${config.services.gitea.group}"
])
];
services.gitea = {
enable = true;
appName = "Simon Gardling's Gitea instance";
@@ -36,11 +44,6 @@
reverse_proxy :${builtins.toString config.services.gitea.settings.server.HTTP_PORT}
'';
systemd.tmpfiles.rules = [
# 0700 for ssh permission reasons
"d ${config.services.gitea.stateDir} 0700 ${config.services.gitea.user} ${config.services.gitea.group}"
];
services.postgresql = {
ensureDatabases = [ config.services.gitea.user ];
ensureUsers = [
@@ -54,7 +57,18 @@
services.openssh.settings.AllowUsers = [ config.services.gitea.user ];
users.users.${username}.extraGroups = [
config.services.gitea.group
];
# Protect Gitea login from brute force attacks
services.fail2ban.jails.gitea = {
enabled = true;
settings = {
backend = "systemd";
port = "http,https";
# defaults: maxretry=5, findtime=10m, bantime=10m
};
filter.Definition = {
failregex = "^.*Failed authentication attempt for .* from <HOST>:.*$";
ignoreregex = "";
journalmatch = "_SYSTEMD_UNIT=gitea.service";
};
};
}

View File

@@ -0,0 +1,16 @@
{
service_configs,
inputs,
pkgs,
...
}:
let
graphing-calculator =
inputs.ytbn-graphing-software.packages.${pkgs.stdenv.targetPlatform.system}.web;
in
{
services.caddy.virtualHosts."graphing.${service_configs.https.domain}".extraConfig = ''
root * ${graphing-calculator}
file_server browse
'';
}

View File

@@ -2,10 +2,22 @@
service_configs,
pkgs,
config,
username,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "immich-server" service_configs.zpool_ssds [
config.services.immich.mediaLocation
])
(lib.serviceMountWithZpool "immich-machine-learning" service_configs.zpool_ssds [
config.services.immich.mediaLocation
])
(lib.serviceFilePerms "immich-server" [
"Z ${config.services.immich.mediaLocation} 0770 ${config.services.immich.user} ${config.services.immich.group}"
])
];
services.immich = {
enable = true;
mediaLocation = service_configs.immich.dir;
@@ -21,10 +33,6 @@
reverse_proxy :${builtins.toString config.services.immich.port}
'';
systemd.tmpfiles.rules = [
"d ${config.services.immich.mediaLocation} 0770 ${config.services.immich.user} ${config.services.immich.group}"
];
environment.systemPackages = with pkgs; [
immich-go
];
@@ -34,7 +42,18 @@
"render"
];
users.users.${username}.extraGroups = [
config.services.immich.group
];
# Protect Immich login from brute force attacks
services.fail2ban.jails.immich = {
enabled = true;
settings = {
backend = "systemd";
port = "http,https";
# defaults: maxretry=5, findtime=10m, bantime=10m
};
filter.Definition = {
failregex = "^.*Failed login attempt for user .* from ip address <HOST>.*$";
ignoreregex = "";
journalmatch = "_SYSTEMD_UNIT=immich-server.service";
};
};
}

View File

@@ -0,0 +1,57 @@
{
pkgs,
service_configs,
config,
...
}:
{
systemd.services."jellyfin-qbittorrent-monitor" = {
description = "Monitor Jellyfin streaming and control qBittorrent rate limits";
after = [
"network.target"
"jellyfin.service"
"qbittorrent.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
export JELLYFIN_API_KEY=$(cat $CREDENTIALS_DIRECTORY/jellyfin-api-key)
exec ${
pkgs.python3.withPackages (ps: with ps; [ requests ])
}/bin/python ${./jellyfin-qbittorrent-monitor.py}
'';
Restart = "always";
RestartSec = "10s";
# Security hardening
DynamicUser = true;
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
# Load credentials from agenix secrets
LoadCredential = "jellyfin-api-key:${config.age.secrets.jellyfin-api-key.path}";
};
environment = {
JELLYFIN_URL = "http://localhost:${builtins.toString service_configs.ports.jellyfin}";
QBITTORRENT_URL = "http://${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString service_configs.ports.torrent}";
CHECK_INTERVAL = "30";
# Bandwidth budget configuration
TOTAL_BANDWIDTH_BUDGET = "30000000"; # 30 Mbps in bits per second
SERVICE_BUFFER = "5000000"; # 5 Mbps reserved for other services (bps)
DEFAULT_STREAM_BITRATE = "10000000"; # 10 Mbps fallback when bitrate unknown (bps)
MIN_TORRENT_SPEED = "100"; # KB/s - below this, pause torrents instead
STREAM_BITRATE_HEADROOM = "1.1"; # multiplier per stream for bitrate fluctuations
};
};
}

View File

@@ -0,0 +1,439 @@
#!/usr/bin/env python3
import requests
import time
import logging
import sys
import signal
import json
import ipaddress
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
class ServiceUnavailable(Exception):
"""Raised when a monitored service is temporarily unavailable."""
pass
class JellyfinQBittorrentMonitor:
def __init__(
self,
jellyfin_url="http://localhost:8096",
qbittorrent_url="http://localhost:8080",
check_interval=30,
jellyfin_api_key=None,
streaming_start_delay=10,
streaming_stop_delay=60,
total_bandwidth_budget=30000000,
service_buffer=5000000,
default_stream_bitrate=10000000,
min_torrent_speed=100,
stream_bitrate_headroom=1.1,
):
self.jellyfin_url = jellyfin_url
self.qbittorrent_url = qbittorrent_url
self.check_interval = check_interval
self.jellyfin_api_key = jellyfin_api_key
self.total_bandwidth_budget = total_bandwidth_budget
self.service_buffer = service_buffer
self.default_stream_bitrate = default_stream_bitrate
self.min_torrent_speed = min_torrent_speed
self.stream_bitrate_headroom = stream_bitrate_headroom
self.last_streaming_state = None
self.current_state = "unlimited"
self.torrents_paused = False
self.last_alt_limits = None
self.running = True
self.session = requests.Session() # Use session for cookies
self.last_active_streams = []
# Hysteresis settings to prevent rapid switching
self.streaming_start_delay = streaming_start_delay
self.streaming_stop_delay = streaming_stop_delay
self.last_state_change = 0
# Local network ranges (RFC 1918 private networks + localhost)
self.local_networks = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("::1/128"), # IPv6 localhost
ipaddress.ip_network("fe80::/10"), # IPv6 link-local
]
def is_local_ip(self, ip_address: str) -> bool:
"""Check if an IP address is from a local network"""
try:
ip = ipaddress.ip_address(ip_address)
return any(ip in network for network in self.local_networks)
except ValueError:
logger.warning(f"Invalid IP address format: {ip_address}")
return True # Treat invalid IPs as local for safety
def signal_handler(self, signum, frame):
logger.info("Received shutdown signal, cleaning up...")
self.running = False
self.restore_normal_limits()
sys.exit(0)
def check_jellyfin_sessions(self) -> list[dict]:
headers = (
{"X-Emby-Token": self.jellyfin_api_key} if self.jellyfin_api_key else {}
)
try:
response = requests.get(
f"{self.jellyfin_url}/Sessions", headers=headers, timeout=10
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error(f"Failed to check Jellyfin sessions: {e}")
raise ServiceUnavailable(f"Jellyfin unavailable: {e}") from e
try:
sessions = response.json()
except json.JSONDecodeError as e:
logger.error(f"Failed to parse Jellyfin response: {e}")
raise ServiceUnavailable(f"Jellyfin returned invalid JSON: {e}") from e
active_streams = []
for session in sessions:
if (
"NowPlayingItem" in session
and not session.get("PlayState", {}).get("IsPaused", True)
and not self.is_local_ip(session.get("RemoteEndPoint", ""))
):
item = session["NowPlayingItem"]
item_type = item.get("Type", "").lower()
if item_type in ["movie", "episode", "video"]:
user = session.get("UserName", "Unknown")
stream_name = f"{user}: {item.get('Name', 'Unknown')}"
if session.get("TranscodingInfo") and session[
"TranscodingInfo"
].get("Bitrate"):
bitrate = session["TranscodingInfo"]["Bitrate"]
elif item.get("Bitrate"):
bitrate = item["Bitrate"]
elif item.get("MediaSources", [{}])[0].get("Bitrate"):
bitrate = item["MediaSources"][0]["Bitrate"]
else:
bitrate = self.default_stream_bitrate
bitrate = min(int(bitrate), 100_000_000)
# Add headroom to account for bitrate fluctuations
bitrate = int(bitrate * self.stream_bitrate_headroom)
active_streams.append({"name": stream_name, "bitrate_bps": bitrate})
return active_streams
def check_qbittorrent_alternate_limits(self) -> bool:
try:
response = self.session.get(
f"{self.qbittorrent_url}/api/v2/transfer/speedLimitsMode", timeout=10
)
if response.status_code == 200:
return response.text.strip() == "1"
else:
logger.warning(
f"SpeedLimitsMode endpoint returned HTTP {response.status_code}"
)
raise ServiceUnavailable(
f"qBittorrent returned HTTP {response.status_code}"
)
except requests.exceptions.RequestException as e:
logger.error(f"SpeedLimitsMode endpoint failed: {e}")
raise ServiceUnavailable(f"qBittorrent unavailable: {e}") from e
def use_alt_limits(self, enable: bool) -> None:
action = "enabled" if enable else "disabled"
try:
current_throttle = self.check_qbittorrent_alternate_limits()
if current_throttle == enable:
logger.debug(
f"Alternate speed limits already {action}, no action needed"
)
return
response = self.session.post(
f"{self.qbittorrent_url}/api/v2/transfer/toggleSpeedLimitsMode",
timeout=10,
)
response.raise_for_status()
new_state = self.check_qbittorrent_alternate_limits()
if new_state == enable:
logger.info(f"Alternate speed limits {action}")
else:
logger.warning(
f"Toggle may have failed: expected {enable}, got {new_state}"
)
except ServiceUnavailable:
logger.warning(
f"qBittorrent unavailable, cannot {action} alternate speed limits"
)
except requests.exceptions.RequestException as e:
logger.error(f"Failed to {action} alternate speed limits: {e}")
def pause_all_torrents(self) -> None:
try:
response = self.session.post(
f"{self.qbittorrent_url}/api/v2/torrents/stop",
data={"hashes": "all"},
timeout=10,
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error(f"Failed to pause torrents: {e}")
def resume_all_torrents(self) -> None:
try:
response = self.session.post(
f"{self.qbittorrent_url}/api/v2/torrents/start",
data={"hashes": "all"},
timeout=10,
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error(f"Failed to resume torrents: {e}")
def set_alt_speed_limits(self, dl_kbs: float, ul_kbs: float) -> None:
try:
payload = {
"alt_dl_limit": int(dl_kbs * 1024),
"alt_up_limit": int(ul_kbs * 1024),
}
response = self.session.post(
f"{self.qbittorrent_url}/api/v2/app/setPreferences",
data={"json": json.dumps(payload)},
timeout=10,
)
response.raise_for_status()
self.last_alt_limits = (dl_kbs, ul_kbs)
except requests.exceptions.RequestException as e:
logger.error(f"Failed to set alternate speed limits: {e}")
def restore_normal_limits(self) -> None:
if self.torrents_paused:
logger.info("Resuming all torrents before shutdown...")
self.resume_all_torrents()
self.torrents_paused = False
if self.current_state != "unlimited":
logger.info("Restoring normal speed limits before shutdown...")
self.use_alt_limits(False)
self.current_state = "unlimited"
def sync_qbittorrent_state(self) -> None:
try:
if self.current_state == "unlimited":
actual_state = self.check_qbittorrent_alternate_limits()
if actual_state:
logger.warning(
"qBittorrent state mismatch detected: expected alt speed OFF, got ON. Re-syncing..."
)
self.use_alt_limits(False)
elif self.current_state == "throttled":
if self.last_alt_limits:
self.set_alt_speed_limits(*self.last_alt_limits)
actual_state = self.check_qbittorrent_alternate_limits()
if not actual_state:
logger.warning(
"qBittorrent state mismatch detected: expected alt speed ON, got OFF. Re-syncing..."
)
self.use_alt_limits(True)
elif self.current_state == "paused":
self.pause_all_torrents()
self.torrents_paused = True
except ServiceUnavailable:
pass
def should_change_state(self, new_streaming_state: bool) -> bool:
"""Apply hysteresis to prevent rapid state changes"""
now = time.time()
if new_streaming_state == self.last_streaming_state:
return False
time_since_change = now - self.last_state_change
if new_streaming_state and not self.last_streaming_state:
if time_since_change >= self.streaming_start_delay:
self.last_state_change = now
return True
else:
remaining = self.streaming_start_delay - time_since_change
logger.info(
f"Streaming started - waiting {remaining:.1f}s before enforcing limits"
)
elif not new_streaming_state and self.last_streaming_state:
if time_since_change >= self.streaming_stop_delay:
self.last_state_change = now
return True
else:
remaining = self.streaming_stop_delay - time_since_change
logger.info(
f"Streaming stopped - waiting {remaining:.1f}s before restoring unlimited mode"
)
return False
def run(self):
logger.info("Starting Jellyfin-qBittorrent monitor")
logger.info(f"Jellyfin URL: {self.jellyfin_url}")
logger.info(f"qBittorrent URL: {self.qbittorrent_url}")
logger.info(f"Check interval: {self.check_interval}s")
logger.info(f"Streaming start delay: {self.streaming_start_delay}s")
logger.info(f"Streaming stop delay: {self.streaming_stop_delay}s")
logger.info(f"Total bandwidth budget: {self.total_bandwidth_budget} bps")
logger.info(f"Service buffer: {self.service_buffer} bps")
logger.info(f"Default stream bitrate: {self.default_stream_bitrate} bps")
logger.info(f"Minimum torrent speed: {self.min_torrent_speed} KB/s")
logger.info(f"Stream bitrate headroom: {self.stream_bitrate_headroom}x")
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
while self.running:
try:
self.sync_qbittorrent_state()
try:
active_streams = self.check_jellyfin_sessions()
except ServiceUnavailable:
logger.warning("Jellyfin unavailable, maintaining current state")
time.sleep(self.check_interval)
continue
streaming_active = len(active_streams) > 0
if active_streams:
for stream in active_streams:
logger.debug(
f"Active stream: {stream['name']} ({stream['bitrate_bps']} bps)"
)
if active_streams != self.last_active_streams:
if streaming_active:
stream_names = ", ".join(
stream["name"] for stream in active_streams
)
logger.info(
f"Active streams ({len(active_streams)}): {stream_names}"
)
elif len(active_streams) == 0 and self.last_streaming_state:
logger.info("No active streaming sessions")
if self.should_change_state(streaming_active):
self.last_streaming_state = streaming_active
streaming_state = bool(self.last_streaming_state)
total_streaming_bps = sum(
stream["bitrate_bps"] for stream in active_streams
)
remaining_bps = (
self.total_bandwidth_budget
- self.service_buffer
- total_streaming_bps
)
remaining_kbs = max(0, remaining_bps) / 8 / 1024
if not streaming_state:
desired_state = "unlimited"
elif streaming_active:
if remaining_kbs >= self.min_torrent_speed:
desired_state = "throttled"
else:
desired_state = "paused"
else:
desired_state = self.current_state
if desired_state != self.current_state:
if desired_state == "unlimited":
action = "resume torrents, disable alt speed"
elif desired_state == "throttled":
action = (
"set alt limits "
f"dl={int(remaining_kbs)}KB/s ul={int(remaining_kbs)}KB/s, enable alt speed"
)
else:
action = "pause torrents"
logger.info(
"State change %s -> %s | streams=%d total_bps=%d remaining_bps=%d action=%s",
self.current_state,
desired_state,
len(active_streams),
total_streaming_bps,
remaining_bps,
action,
)
if desired_state == "unlimited":
if self.torrents_paused:
self.resume_all_torrents()
self.torrents_paused = False
self.use_alt_limits(False)
elif desired_state == "throttled":
if self.torrents_paused:
self.resume_all_torrents()
self.torrents_paused = False
self.set_alt_speed_limits(remaining_kbs, remaining_kbs)
self.use_alt_limits(True)
else:
if not self.torrents_paused:
self.pause_all_torrents()
self.torrents_paused = True
self.current_state = desired_state
self.last_active_streams = active_streams
time.sleep(self.check_interval)
except KeyboardInterrupt:
break
except Exception as e:
logger.error(f"Unexpected error in monitoring loop: {e}")
time.sleep(self.check_interval)
self.restore_normal_limits()
logger.info("Monitor stopped")
if __name__ == "__main__":
import os
# Configuration from environment variables
jellyfin_url = os.getenv("JELLYFIN_URL", "http://localhost:8096")
qbittorrent_url = os.getenv("QBITTORRENT_URL", "http://localhost:8080")
check_interval = int(os.getenv("CHECK_INTERVAL", "30"))
jellyfin_api_key = os.getenv("JELLYFIN_API_KEY")
streaming_start_delay = int(os.getenv("STREAMING_START_DELAY", "10"))
streaming_stop_delay = int(os.getenv("STREAMING_STOP_DELAY", "60"))
total_bandwidth_budget = int(os.getenv("TOTAL_BANDWIDTH_BUDGET", "30000000"))
service_buffer = int(os.getenv("SERVICE_BUFFER", "5000000"))
default_stream_bitrate = int(os.getenv("DEFAULT_STREAM_BITRATE", "10000000"))
min_torrent_speed = int(os.getenv("MIN_TORRENT_SPEED", "100"))
stream_bitrate_headroom = float(os.getenv("STREAM_BITRATE_HEADROOM", "1.1"))
monitor = JellyfinQBittorrentMonitor(
jellyfin_url=jellyfin_url,
qbittorrent_url=qbittorrent_url,
check_interval=check_interval,
jellyfin_api_key=jellyfin_api_key,
streaming_start_delay=streaming_start_delay,
streaming_stop_delay=streaming_stop_delay,
total_bandwidth_budget=total_bandwidth_budget,
service_buffer=service_buffer,
default_stream_bitrate=default_stream_bitrate,
min_torrent_speed=min_torrent_speed,
stream_bitrate_headroom=stream_bitrate_headroom,
)
monitor.run()

View File

@@ -2,44 +2,59 @@
pkgs,
config,
service_configs,
username,
lib,
...
}:
{
environment.systemPackages = with pkgs; [
jellyfin
jellyfin-web
jellyfin-ffmpeg
imports = [
(lib.serviceMountWithZpool "jellyfin" service_configs.zpool_ssds [
config.services.jellyfin.dataDir
config.services.jellyfin.cacheDir
])
(lib.serviceFilePerms "jellyfin" [
"Z ${config.services.jellyfin.dataDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}"
"Z ${config.services.jellyfin.cacheDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}"
])
];
services.jellyfin = rec {
services.jellyfin = {
enable = true;
# used for local streaming
openFirewall = true;
package = pkgs.jellyfin.override { jellyfin-ffmpeg = (lib.optimizePackage pkgs.jellyfin-ffmpeg); };
dataDir = service_configs.jellyfin.dir;
cacheDir = dataDir + "_cache";
inherit (service_configs.jellyfin) dataDir cacheDir;
};
services.caddy.virtualHosts."jellyfin.${service_configs.https.domain}".extraConfig = ''
reverse_proxy :${builtins.toString service_configs.ports.jellyfin}
reverse_proxy :${builtins.toString service_configs.ports.jellyfin} {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
request_body {
max_size 4096MB
}
'';
systemd.tmpfiles.rules = [
"d ${config.services.jellyfin.dataDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}"
"d ${config.services.jellyfin.cacheDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}"
];
users.users.${config.services.jellyfin.user}.extraGroups = [
"video"
"render"
service_configs.torrent_group
service_configs.media_group
];
users.users.${username}.extraGroups = [
config.services.jellyfin.group
];
# Protect Jellyfin login from brute force attacks
services.fail2ban.jails.jellyfin = {
enabled = true;
settings = {
backend = "auto";
port = "http,https";
logpath = "${config.services.jellyfin.dataDir}/log/log_*.log";
# defaults: maxretry=5, findtime=10m, bantime=10m
};
filter.Definition = {
failregex = ''^.*Authentication request for .* has been denied \(IP: "<ADDR>"\)\..*$'';
ignoreregex = "";
};
};
}

53
services/livekit.nix Normal file
View File

@@ -0,0 +1,53 @@
{
service_configs,
...
}:
let
keyFile = ../secrets/livekit_keys;
ports = service_configs.ports;
in
{
services.livekit = {
enable = true;
inherit keyFile;
openFirewall = true;
settings = {
port = ports.livekit;
bind_addresses = [ "127.0.0.1" ];
rtc = {
port_range_start = 50100;
port_range_end = 50200;
use_external_ip = true;
};
# Disable LiveKit's built-in TURN; coturn is already running
turn = {
enabled = false;
};
logging = {
level = "info";
};
};
};
services.lk-jwt-service = {
enable = true;
inherit keyFile;
livekitUrl = "wss://${service_configs.livekit.domain}";
port = ports.lk_jwt;
};
services.caddy.virtualHosts."${service_configs.livekit.domain}".extraConfig = ''
@jwt path /sfu/get /healthz
handle @jwt {
reverse_proxy :${builtins.toString ports.lk_jwt}
}
handle {
reverse_proxy :${builtins.toString ports.livekit}
}
'';
}

View File

@@ -1,39 +1,53 @@
{
pkgs,
config,
pkgs,
service_configs,
lib,
...
}:
let
package =
let
src = pkgs.fetchFromGitea {
domain = "forgejo.ellis.link";
owner = "continuwuation";
repo = "continuwuity";
rev = "052c4dfa2165fdc4839fed95b71446120273cf23";
hash = "sha256-kQV4glRrKczoJpn9QIMgB5ac+saZQjSZPel+9K9Ykcs=";
};
in
pkgs.matrix-continuwuity.overrideAttrs (old: {
inherit src;
cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
inherit src;
name = "${old.pname}-vendor";
hash = "sha256-vlOXQL8wwEGFX+w0G/eIeHW3J1UDzhJ501kYhAghDV8=";
};
patches = (old.patches or [ ]) ++ [
];
});
in
{
services.matrix-conduit.settings.global.registration_token =
builtins.readFile ../secrets/matrix_reg_token;
imports = [
(lib.serviceMountWithZpool "continuwuity" service_configs.zpool_ssds [
"/var/lib/private/continuwuity"
])
(lib.serviceFilePerms "continuwuity" [
"Z /var/lib/private/continuwuity 0770 ${config.services.matrix-continuwuity.user} ${config.services.matrix-continuwuity.group}"
])
];
services.caddy.virtualHosts.${service_configs.https.domain}.extraConfig = lib.mkBefore ''
header /.well-known/matrix/* Content-Type application/json
header /.well-known/matrix/* Access-Control-Allow-Origin *
respond /.well-known/matrix/server `{"m.server": "${service_configs.https.matrix_hostname}:443"}`
respond /.well-known/matrix/client `{"m.server":{"base_url":"https://${service_configs.https.matrix_hostname}"},"m.homeserver":{"base_url":"https://${service_configs.https.matrix_hostname}"},"org.matrix.msc3575.proxy":{"base_url":"https://${config.services.matrix-conduit.settings.global.server_name}"}}`
'';
services.caddy.virtualHosts."${service_configs.https.matrix_hostname}".extraConfig = ''
reverse_proxy :${builtins.toString config.services.matrix-conduit.settings.global.port}
'';
# Exact duplicate
services.caddy.virtualHosts."${service_configs.https.matrix_hostname}:8448".extraConfig =
config.services.caddy.virtualHosts."${config.services.matrix-conduit.settings.global.server_name
}".extraConfig;
services.matrix-conduit = {
services.matrix-continuwuity = {
enable = true;
package = pkgs.conduwuit;
inherit package;
settings.global = {
port = 6167;
port = [ service_configs.ports.matrix ];
server_name = service_configs.https.domain;
database_backend = "rocksdb";
allow_registration = true;
registration_token = lib.strings.trim (builtins.readFile ../secrets/matrix_reg_token);
new_user_displayname_suffix = "";
@@ -44,12 +58,42 @@
"envs.net"
];
# without this, conduit fails to start
address = "0.0.0.0";
address = [
"0.0.0.0"
];
# TURN server config (coturn)
turn_secret = config.services.coturn.static-auth-secret;
turn_uris = [
"turn:${service_configs.https.domain}?transport=udp"
"turn:${service_configs.https.domain}?transport=tcp"
];
turn_ttl = 86400;
};
};
systemd.tmpfiles.rules = [
"d /var/lib/private/matrix-conduit 0770 conduit conduit"
services.caddy.virtualHosts.${service_configs.https.domain}.extraConfig = lib.mkBefore ''
header /.well-known/matrix/* Content-Type application/json
header /.well-known/matrix/* Access-Control-Allow-Origin *
respond /.well-known/matrix/server `{"m.server": "${service_configs.matrix.domain}:${builtins.toString service_configs.ports.https}"}`
respond /.well-known/matrix/client `{"m.server":{"base_url":"https://${service_configs.matrix.domain}"},"m.homeserver":{"base_url":"https://${service_configs.matrix.domain}"},"org.matrix.msc3575.proxy":{"base_url":"https://${config.services.matrix-continuwuity.settings.global.server_name}"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://${service_configs.livekit.domain}"}]}`
'';
services.caddy.virtualHosts."${service_configs.matrix.domain}".extraConfig = ''
reverse_proxy :${builtins.toString service_configs.ports.matrix}
'';
# Exact duplicate for federation port
services.caddy.virtualHosts."${service_configs.matrix.domain}:${builtins.toString service_configs.ports.matrix_federation}".extraConfig =
config.services.caddy.virtualHosts."${service_configs.matrix.domain}".extraConfig;
# for federation
networking.firewall.allowedTCPPorts = [
service_configs.ports.matrix_federation
];
# for federation
networking.firewall.allowedUDPPorts = [
service_configs.ports.matrix_federation
];
}

View File

@@ -2,26 +2,25 @@
pkgs,
service_configs,
lib,
username,
config,
inputs,
...
}:
let
heap_size = "4000M";
in
{
environment.systemPackages = [
(pkgs.writeScriptBin "mc-console" ''
#!/bin/sh
${pkgs.tmux}/bin/tmux -S /run/minecraft/${service_configs.minecraft.server_name}.sock attach
'')
imports = [
(lib.serviceMountWithZpool "minecraft-server-${service_configs.minecraft.server_name}"
service_configs.zpool_ssds
[
"${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}"
]
)
inputs.nix-minecraft.nixosModules.minecraft-servers
(lib.serviceFilePerms "minecraft-server-${service_configs.minecraft.server_name}" [
"Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 700 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}"
"Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap/web 750 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}"
])
];
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"minecraft-server"
];
services.minecraft-servers = {
enable = true;
eula = true;
@@ -30,18 +29,56 @@ in
servers.${service_configs.minecraft.server_name} = {
enable = true;
package = pkgs.fabricServers.fabric-1_21_4;
package = pkgs.fabricServers.fabric-1_21_11;
jvmOpts = "-Xmx${heap_size} -Xms${heap_size} -XX:+UseZGC -XX:+ZGenerational";
jvmOpts =
let
heap_size = "4000M";
in
lib.concatStringsSep " " [
# Memory
"-Xmx${heap_size}"
"-Xms${heap_size}"
# GC
"-XX:+UseZGC"
"-XX:+ZGenerational"
# Base JVM optimizations (brucethemoose/Minecraft-Performance-Flags-Benchmarks)
"-XX:+UnlockExperimentalVMOptions"
"-XX:+UnlockDiagnosticVMOptions"
"-XX:+AlwaysActAsServerClassMachine"
"-XX:+AlwaysPreTouch"
"-XX:+DisableExplicitGC"
"-XX:+UseNUMA"
"-XX:+PerfDisableSharedMem"
"-XX:+UseFastUnorderedTimeStamps"
"-XX:+UseCriticalJavaThreadPriority"
"-XX:ThreadPriorityPolicy=1"
"-XX:AllocatePrefetchStyle=3"
"-XX:-DontCompileHugeMethods"
"-XX:MaxNodeLimit=240000"
"-XX:NodeLimitFudgeFactor=8000"
"-XX:ReservedCodeCacheSize=400M"
"-XX:NonNMethodCodeHeapSize=12M"
"-XX:ProfiledCodeHeapSize=194M"
"-XX:NonProfiledCodeHeapSize=194M"
"-XX:NmethodSweepActivity=1"
"-XX:+UseVectorCmov"
# Large pages (requires vm.nr_hugepages sysctl)
"-XX:+UseLargePages"
"-XX:LargePageSizeInBytes=2m"
];
serverProperties = {
server-port = 25565;
server-port = service_configs.ports.minecraft;
enforce-whitelist = true;
gamemode = "survival";
white-list = true;
difficulty = "easy";
motd = "A Minecraft Server";
view-distance = 12;
view-distance = 10;
simulation-distance = 6;
sync-chunk-writes = false;
spawn-protection = 0;
};
whitelist = import ../secrets/minecraft-whitelist.nix;
@@ -51,67 +88,102 @@ in
with pkgs;
builtins.attrValues {
FabricApi = fetchurl {
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/HbTXYTBz/fabric-api-0.119.0%2B1.21.4.jar";
sha512 = "f2e44507dcf7c34ac5104bf78c0f0f0ab99840272d0c1afc51236b7f8a56541bd5c2024953a83599034e1b55191e38b3e437b6b80736137e2ee4d7d571f42c82";
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/i5tSkVBH/fabric-api-0.141.3%2B1.21.11.jar";
sha512 = "c20c017e23d6d2774690d0dd774cec84c16bfac5461da2d9345a1cd95eee495b1954333c421e3d1c66186284d24a433f6b0cced8021f62e0bfa617d2384d0471";
};
FerriteCore = fetchurl {
url = "https://cdn.modrinth.com/data/uXXizFIs/versions/IPM0JlHd/ferritecore-7.1.1-fabric.jar";
sha512 = "f41dc9e8b28327a1e29b14667cb42ae5e7e17bcfa4495260f6f851a80d4b08d98a30d5c52b110007ee325f02dac7431e3fad4560c6840af0bf347afad48c5aac";
url = "https://cdn.modrinth.com/data/uXXizFIs/versions/Ii0gP3D8/ferritecore-8.2.0-fabric.jar";
sha512 = "3210926a82eb32efd9bcebabe2f6c053daf5c4337eebc6d5bacba96d283510afbde646e7e195751de795ec70a2ea44fef77cb54bf22c8e57bb832d6217418869";
};
Lithium = fetchurl {
url = "https://cdn.modrinth.com/data/gvQqBUqZ/versions/kLc5Oxr4/lithium-fabric-0.14.8%2Bmc1.21.4.jar";
sha512 = "ea0d7a4aea29b32527245d933227c85d0606e17c88cc05ed9918a1b966f22011961bfa85e33ab318e729f1ac3e69217d37709413bf70d1dc5a3acc9fd75ef317";
url = "https://cdn.modrinth.com/data/gvQqBUqZ/versions/qvNsoO3l/lithium-fabric-0.21.3%2Bmc1.21.11.jar";
sha512 = "2883739303f0bb602d3797cc601ed86ce6833e5ec313ddce675f3d6af3ee6a40b9b0a06dafe39d308d919669325e95c0aafd08d78c97acd976efde899c7810fd";
};
NoChatReports = fetchurl {
url = "https://cdn.modrinth.com/data/qQyHxfxd/versions/9xt05630/NoChatReports-FABRIC-1.21.4-v2.11.0.jar";
sha512 = "d343b05c8e50f1de15791ff622ad44eeca6cdcb21e960a267a17d71506c61ca79b1c824167779e44d778ca18dcbdebe594ff234fbe355b68d25cdb5b6afd6e4f";
};
moonrise = fetchurl {
url = "https://cdn.modrinth.com/data/KOHu7RCS/versions/6Dgh9jQx/Moonrise-Fabric-0.2.0-beta.9%2Bac0c7de.jar";
sha512 = "c101f1a41db4095d651d32eae47bd7e6f7358f7390898610d1bf261ebfc7e0f4165fd551c08a99cca31a3308f1989a16b8c75c1ece60ef9cd475107ca4f4219e";
url = "https://cdn.modrinth.com/data/qQyHxfxd/versions/rhykGstm/NoChatReports-FABRIC-1.21.11-v2.18.0.jar";
sha512 = "d2c35cc8d624616f441665aff67c0e366e4101dba243bad25ed3518170942c1a3c1a477b28805cd1a36c44513693b1c55e76bea627d3fced13927a3d67022ccc";
};
squaremap = fetchurl {
url = "https://cdn.modrinth.com/data/PFb7ZqK6/versions/9i2KwI5R/squaremap-fabric-mc1.21.4-1.3.4.jar";
sha512 = "6eb44061f057d1bbd0bb6f9186d03d496479dcd953af8f09f70099c2e67e567e5dca626972d45af0315c2e2714c3dd74beef97575396e3bb90b7c670f5c80fef";
url = "https://cdn.modrinth.com/data/PFb7ZqK6/versions/BW8lMXBi/squaremap-fabric-mc1.21.11-1.3.12.jar";
sha512 = "f62eb791a3f5812eb174565d318f2e6925353f846ef8ac56b4e595f481494e0c281f26b9e9fcfdefa855093c96b735b12f67ee17c07c2477aa7a3439238670d9";
};
modernfix = fetchurl {
url = "https://cdn.modrinth.com/data/nmDcB62a/versions/ZGxQddYr/modernfix-fabric-5.20.3%2Bmc1.21.4.jar";
sha512 = "ae49114c92a048c9ce79e197fc4df028e186cf13546e710f72247382fa8076f0b70d6aa3224951f4a36c886ca236f099a011f20b021a2b0d1a75c631da4d7d52";
scalablelux = fetchurl {
url = "https://cdn.modrinth.com/data/Ps1zyz6x/versions/PV9KcrYQ/ScalableLux-0.1.6%2Bfabric.c25518a-all.jar";
sha512 = "729515c1e75cf8d9cd704f12b3487ddb9664cf9928e7b85b12289c8fbbc7ed82d0211e1851375cbd5b385820b4fedbc3f617038fff5e30b302047b0937042ae7";
};
alternatecurrent = fetchurl {
url = "https://cdn.modrinth.com/data/r0v8vy1s/versions/DwfiGUVU/alternate-current-mc1.21.2-1.9.1.jar";
sha512 = "8ed44291a8aed3e1c9750cfce85e0de679daeff7c3b1bc8f6329b41ba4570442750b8039d2d5c79c32655fc9372ea35843c60805438d33888b30e28731c39137";
c2me = fetchurl {
url = "https://cdn.modrinth.com/data/VSNURh3q/versions/QdLiMUjx/c2me-fabric-mc1.21.11-0.3.7%2Balpha.0.7.jar";
sha512 = "f9543febe2d649a82acd6d5b66189b6a3d820cf24aa503ba493fdb3bbd4e52e30912c4c763fe50006f9a46947ae8cd737d420838c61b93429542573ed67f958e";
};
# fix `Error sending packet clientbound/minecraft:disconnect` error
disconnectpacketfix = fetchurl {
krypton = fetchurl {
url = "https://cdn.modrinth.com/data/fQEb0iXm/versions/O9LmWYR7/krypton-0.2.10.jar";
sha512 = "4dcd7228d1890ddfc78c99ff284b45f9cf40aae77ef6359308e26d06fa0d938365255696af4cc12d524c46c4886cdcd19268c165a2bf0a2835202fe857da5cab";
};
better-fabric-console = fetchurl {
url = "https://cdn.modrinth.com/data/Y8o1j1Sf/versions/6aIKl5wy/better-fabric-console-mc1.21.11-1.2.9.jar";
sha512 = "427247dafd99df202ee10b4bf60ffcbbecbabfadb01c167097ffb5b85670edb811f4d061c2551be816295cbbc6b8ec5ec464c14a6ff41912ef1f6c57b038d320";
};
disconnect-packet-fix = fetchurl {
url = "https://cdn.modrinth.com/data/rd9rKuJT/versions/Gv74xveQ/disconnect-packet-fix-fabric-2.0.0.jar";
sha512 = "1fd6f09a41ce36284e1a8e9def53f3f6834d7201e69e54e24933be56445ba569fbc26278f28300d36926ba92db6f4f9c0ae245d23576aaa790530345587316db";
};
packet-fixer = fetchurl {
url = "https://cdn.modrinth.com/data/c7m1mi73/versions/CUh1DWeO/packetfixer-fabric-3.3.4-1.21.11.jar";
sha512 = "33331b16cb40c5e6fbaade3cacc26f3a0e8fa5805a7186f94d7366a0e14dbeee9de2d2e8c76fa71f5e9dd24eb1c261667c35447e32570ea965ca0f154fdfba0a";
};
# fork of Modernfix for 1.21.11 (upstream will support 26.1)
modernfix = fetchurl {
url = "https://cdn.modrinth.com/data/TjSm1wrD/versions/JwSO8JCN/modernfix-5.25.2-build.4.jar";
sha512 = "0d65c05ac0475408c58ef54215714e6301113101bf98bfe4bb2ba949fbfddd98225ac4e2093a5f9206a9e01ba80a931424b237bdfa3b6e178c741ca6f7f8c6a3";
};
debugify = fetchurl {
url = "https://cdn.modrinth.com/data/QwxR6Gcd/versions/8Q49lnaU/debugify-1.21.11%2B1.0.jar";
sha512 = "04d82dd33f44ced37045f1f9a54ad4eacd70861ff74a8800f2d2df358579e6cb0ea86a34b0086b3e87026b1a0691dd6594b4fdc49f89106466eea840518beb03";
};
}
);
};
};
};
services.caddy.virtualHosts."map.${service_configs.https.domain}".extraConfig = ''
# tls internal
root * ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap/web
file_server browse
'';
systemd.services.minecraft-server-main = {
serviceConfig = {
Nice = -5;
IOSchedulingPriority = 0;
LimitMEMLOCK = "infinity"; # Required for large pages
};
};
services.caddy.virtualHosts = lib.mkIf (config.services.caddy.enable) {
"map.${service_configs.https.domain}".extraConfig = ''
root * ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap/web
file_server browse
'';
};
users.users = lib.mkIf (config.services.caddy.enable) {
${config.services.caddy.user}.extraGroups = [
# for `map.gardling.com`
config.services.minecraft-servers.group
];
};
systemd.tmpfiles.rules = [
"d ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 0770 minecraft minecraft"
];
users.users.${username}.extraGroups = [
"minecraft"
# Allow caddy (in minecraft group) to traverse to squaremap/web for map.gardling.com
"z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 710 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}"
"z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap 710 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}"
];
}

23
services/monero.nix Normal file
View File

@@ -0,0 +1,23 @@
{
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "monero" service_configs.zpool_hdds [
service_configs.monero.dataDir
])
(lib.serviceFilePerms "monero" [
"Z ${service_configs.monero.dataDir} 0700 monero monero"
])
];
services.monero = {
enable = true;
dataDir = service_configs.monero.dataDir;
rpc = {
restricted = true;
};
};
}

10
services/ntfy-alerts.nix Normal file
View File

@@ -0,0 +1,10 @@
{ config, service_configs, ... }:
{
services.ntfyAlerts = {
enable = true;
serverUrl = "https://${service_configs.ntfy.domain}";
topicFile = config.age.secrets.ntfy-alerts-topic.path;
tokenFile = config.age.secrets.ntfy-alerts-token.path;
};
}

34
services/ntfy.nix Normal file
View File

@@ -0,0 +1,34 @@
{
config,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "ntfy-sh" service_configs.zpool_ssds [
"/var/lib/private/ntfy-sh"
])
(lib.serviceFilePerms "ntfy-sh" [
"Z /var/lib/private/ntfy-sh 0700 ${config.services.ntfy-sh.user} ${config.services.ntfy-sh.group}"
])
];
services.ntfy-sh = {
enable = true;
settings = {
base-url = "https://${service_configs.ntfy.domain}";
listen-http = "127.0.0.1:${builtins.toString service_configs.ports.ntfy}";
behind-proxy = true;
auth-default-access = "deny-all";
enable-login = true;
enable-signup = false;
};
};
services.caddy.virtualHosts."${service_configs.ntfy.domain}".extraConfig = ''
reverse_proxy :${builtins.toString service_configs.ports.ntfy}
'';
}

View File

@@ -1,48 +0,0 @@
{
pkgs,
service_configs,
username,
...
}:
let
owntracks_pkg = pkgs.owntracks-recorder.overrideAttrs (old: {
installPhase =
old.installPhase
+ ''
mkdir -p $out/usr/share/ot-recorder
cp -R docroot/* $out/usr/share/ot-recorder'';
});
in
{
users.groups.owntracks = { };
users.users.owntracks = {
isNormalUser = true;
group = "owntracks";
};
systemd.services.owntracks = {
enable = true;
description = "Store and access data published by OwnTracks apps";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "owntracks";
Group = "owntracks";
WorkingDirectory = "${owntracks_pkg}";
ExecStart = "${owntracks_pkg}/bin/ot-recorder -S ${service_configs.owntracks.data_dir} --doc-root usr/share/ot-recorder --http-port ${builtins.toString service_configs.ports.owntracks} --port 0";
};
};
systemd.tmpfiles.rules = [
"d ${service_configs.owntracks.data_dir} 0770 owntracks owntracks"
];
services.caddy.virtualHosts."owntracks.${service_configs.https.domain}".extraConfig = ''
${builtins.readFile ../secrets/owntracks_caddy_auth}
reverse_proxy :${builtins.toString service_configs.ports.owntracks}
'';
users.users.${username}.extraGroups = [
"owntracks"
];
}

View File

@@ -1,22 +1,24 @@
{
pkgs,
config,
username,
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "postgresql" service_configs.zpool_ssds [
config.services.postgresql.dataDir
])
(lib.serviceFilePerms "postgresql" [
"Z ${config.services.postgresql.dataDir} 0700 postgres postgres"
])
];
services.postgresql = {
enable = true;
package = pkgs.postgresql_16;
dataDir = "/tank/services/sql";
dataDir = service_configs.postgres.dataDir;
};
systemd.tmpfiles.rules = [
# postgresql requires 0700
"d ${config.services.postgresql.dataDir} 0700 postgresql postgresql"
];
users.users.${username}.extraGroups = [
"postgresql"
];
}

View File

@@ -2,44 +2,46 @@
pkgs,
config,
service_configs,
username,
lib,
inputs,
...
}:
{
# network namespace that is proxied through mullvad
vpnNamespaces.wg = {
portMappings = [
{
from = config.services.qbittorrent.webuiPort;
to = config.services.qbittorrent.webuiPort;
}
];
imports = [
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_hdds [
service_configs.torrents_path
config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath
openVPNPorts = [
{
port = config.services.qbittorrent.webuiPort;
protocol = "both";
}
];
};
])
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_ssds [
"${config.services.qbittorrent.profileDir}/qBittorrent"
])
(lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent")
(lib.serviceFilePerms "qbittorrent" [
# 0770: group (media) needs write to delete files during upgrades —
# Radarr/Sonarr must unlink the old file before placing the new one.
"Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0770 ${config.services.qbittorrent.user} ${service_configs.media_group}"
"Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
"Z ${config.services.qbittorrent.profileDir} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
])
];
services.qbittorrent = {
enable = true;
package = pkgs.qbittorrent-nox;
webuiPort = service_configs.ports.torrent;
profileDir = "/var/lib/qBittorrent";
# Set the service group to 'media' so the systemd unit runs with media as
# the primary GID. Linux assigns new file ownership from the process's GID
# (set by systemd's Group= directive), not from /etc/passwd. Without this,
# downloads land as qbittorrent:qbittorrent (0700), blocking Radarr/Sonarr.
group = service_configs.media_group;
serverConfig.LegalNotice.Accepted = true;
serverConfig.Preferences = {
WebUI = {
AlternativeUIEnabled = true;
RootFolder = builtins.toString (
pkgs.fetchzip {
url = "https://github.com/VueTorrent/VueTorrent/releases/download/v2.23.1/vuetorrent.zip";
sha256 = "yZmnRmYoinJ8uSuUpjGIRCQWBrK59hwyEkCq8aWiOvQ=";
}
);
RootFolder = "${pkgs.vuetorrent}/share/vuetorrent";
# disable auth because we use caddy for auth
AuthSubnetWhitelist = "0.0.0.0/0";
@@ -47,83 +49,77 @@
};
Downloads = {
SavePath = service_configs.torrent.SavePath;
TempPath = service_configs.torrent.TempPath;
inherit (service_configs.torrent) SavePath TempPath;
};
};
serverConfig.BitTorrent = {
Session = {
GlobalUPSpeedLimit = 1500; # 500 KiB/s
GlobalDLSpeedLimit = 5000; # 5 MiB/s
MaxConnectionsPerTorrent = 50;
MaxUploadsPerTorrent = 10;
MaxConnections = -1;
MaxUploads = -1;
IgnoreLimitsOnLAN = true;
MaxActiveCheckingTorrents = 5;
# queueing
QueueingSystemEnabled = true;
MaxActiveDownloads = 5; # keep focused: fewer torrents, each gets more bandwidth
MaxActiveUploads = -1;
MaxActiveTorrents = -1;
IgnoreSlowTorrentsForQueueing = true;
GlobalUPSpeedLimit = 0;
GlobalDLSpeedLimit = 0;
# Alternate speed limits for when Jellyfin is streaming
AlternativeGlobalUPSpeedLimit = 500; # 500 KB/s when throttled
AlternativeGlobalDLSpeedLimit = 800; # 800 KB/s when throttled
IncludeOverheadInLimits = true;
GlobalMaxRatio = -1;
QueueingSystemEnabled = false; # seed all torrents all the time
GlobalMaxRatio = 7.0;
AddTrackersEnabled = true;
AdditionalTrackers = (
lib.concatStringsSep "\\n" [
"udp://tracker.opentrackr.org:1337/announce"
"udp://open.stealth.si:80/announce"
"udp://open.demonii.com:1337"
"udp://exodus.desync.com:6969/announce"
"udp://tracker.dler.org:6969/announce"
"udp://tracker.bittor.pw:1337/announce"
"udp://tracker.torrent.eu.org:451/announce"
"udp://explodie.org:6969/announce"
"http://tracker.files.fm:6969/announce"
"udp://tracker.tiny-vps.com:6969/announce"
"udp://p4p.arenabg.com:1337/announce"
"udp://tracker.dler.com:6969/announce"
"udp://inferno.demonoid.is:3391/announce"
"udp://tracker.torrent.eu.org:451/announce"
"udp://tracker.ololosh.space:6969/announce"
"udp://ns-1.x-fins.com:6969/announce"
"udp://leet-tracker.moe:1337/announce"
"http://tracker.vanitycore.co:6969/announce"
"http://tracker.sbsub.com:2710/announce"
"http://tracker.moxing.party:6969/announce"
"http://tracker.ipv6tracker.org:80/announce"
"http://tracker.corpscorp.online:80/announce"
"http://shubt.net:2710/announce"
"http://share.hkg-fansub.info:80/announce.php"
"http://servandroidkino.ru:80/announce"
"http://bt.poletracker.org:2710/announce"
"http://0d.kebhana.mx:443/announce"
]
AdditionalTrackers = lib.concatStringsSep "\\n" (
lib.lists.filter (x: x != "") (
lib.strings.splitString "\n" (builtins.readFile "${inputs.trackerlist}/trackers_all.txt")
)
);
AnnounceToAllTrackers = true;
# idk why it also has to be specified here too?
inherit (config.services.qbittorrent.serverConfig.Preferences.Downloads) TempPath;
TempPathEnabled = true;
ConnectionSpeed = 200;
# Automatic Torrent Management: use category save paths for new torrents
DisableAutoTMMByDefault = false;
DisableAutoTMMTriggers.CategorySavePathChanged = false;
DisableAutoTMMTriggers.DefaultSavePathChanged = false;
ChokingAlgorithm = "RateBased";
PieceExtentAffinity = true;
SuggestMode = true;
CoalesceReadWrite = true;
};
Network = {
# traffic is routed through a vpn, we don't need
# port forwarding
PortForwardingEnabled = false;
};
};
};
systemd.tmpfiles.rules = [
"d ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0770 ${config.services.qbittorrent.user} ${service_configs.torrent_group}"
"d ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0770 ${config.services.qbittorrent.user} ${service_configs.torrent_group}"
];
# make qbittorrent use a vpn
systemd.services.qbittorrent.vpnConfinement = {
enable = true;
vpnNamespace = "wg";
};
systemd.services.qbittorrent.serviceConfig.TimeoutStopSec = lib.mkForce 10;
services.caddy.virtualHosts."torrent.${service_configs.https.domain}".extraConfig = ''
# tls internal
${builtins.readFile ../secrets/caddy_auth}
reverse_proxy ${service_configs.https.wg_ip}:${builtins.toString config.services.qbittorrent.webuiPort}
import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString config.services.qbittorrent.webuiPort}
'';
users.users.${config.services.qbittorrent.user}.extraGroups = [
service_configs.torrent_group
service_configs.media_group
];
users.users.${username}.extraGroups = [
config.services.qbittorrent.group
];
}

View File

@@ -10,13 +10,27 @@ let
slskd_env = "/etc/slskd_env";
in
{
imports = [
(lib.serviceMountWithZpool "slskd" "" [
service_configs.slskd.base
service_configs.slskd.downloads
service_configs.slskd.incomplete
])
(lib.serviceFilePerms "slskd" [
"Z ${service_configs.music_dir} 0750 ${username} music"
"Z ${service_configs.slskd.base} 0750 ${config.services.slskd.user} ${config.services.slskd.group}"
"Z ${service_configs.slskd.downloads} 0750 ${config.services.slskd.user} music"
"Z ${service_configs.slskd.incomplete} 0750 ${config.services.slskd.user} music"
])
];
users.groups."music" = { };
system.activationScripts = {
"zfs-key".text = ''
"skskd_env".text = ''
#!/bin/sh
rm -fr ${slskd_env} || true
cp ${../secrets/slskd_env} ${slskd_env}
cp ${config.age.secrets.slskd_env.path} ${slskd_env}
chmod 0500 ${slskd_env}
chown ${config.services.slskd.user}:${config.services.slskd.group} ${slskd_env}
'';
@@ -42,12 +56,12 @@ in
global = {
download = {
slots = 3;
speed_limit = 500;
slots = -1;
speed_limit = -1;
};
upload = {
slots = 4;
speed_limit = 500;
speed_limit = 2000;
};
};
};
@@ -55,11 +69,7 @@ in
users.users.${config.services.slskd.user}.extraGroups = [ "music" ];
users.users.${config.services.jellyfin.user}.extraGroups = [ "music" ];
systemd.tmpfiles.rules = [
"d ${service_configs.music_dir} 0750 ${username} music"
"d ${service_configs.music_dir} 0750 ${username} music"
];
users.users.${username}.extraGroups = [ "music" ];
# doesn't work with auth????
services.caddy.virtualHosts."soulseek.${service_configs.https.domain}".extraConfig = ''

35
services/ssh.nix Normal file
View File

@@ -0,0 +1,35 @@
{
config,
lib,
pkgs,
username,
...
}:
{
# Enable the OpenSSH daemon.
services.openssh = {
enable = true;
settings = {
AllowUsers = [
username
"root"
];
PasswordAuthentication = false;
PermitRootLogin = "yes"; # for deploying configs
};
};
systemd.tmpfiles.rules = [
"Z /etc/ssh 755 root root"
"Z /etc/ssh/ssh_host_* 600 root root"
];
users.users.${username}.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO4jL6gYOunUlUtPvGdML0cpbKSsPNqQ1jit4E7U1RyH" # laptop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBJjT5QZ3zRDb+V6Em20EYpSEgPW5e/U+06uQGJdraxi" # desktop
];
# used for deploying configs to server
users.users.root.openssh.authorizedKeys.keys =
config.users.users.${username}.openssh.authorizedKeys.keys;
}

54
services/syncthing.nix Normal file
View File

@@ -0,0 +1,54 @@
{
config,
lib,
pkgs,
service_configs,
...
}:
{
imports = [
(lib.serviceMountWithZpool "syncthing" service_configs.zpool_ssds [
service_configs.syncthing.dataDir
service_configs.syncthing.signalBackupDir
service_configs.syncthing.grayjayBackupDir
])
(lib.serviceFilePerms "syncthing" [
"Z ${service_configs.syncthing.dataDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}"
"Z ${service_configs.syncthing.signalBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}"
"Z ${service_configs.syncthing.grayjayBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}"
])
];
services.syncthing = {
enable = true;
dataDir = service_configs.syncthing.dataDir;
guiAddress = "127.0.0.1:${toString service_configs.ports.syncthing_gui}";
overrideDevices = false;
overrideFolders = false;
settings = {
gui = {
insecureSkipHostcheck = true; # Allow access via reverse proxy
};
options = {
urAccepted = 1; # enable usage reporting
relaysEnabled = true;
};
};
};
# Open firewall ports for syncthing protocol
networking.firewall = {
allowedTCPPorts = [ service_configs.ports.syncthing_protocol ];
allowedUDPPorts = [ service_configs.ports.syncthing_discovery ];
};
services.caddy.virtualHosts."syncthing.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${toString service_configs.ports.syncthing_gui}
'';
}

22
services/ups.nix Normal file
View File

@@ -0,0 +1,22 @@
{
config,
lib,
pkgs,
...
}:
{
services.apcupsd = {
enable = true;
configText = ''
UPSTYPE usb
NISIP 127.0.0.1
BATTERYLEVEL 5 # shutdown after reaching 5% battery
MINUTES 5 # shutdown if estimated runtime on battery reaches 5 minutes
'';
hooks = {
# command to run when shutdown condition is met
doshutdown = "systemctl poweroff";
};
};
}

View File

@@ -1,9 +1,18 @@
{ pkgs, service_configs, ... }:
{
pkgs,
config,
inputs,
...
}:
{
imports = [
inputs.vpn-confinement.nixosModules.default
];
# network namespace that is proxied through mullvad
vpnNamespaces.wg = {
enable = true;
wireguardConfigFile = ../secrets/wg0.conf;
wireguardConfigFile = config.age.secrets.wg0-conf.path;
accessibleFrom = [
# "192.168.0.0/24"
];

63
services/xmrig.nix Normal file
View File

@@ -0,0 +1,63 @@
{
config,
lib,
pkgs,
hostname,
...
}:
let
walletAddress = lib.strings.trim (builtins.readFile ../secrets/xmrig-wallet);
threadCount = 12;
in
{
services.xmrig = {
enable = true;
package = pkgs.xmrig;
settings = {
autosave = true;
cpu = {
enabled = true;
huge-pages = true;
hw-aes = true;
rx = lib.range 0 (threadCount - 1);
};
randomx = {
"1gb-pages" = true;
};
opencl = false;
cuda = false;
pools = [
{
url = "gulf.moneroocean.stream:20128";
user = walletAddress;
pass = hostname + "~rx/0";
keepalive = true;
tls = true;
}
];
};
};
systemd.services.xmrig.serviceConfig = {
Nice = 19;
CPUSchedulingPolicy = "idle";
IOSchedulingClass = "idle";
};
# Stop mining on UPS battery to conserve power
services.apcupsd.hooks = lib.mkIf config.services.apcupsd.enable {
onbattery = "systemctl stop xmrig";
offbattery = "systemctl start xmrig";
};
# Reserve 1GB huge pages for RandomX (dataset is ~2GB)
boot.kernelParams = [
"hugepagesz=1G"
"hugepages=3"
];
}

124
tests/fail2ban-caddy.nix Normal file
View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
...
}:
pkgs.testers.runNixOSTest {
name = "fail2ban-caddy";
nodes = {
server =
{
config,
pkgs,
lib,
...
}:
{
imports = [
../modules/security.nix
];
# Set up Caddy with basic auth (minimal config, no production stuff)
# Using bcrypt hash generated with: caddy hash-password --plaintext testpass
services.caddy = {
enable = true;
virtualHosts.":80".extraConfig = ''
log {
output file /var/log/caddy/access-server.log
format json
}
basic_auth {
testuser $2a$14$XqaQlGTdmofswciqrLlMz.rv0/jiGQq8aU.fP6mh6gCGiLf6Cl3.a
}
respond "Authenticated!" 200
'';
};
# Add the fail2ban jail for caddy-auth (same as in services/caddy.nix)
services.fail2ban.jails.caddy-auth = {
enabled = true;
settings = {
backend = "auto";
port = "http,https";
logpath = "/var/log/caddy/access-*.log";
maxretry = 3; # Lower for testing
};
filter.Definition = {
# Only match 401s where an Authorization header was actually sent
failregex = ''^.*"remote_ip":"<HOST>".*"Authorization":\["REDACTED"\].*"status":401.*$'';
ignoreregex = "";
datepattern = ''"ts":{Epoch}\.'';
};
};
# Create log directory and initial log file so fail2ban can start
systemd.tmpfiles.rules = [
"d /var/log/caddy 755 caddy caddy"
"f /var/log/caddy/access-server.log 644 caddy caddy"
];
networking.firewall.allowedTCPPorts = [ 80 ];
};
client = {
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
import time
import re
start_all()
server.wait_for_unit("caddy.service")
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(80)
time.sleep(2)
with subtest("Verify caddy-auth jail is active"):
status = server.succeed("fail2ban-client status")
assert "caddy-auth" in status, f"caddy-auth jail not found in: {status}"
with subtest("Verify correct password works"):
# Use -4 to force IPv4 for consistency
result = client.succeed("curl -4 -s -u testuser:testpass http://server/")
print(f"Curl result: {result}")
assert "Authenticated" in result, f"Auth should succeed: {result}"
with subtest("Unauthenticated requests (browser probes) should not trigger ban"):
# Simulate browser probe requests - no Authorization header sent
# This is the normal HTTP Basic Auth challenge-response flow:
# browser sends request without credentials, gets 401, then resends with credentials
for i in range(5):
client.execute("curl -4 -s http://server/ || true")
time.sleep(0.5)
time.sleep(3)
status = server.succeed("fail2ban-client status caddy-auth")
print(f"caddy-auth jail status after unauthenticated requests: {status}")
match = re.search(r"Currently banned:\s*(\d+)", status)
banned = int(match.group(1)) if match else 0
assert banned == 0, f"Unauthenticated 401s should NOT trigger ban, but {banned} IPs were banned: {status}"
with subtest("Generate failed basic auth attempts (wrong password)"):
# Use -4 to force IPv4 for consistent IP tracking
# These send an Authorization header with wrong credentials
for i in range(4):
client.execute("curl -4 -s -u testuser:wrongpass http://server/ || true")
time.sleep(1)
with subtest("Verify IP is banned after wrong password attempts"):
time.sleep(5)
status = server.succeed("fail2ban-client status caddy-auth")
print(f"caddy-auth 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/ 2>&1")[0]
assert exit_code != 0, "Connection should be blocked"
'';
}

123
tests/fail2ban-gitea.nix Normal file
View File

@@ -0,0 +1,123 @@
{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
gitea = {
dir = "/var/lib/gitea";
domain = "git.test.local";
};
postgres = {
socket = "/run/postgresql";
};
ports = {
gitea = 3000;
};
};
testLib = lib.extend (
final: prev: {
serviceMountWithZpool =
serviceName: zpool: dirs:
{ ... }:
{ };
serviceFilePerms = serviceName: tmpfilesRules: { ... }: { };
}
);
giteaModule =
{ config, pkgs, ... }:
{
imports = [
(import ../services/gitea.nix {
inherit config pkgs;
lib = testLib;
service_configs = testServiceConfigs;
})
];
};
in
pkgs.testers.runNixOSTest {
name = "fail2ban-gitea";
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [
../modules/security.nix
giteaModule
];
# Enable postgres for gitea
services.postgresql.enable = true;
# Disable ZFS mount dependency
systemd.services."gitea-mounts".enable = lib.mkForce false;
systemd.services.gitea = {
wants = lib.mkForce [ ];
after = lib.mkForce [ "postgresql.service" ];
requires = lib.mkForce [ ];
};
# Override for faster testing and correct port
services.fail2ban.jails.gitea.settings = {
maxretry = lib.mkForce 3;
# In test, we connect directly to Gitea port, not via Caddy
port = lib.mkForce "3000";
};
networking.firewall.allowedTCPPorts = [ 3000 ];
};
client = {
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
import time
import re
start_all()
server.wait_for_unit("postgresql.service")
server.wait_for_unit("gitea.service")
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(3000)
time.sleep(3)
with subtest("Verify gitea jail is active"):
status = server.succeed("fail2ban-client status")
assert "gitea" in status, f"gitea 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:3000/user/login -d 'user_name=baduser&password=badpass' || true"
)
time.sleep(0.5)
with subtest("Verify IP is banned"):
time.sleep(3)
status = server.succeed("fail2ban-client status gitea")
print(f"gitea 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:3000/ 2>&1")[0]
assert exit_code != 0, "Connection should be blocked"
'';
}

135
tests/fail2ban-immich.nix Normal file
View File

@@ -0,0 +1,135 @@
{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
https = {
domain = "test.local";
};
ports = {
immich = 2283;
};
immich = {
dir = "/var/lib/immich";
};
};
testLib = lib.extend (
final: prev: {
serviceMountWithZpool =
serviceName: zpool: dirs:
{ ... }:
{ };
serviceFilePerms = serviceName: tmpfilesRules: { ... }: { };
}
);
immichModule =
{ config, pkgs, ... }:
{
imports = [
(import ../services/immich.nix {
inherit config pkgs;
lib = testLib;
service_configs = testServiceConfigs;
})
];
};
in
pkgs.testers.runNixOSTest {
name = "fail2ban-immich";
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [
../modules/security.nix
immichModule
];
# Immich needs postgres
services.postgresql.enable = true;
# Let immich create its own DB for testing
services.immich.database.createDB = lib.mkForce true;
# Disable ZFS mount dependencies
systemd.services."immich-server-mounts".enable = lib.mkForce false;
systemd.services."immich-machine-learning-mounts".enable = lib.mkForce false;
systemd.services.immich-server = {
wants = lib.mkForce [ ];
after = lib.mkForce [ "postgresql.service" ];
requires = lib.mkForce [ ];
};
systemd.services.immich-machine-learning = {
wants = lib.mkForce [ ];
after = lib.mkForce [ ];
requires = lib.mkForce [ ];
};
# Override for faster testing and correct port
services.fail2ban.jails.immich.settings = {
maxretry = lib.mkForce 3;
# In test, we connect directly to Immich port, not via Caddy
port = lib.mkForce "2283";
};
networking.firewall.allowedTCPPorts = [ 2283 ];
# Immich needs more resources
virtualisation.diskSize = 4 * 1024;
virtualisation.memorySize = 4 * 1024; # 4GB RAM for Immich
};
client = {
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
import time
import re
start_all()
server.wait_for_unit("postgresql.service")
server.wait_for_unit("immich-server.service", timeout=120)
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(2283, timeout=60)
time.sleep(3)
with subtest("Verify immich jail is active"):
status = server.succeed("fail2ban-client status")
assert "immich" in status, f"immich 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:2283/api/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"bad@user.com\",\"password\":\"badpass\"}' || true"
)
time.sleep(0.5)
with subtest("Verify IP is banned"):
time.sleep(3)
status = server.succeed("fail2ban-client status immich")
print(f"immich 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:2283/ 2>&1")[0]
assert exit_code != 0, "Connection should be blocked"
'';
}

147
tests/fail2ban-jellyfin.nix Normal file
View File

@@ -0,0 +1,147 @@
{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
https = {
domain = "test.local";
};
ports = {
jellyfin = 8096;
};
jellyfin = {
dataDir = "/var/lib/jellyfin";
cacheDir = "/var/cache/jellyfin";
};
media_group = "media";
};
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
];
# 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"
'';
}

104
tests/fail2ban-ssh.nix Normal file
View File

@@ -0,0 +1,104 @@
{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
zpool_hdds = "";
};
securityModule = import ../modules/security.nix;
sshModule =
{
config,
lib,
pkgs,
...
}:
{
imports = [
(import ../services/ssh.nix {
inherit config lib pkgs;
username = "testuser";
})
];
};
in
pkgs.testers.runNixOSTest {
name = "fail2ban-ssh";
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [
securityModule
sshModule
];
# Override for testing - enable password auth
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
users.users.testuser = {
isNormalUser = true;
password = "correctpassword";
};
networking.firewall.allowedTCPPorts = [ 22 ];
};
client = {
environment.systemPackages = with pkgs; [
sshpass
openssh
];
};
};
testScript = ''
import time
start_all()
server.wait_for_unit("sshd.service")
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(22)
time.sleep(2)
with subtest("Verify sshd jail is active"):
status = server.succeed("fail2ban-client status")
assert "sshd" in status, f"sshd jail not found in: {status}"
with subtest("Generate failed SSH login attempts"):
# Use -4 to force IPv4, timeout and NumberOfPasswordPrompts=1 to ensure quick failure
# maxRetry is 3 in our config, so 4 attempts should trigger a ban
for i in range(4):
client.execute(
"timeout 5 sshpass -p 'wrongpassword' ssh -4 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 -o NumberOfPasswordPrompts=1 testuser@server echo test 2>/dev/null || true"
)
time.sleep(1)
with subtest("Verify IP is banned"):
# Wait for fail2ban to process the logs and apply the ban
time.sleep(5)
status = server.succeed("fail2ban-client status sshd")
print(f"sshd jail status: {status}")
# Check that at least 1 IP is banned
import re
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("timeout 3 nc -4 -z -w 2 server 22")[0]
assert exit_code != 0, "Connection should be blocked for banned IP"
'';
}

View File

@@ -0,0 +1,137 @@
{
config,
lib,
pkgs,
...
}:
let
testServiceConfigs = {
zpool_ssds = "";
https = {
domain = "test.local";
};
ports = {
vaultwarden = 8222;
};
vaultwarden = {
path = "/var/lib/vaultwarden";
};
};
testLib = lib.extend (
final: prev: {
serviceMountWithZpool =
serviceName: zpool: dirs:
{ ... }:
{ };
serviceFilePerms = serviceName: tmpfilesRules: { ... }: { };
}
);
vaultwardenModule =
{ config, pkgs, ... }:
{
imports = [
(import ../services/bitwarden.nix {
inherit config pkgs;
lib = testLib;
service_configs = testServiceConfigs;
})
];
};
in
pkgs.testers.runNixOSTest {
name = "fail2ban-vaultwarden";
nodes = {
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [
../modules/security.nix
vaultwardenModule
];
# Disable ZFS mount dependencies
systemd.services."vaultwarden-mounts".enable = lib.mkForce false;
systemd.services."backup-vaultwarden-mounts".enable = lib.mkForce false;
systemd.services.vaultwarden = {
wants = lib.mkForce [ ];
after = lib.mkForce [ ];
requires = lib.mkForce [ ];
};
systemd.services.backup-vaultwarden = {
wants = lib.mkForce [ ];
after = lib.mkForce [ ];
requires = lib.mkForce [ ];
};
# Override Vaultwarden settings for testing
# - Listen on all interfaces (not just localhost)
# - Enable logging at info level to capture failed login attempts
services.vaultwarden.config = {
ROCKET_ADDRESS = lib.mkForce "0.0.0.0";
ROCKET_LOG = lib.mkForce "info";
};
# Override for faster testing and correct port
services.fail2ban.jails.vaultwarden.settings = {
maxretry = lib.mkForce 3;
# In test, we connect directly to Vaultwarden port, not via Caddy
port = lib.mkForce "8222";
};
networking.firewall.allowedTCPPorts = [ 8222 ];
};
client = {
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
import time
import re
start_all()
server.wait_for_unit("vaultwarden.service")
server.wait_for_unit("fail2ban.service")
server.wait_for_open_port(8222)
time.sleep(2)
with subtest("Verify vaultwarden jail is active"):
status = server.succeed("fail2ban-client status")
assert "vaultwarden" in status, f"vaultwarden 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:8222/identity/connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Bitwarden-Client-Name: web' \
-H 'Bitwarden-Client-Version: 2024.1.0' \
-d 'grant_type=password&username=bad@user.com&password=badpass&scope=api+offline_access&client_id=web&deviceType=10&deviceIdentifier=test&deviceName=test' \
|| true
""")
time.sleep(0.5)
with subtest("Verify IP is banned"):
time.sleep(3)
status = server.succeed("fail2ban-client status vaultwarden")
print(f"vaultwarden 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:8222/ 2>&1")[0]
assert exit_code != 0, "Connection should be blocked"
'';
}

53
tests/file-perms.nix Normal file
View File

@@ -0,0 +1,53 @@
{
config,
lib,
pkgs,
...
}:
let
testPkgs = pkgs.appendOverlays [ (import ../modules/overlays.nix) ];
in
testPkgs.testers.runNixOSTest {
name = "file-perms test";
nodes.machine =
{ pkgs, ... }:
{
imports = [
(lib.serviceFilePerms "test-service" [
"Z /tmp/test-perms-dir 0750 nobody nogroup"
])
];
systemd.services."test-service" = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe pkgs.bash;
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
# Create test directory with wrong permissions
machine.succeed("mkdir -p /tmp/test-perms-dir")
machine.succeed("chown root:root /tmp/test-perms-dir")
machine.succeed("chmod 700 /tmp/test-perms-dir")
# Start service -- this should pull in test-service-file-perms
machine.succeed("systemctl start test-service")
# Verify file-perms service ran and is active
machine.succeed("systemctl is-active test-service-file-perms.service")
# Verify permissions were fixed by tmpfiles
result = machine.succeed("stat -c '%U:%G' /tmp/test-perms-dir").strip()
assert result == "nobody:nogroup", f"Expected nobody:nogroup, got {result}"
result = machine.succeed("stat -c '%a' /tmp/test-perms-dir").strip()
assert result == "750", f"Expected 750, got {result}"
'';
}

View File

@@ -0,0 +1,583 @@
{
lib,
pkgs,
inputs,
...
}:
let
payloads = {
auth = pkgs.writeText "auth.json" (builtins.toJSON { Username = "jellyfin"; });
empty = pkgs.writeText "empty.json" (builtins.toJSON { });
};
in
pkgs.testers.runNixOSTest {
name = "jellyfin-qbittorrent-monitor";
nodes = {
server =
{ ... }:
{
imports = [
inputs.vpn-confinement.nixosModules.default
];
services.jellyfin.enable = true;
# Real qBittorrent service
services.qbittorrent = {
enable = true;
webuiPort = 8080;
openFirewall = true;
serverConfig.LegalNotice.Accepted = true;
serverConfig.Preferences = {
WebUI = {
# Disable authentication for testing
AuthSubnetWhitelist = "0.0.0.0/0,::/0";
AuthSubnetWhitelistEnabled = true;
LocalHostAuth = false;
};
Downloads = {
SavePath = "/var/lib/qbittorrent/downloads";
TempPath = "/var/lib/qbittorrent/incomplete";
};
};
serverConfig.BitTorrent.Session = {
# Normal speed - unlimited
GlobalUPSpeedLimit = 0;
GlobalDLSpeedLimit = 0;
# Alternate speed limits for when Jellyfin is streaming
AlternativeGlobalUPSpeedLimit = 100;
AlternativeGlobalDLSpeedLimit = 100;
};
};
environment.systemPackages = with pkgs; [
curl
ffmpeg
];
virtualisation.diskSize = 3 * 1024;
networking.firewall.allowedTCPPorts = [
8096
8080
];
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
{
address = "192.168.1.1";
prefixLength = 24;
}
];
networking.interfaces.eth1.ipv4.routes = [
{
address = "203.0.113.0";
prefixLength = 24;
}
];
# Create directories for qBittorrent
systemd.tmpfiles.rules = [
"d /var/lib/qbittorrent/downloads 0755 qbittorrent qbittorrent"
"d /var/lib/qbittorrent/incomplete 0755 qbittorrent qbittorrent"
];
};
# Public test IP (RFC 5737 TEST-NET-3) so Jellyfin sees it as external
client = {
environment.systemPackages = [ pkgs.curl ];
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
{
address = "203.0.113.10";
prefixLength = 24;
}
];
networking.interfaces.eth1.ipv4.routes = [
{
address = "192.168.1.0";
prefixLength = 24;
}
];
};
};
testScript = ''
import json
import time
from urllib.parse import urlencode
auth_header = 'MediaBrowser Client="NixOS Test", DeviceId="test-1337", Device="TestDevice", Version="1.0"'
def api_get(path, token=None):
header = auth_header + (f", Token={token}" if token else "")
return f"curl -sf 'http://server:8096{path}' -H 'X-Emby-Authorization:{header}'"
def api_post(path, json_file=None, token=None):
header = auth_header + (f", Token={token}" if token else "")
if json_file:
return f"curl -sf -X POST 'http://server:8096{path}' -d '@{json_file}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{header}'"
return f"curl -sf -X POST 'http://server:8096{path}' -H 'X-Emby-Authorization:{header}'"
def is_throttled():
return server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode").strip() == "1"
def get_alt_dl_limit():
prefs = json.loads(server.succeed("curl -s http://localhost:8080/api/v2/app/preferences"))
return prefs["alt_dl_limit"]
def get_alt_up_limit():
prefs = json.loads(server.succeed("curl -s http://localhost:8080/api/v2/app/preferences"))
return prefs["alt_up_limit"]
def are_torrents_paused():
torrents = json.loads(server.succeed("curl -s 'http://localhost:8080/api/v2/torrents/info'"))
if not torrents:
return False
return all(t["state"].startswith("stopped") for t in torrents)
movie_id: str = ""
media_source_id: str = ""
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_open_port(8096)
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080)
# Wait for qBittorrent WebUI to be responsive
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
with subtest("Complete Jellyfin setup wizard"):
server.wait_until_succeeds(api_get("/Startup/Configuration"))
server.succeed(api_get("/Startup/FirstUser"))
server.succeed(api_post("/Startup/Complete"))
with subtest("Authenticate and get token"):
auth_result = json.loads(server.succeed(api_post("/Users/AuthenticateByName", "${payloads.auth}")))
token = auth_result["AccessToken"]
user_id = auth_result["User"]["Id"]
with subtest("Create test video library"):
tempdir = server.succeed("mktemp -d -p /var/lib/jellyfin").strip()
server.succeed(f"chmod 755 '{tempdir}'")
server.succeed(f"ffmpeg -f lavfi -i testsrc2=duration=5 '{tempdir}/Test Movie (2024) [1080p].mkv'")
add_folder_query = urlencode({
"name": "Test Library",
"collectionType": "Movies",
"paths": tempdir,
"refreshLibrary": "true",
})
server.succeed(api_post(f"/Library/VirtualFolders?{add_folder_query}", "${payloads.empty}", token))
def is_library_ready(_):
folders = json.loads(server.succeed(api_get("/Library/VirtualFolders", token)))
return all(f.get("RefreshStatus") == "Idle" for f in folders)
retry(is_library_ready, timeout=60)
def get_movie(_):
global movie_id, media_source_id
items = json.loads(server.succeed(api_get(f"/Users/{user_id}/Items?IncludeItemTypes=Movie&Recursive=true", token)))
if items["TotalRecordCount"] > 0:
movie_id = items["Items"][0]["Id"]
item_info = json.loads(server.succeed(api_get(f"/Users/{user_id}/Items/{movie_id}", token)))
media_source_id = item_info["MediaSources"][0]["Id"]
return True
return False
retry(get_movie, timeout=60)
with subtest("Start monitor service"):
python = "${pkgs.python3.withPackages (ps: [ ps.requests ])}/bin/python"
monitor = "${../services/jellyfin-qbittorrent-monitor.py}"
server.succeed(f"""
systemd-run --unit=monitor-test \
--setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \
--setenv=TOTAL_BANDWIDTH_BUDGET=50000000 \
--setenv=SERVICE_BUFFER=2000000 \
--setenv=DEFAULT_STREAM_BITRATE=10000000 \
--setenv=MIN_TORRENT_SPEED=100 \
{python} {monitor}
""")
time.sleep(2)
assert not is_throttled(), "Should start unthrottled"
client_auth = 'MediaBrowser Client="External Client", DeviceId="external-9999", Device="ExternalDevice", Version="1.0"'
client_auth2 = 'MediaBrowser Client="External Client 2", DeviceId="external-8888", Device="ExternalDevice2", Version="1.0"'
server_ip = "192.168.1.1"
with subtest("Client authenticates from external network"):
auth_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}'"
client_auth_result = json.loads(client.succeed(auth_cmd))
client_token = client_auth_result["AccessToken"]
with subtest("Second client authenticates from external network"):
auth_cmd2 = f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth2}'"
client_auth_result2 = json.loads(client.succeed(auth_cmd2))
client_token2 = client_auth_result2["AccessToken"]
with subtest("External video playback triggers throttling"):
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-1",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(2)
assert is_throttled(), "Should throttle for external video playback"
with subtest("Pausing disables throttling"):
playback_progress = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-1",
"IsPaused": True,
"PositionTicks": 10000000,
}
progress_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Progress' -d '{json.dumps(playback_progress)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(progress_cmd)
time.sleep(2)
assert not is_throttled(), "Should unthrottle when paused"
with subtest("Resuming re-enables throttling"):
playback_progress["IsPaused"] = False
playback_progress["PositionTicks"] = 20000000
progress_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Progress' -d '{json.dumps(playback_progress)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(progress_cmd)
time.sleep(2)
assert is_throttled(), "Should re-throttle when resumed"
with subtest("Stopping playback disables throttling"):
playback_stop = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-1",
"PositionTicks": 50000000,
}
stop_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(playback_stop)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(stop_cmd)
time.sleep(2)
assert not is_throttled(), "Should unthrottle when playback stops"
with subtest("Single stream sets proportional alt speed limits"):
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-proportional",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(3)
assert is_throttled(), "Should be in alt speed mode during streaming"
dl_limit = get_alt_dl_limit()
ul_limit = get_alt_up_limit()
# Both upload and download should get remaining bandwidth (proportional)
assert dl_limit > 0, f"Download limit should be > 0, got {dl_limit}"
assert ul_limit == dl_limit, f"Upload limit ({ul_limit}) should equal download limit ({dl_limit})"
# Stop playback
playback_stop = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-proportional",
"PositionTicks": 50000000,
}
stop_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(playback_stop)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(stop_cmd)
time.sleep(3)
with subtest("Multiple streams reduce available bandwidth"):
# Start first stream
playback1 = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-multi-1",
"CanSeek": True,
"IsPaused": False,
}
start_cmd1 = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback1)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd1)
time.sleep(3)
single_dl_limit = get_alt_dl_limit()
# Start second stream with different client identity
playback2 = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-multi-2",
"CanSeek": True,
"IsPaused": False,
}
start_cmd2 = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback2)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth2}, Token={client_token2}'"
client.succeed(start_cmd2)
time.sleep(3)
dual_dl_limit = get_alt_dl_limit()
# Two streams should leave less bandwidth than one stream
assert dual_dl_limit < single_dl_limit, f"Two streams ({dual_dl_limit}) should have lower limit than one ({single_dl_limit})"
# Stop both streams
stop1 = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-multi-1",
"PositionTicks": 50000000,
}
stop_cmd1 = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(stop1)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(stop_cmd1)
stop2 = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-multi-2",
"PositionTicks": 50000000,
}
stop_cmd2 = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(stop2)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth2}, Token={client_token2}'"
client.succeed(stop_cmd2)
time.sleep(3)
with subtest("Budget exhaustion pauses all torrents"):
# Stop current monitor
server.succeed("systemctl stop monitor-test || true")
time.sleep(1)
# Add a dummy torrent so we can check pause state
server.succeed("curl -sf -X POST 'http://localhost:8080/api/v2/torrents/add' -d 'urls=magnet:?xt=urn:btih:0000000000000000000000000000000000000001%26dn=test-torrent'")
time.sleep(2)
# Start monitor with impossibly low budget
server.succeed(f"""
systemd-run --unit=monitor-exhaust \
--setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \
--setenv=TOTAL_BANDWIDTH_BUDGET=1000 \
--setenv=SERVICE_BUFFER=500 \
--setenv=DEFAULT_STREAM_BITRATE=10000000 \
--setenv=MIN_TORRENT_SPEED=100 \
{python} {monitor}
""")
time.sleep(2)
# Start a stream - this will exceed the tiny budget
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-exhaust",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(3)
assert are_torrents_paused(), "Torrents should be paused when budget is exhausted"
with subtest("Recovery from pause restores unlimited"):
# Stop the stream
playback_stop = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-exhaust",
"PositionTicks": 50000000,
}
stop_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(playback_stop)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(stop_cmd)
time.sleep(3)
assert not is_throttled(), "Should return to unlimited after streams stop"
assert not are_torrents_paused(), "Torrents should be resumed after streams stop"
# Clean up: stop exhaust monitor, restart normal monitor
server.succeed("systemctl stop monitor-exhaust || true")
time.sleep(1)
server.succeed(f"""
systemd-run --unit=monitor-test \
--setenv=JELLYFIN_URL=http://localhost:8096 \
--setenv=JELLYFIN_API_KEY={token} \
--setenv=QBITTORRENT_URL=http://localhost:8080 \
--setenv=CHECK_INTERVAL=1 \
--setenv=STREAMING_START_DELAY=1 \
--setenv=STREAMING_STOP_DELAY=1 \
--setenv=TOTAL_BANDWIDTH_BUDGET=50000000 \
--setenv=SERVICE_BUFFER=2000000 \
--setenv=DEFAULT_STREAM_BITRATE=10000000 \
--setenv=MIN_TORRENT_SPEED=100 \
{python} {monitor}
""")
time.sleep(2)
with subtest("Local playback does NOT trigger throttling"):
local_auth = 'MediaBrowser Client="Local Client", DeviceId="local-1111", Device="LocalDevice", Version="1.0"'
local_auth_result = json.loads(server.succeed(
f"curl -sf -X POST 'http://localhost:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{local_auth}'"
))
local_token = local_auth_result["AccessToken"]
local_playback = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-local",
"CanSeek": True,
"IsPaused": False,
}
server.succeed(f"curl -sf -X POST 'http://localhost:8096/Sessions/Playing' -d '{json.dumps(local_playback)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{local_auth}, Token={local_token}'")
time.sleep(2)
assert not is_throttled(), "Should NOT throttle for local playback"
local_playback["PositionTicks"] = 50000000
server.succeed(f"curl -sf -X POST 'http://localhost:8096/Sessions/Playing/Stopped' -d '{json.dumps(local_playback)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{local_auth}, Token={local_token}'")
# === SERVICE RESTART TESTS ===
with subtest("qBittorrent restart during throttled state re-applies throttling"):
# Start external playback to trigger throttling
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-restart-1",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(2)
assert is_throttled(), "Should be throttled before qBittorrent restart"
# Restart qBittorrent (this resets alt_speed to its config default - disabled)
server.succeed("systemctl restart qbittorrent.service")
server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080)
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
# qBittorrent restarted - alt_speed is now False (default on startup)
# The monitor should detect this and re-apply throttling
time.sleep(3) # Give monitor time to detect and re-apply
assert is_throttled(), "Monitor should re-apply throttling after qBittorrent restart"
# Stop playback to clean up
playback_stop = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-restart-1",
"PositionTicks": 50000000,
}
stop_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing/Stopped' -d '{json.dumps(playback_stop)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(stop_cmd)
time.sleep(2)
with subtest("qBittorrent restart during unthrottled state stays unthrottled"):
# Verify we're unthrottled (no active streams)
assert not is_throttled(), "Should be unthrottled before test"
# Restart qBittorrent
server.succeed("systemctl restart qbittorrent.service")
server.wait_for_unit("qbittorrent.service")
server.wait_for_open_port(8080)
server.wait_until_succeeds("curl -sf http://localhost:8080/api/v2/app/version", timeout=30)
# Give monitor time to check state
time.sleep(3)
assert not is_throttled(), "Should remain unthrottled after qBittorrent restart with no streams"
with subtest("Jellyfin restart during throttled state maintains throttling"):
# Start external playback to trigger throttling
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-restart-2",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(2)
assert is_throttled(), "Should be throttled before Jellyfin restart"
# Restart Jellyfin
server.succeed("systemctl restart jellyfin.service")
server.wait_for_unit("jellyfin.service")
server.wait_for_open_port(8096)
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
# During Jellyfin restart, monitor can't reach Jellyfin
# After restart, sessions are cleared - monitor should eventually unthrottle
# But during the unavailability window, throttling should be maintained (fail-safe)
time.sleep(3)
# Re-authenticate (old token invalid after restart)
client_auth_result = json.loads(client.succeed(
f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}'"
))
client_token = client_auth_result["AccessToken"]
client_auth_result2 = json.loads(client.succeed(
f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth2}'"
))
client_token2 = client_auth_result2["AccessToken"]
# No active streams after Jellyfin restart, should eventually unthrottle
time.sleep(3)
assert not is_throttled(), "Should unthrottle after Jellyfin restart clears sessions"
with subtest("Monitor recovers after Jellyfin temporary unavailability"):
# Re-authenticate with fresh token
client_auth_result = json.loads(client.succeed(
f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}'"
))
client_token = client_auth_result["AccessToken"]
client_auth_result2 = json.loads(client.succeed(
f"curl -sf -X POST 'http://{server_ip}:8096/Users/AuthenticateByName' -d '@${payloads.auth}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth2}'"
))
client_token2 = client_auth_result2["AccessToken"]
# Start playback
playback_start = {
"ItemId": movie_id,
"MediaSourceId": media_source_id,
"PlaySessionId": "test-play-session-restart-3",
"CanSeek": True,
"IsPaused": False,
}
start_cmd = f"curl -sf -X POST 'http://{server_ip}:8096/Sessions/Playing' -d '{json.dumps(playback_start)}' -H 'Content-Type:application/json' -H 'X-Emby-Authorization:{client_auth}, Token={client_token}'"
client.succeed(start_cmd)
time.sleep(2)
assert is_throttled(), "Should be throttled"
# Stop Jellyfin briefly (simulating temporary unavailability)
server.succeed("systemctl stop jellyfin.service")
time.sleep(2)
# During unavailability, throttle state should be maintained (fail-safe)
assert is_throttled(), "Should maintain throttle during Jellyfin unavailability"
# Bring Jellyfin back
server.succeed("systemctl start jellyfin.service")
server.wait_for_unit("jellyfin.service")
server.wait_for_open_port(8096)
server.wait_until_succeeds("curl -sf http://localhost:8096/health | grep -q Healthy", timeout=60)
# After Jellyfin comes back, sessions are gone - should unthrottle
time.sleep(3)
assert not is_throttled(), "Should unthrottle after Jellyfin returns with no sessions"
'';
}

98
tests/minecraft.nix Normal file
View File

@@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
inputs,
...
}:
let
testServiceConfigs = {
minecraft = {
server_name = "main";
parent_dir = "/var/lib/minecraft";
};
https = {
domain = "test.local";
};
ports = {
minecraft = 25565;
};
zpool_ssds = "";
};
# Create pkgs with nix-minecraft overlay and unfree packages allowed
testPkgs = import inputs.nixpkgs {
system = pkgs.stdenv.targetPlatform.system;
config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "minecraft-server" ];
overlays = [
inputs.nix-minecraft.overlay
(import ../modules/overlays.nix)
];
};
in
testPkgs.testers.runNixOSTest {
name = "minecraft server startup test";
node.specialArgs = {
inherit inputs lib;
service_configs = testServiceConfigs;
username = "testuser";
};
nodes.machine =
{ lib, ... }:
{
imports = [
../services/minecraft.nix
];
# Enable caddy service (required by minecraft service)
services.caddy.enable = true;
# Enable networking for the test (needed for minecraft mods to download mappings)
networking.dhcpcd.enable = true;
# Disable the ZFS mount dependency service in test environment
systemd.services."minecraft-server-main_mounts".enable = lib.mkForce false;
# Remove service dependencies that require ZFS
systemd.services.minecraft-server-main = {
wants = lib.mkForce [ ];
after = lib.mkForce [ ];
requires = lib.mkForce [ ];
serviceConfig = {
Nice = lib.mkForce 0;
LimitMEMLOCK = lib.mkForce "infinity";
};
};
# Test-specific overrides only - reduce memory for testing
services.minecraft-servers.servers.main.jvmOpts = lib.mkForce "-Xmx1G -Xms1G";
# Create test user
users.users.testuser = {
isNormalUser = true;
uid = 1000;
extraGroups = [ "minecraft" ];
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
# Wait for minecraft service to be available
machine.wait_for_unit("minecraft-server-main.service")
# Wait up to 60 seconds for the server to complete startup
with machine.nested("Waiting for minecraft server startup completion"):
try:
machine.wait_until_succeeds(
"grep -Eq '\\[[0-9]+:[0-9]+:[0-9]+\\] \\[Server thread/INFO\\]: Done \\([0-9]+\\.[0-9]+s\\)! For help, type \"help\"' /var/lib/minecraft/main/logs/latest.log",
timeout=120
)
except Exception:
print(machine.succeed("cat /var/lib/minecraft/main/logs/latest.log"))
raise
'';
}

174
tests/ntfy-alerts.nix Normal file
View File

@@ -0,0 +1,174 @@
{
config,
lib,
pkgs,
...
}:
let
testPkgs = pkgs.appendOverlays [ (import ../modules/overlays.nix) ];
in
testPkgs.testers.runNixOSTest {
name = "ntfy-alerts";
nodes.machine =
{ pkgs, ... }:
{
imports = [
../modules/ntfy-alerts.nix
];
system.stateVersion = config.system.stateVersion;
virtualisation.memorySize = 2048;
environment.systemPackages = with pkgs; [
curl
jq
];
# Create test topic file
systemd.tmpfiles.rules = [
"f /run/ntfy-test-topic 0644 root root - test-alerts"
];
# Mock ntfy server that records POST requests
systemd.services.mock-ntfy =
let
mockNtfyScript = pkgs.writeScript "mock-ntfy.py" ''
import json
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
REQUESTS_FILE = "/tmp/ntfy-requests.json"
class MockNtfy(BaseHTTPRequestHandler):
def _respond(self, code=200, body=b"Ok"):
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(body if isinstance(body, bytes) else body.encode())
def do_GET(self):
self._respond()
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode() if content_length > 0 else ""
request_data = {
"timestamp": datetime.now().isoformat(),
"path": self.path,
"headers": dict(self.headers),
"body": body,
}
# Load existing requests or start new list
requests = []
if os.path.exists(REQUESTS_FILE):
try:
with open(REQUESTS_FILE, "r") as f:
requests = json.load(f)
except:
requests = []
requests.append(request_data)
with open(REQUESTS_FILE, "w") as f:
json.dump(requests, f, indent=2)
self._respond()
def log_message(self, format, *args):
pass
HTTPServer(("0.0.0.0", 8080), MockNtfy).serve_forever()
'';
in
{
description = "Mock ntfy server";
wantedBy = [ "multi-user.target" ];
before = [ "ntfy-alert@test-fail.service" ];
serviceConfig = {
ExecStart = "${pkgs.python3}/bin/python3 ${mockNtfyScript}";
Type = "simple";
};
};
# Test service that will fail
systemd.services.test-fail = {
description = "Test service that fails";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/false";
};
};
# Configure ntfy-alerts to use mock server
services.ntfyAlerts = {
enable = true;
serverUrl = "http://localhost:8080";
topicFile = "/run/ntfy-test-topic";
};
};
testScript = ''
import json
import time
start_all()
# Wait for mock ntfy server to be ready
machine.wait_for_unit("mock-ntfy.service")
machine.wait_until_succeeds("curl -sf http://localhost:8080/", timeout=30)
# Verify the ntfy-alert@ template service exists
machine.succeed("systemctl list-unit-files | grep ntfy-alert@")
# Verify the global OnFailure drop-in is configured
machine.succeed("cat /etc/systemd/system/service.d/onfailure.conf | grep -q 'OnFailure=ntfy-alert@%p.service'")
# Trigger the test-fail service
machine.succeed("systemctl start test-fail.service || true")
# Wait a moment for the failure notification to be sent
time.sleep(2)
# Verify the ntfy-alert@test-fail service ran
machine.succeed("systemctl is-active ntfy-alert@test-fail.service || systemctl is-failed ntfy-alert@test-fail.service || true")
# Check that the mock server received a POST request
machine.wait_until_succeeds("test -f /tmp/ntfy-requests.json", timeout=30)
# Verify the request content
result = machine.succeed("cat /tmp/ntfy-requests.json")
requests = json.loads(result)
assert len(requests) >= 1, f"Expected at least 1 request, got {len(requests)}"
# Check the first request
req = requests[0]
assert "/test-alerts" in req["path"], f"Expected path to contain /test-alerts, got {req['path']}"
assert "Title" in req["headers"], "Expected Title header"
assert "test-fail" in req["headers"]["Title"], f"Expected Title to contain 'test-fail', got {req['headers']['Title']}"
assert req["headers"]["Priority"] == "high", f"Expected Priority 'high', got {req['headers'].get('Priority')}"
assert req["headers"]["Tags"] == "warning", f"Expected Tags 'warning', got {req['headers'].get('Tags')}"
print(f"Received notification: Title={req['headers']['Title']}, Body={req['body'][:100]}...")
# Idempotency test: trigger failure again
machine.succeed("rm /tmp/ntfy-requests.json")
machine.succeed("systemctl reset-failed test-fail.service || true")
machine.succeed("systemctl start test-fail.service || true")
time.sleep(2)
# Verify another notification was sent
machine.wait_until_succeeds("test -f /tmp/ntfy-requests.json", timeout=30)
result = machine.succeed("cat /tmp/ntfy-requests.json")
requests = json.loads(result)
assert len(requests) >= 1, f"Expected at least 1 request after second failure, got {len(requests)}"
print("All tests passed!")
'';
}

20
tests/testTest.nix Normal file
View File

@@ -0,0 +1,20 @@
{
config,
lib,
pkgs,
...
}:
pkgs.testers.runNixOSTest {
name = "test of tests";
nodes.machine =
{ pkgs, ... }:
{
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("echo hello!")
'';
}

27
tests/tests.nix Normal file
View File

@@ -0,0 +1,27 @@
{
config,
lib,
pkgs,
...
}@args:
let
handleTest = file: import file (args);
in
{
zfsTest = handleTest ./zfs.nix;
testTest = handleTest ./testTest.nix;
minecraftTest = handleTest ./minecraft.nix;
jellyfinQbittorrentMonitorTest = handleTest ./jellyfin-qbittorrent-monitor.nix;
filePermsTest = handleTest ./file-perms.nix;
# fail2ban tests
fail2banSshTest = handleTest ./fail2ban-ssh.nix;
fail2banCaddyTest = handleTest ./fail2ban-caddy.nix;
fail2banGiteaTest = handleTest ./fail2ban-gitea.nix;
fail2banVaultwardenTest = handleTest ./fail2ban-vaultwarden.nix;
fail2banImmichTest = handleTest ./fail2ban-immich.nix;
fail2banJellyfinTest = handleTest ./fail2ban-jellyfin.nix;
# ntfy alerts test
ntfyAlertsTest = handleTest ./ntfy-alerts.nix;
}

153
tests/zfs.nix Normal file
View File

@@ -0,0 +1,153 @@
{
config,
lib,
pkgs,
inputs,
...
}:
let
# Create pkgs with ensureZfsMounts overlay
testPkgs = pkgs.appendOverlays [ (import ../modules/overlays.nix) ];
in
testPkgs.testers.runNixOSTest {
name = "zfs test";
nodes.machine =
{ pkgs, ... }:
{
imports = [
# Test valid paths within zpool
(lib.serviceMountWithZpool "test-service" "rpool" [ "/mnt/rpool_data" ])
# Test service with paths outside zpool (should fail assertion)
(lib.serviceMountWithZpool "invalid-service" "rpool2" [ "/mnt/rpool_data" ])
# Test multi-command logic: service with multiple serviceMountWithZpool calls
(lib.serviceMountWithZpool "multi-service" "rpool" [ "/mnt/rpool_data" ])
(lib.serviceMountWithZpool "multi-service" "rpool2" [ "/mnt/rpool2_data" ])
# Test multi-command logic: service with multiple serviceMountWithZpool calls
# BUT this one should fail as `/mnt/rpool_moar_data` is not on rpool2
(lib.serviceMountWithZpool "multi-service-fail" "rpool" [ "/mnt/rpool_data" ])
(lib.serviceMountWithZpool "multi-service-fail" "rpool2" [ "/mnt/rpool_moar_data" ])
];
virtualisation = {
emptyDiskImages = [
4096
4096
];
# Add this to avoid ZFS hanging issues
additionalPaths = [ pkgs.zfs ];
};
networking.hostId = "deadbeef";
boot.kernelPackages = config.boot.kernelPackages;
boot.zfs.package = config.boot.zfs.package;
boot.supportedFilesystems = [ "zfs" ];
environment.systemPackages = with pkgs; [
parted
ensureZfsMounts
];
systemd.services."test-service" = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe pkgs.bash;
};
};
systemd.services."invalid-service" = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe pkgs.bash;
};
};
systemd.services."multi-service" = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe pkgs.bash;
};
};
systemd.services."multi-service-fail" = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe pkgs.bash;
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
# Setup ZFS pool
machine.succeed(
"parted --script /dev/vdb mklabel msdos",
"parted --script /dev/vdb -- mkpart primary 1024M -1s",
"zpool create rpool /dev/vdb1"
)
# Setup ZFS pool 2
machine.succeed(
"parted --script /dev/vdc mklabel msdos",
"parted --script /dev/vdc -- mkpart primary 1024M -1s",
"zpool create rpool2 /dev/vdc1"
)
machine.succeed("zfs create -o mountpoint=/mnt/rpool_data rpool/data")
machine.succeed("zfs create -o mountpoint=/mnt/rpool2_data rpool2/data")
machine.succeed("zfs create -o mountpoint=/mnt/rpool_moar_data rpool/moar_data")
# Test that valid service starts successfully
machine.succeed("systemctl start test-service")
# Manually test our validation logic by checking the debug output
zfs_output = machine.succeed("zfs list -H -o name,mountpoint")
print("ZFS LIST OUTPUT:")
print(zfs_output)
dataset = machine.succeed("zfs list -H -o name,mountpoint | awk '/\\/mnt\\/rpool_data/ { print $1 }'")
print("DATASET FOR /mnt/rpool_data:")
print(dataset)
# Test that invalid-service mount service fails validation
machine.fail("systemctl start invalid-service.service")
# Check the journal for our detailed validation error message
journal_output = machine.succeed("journalctl -u invalid-service-mounts.service --no-pager")
print("JOURNAL OUTPUT:")
print(journal_output)
# Verify our validation error is in the journal using Python string matching
assert "ERROR: ZFS pool mismatch for /mnt/rpool_data" in journal_output
assert "Expected pool: rpool2" in journal_output
assert "Actual pool: rpool" in journal_output
# Test that invalid-service mount service fails validation
machine.fail("systemctl start multi-service-fail.service")
# Check the journal for our detailed validation error message
journal_output = machine.succeed("journalctl -u multi-service-fail-mounts.service --no-pager")
print("JOURNAL OUTPUT:")
print(journal_output)
# Verify our validation error is in the journal using Python string matching
assert "ERROR: ZFS pool mismatch for /mnt/rpool_moar_data" in journal_output, "no zfs pool mismatch found (1)"
assert "Expected pool: rpool2" in journal_output, "no zfs pool mismatch found (2)"
assert "Actual pool: rpool" in journal_output, "no zfs pool mismatch found (3)"
machine.succeed("systemctl start multi-service")
machine.succeed("systemctl is-active multi-service-mounts.service")
'';
}

44
usb-secrets/setup-usb.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash -p parted dosfstools
set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
USB_DEVICE="$1"
if [[ -z "${USB_DEVICE:-}" ]]; then
echo "Usage: $0 <usb_device>"
echo "Example: $0 /dev/sdb"
exit 1
fi
if [[ ! -b "$USB_DEVICE" ]]; then
echo "Error: $USB_DEVICE is not a block device"
exit 1
fi
if [[ ! -f "$SCRIPT_DIR/usb-secrets/usb-secrets-key" ]]; then
echo "Error: usb-secrets-key not found at $SCRIPT_DIR/usb-secrets/usb-secrets-key"
exit 1
fi
echo "WARNING: This will completely wipe $USB_DEVICE"
echo "Press Ctrl+C to abort, or Enter to continue..."
read
echo "Creating partition and formatting as FAT32..."
parted -s "$USB_DEVICE" mklabel msdos
parted -s "$USB_DEVICE" mkpart primary fat32 0% 100%
parted -s "$USB_DEVICE" set 1 boot on
USB_PARTITION="${USB_DEVICE}1"
mkfs.fat -F 32 -n "SECRETS" "$USB_PARTITION"
echo "Copying key to USB..."
MOUNT_POINT=$(mktemp -d)
trap "umount $MOUNT_POINT 2>/dev/null || true; rmdir $MOUNT_POINT" EXIT
mount "$USB_PARTITION" "$MOUNT_POINT"
cp "$SCRIPT_DIR/usb-secrets/usb-secrets-key" "$MOUNT_POINT/"
umount "$MOUNT_POINT"
echo "USB setup complete! Label: SECRETS"
echo "Create multiple backup USB keys for redundancy."

BIN
usb-secrets/usb-secrets-key Normal file

Binary file not shown.