diff --git a/flake.lock b/flake.lock index d37dc7e..b3a1c22 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,27 @@ { "nodes": { + "agenix": { + "inputs": { + "darwin": "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", + "rev": "b027ee29d959fda4b60b57566d64c98a202e0feb", + "type": "github" + } + }, "bun2nix": { "inputs": { "flake-parts": [ @@ -34,11 +56,29 @@ "type": "github" } }, + "catppuccin": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1789553013, + "narHash": "sha256-W5dvgFOuVs24X3G5tUb8C2IHU7ICXNvyGPiWWFjfbuo=", + "owner": "catppuccin", + "repo": "nix", + "rev": "89b3eacf59d6b5eefbc2d69c3a4eb5aaf66d63bc", + "type": "github" + }, + "original": { + "owner": "catppuccin", + "repo": "nix", + "type": "github" + } + }, "chaotic": { "inputs": { "flake-schemas": "flake-schemas", - "home-manager": "home-manager", - "nixpkgs": "nixpkgs" + "home-manager": "home-manager_2", + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1788855501, @@ -70,6 +110,59 @@ "type": "github" } }, + "darwin": { + "inputs": { + "nixpkgs": [ + "agenix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1744478979, + "narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=", + "owner": "lnl7", + "repo": "nix-darwin", + "rev": "43975d782b418ebf4969e9ccba82466728c2851b", + "type": "github" + }, + "original": { + "owner": "lnl7", + "ref": "master", + "repo": "nix-darwin", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -91,6 +184,42 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1788450739, + "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1772408722, + "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-schemas": { "locked": { "lastModified": 1780327564, @@ -107,7 +236,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems_3" }, "locked": { "lastModified": 1731533236, @@ -123,7 +252,53 @@ "type": "github" } }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "nix-gaming", + "flake-compat" + ], + "nixpkgs": [ + "nix-gaming", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790091288, + "narHash": "sha256-2dUuLTiQrf2gUFVLlayDq/hgluitplAMv6Y9bYwQQ+c=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "0d3997c4d3253505f77c9bcea63904bb575da3c5", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "home-manager": { + "inputs": { + "nixpkgs": [ + "agenix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1745494811, + "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { "inputs": { "nixpkgs": [ "chaotic", @@ -144,12 +319,33 @@ "type": "github" } }, + "home-manager_3": { + "inputs": { + "nixpkgs": [ + "zen-browser", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1789430117, + "narHash": "sha256-t+U1mijItLJ64xZOifpfQ17kpAL73nU7pDI48ScacVc=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "cda90fd8838825c689fde9d3f3b4e937937790df", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, "llm-agents": { "inputs": { "bun2nix": "bun2nix", "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs_2", - "systems": "systems", + "nixpkgs": "nixpkgs_4", + "systems": "systems_2", "treefmt-nix": "treefmt-nix" }, "locked": { @@ -169,7 +365,7 @@ "llm-bridge": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_5" }, "locked": { "lastModified": 1790067159, @@ -187,7 +383,7 @@ }, "meowtd": { "inputs": { - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_6" }, "locked": { "lastModified": 1785527457, @@ -203,7 +399,141 @@ "url": "https://git.koi.rip/koi/meowtd" } }, + "niri": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790353586, + "narHash": "sha256-oEvDG8PTqiy6lvKKWj9D+XZyPzYcl4rm5Qs7qKR0pSk=", + "owner": "niri-wm", + "repo": "niri", + "rev": "1f03391ea644c2a43597de7f637269e26d1e1b49", + "type": "github" + }, + "original": { + "owner": "niri-wm", + "repo": "niri", + "type": "github" + } + }, + "nix-gaming": { + "inputs": { + "flake-compat": "flake-compat", + "flake-parts": "flake-parts_2", + "git-hooks": "git-hooks", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790483338, + "narHash": "sha256-srUVCAD22Bcbg6GJbZEijDI22HMD6Lo2qOoUKtH22dg=", + "owner": "fufexan", + "repo": "nix-gaming", + "rev": "b193ce18777d01469dbeb3b9c4110de2426e0edf", + "type": "github" + }, + "original": { + "owner": "fufexan", + "repo": "nix-gaming", + "type": "github" + } + }, "nixpkgs": { + "locked": { + "lastModified": 1754028485, + "narHash": "sha256-IiiXB3BDTi6UqzAZcf2S797hWEPCRZOwyNThJIYhUfk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "59e69648d345d6e8fef86158c555730fa12af9de", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1788057806, + "narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1772328832, + "narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-libvncserver": { + "locked": { + "lastModified": 1750111231, + "narHash": "sha256-3a7Tha/RwYlzH/v3PJrG7+HjOj4c6YOv2K8sqdGsHVQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e6f23dc08d3624daab7094b701aa3954923c6bbb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e6f23dc08d3624daab7094b701aa3954923c6bbb", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1787753485, + "narHash": "sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "062346a6d85bc4b49dfaa61c986e9c5be21217d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1789044656, + "narHash": "sha256-sDGcZgdRVR58ceKz0RmkBtdUomBvu5vzbf6Aw5rUCo0=", + "rev": "1927682e0d808b4a695910f76562b11e2ddecab4", + "type": "tarball", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1070934.1927682e0d80/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1788752844, "narHash": "sha256-VaWGJ6+cIYN2erfSecbRV+4ljI185Ty2wUrXyvQbgOw=", @@ -219,7 +549,7 @@ "type": "github" } }, - "nixpkgs_2": { + "nixpkgs_4": { "locked": { "lastModified": 1788894124, "narHash": "sha256-guyexwrrF5GBKqjO0eg9LNIvrXn4j2frNak63sDr8zg=", @@ -235,7 +565,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_5": { "locked": { "lastModified": 1779560665, "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", @@ -251,7 +581,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_6": { "locked": { "lastModified": 1781074563, "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", @@ -266,7 +596,7 @@ "type": "indirect" } }, - "nixpkgs_5": { + "nixpkgs_7": { "locked": { "lastModified": 1788881743, "narHash": "sha256-151taSq/21cxagiIu7hyMVl68+FbHfwBP4lh6TB6KOM=", @@ -279,6 +609,59 @@ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, + "nixpkgs_8": { + "locked": { + "lastModified": 1790323409, + "narHash": "sha256-m4DGo58Fza5ImOegtWOeE18nhpSO1IGzsVA6Lpsb4zw=", + "rev": "e94cb152ed51bd6e24eb4a41f1460252beb52cd2", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1079315.e94cb152ed51/nixexprs.tar.zst" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.zst" + } + }, + "noctalia": { + "inputs": { + "nixpkgs": "nixpkgs_8" + }, + "locked": { + "lastModified": 1790523212, + "narHash": "sha256-Z8SuI0YgAZaF0sumKPBF85PxPtTjpo8jvtphbgoITv8=", + "owner": "noctalia-dev", + "repo": "noctalia", + "rev": "08c30392b1350b4f3d3fb77e23732560559f1638", + "type": "github" + }, + "original": { + "owner": "noctalia-dev", + "ref": "cachix", + "repo": "noctalia", + "type": "github" + } + }, + "proxmox-nixos": { + "inputs": { + "flake-compat": "flake-compat_2", + "nixpkgs-libvncserver": "nixpkgs-libvncserver", + "nixpkgs-stable": "nixpkgs-stable", + "utils": "utils" + }, + "locked": { + "lastModified": 1789217472, + "narHash": "sha256-5+iviW11rRXppx5/oTkErbauwk+9e3XhENhdKTG6QJI=", + "owner": "SaumonNet", + "repo": "proxmox-nixos", + "rev": "fc773dcf59bf188fcc806c78af635e5917ca2983", + "type": "github" + }, + "original": { + "owner": "SaumonNet", + "repo": "proxmox-nixos", + "type": "github" + } + }, "ratlogin": { "inputs": { "crane": "crane", @@ -304,12 +687,20 @@ }, "root": { "inputs": { + "agenix": "agenix", + "catppuccin": "catppuccin", "chaotic": "chaotic", "llm-agents": "llm-agents", "llm-bridge": "llm-bridge", "meowtd": "meowtd", - "nixpkgs": "nixpkgs_5", - "ratlogin": "ratlogin" + "niri": "niri", + "nix-gaming": "nix-gaming", + "nixpkgs": "nixpkgs_7", + "noctalia": "noctalia", + "proxmox-nixos": "proxmox-nixos", + "ratlogin": "ratlogin", + "vscode-server": "vscode-server", + "zen-browser": "zen-browser" } }, "rust-overlay": { @@ -363,6 +754,36 @@ "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" + } + }, "tranquil": { "inputs": { "nixpkgs": [ @@ -405,6 +826,63 @@ "repo": "treefmt-nix", "type": "github" } + }, + "utils": { + "inputs": { + "systems": "systems_4" + }, + "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" + } + }, + "vscode-server": { + "inputs": { + "flake-parts": "flake-parts_3" + }, + "locked": { + "lastModified": 1784312229, + "narHash": "sha256-2uHCSUw341o3my1R0U0YCfbnMEazylxb58evWsjGL50=", + "owner": "nix-community", + "repo": "nixos-vscode-server", + "rev": "2f984dfbe7e5271b5c413d3e734374cc1306c921", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-vscode-server", + "type": "github" + } + }, + "zen-browser": { + "inputs": { + "home-manager": "home-manager_3", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790568199, + "narHash": "sha256-h3AHjsOZr9iBEkNfK+Zh7/DoN5iMRpJd/6h/5NxCz9I=", + "owner": "0xc000022070", + "repo": "zen-browser-flake", + "rev": "e50ed94ebf28bae95397e482dbb1c020f50c20c1", + "type": "github" + }, + "original": { + "owner": "0xc000022070", + "repo": "zen-browser-flake", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 3431e75..629e9f7 100644 --- a/flake.nix +++ b/flake.nix @@ -17,6 +17,18 @@ inputs.ratlogin.url = "git+https://tangled.org/ptr.pet/ratlogin"; inputs.ratlogin.inputs.nixpkgs.follows = "nixpkgs"; + inputs.agenix.url = "github:ryantm/agenix/b027ee29d959fda4b60b57566d64c98a202e0feb"; + inputs.proxmox-nixos.url = "github:SaumonNet/proxmox-nixos"; + inputs.vscode-server.url = "github:nix-community/nixos-vscode-server"; + inputs.catppuccin.url = "github:catppuccin/nix"; + inputs.noctalia.url = "github:noctalia-dev/noctalia/cachix"; + inputs.niri.url = "github:niri-wm/niri"; + inputs.niri.inputs.nixpkgs.follows = "nixpkgs"; + inputs.nix-gaming.url = "github:fufexan/nix-gaming"; + inputs.nix-gaming.inputs.nixpkgs.follows = "nixpkgs"; + inputs.zen-browser.url = "github:0xc000022070/zen-browser-flake"; + inputs.zen-browser.inputs.nixpkgs.follows = "nixpkgs"; + outputs = flakeInputs: let diff --git a/hosts/default.nix b/hosts/default.nix index b0d23b6..b8389ff 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -36,6 +36,12 @@ let chernobog = allPkgsSets.x86_64-linux; trimounts = allPkgsSets.x86_64-linux; pupos = allPkgsSets.x86_64-linux; + focalor = allPkgsSets.x86_64-linux; + valefar = allPkgsSets.x86_64-linux // { + pkgs = allPkgsSets.x86_64-linux.pkgs.appendOverlays [ + allPkgsSets.x86_64-linux.inputs.proxmox-nixos.overlays.x86_64-linux + ]; + }; }; in lib.mapAttrs mkSystem systems diff --git a/hosts/focalor/default.nix b/hosts/focalor/default.nix new file mode 100644 index 0000000..d59c6cb --- /dev/null +++ b/hosts/focalor/default.nix @@ -0,0 +1,115 @@ +{ + inputs, + lib, + pkgs, + tlib, + ... +}: +let + system = pkgs.stdenv.hostPlatform.system; +in +{ + imports = [ + "${inputs.home}/nixos" + inputs.catppuccin.nixosModules.catppuccin + inputs.nix-gaming.nixosModules.platformOptimizations + inputs.noctalia.nixosModules.default + inputs.vscode-server.nixosModules.default + ../../modules + ../../modules/stylix-null.nix + ../../users/regent + ./hardware.nix + ] + ++ (tlib.importFolder (toString ./modules)); + + home-manager = { + useGlobalPkgs = true; + backupFileExtension = "HMBackup"; + extraSpecialArgs = { inherit inputs system; }; + users.regent.imports = [ + ../../users/regent/home.nix + inputs.catppuccin.homeModules.catppuccin + inputs.noctalia.homeModules.default + ]; + }; + + modules.llama-cpp.enable = true; + + system.stateVersion = "25.05"; + catppuccin = { + enable = false; + autoEnable = false; + }; + + boot = { + binfmt.emulatedSystems = [ "aarch64-linux" ]; + kernelModules = [ "nct6775" ]; + loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; + supportedFilesystems = [ "nfs" ]; + }; + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + nix.settings = { + trusted-users = lib.mkForce [ "root" ]; + extra-platforms = [ "aarch64-linux" ]; + extra-substituters = [ + "https://noctalia.cachix.org" + "https://cache.numtide.com" + ]; + extra-trusted-public-keys = [ + "noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4=" + "niks3.numtide.com-1:DTx8wZduET09hRmMtKdQDxNNthLQETkc/yaX7M4qK0g=" + ]; + }; + + time.timeZone = "America/New_York"; + i18n.defaultLocale = "en_US.UTF-8"; + environment.variables.EDITOR = lib.mkForce "vim"; + programs.nix-ld.enable = true; + + environment.systemPackages = with pkgs; [ + inputs.agenix.packages.${system}.default + (prismlauncher.override { + jdks = [ + jdk8 + jdk11 + jdk17 + jdk21 + jdk25 + ]; + }) + temurin-bin + signal-desktop + google-chrome + osu-lazer-bin + qpwgraph + easyeffects + pavucontrol + inputs.llm-agents.packages.${system}.omp + inputs.llm-agents.packages.${system}.pi + inputs.llm-agents.packages.${system}.beads + imagemagick + smartmontools + ffmpeg + nodejs_26 + vim + wget + fastfetch + lsof + btop + git + openssl + stdenv + gnumake + parted + zfs + nixos-generators + sqlite + bun + inputs.llm-agents.packages.${system}.prime-agent + unzip + uv + ]; +} diff --git a/hosts/focalor/hardware.nix b/hosts/focalor/hardware.nix new file mode 100644 index 0000000..7d131a0 --- /dev/null +++ b/hosts/focalor/hardware.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + modulesPath, + ... +}: +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot.initrd.availableKernelModules = [ + "nvme" + "xhci_pci" + "ahci" + "uas" + "usbhid" + "sd_mod" + ]; + boot.kernelModules = [ "kvm-amd" ]; + + fileSystems = { + "/" = { + device = "/dev/sda2"; + fsType = "btrfs"; + options = [ + "subvol=root" + "compress=zstd:3" + "noatime" + ]; + }; + "/home" = { + device = "/dev/sda2"; + fsType = "btrfs"; + options = [ + "subvol=home" + "compress=zstd:3" + "noatime" + ]; + }; + "/nix" = { + device = "/dev/sda2"; + fsType = "btrfs"; + options = [ + "subvol=nix" + "compress=zstd:3" + "noatime" + ]; + }; + "/boot" = { + device = "/dev/disk/by-uuid/3F27-30E5"; + fsType = "vfat"; + options = [ "umask=0077" ]; + }; + }; + + swapDevices = [ ]; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/focalor/modules/dawn.nix b/hosts/focalor/modules/dawn.nix new file mode 100644 index 0000000..a2f29b1 --- /dev/null +++ b/hosts/focalor/modules/dawn.nix @@ -0,0 +1,14 @@ +{ pkgs, ... }: +{ + users.users.dawn = { + isNormalUser = true; + createHome = true; + home = "/home/dawn"; + extraGroups = [ "wheel" ]; + shell = pkgs.bashInteractive; + hashedPassword = "$y$j9T$TxLlqj0RWsBtIrEhOTyqh1$mfvSCn5j7VAUymWe2/qUTB7.JdwXbqF5qWqUjqQCMu3"; + openssh.authorizedKeys.keys = [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDUeDBW4hnHgnT0SjVFeGDztht9owObSmiyWXmmIEGQp2IMPqFpCxkOU61osvfCf4ZT92Ok9iJTohLwFBvHJRD/+CH8/b54sgFAx1lLObkJOMLz4iWZ5y4fNtBYuIA2McQSQoMDpDz6TDym7v7HF7zoUyBmfHMT/9WiX/z6Ft9hY63eh9DcF7WVURzeUvXLApt9wUYUxxdC2KZ/VrDPrIOxCcOgj3le+1zTiD8zwfAGhkzRD3IEx0yCYK6oztrh6WTwA5ZW+cLziH2sVEvSHFa2O398gIvZpzsdYTcQt06d/oyZIvftcpxD8IvjpGgHEsN/mAg0ovexyqAVk+TV/1XySKaoPPVCekap0R50CVD9kEk+GlD78XBYi++aAMIq0/D+NOXkgksfODt3yJPPzQx4KH8gcn0dQJM5zeyTwDfclzMRqCwL1eVHY00EbtG9IcLmMsWk/lM6vpHfyHqHlqNJ3CnUuDBccz9p5ORC1cuj4r9CmXPPmh7OYk7gGiQb4oxuqsYClzp93qmU7qMvGwmxBJaVagNIJgBqb5fsne0OMlcer5CH4L31ozszkSkzCXtFWNoTdgQHU1J3DxxL9WQJCfKku4EPJadYOh80USnauOke5CqfsGtf6uMq4l5Ylcc1QcNhRqxpeTLAIZx0EYDhmQ4eGjAZbiv6ddUp9dAdiQ==" + ]; + }; +} diff --git a/hosts/focalor/modules/desktop.nix b/hosts/focalor/modules/desktop.nix new file mode 100644 index 0000000..b3fed41 --- /dev/null +++ b/hosts/focalor/modules/desktop.nix @@ -0,0 +1,155 @@ +{ + config, + inputs, + lib, + pkgs, + ... +}: +let + system = pkgs.stdenv.hostPlatform.system; +in +{ + programs = { + niri = { + enable = true; + package = inputs.niri.packages.${system}.niri; + }; + noctalia = { + enable = true; + recommendedServices.enable = true; + }; + steam = { + enable = true; + platformOptimizations.enable = true; + }; + gamescope.enable = true; + dconf.enable = true; + obs-studio = { + enable = true; + enableVirtualCamera = true; + plugins = [ pkgs.obs-studio-plugins.droidcam-obs ]; + }; + }; + + services = { + greetd = { + enable = true; + settings.default_session = { + command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd niri-session"; + user = "greeter"; + }; + }; + xserver.enable = true; + displayManager.sddm.enable = true; + displayManager.defaultSession = lib.mkForce "niri"; + desktopManager.plasma6.enable = true; + xrdp = { + enable = true; + defaultWindowManager = "startplasma-x11"; + openFirewall = true; + }; + dbus.enable = true; + gvfs.enable = true; + gnome.gnome-keyring.enable = true; + upower.enable = true; + power-profiles-daemon.enable = true; + blueman.enable = true; + pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + }; + }; + + security = { + polkit.enable = true; + rtkit.enable = true; + pam.services.greetd.enableGnomeKeyring = true; + }; + + hardware = { + bluetooth = { + enable = true; + powerOnBoot = true; + }; + graphics.enable = true; + nvidia = { + modesetting.enable = true; + powerManagement.enable = false; + powerManagement.finegrained = false; + open = true; + nvidiaSettings = true; + package = config.boot.kernelPackages.nvidiaPackages.latest; + }; + }; + services.xserver.videoDrivers = [ "nvidia" ]; + + environment = { + variables = { + GBM_BACKEND = "nvidia-drm"; + __GLX_VENDOR_LIBRARY_NAME = "nvidia"; + }; + sessionVariables.NIXOS_OZONE_WL = "1"; + systemPackages = with pkgs; [ + kitty + vscode + zed-editor + fastfetch + hyfetch + pamixer + zellij + firefox + chromium + kpcli + eyedropper + krita + thunar + libreoffice + signal-desktop + haruna + (symlinkJoin { + name = "equibop-wrapped"; + paths = [ equibop ]; + nativeBuildInputs = [ makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/equibop \ + --add-flags "--disable-features=WebRtcAllowInputVolumeAdjustment" + ''; + }) + inputs.zen-browser.packages.${system}.default + grim + slurp + wl-clipboard + xwayland-satellite + ]; + }; + + fonts = { + packages = with pkgs; [ + nerd-fonts.fira-code + comic-neue + comic-mono + corefonts + ]; + fontconfig.defaultFonts = { + sansSerif = [ + "Comic Neue" + "Comic Sans MS" + ]; + serif = [ + "Comic Neue" + "Comic Sans MS" + ]; + monospace = [ "Comic Mono" ]; + }; + }; + + xdg.portal = { + enable = true; + extraPortals = with pkgs; [ + xdg-desktop-portal-gtk + xdg-desktop-portal-gnome + ]; + }; +} diff --git a/hosts/focalor/modules/llama-cpp.nix b/hosts/focalor/modules/llama-cpp.nix new file mode 100644 index 0000000..ef05d89 --- /dev/null +++ b/hosts/focalor/modules/llama-cpp.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.modules.llama-cpp; + llamaCppCuda = pkgs.llama-cpp.override { cudaSupport = true; }; +in +{ + options.modules.llama-cpp.enable = lib.mkEnableOption "the llama.cpp Gemma service"; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ llamaCppCuda ]; + + networking.firewall.interfaces.br0.allowedTCPPorts = [ 8080 ]; + + systemd.services.llama-gemma = { + description = "Gemma 4 12B Unified via llama.cpp"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOME = "/home/regent"; + XDG_CACHE_HOME = "/home/regent/.cache"; + CUDA_VISIBLE_DEVICES = "1"; + LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib"; + }; + + serviceConfig = { + Type = "simple"; + User = "regent"; + Group = "users"; + WorkingDirectory = "/home/regent"; + ExecStart = '' + /home/regent/Developer/llama.cpp-gemma4/build-cuda-gcc14/bin/llama-server -m /home/regent/models/gemma-4-12b-unsloth-2026-07-17/gemma-4-12b-it-Q4_K_M.gguf --mmproj /home/regent/models/gemma-4-12b-unsloth-2026-07-17/mmproj-F16.gguf --lora-scaled /home/regent/Developer/web-extract-sft/artifacts/runs/query-preview-gemma4-12b-r16a8-v11/adapters/step192-f16.gguf:0.5 --model-draft /home/regent/models/gemma-4-12b/gemma-4-12B-it-qat-assistant-MTP-Q8_0.gguf --spec-type draft-mtp --spec-draft-n-max 4 --alias gemma-4-12b,gemma4 --host 10.0.0.13 --port 8080 --device CUDA0 --split-mode none --n-gpu-layers all --ctx-size 12288 --flash-attn on --parallel 1 --threads 2 --threads-batch 4 --batch-size 2048 --ubatch-size 512 --cont-batching --jinja --reasoning off --cache-ram 0 --metrics --no-webui + ''; + Restart = "on-failure"; + RestartSec = "5s"; + TimeoutStartSec = "infinity"; + LimitNOFILE = 1048576; + }; + }; + }; +} diff --git a/hosts/focalor/modules/network.nix b/hosts/focalor/modules/network.nix new file mode 100644 index 0000000..f47454a --- /dev/null +++ b/hosts/focalor/modules/network.nix @@ -0,0 +1,79 @@ +{ + networking = { + hostName = "focalor"; + hostId = "84bdc587"; + useDHCP = false; + nameservers = [ + "10.0.0.210" + "1.1.1.1" + ]; + firewall = { + enable = true; + trustedInterfaces = [ "tailscale0" ]; + allowedTCPPorts = [ + 22 + 3002 + ]; + }; + networkmanager = { + enable = true; + unmanaged = [ + "interface-name:enp5s0" + "interface-name:br0" + ]; + }; + }; + + systemd.network = { + enable = true; + wait-online.extraArgs = [ "--interface=enp4s0" ]; + netdevs.br0.netdevConfig = { + Name = "br0"; + Kind = "bridge"; + }; + networks = { + "10-lan" = { + matchConfig.Name = [ + "enp5s0" + "vm-*" + ]; + networkConfig.Bridge = "br0"; + }; + "10-lan-bridge" = { + matchConfig.Name = "br0"; + networkConfig = { + Address = [ "10.0.0.13/24" ]; + Gateway = "10.0.0.1"; + DNS = [ + "10.0.0.210" + "1.1.1.1" + ]; + IPv6AcceptRA = true; + }; + linkConfig.RequiredForOnline = "routable"; + }; + }; + }; + + services = { + openssh.enable = true; + printing.enable = true; + tailscale = { + enable = true; + useRoutingFeatures = "both"; + extraUpFlags = [ "--login-server=https://vpn.klbr.net" ]; + }; + resolved = { + enable = true; + settings.Resolve = { + DNSSEC = "true"; + Domains = [ "~." ]; + FallbackDNS = [ + "10.0.0.210" + "1.0.0.1#one.one.one.one" + ]; + DNSOverTLS = "true"; + }; + }; + }; +} diff --git a/hosts/focalor/modules/services.nix b/hosts/focalor/modules/services.nix new file mode 100644 index 0000000..aa3ac94 --- /dev/null +++ b/hosts/focalor/modules/services.nix @@ -0,0 +1,61 @@ +{ pkgs, ... }: +{ + services = { + syncthing = { + enable = true; + openDefaultPorts = true; + user = "regent"; + dataDir = "/home/regent"; + configDir = "/home/regent/.config/syncthing"; + }; + vscode-server = { + enable = true; + nodejsPackage = pkgs.nodejs_24; + }; + udev.extraRules = '' + KERNEL=="hidraw*", ATTRS{idVendor}=="1b1c", MODE="0666" + ''; + }; + + systemd.services = { + custom-fan-control = { + description = "Custom Ryzen CPU Fan Control Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.python3}/bin/python -u /home/regent/Developer/fan_control.py"; + Restart = "always"; + RestartSec = 5; + }; + }; + osu-keysounds = { + description = "osu! Typing Sounds Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ + "sound.target" + "pipewire.service" + ]; + serviceConfig = { + Type = "simple"; + User = "regent"; + SupplementaryGroups = [ "input" ]; + Environment = [ + "XDG_RUNTIME_DIR=/run/user/1000" + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" + "HOME=/home/regent" + "OSUCLACK_VOLUME=1.0" + ]; + WorkingDirectory = "/home/regent/Developer"; + ExecStart = "/home/regent/Developer/osuclack"; + Restart = "always"; + RestartSec = 3; + }; + }; + }; + + virtualisation.docker = { + enable = true; + enableOnBoot = true; + }; +} diff --git a/hosts/valefar/boot-resilience.nix b/hosts/valefar/boot-resilience.nix new file mode 100644 index 0000000..6083fd8 --- /dev/null +++ b/hosts/valefar/boot-resilience.nix @@ -0,0 +1,64 @@ +# Keep valefar reachable over the network through a bad boot: it lives +# headless, so a boot that stalls at a local console is a boot we cannot fix. +{ config, lib, ... }: +let + lanMac = "54:fb:66:01:74:ca"; +in +{ + # Vaultwarden, sonarr, prowlarr, radarr and several host-network containers + # bind 100.64.0.11, which only exists once tailscaled is up. Without this + # they fail with EADDRNOTAVAIL at boot and vaultwarden hits its start limit. + boot.kernel.sysctl = { + "net.ipv4.ip_nonlocal_bind" = 1; + "net.ipv6.ip_nonlocal_bind" = 1; + }; + + # These write under /storage. Order them after zfs-mount and refuse to + # start if /storage is not actually mounted, rather than writing into the + # root filesystem underneath the mountpoint. + systemd.services = + lib.genAttrs + [ + "docker" + "jellyfin" + "sonarr" + "radarr" + "container@pia-qbittorrent" + "radio" + ] + (_: { + after = [ "zfs-mount.service" ]; + requires = [ "zfs-mount.service" ]; + unitConfig.AssertPathIsMountPoint = "/storage"; + }); + + # A failed fstab mount would otherwise stop in emergency.target with no + # network. Carry on to multi-user so sshd and tailscale come up. + systemd.enableEmergencyMode = false; + + # Stage-1 SSH on the LAN (port 2222, root, regent's keys) for boots that + # stall before switch-root. Host key is generated once on the host: + # ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key + boot.initrd.systemd.enable = true; + boot.initrd.availableKernelModules = [ "r8169" ]; + boot.initrd.network = { + enable = true; + # Drop the stage-1 address so stage 2 can enslave the NIC to vmbr0 cleanly. + flushBeforeStage2 = true; + ssh = { + enable = true; + port = 2222; + hostKeys = [ "/etc/secrets/initrd/ssh_host_ed25519_key" ]; + authorizedKeys = config.users.users.regent.openssh.authorizedKeys.keys; + }; + }; + boot.initrd.systemd.network = { + enable = true; + networks."10-lan" = { + matchConfig.PermanentMACAddress = lanMac; + address = [ "10.0.0.30/24" ]; + gateway = [ "10.0.0.1" ]; + linkConfig.RequiredForOnline = "routable"; + }; + }; +} diff --git a/hosts/valefar/default.nix b/hosts/valefar/default.nix new file mode 100644 index 0000000..28bc7b5 --- /dev/null +++ b/hosts/valefar/default.nix @@ -0,0 +1,381 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +{ + imports = [ + "${inputs.agenix}/modules/age.nix" + "${inputs.home}/nixos" + inputs.proxmox-nixos.nixosModules.proxmox-ve + ../../modules + ../../users/regent + ./hardware.nix + ./secrets.nix + ./boot-resilience.nix + ./nat-guard.nix + ./pia-qbittorrent.nix + ./pia-exit.nix + ./wg-mesh.nix + ]; + + services.proxmox-ve = { + enable = true; + ipAddress = "10.0.0.30"; + bridges = [ "vmbr0" ]; + }; + + nix.gc = { + automatic = lib.mkForce true; + dates = "weekly"; + options = "--delete-older-than 14d"; + }; + nix.settings = { + auto-optimise-store = true; + trusted-users = lib.mkForce [ + "root" + "regent" + ]; + substituters = [ "https://cache.saumon.network/proxmox-nixos" ]; + trusted-public-keys = [ "proxmox-nixos:D9RYSWpQQC/msZUWphOY2I5RLH5Dd6yQcaHIuug7dWM=" ]; + }; + services.journald.settings.Journal.SystemMaxUse = "1G"; + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + time.timeZone = "America/New_York"; + i18n.defaultLocale = "en_US.UTF-8"; + programs.nix-ld.enable = true; + services.openssh.enable = true; + services.tailscale = { + enable = true; + useRoutingFeatures = "both"; + extraUpFlags = [ "--login-server=https://vpn.klbr.net" ]; + }; + boot.loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; + fileSystems."/boot".options = [ "umask=0077" ]; + hardware.graphics.enable = true; + services.xserver.videoDrivers = [ "nvidia" ]; + hardware.nvidia = { + modesetting.enable = true; + powerManagement.enable = false; + powerManagement.finegrained = false; + open = true; + nvidiaSettings = true; + package = config.boot.kernelPackages.nvidiaPackages.latest; + }; + environment.variables = { + GBM_BACKEND = "nvidia-drm"; + __GLX_VENDOR_LIBRARY_NAME = "nvidia"; + }; + + services.pocket-id = { + enable = true; + dataDir = "/var/lib/pocket-id"; + credentials = { + ENCRYPTION_KEY = config.age.secrets."pocket-id-encryption-key".path; + MAXMIND_LICENSE_KEY = config.age.secrets."pocket-id-maxmind-license-key".path; + }; + settings = { + APP_URL = "https://pocketid.nekomimi.pet"; + DB_CONNECTION_STRING = "pocket-id.db"; + GEOLITE_DB_PATH = "GeoLite2-City.mmdb"; + PORT = 3000; + TRUST_PROXY = true; + UPLOAD_PATH = "uploads"; + }; + }; + + networking = { + useNetworkd = true; + useDHCP = false; + hostName = "valefar"; + hostId = "2a07da90"; + firewall.enable = false; + }; + systemd.network = { + enable = true; + links."10-lan" = { + matchConfig.PermanentMACAddress = "54:fb:66:01:74:ca"; + linkConfig = { + NamePolicy = "keep kernel database onboard slot path"; + AlternativeNamesPolicy = "database onboard slot path"; + MACAddressPolicy = "persistent"; + WakeOnLan = "magic"; + }; + }; + networks = { + "10-lan" = { + matchConfig.PermanentMACAddress = "54:fb:66:01:74:ca"; + networkConfig = { + Bridge = "vmbr0"; + DHCP = "no"; + LinkLocalAddressing = "no"; + IPv6AcceptRA = false; + }; + }; + "11-lan-by-name" = { + matchConfig.Name = "enp5s0"; + networkConfig = { + Bridge = "vmbr0"; + DHCP = "no"; + LinkLocalAddressing = "no"; + IPv6AcceptRA = false; + }; + }; + "10-lan-bridge" = { + matchConfig.Name = "vmbr0"; + networkConfig = { + Address = [ + "10.0.0.30/24" + "2601:5c2:8400:26c0::30/64" + ]; + Gateway = "10.0.0.1"; + DNS = [ + "10.0.0.210" + "1.1.1.1" + "1.0.0.1" + ]; + IPv6AcceptRA = true; + }; + routes = [ + { + Destination = "0.0.0.0/0"; + Gateway = "10.0.0.1"; + } + ]; + linkConfig.RequiredForOnline = "routable"; + }; + }; + netdevs.br0.netdevConfig = { + Name = "vmbr0"; + Kind = "bridge"; + }; + }; + services.resolved = { + enable = true; + settings.Resolve = { + DNSSEC = "false"; + Domains = [ "~." ]; + FallbackDNS = [ + "10.0.0.210" + "1.1.1.1" + ]; + DNSOverTLS = "false"; + }; + }; + + boot = { + supportedFilesystems = [ "zfs" ]; + kernelModules = [ + "nct6775" + "coretemp" + ]; + zfs = { + extraPools = [ "storage" ]; + devNodes = "/dev/disk/by-id"; + forceImportAll = true; + forceImportRoot = true; + }; + }; + systemd.services.zfs-import-cache.enable = false; + services.zfs = { + autoScrub.enable = true; + trim.enable = true; + }; + + services.jellyfin = { + enable = true; + dataDir = "/storage/jellyfin"; + }; + services.prowlarr = { + enable = true; + settings.server = { + bindaddress = "100.64.0.11"; + port = 9696; + }; + }; + services.sonarr = { + enable = true; + dataDir = "/storage/sonarr"; + settings = { + server = { + bindAddress = "100.64.0.11"; + port = 8989; + }; + update = { + automatically = false; + mechanism = "external"; + }; + log.analyticsEnabled = false; + }; + }; + services.radarr = { + enable = true; + dataDir = "/storage/radarr"; + settings = { + server = { + bindAddress = "100.64.0.11"; + port = 7878; + }; + update = { + automatically = false; + mechanism = "external"; + }; + log.analyticsEnabled = false; + }; + }; + systemd.services.radarr.serviceConfig.PrivateUsers = lib.mkForce false; + systemd.services.sonarr.serviceConfig.PrivateUsers = lib.mkForce false; + users.users.radarr = { + isSystemUser = true; + group = "radarr"; + home = "/storage/radarr"; + uid = config.ids.uids.radarr; + extraGroups = [ "jellyfin" ]; + }; + users.groups.radarr.gid = config.ids.gids.radarr; + users.users.sonarr = { + isSystemUser = true; + group = "sonarr"; + home = "/storage/sonarr"; + uid = config.ids.uids.sonarr; + extraGroups = [ "jellyfin" ]; + }; + users.groups.sonarr.gid = config.ids.gids.sonarr; + services.vaultwarden = { + enable = true; + config = { + DOMAIN = "https://vault.nekomimi.pet"; + ROCKET_ADDRESS = "100.64.0.11"; + ROCKET_PORT = 8222; + SIGNUPS_ALLOWED = false; + SSO_ENABLED = true; + SSO_ONLY = true; + SSO_PKCE = true; + SSO_SCOPES = "email profile groups offline_access"; + SSO_AUTHORITY = "https://pocketid.nekomimi.pet"; + }; + environmentFile = config.age.secrets."vaultwarden-oidc.env".path; + }; + + systemd.tmpfiles.rules = [ + "d /storage/tm_share 0755 regent users" + "d /storage/media 0755 jellyfin jellyfin -" + "d /storage/media/.incoming 2775 jellyfin jellyfin -" + "d /storage/media/tv 2775 jellyfin jellyfin -" + ]; + services.samba = { + enable = true; + settings = { + global = { + "workgroup" = "WORKGROUP"; + "server string" = "valefar"; + "netbios name" = "valefar"; + "security" = "user"; + "hosts allow" = "100.64.0.0/10 10.0.0.0/24 127.0.0.1 localhost"; + "hosts deny" = "0.0.0.0/0"; + "guest account" = "nobody"; + "map to guest" = "bad user"; + }; + tm_share = { + path = "/storage/tm_share"; + "valid users" = "regent"; + public = "yes"; + writeable = "yes"; + "force user" = "regent"; + "fruit:aapl" = "yes"; + "fruit:time machine" = "yes"; + "vfs objects" = "catia fruit streams_xattr"; + }; + }; + }; + services.netatalk = { + enable = true; + settings.time-machine = { + path = "/storage/timemachine"; + "valid users" = "regent"; + "time machine" = true; + }; + }; + services.avahi = { + enable = true; + nssmdns4 = true; + publish = { + enable = true; + userServices = true; + }; + extraServiceFiles.timemachine = '' + + + + %h + + _smb._tcp + 445 + + + _device-info._tcp + 0 + model=TimeCapsule8,119 + + + _adisk._tcp + dk0=adVN=tm_share,adVF=0x82 + sys=waMa=0,adVF=0x100 + + + ''; + }; + + users.users.niri = { + isSystemUser = true; + uid = 988; + group = "users"; + shell = pkgs.bashInteractive; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ7Y9Je7H3gC72cgdEH4wifUDsmhKMeU5Z4oL1s1WcSE niri@nekomimi.pet" + ]; + }; + systemd.services.radio = { + description = "faint signal fm"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment.NO_COLOR = "1"; + path = [ + pkgs.ffmpeg + pkgs.yt-dlp + ]; + serviceConfig = { + User = "regent"; + Group = "users"; + WorkingDirectory = "/home/regent/radio"; + ExecStart = "/home/regent/radio/target/release/radio"; + Restart = "always"; + RestartSec = "5s"; + }; + }; + services.syncthing = { + guiAddress = "0.0.0.0:8384"; + enable = true; + }; + virtualisation.docker = { + enable = true; + enableOnBoot = true; + }; + environment.systemPackages = with pkgs; [ + code-server + ffmpeg + yt-dlp + nodejs + python3 + smartmontools + ]; + + system.stateVersion = "24.11"; +} diff --git a/hosts/valefar/hardware.nix b/hosts/valefar/hardware.nix new file mode 100644 index 0000000..0533d98 --- /dev/null +++ b/hosts/valefar/hardware.nix @@ -0,0 +1,44 @@ +{ + config, + lib, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "ahci" + "mpt3sas" + "nvme" + "usbhid" + "uas" + "sd_mod" + ]; + boot.kernelModules = [ "kvm-amd" ]; + + fileSystems."/" = { + device = "/dev/disk/by-uuid/e02d1d07-3bc8-4d1d-a301-6d589f4b4b6d"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-uuid/B3DE-0187"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ + { device = "/dev/disk/by-uuid/c8f24f31-49e0-486c-9f63-1d31b2e36ce9"; } + ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/valefar/nat-guard.nix b/hosts/valefar/nat-guard.nix new file mode 100644 index 0000000..eafa6d7 --- /dev/null +++ b/hosts/valefar/nat-guard.nix @@ -0,0 +1,107 @@ +{ config, lib, ... }: +let + nat = config.networking.nat; + outgoing = lib.optionals (nat.externalInterface != null) [ + "-o" + nat.externalInterface + ]; + translation = + if nat.externalIP == null then + [ + "-j" + "MASQUERADE" + ] + else + [ + "-j" + "SNAT" + "--to-source" + nat.externalIP + ]; +in +{ + config = + lib.mkIf (nat.enable && !config.networking.firewall.enable && !config.networking.nftables.enable) + { + assertions = [ + { + assertion = lib.all (name: builtins.stringLength name < 16) ( + nat.internalInterfaces ++ lib.optional (nat.externalInterface != null) nat.externalInterface + ); + message = "valefar NAT interface names must be at most 15 bytes; use internalIPs for long container names."; + } + ]; + + systemd.services.nat = { + wantedBy = [ "multi-user.target" ]; + # Restart in the new generation, not stop before activation then start later. + stopIfChanged = false; + serviceConfig = { + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + + # A successful oneshot can be stopped or retain stale kernel rules without + # becoming failed. Check the live rules against the merged NAT configuration. + systemd.services.nat-healthcheck = { + description = "Check and repair container NAT"; + after = [ "nat.service" ]; + path = [ + config.networking.firewall.package + config.systemd.package + ]; + serviceConfig = { + Type = "oneshot"; + TimeoutStartSec = "60s"; + }; + enableStrictShellChecks = true; + script = '' + check_nat() { + systemctl is-active --quiet nat.service || return 1 + iptables -w 5 -t nat -C POSTROUTING -j nixos-nat-post || return 1 + iptables -w 5 -t filter -C FORWARD -j nixos-filter-forward || return 1 + ${lib.concatMapStringsSep "\n" (subnet: '' + iptables -w 5 -t nat -C nixos-nat-post ${ + lib.escapeShellArgs ( + [ + "-s" + subnet + ] + ++ outgoing + ++ translation + ) + } || return 1 + iptables -w 5 -t filter -C nixos-filter-forward ${ + lib.escapeShellArgs ( + [ + "-s" + subnet + ] + ++ outgoing + ++ [ + "-j" + "ACCEPT" + ] + ) + } || return 1 + '') nat.internalIPs} + } + + if ! check_nat; then + echo "container NAT is inactive or incomplete; restarting nat.service" >&2 + systemctl restart nat.service + check_nat + fi + ''; + }; + systemd.timers.nat-healthcheck = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "30s"; + OnUnitInactiveSec = "60s"; + AccuracySec = "1s"; + }; + }; + }; +} diff --git a/hosts/valefar/pia-exit.nix b/hosts/valefar/pia-exit.nix new file mode 100644 index 0000000..0b21e3a --- /dev/null +++ b/hosts/valefar/pia-exit.nix @@ -0,0 +1,297 @@ +{ + config, + lib, + pkgs, + ... +}: +let + wgAuth = config.age.secrets."pia-wireguard-auth.env".path; + piaSource = ./pia-manual; + # PIA rotates and repurposes endpoint fleets without notice. 2026-09-21: + # the old WireGuard endpoint was retired and OpenVPN tcp/443 adopted. + # 2026-09-24: the entire 45.88.217.x DC fleet vanished and its :443 began + # answering as a plain nginx front, so openvpn looped on "Bad encapsulated + # packet length" (it was reading ASCII "HT" from an HTTP response). + # Rotation recipe: fetch https://serverlist.piaservers.net/vpninfo/servers/v6 + # (JSON is line 1), pick an "ovpntcp" server, verify it handshakes, change + # `endpoint` here and in pia-qbittorrent.nix, rebuild. + endpoint = "206.206.95.51"; # us-washingtondc / ovpntcp + gateway = "192.168.173.1"; + headscale = "94.237.26.47"; # vpn.klbr.net + # One-time Tailnet enrollment bootstrap. Kept mounted so a wiped node state + # re-enrolls itself. Read-only; the autoconnect script only reads it when the + # node has no valid state, so it is not consulted on a healthy boot. + authKey = "/home/regent/.pia-exit-authkey"; + # Host-side bootstrap: token + OpenVPN config + credentials land under + # /run/pia-exit and are bind-mounted read-only into the guest. The container + # never needs clear-net DNS or HTTPS. + prepare = pkgs.writeShellScript "pia-exit-openvpn-prepare" '' + set -euo pipefail + umask 077 + export PATH=${ + lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.jq + ] + } + set -a + . ${wgAuth} + set +a + token=$(curl --fail --silent --show-error --max-time 30 \ + --form "username=$PIA_USER" --form "password=$PIA_PASS" \ + https://www.privateinternetaccess.com/api/client/v2/token \ + | jq -er '.token | strings | select(length > 0)') + # OpenVPN token auth: username is the first 62 chars, password the rest. + printf '%s\n%s\n' "''${token:0:62}" "''${token:62}" > /run/pia-exit/pia-creds + { + echo "client" + echo "dev pia" + echo "dev-type tun" + echo "proto tcp" + echo "remote ${endpoint} 443" + echo "resolv-retry infinite" + echo "nobind" + echo "persist-key" + echo "persist-tun" + echo "remote-cert-tls server" + echo "redirect-gateway def1" + echo "reneg-sec 0" + echo "verb 1" + echo "auth-user-pass /etc/openvpn/pia-creds" + echo "" + cat ${piaSource}/ca.rsa.4096.crt + echo "" + } > /run/pia-exit/pia.ovpn + ''; +in +{ + # Host fetches PIA bootstrap material; the container never needs clear-net DNS/HTTPS. + # + # NixOS adds the host-side gateway and brings the veth up in ITS OWN postStart, + # after guest readiness. So the tunnel must NOT live in the guest's boot path: + # start it from here instead, after the host side exists. + systemd.services."container@pia-exit" = { + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + preStart = lib.mkBefore "${prepare}"; + postStart = lib.mkAfter '' + ${config.systemd.package}/bin/systemctl -M pia-exit start --no-block tailscaled-autoconnect.service + ${config.systemd.package}/bin/systemctl -M pia-exit start --no-block pia-openvpn.service + ''; + serviceConfig.RuntimeDirectory = "pia-exit"; + serviceConfig.RuntimeDirectoryMode = "0700"; + serviceConfig.TimeoutStartSec = lib.mkForce "4min"; + }; + # NixOS owns this veth; stop networkd's generic container DHCP/NAT handling. + systemd.network.networks."40-pia-exit" = { + matchConfig.Name = "ve-pia-exit"; + linkConfig.Unmanaged = true; + }; + containers.pia-exit = { + autoStart = true; + privateNetwork = true; + enableTun = true; + hostAddress = gateway; + localAddress = "192.168.173.2"; + bindMounts = { + "/etc/openvpn/pia.ovpn" = { + hostPath = "/run/pia-exit/pia.ovpn"; + isReadOnly = true; + }; + "/etc/openvpn/pia-creds" = { + hostPath = "/run/pia-exit/pia-creds"; + isReadOnly = true; + }; + # Enrollment key, read-only. Kept so a wiped node state can re-enroll itself. + "/run/tailscale-authkey" = { + hostPath = authKey; + isReadOnly = true; + }; + }; + config = + { + config, + lib, + pkgs, + ... + }: + { + networking.useDHCP = false; + networking.enableIPv6 = false; + # Static resolver: resolvconf would otherwise inherit the host's 127.0.0.53 + # stub, and systemd-resolved is not running in here. + networking.nameservers = [ "1.1.1.1" ]; + networking.resolvconf.enable = false; + environment.etc."resolv.conf".text = "nameserver 1.1.1.1\n"; + # Pin the coordination server so enrollment never depends on DNS at all. + networking.hosts."${headscale}" = [ "vpn.klbr.net" ]; + networking.interfaces.eth0.ipv4.routes = [ + { + address = endpoint; + prefixLength = 32; + via = gateway; + } + ]; + environment.systemPackages = with pkgs; [ + bash + coreutils + curl + gnugrep + gnused + jq + openvpn + iproute2 + ]; + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + networking.firewall = { + enable = true; + checkReversePath = "loose"; + allowedUDPPorts = [ 41641 ]; + # Default-deny OUTPUT. Everything leaves through the tunnel, except a + # narrow control plane so the node can enroll and stay reachable. + extraCommands = '' + ${pkgs.iptables}/bin/iptables-restore --noflush <<'RULES' + *filter + :OUTPUT DROP [0:0] + :FORWARD DROP [0:0] + -F OUTPUT + -F FORWARD + -A OUTPUT -o lo -j ACCEPT + -A OUTPUT -o pia -j ACCEPT + -A OUTPUT -o tailscale0 -j ACCEPT + -A OUTPUT -o eth0 -d ${endpoint}/32 -p tcp --dport 443 -j ACCEPT + -A OUTPUT -o eth0 -d ${headscale}/32 -p tcp --dport 443 -j ACCEPT + -A OUTPUT -o eth0 -d ${headscale}/32 -p udp --dport 3478 -j ACCEPT + -A OUTPUT -o eth0 -d 1.1.1.1/32 -p udp --dport 53 -j ACCEPT + -A OUTPUT -o eth0 -d 1.1.1.1/32 -p tcp --dport 53 -j ACCEPT + -A OUTPUT -o eth0 -d ${gateway}/32 -j ACCEPT + -A FORWARD -i tailscale0 -o pia -j ACCEPT + -A FORWARD -i pia -o tailscale0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + COMMIT + *nat + :POSTROUTING ACCEPT [0:0] + -A POSTROUTING -o pia -s 100.64.0.0/10 -j MASQUERADE + COMMIT + RULES + ${pkgs.iptables}/bin/ip6tables -P OUTPUT DROP + ${pkgs.iptables}/bin/ip6tables -P FORWARD DROP + ''; + }; + # Enrollment also needs the host-side gateway, which NixOS only wires after + # guest readiness. Keep it out of the boot transaction; the host starts it. + systemd.services.tailscaled-autoconnect.wantedBy = lib.mkForce [ ]; + services.tailscale = { + enable = true; + useRoutingFeatures = "server"; + authKeyFile = "/run/tailscale-authkey"; + extraUpFlags = [ + "--login-server=https://vpn.klbr.net" + "--hostname=pia-exit" + "--accept-dns=false" + "--netfilter-mode=off" + "--advertise-exit-node" + ]; + }; + # Deliberately NOT wantedBy multi-user.target: the host veth is not wired + # until after guest readiness. Started from the host postStart instead. + systemd.services.pia-openvpn = { + requires = [ "firewall.service" ]; + after = [ + "network-online.target" + "firewall.service" + ]; + wants = [ "network-online.target" ]; + path = [ + pkgs.curl + pkgs.iproute2 + pkgs.gnugrep + ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = "15s"; + # The endpoint must leave via eth0; the static pin is applied by + # networkd at guest boot. Wait it out rather than race it. + ExecStartPre = pkgs.writeShellScript "pia-openvpn-pin" '' + for i in $(seq 1 30); do + if ip -4 route get ${endpoint} 2>/dev/null | grep -q " dev eth0"; then + exit 0 + fi + sleep 1 + done + exit 1 + ''; + ExecStart = "${pkgs.openvpn}/bin/openvpn --config /etc/openvpn/pia.ovpn"; + }; + # Fail closed: the unit only counts as up once the tunnel really + # carries traffic. A failing probe restarts openvpn (Restart=always). + postStart = '' + ok=0 + for i in $(seq 1 30); do + if curl --fail --silent --max-time 5 --interface pia \ + https://1.1.1.1/cdn-cgi/trace | grep -q '^ip='; then + ok=1 + break + fi + sleep 1 + done + [ "$ok" = 1 ] || exit 1 + systemctl start --no-block pia-tailscale-kick.service + ''; + }; + # tailscaled holds one long-lived control connection to the headscale + # server. When the tunnel's default route flips (endpoint rotation or + # reconnect), that TCP conn silently dies and the backend wedges in + # NoState forever. Give autoconnect its full 90s window first, then + # restart tailscaled once if the backend did not reach Running. + systemd.services.pia-tailscale-kick = { + description = "Restart tailscaled when its control connection goes stale across a tunnel flip"; + after = [ + "pia-openvpn.service" + "tailscaled.service" + ]; + path = [ + pkgs.tailscale + pkgs.jq + ]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + sleep 95 + state=$(tailscale status --json --peers=false 2>/dev/null | jq -r '.BackendState') + if [ "$state" != "Running" ]; then + systemctl restart tailscaled.service + fi + for i in $(seq 1 30); do + state=$(tailscale status --json --peers=false 2>/dev/null | jq -r '.BackendState') + [ "$state" = "Running" ] && exit 0 + sleep 2 + done + exit 1 + ''; + }; + # Tailnet traffic must not follow the tunnel's redirect-gateway routes + # (0.0.0.0/1, 128.0.0.0/1 in main); the /10 route is more specific and + # wins, keeping tailscale0 reachable for exit-node clients. + systemd.services.pia-exit-tailnet-route = { + description = "Route tailnet traffic out tailscale0, ahead of the tunnel default"; + after = [ "tailscaled.service" ]; + wants = [ "tailscaled.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.iproute2 ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ip route replace 100.64.0.0/10 dev tailscale0 table main + ''; + }; + system.stateVersion = "26.05"; + }; + }; + networking.nat.internalIPs = [ "192.168.173.0/24" ]; +} diff --git a/hosts/valefar/pia-manual/SOURCE b/hosts/valefar/pia-manual/SOURCE new file mode 100644 index 0000000..1f224c7 --- /dev/null +++ b/hosts/valefar/pia-manual/SOURCE @@ -0,0 +1 @@ +vendor copy from pia-foss/manual-connections master, retrieved 2026-09-12; source: https://github.com/pia-foss/manual-connections diff --git a/hosts/valefar/pia-manual/ca.rsa.4096.crt b/hosts/valefar/pia-manual/ca.rsa.4096.crt new file mode 100644 index 0000000..82dec69 --- /dev/null +++ b/hosts/valefar/pia-manual/ca.rsa.4096.crt @@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIHqzCCBZOgAwIBAgIJAJ0u+vODZJntMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV +BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu +dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx +IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB +FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzQw +MzNaFw0zNDA0MTIxNzQwMzNaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg +QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE +AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50 +ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy +bmV0YWNjZXNzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVk +hjumaqBbL8aSgj6xbX1QPTfTd1qHsAZd2B97m8Vw31c/2yQgZNf5qZY0+jOIHULN +De4R9TIvyBEbvnAg/OkPw8n/+ScgYOeH876VUXzjLDBnDb8DLr/+w9oVsuDeFJ9K +V2UFM1OYX0SnkHnrYAN2QLF98ESK4NCSU01h5zkcgmQ+qKSfA9Ny0/UpsKPBFqsQ +25NvjDWFhCpeqCHKUJ4Be27CDbSl7lAkBuHMPHJs8f8xPgAbHRXZOxVCpayZ2SND +fCwsnGWpWFoMGvdMbygngCn6jA/W1VSFOlRlfLuuGe7QFfDwA0jaLCxuWt/BgZyl +p7tAzYKR8lnWmtUCPm4+BtjyVDYtDCiGBD9Z4P13RFWvJHw5aapx/5W/CuvVyI7p +Kwvc2IT+KPxCUhH1XI8ca5RN3C9NoPJJf6qpg4g0rJH3aaWkoMRrYvQ+5PXXYUzj +tRHImghRGd/ydERYoAZXuGSbPkm9Y/p2X8unLcW+F0xpJD98+ZI+tzSsI99Zs5wi +jSUGYr9/j18KHFTMQ8n+1jauc5bCCegN27dPeKXNSZ5riXFL2XX6BkY68y58UaNz +meGMiUL9BOV1iV+PMb7B7PYs7oFLjAhh0EdyvfHkrh/ZV9BEhtFa7yXp8XR0J6vz +1YV9R6DYJmLjOEbhU8N0gc3tZm4Qz39lIIG6w3FDAgMBAAGjggFUMIIBUDAdBgNV +HQ4EFgQUrsRtyWJftjpdRM0+925Y6Cl08SUwggEfBgNVHSMEggEWMIIBEoAUrsRt +yWJftjpdRM0+925Y6Cl08SWhge6kgeswgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl +cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw +HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0 +ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl +aW50ZXJuZXRhY2Nlc3MuY29tggkAnS7684Nkme0wDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQ0FAAOCAgEAJsfhsPk3r8kLXLxY+v+vHzbr4ufNtqnL9/1Uuf8NrsCt +pXAoyZ0YqfbkWx3NHTZ7OE9ZRhdMP/RqHQE1p4N4Sa1nZKhTKasV6KhHDqSCt/dv +Em89xWm2MVA7nyzQxVlHa9AkcBaemcXEiyT19XdpiXOP4Vhs+J1R5m8zQOxZlV1G +tF9vsXmJqWZpOVPmZ8f35BCsYPvv4yMewnrtAC8PFEK/bOPeYcKN50bol22QYaZu +LfpkHfNiFTnfMh8sl/ablPyNY7DUNiP5DRcMdIwmfGQxR5WEQoHL3yPJ42LkB5zs +6jIm26DGNXfwura/mi105+ENH1CaROtRYwkiHb08U6qLXXJz80mWJkT90nr8Asj3 +5xN2cUppg74nG3YVav/38P48T56hG1NHbYF5uOCske19F6wi9maUoto/3vEr0rnX +JUp2KODmKdvBI7co245lHBABWikk8VfejQSlCtDBXn644ZMtAdoxKNfR2WTFVEwJ +iyd1Fzx0yujuiXDROLhISLQDRjVVAvawrAtLZWYK31bY7KlezPlQnl/D9Asxe85l +8jO5+0LdJ6VyOs/Hd4w52alDW/MFySDZSfQHMTIc30hLBJ8OnCEIvluVQQ2UQvoW ++no177N9L2Y+M9TcTA62ZyMXShHQGeh20rb4kK8f+iFX8NxtdHVSkxMEFSfDDyQ= +-----END CERTIFICATE----- diff --git a/hosts/valefar/pia-qbittorrent.nix b/hosts/valefar/pia-qbittorrent.nix new file mode 100644 index 0000000..4e24c4c --- /dev/null +++ b/hosts/valefar/pia-qbittorrent.nix @@ -0,0 +1,241 @@ +{ + config, + lib, + pkgs, + ... +}: +let + wgAuth = config.age.secrets."pia-wireguard-auth.env".path; + piaSource = ./pia-manual; + # Same story as pia-exit.nix: PIA rotates endpoint fleets without notice; + # rotate `endpoint` in lockstep with pia-exit.nix. + endpoint = "206.206.95.51"; # us-washingtondc / ovpntcp + gateway = "192.168.172.1"; + # Host-side bootstrap: token + OpenVPN config + credentials under + # /run/pia-qbittorrent, bind-mounted read-only into the guest. + prepare = pkgs.writeShellScript "pia-qbittorrent-openvpn-prepare" '' + set -euo pipefail + umask 077 + export PATH=${ + lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.jq + ] + } + set -a + . ${wgAuth} + set +a + token=$(curl --fail --silent --show-error --max-time 30 \ + --form "username=$PIA_USER" --form "password=$PIA_PASS" \ + https://www.privateinternetaccess.com/api/client/v2/token \ + | jq -er '.token | strings | select(length > 0)') + # OpenVPN token auth: username is the first 62 chars, password the rest. + printf '%s\n%s\n' "''${token:0:62}" "''${token:62}" > /run/pia-qbittorrent/pia-creds + { + echo "client" + echo "dev pia" + echo "dev-type tun" + echo "proto tcp" + echo "remote ${endpoint} 443" + echo "resolv-retry infinite" + echo "nobind" + echo "persist-key" + echo "persist-tun" + echo "remote-cert-tls server" + echo "redirect-gateway def1" + echo "reneg-sec 0" + echo "verb 1" + echo "auth-user-pass /etc/openvpn/pia-creds" + echo "" + cat ${piaSource}/ca.rsa.4096.crt + echo "" + } > /run/pia-qbittorrent/pia.ovpn + ''; +in +{ + # Bootstrap on the host: the container never needs clear-net DNS or HTTPS. + systemd.services."container@pia-qbittorrent" = { + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + preStart = lib.mkBefore "${prepare}"; + # NixOS adds the host-side gateway in its postStart, after guest readiness. + postStart = lib.mkAfter "${config.systemd.package}/bin/systemctl -M pia-qbittorrent start pia-openvpn.service"; + serviceConfig.RuntimeDirectory = "pia-qbittorrent"; + serviceConfig.RuntimeDirectoryMode = "0700"; + serviceConfig.TimeoutStartSec = lib.mkForce "2min"; + }; + # NixOS owns this veth; prevent networkd's generic container DHCP/NAT setup. + # networkd matches the full altname too; Unmanaged=true reports Network File: n/a. + # Unlike this match, iptables must never use the overlong altname. + systemd.network.networks."40-pia-qbittorrent" = { + matchConfig.Name = "ve-pia-qbittorrent"; + linkConfig.Unmanaged = true; + }; + containers.pia-qbittorrent = { + autoStart = true; + privateNetwork = true; + enableTun = true; + hostAddress = gateway; + localAddress = "192.168.172.2"; + forwardPorts = [ + { + containerPort = 8080; + hostPort = 8081; + protocol = "tcp"; + } + ]; + bindMounts."/etc/openvpn/pia.ovpn" = { + hostPath = "/run/pia-qbittorrent/pia.ovpn"; + isReadOnly = true; + }; + bindMounts."/etc/openvpn/pia-creds" = { + hostPath = "/run/pia-qbittorrent/pia-creds"; + isReadOnly = true; + }; + # qBittorrent's ONLY writable host path: the constrained staging shelf. + bindMounts."/downloads" = { + hostPath = "/storage/media/.incoming"; + isReadOnly = false; + }; + config = + { + config, + lib, + pkgs, + ... + }: + { + networking.useDHCP = false; + networking.enableIPv6 = false; + networking.nameservers = [ "1.1.1.1" ]; + networking.resolvconf.enable = false; + environment.etc."resolv.conf".text = "nameserver 1.1.1.1\n"; + environment.systemPackages = with pkgs; [ + bash + coreutils + curl + gnugrep + gnused + jq + openvpn + ]; + networking.interfaces.eth0.ipv4.routes = [ + { + address = endpoint; + prefixLength = 32; + via = gateway; + } + ]; + # The bind-mounted staging shelf is Jellyfin-owned on the host (gid 983). + # Match that numeric group inside the container; qBittorrent only sees + # the .incoming bind mount, never the final library. + users.groups.qbittorrent = { }; + users.groups.jellyfin.gid = 983; + users.users.qbittorrent = { + isSystemUser = true; + group = "qbittorrent"; + extraGroups = [ "jellyfin" ]; + home = "/var/lib/qbittorrent"; + createHome = true; + }; + networking.firewall = { + enable = true; + allowedTCPPorts = [ 8080 ]; + # Replace OUTPUT atomically; retain default-deny even on firewall stop. + # Do not allow arbitrary ESTABLISHED flows to fall back onto eth0. + extraCommands = '' + ${pkgs.iptables}/bin/iptables-restore --noflush <<'RULES' + *filter + :OUTPUT DROP [0:0] + :FORWARD DROP [0:0] + -F OUTPUT + -A OUTPUT -o lo -j ACCEPT + -A OUTPUT -o pia -j ACCEPT + -A OUTPUT -o eth0 -d ${endpoint}/32 -p tcp --dport 443 -j ACCEPT + -A OUTPUT -o eth0 -d 100.64.0.0/10 -p tcp --sport 8080 -m conntrack --ctstate ESTABLISHED --ctdir REPLY -j ACCEPT + -A OUTPUT -o eth0 -d ${gateway}/32 -p tcp --sport 8080 -m conntrack --ctstate ESTABLISHED --ctdir REPLY -j ACCEPT + COMMIT + RULES + ${pkgs.iptables}/bin/ip6tables -P OUTPUT DROP + ${pkgs.iptables}/bin/ip6tables -P FORWARD DROP + ''; + }; + systemd.services.qbittorrent = { + wantedBy = [ "pia-openvpn.service" ]; + requires = [ "pia-openvpn.service" ]; + after = [ "pia-openvpn.service" ]; + partOf = [ "pia-openvpn.service" ]; + serviceConfig = { + User = "qbittorrent"; + Group = "qbittorrent"; + StateDirectory = "qbittorrent"; + WorkingDirectory = "/var/lib/qbittorrent"; + ExecStart = "${pkgs.qbittorrent-nox}/bin/qbittorrent-nox --profile=/var/lib/qbittorrent --webui-port=8080"; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + systemd.services.pia-openvpn = { + requires = [ "firewall.service" ]; + after = [ + "network-online.target" + "firewall.service" + ]; + wants = [ "network-online.target" ]; + path = [ + pkgs.curl + pkgs.iproute2 + pkgs.gnugrep + ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = "15s"; + ExecStartPre = pkgs.writeShellScript "pia-openvpn-pin" '' + for i in $(seq 1 30); do + if ip -4 route get ${endpoint} 2>/dev/null | grep -q " dev eth0"; then + exit 0 + fi + sleep 1 + done + exit 1 + ''; + ExecStart = "${pkgs.openvpn}/bin/openvpn --config /etc/openvpn/pia.ovpn"; + # Replies to tailnet clients must leave via eth0, not the tunnel: + # the host forwards webui connections from tailnet sources, and the + # host's conntrack only reverses flows that come back through it. + # A dedicated table escapes redirect-gateway's 0.0.0.0/1 in main. + ExecStartPost = pkgs.writeShellScript "pia-openvpn-tailnet-route" '' + ip rule add to 100.64.0.0/10 lookup 100 priority 100 2>/dev/null || true + ip route replace default via ${gateway} dev eth0 table 100 + ''; + ExecStopPost = pkgs.writeShellScript "pia-openvpn-tailnet-undo" '' + ip rule del to 100.64.0.0/10 lookup 100 priority 100 2>/dev/null || true + ''; + }; + # Fail closed: the unit only counts as up once the tunnel really + # carries traffic, and qbittorrent requires this unit, so the client + # never starts on a dead tunnel. + postStart = '' + for i in $(seq 1 30); do + if curl --fail --silent --max-time 5 --interface pia \ + https://1.1.1.1/cdn-cgi/trace | grep -q '^ip='; then + exit 0 + fi + sleep 1 + done + exit 1 + ''; + }; + system.stateVersion = "26.05"; + }; + }; + # systemd shortens long veth names; match the private subnet, not that name. + networking.nat = { + enable = true; + externalInterface = "vmbr0"; + internalIPs = [ "192.168.172.0/24" ]; + }; +} diff --git a/hosts/valefar/secrets.nix b/hosts/valefar/secrets.nix new file mode 100644 index 0000000..4deb735 --- /dev/null +++ b/hosts/valefar/secrets.nix @@ -0,0 +1,22 @@ +{ + age.secrets = { + "pocket-id-encryption-key" = { + file = ../../secrets/pocket-id-encryption-key.age; + mode = "0400"; + }; + "pocket-id-maxmind-license-key" = { + file = ../../secrets/pocket-id-maxmind-license-key.age; + mode = "0400"; + }; + "pia-wireguard-auth.env" = { + file = ../../secrets/pia-wireguard-auth.env.age; + mode = "0400"; + }; + "vaultwarden-oidc.env" = { + file = ../../secrets/vaultwarden-oidc.env.age; + owner = "vaultwarden"; + group = "vaultwarden"; + mode = "0400"; + }; + }; +} diff --git a/hosts/valefar/tests/nat-guard.nix b/hosts/valefar/tests/nat-guard.nix new file mode 100644 index 0000000..fa750e4 --- /dev/null +++ b/hosts/valefar/tests/nat-guard.nix @@ -0,0 +1,70 @@ +# Run with the flake's pinned nixpkgs: +# nix build --impure --expr 'let f = builtins.getFlake (toString ./.); in import ./hosts/valefar/tests/nat-guard.nix { pkgs = f.inputs.nixpkgs.legacyPackages.x86_64-linux; }' +{ pkgs }: +pkgs.testers.runNixOSTest { + name = "valefar-nat-guard"; + nodes.machine = { lib, ... }: { + imports = [ ../nat-guard.nix ]; + networking.firewall.enable = false; + networking.nat = { + enable = true; + externalInterface = "eth0"; + internalIPs = [ + "192.168.172.0/24" + "192.168.173.0/24" + ]; + }; + systemd.services.nat = { + preStart = "test ! -e /run/fail-nat"; + serviceConfig.RestartSec = lib.mkForce "1s"; + }; + systemd.timers.nat-healthcheck.timerConfig = { + OnBootSec = lib.mkForce "1s"; + OnUnitInactiveSec = lib.mkForce "2s"; + }; + }; + testScript = '' + start_all() + rules = " && ".join([ + "systemctl is-active --quiet nat.service", + "iptables -t nat -C POSTROUTING -j nixos-nat-post", + "iptables -t filter -C FORWARD -j nixos-filter-forward", + *[ + f"iptables -t nat -C nixos-nat-post -s {subnet} -o eth0 -j MASQUERADE" + for subnet in ["192.168.172.0/24", "192.168.173.0/24"] + ], + ]) + + with subtest("boot installs both subnets"): + machine.wait_for_unit("nat.service") + machine.wait_for_unit("nat-healthcheck.timer") + machine.succeed(rules) + + with subtest("healthy checks do not restart NAT"): + invocation = machine.succeed("systemctl show nat -p InvocationID --value") + machine.succeed("systemctl start nat-healthcheck") + assert machine.succeed("systemctl show nat -p InvocationID --value") == invocation + + with subtest("timer recovers a successfully stopped unit"): + machine.succeed("systemctl stop nat") + machine.wait_until_succeeds(rules) + + with subtest("timer repairs a missing subnet in an active unit"): + machine.succeed("iptables -t nat -D nixos-nat-post -s 192.168.173.0/24 -o eth0 -j MASQUERADE") + machine.wait_until_succeeds(rules) + + with subtest("timer repairs a detached chain"): + machine.succeed("iptables -t nat -D POSTROUTING -j nixos-nat-post") + machine.wait_until_succeeds(rules) + + with subtest("failed repair is visible and retried"): + machine.succeed("systemctl stop nat-healthcheck.timer; touch /run/fail-nat") + machine.fail("systemctl restart nat") + machine.fail("systemctl start nat-healthcheck") + machine.succeed("systemctl is-failed nat-healthcheck") + machine.succeed("mv /run/fail-nat /run/nat-failure-tested") + machine.wait_until_succeeds(rules) + machine.succeed("systemctl start nat-healthcheck.timer") + machine.wait_until_succeeds("test $(systemctl show nat-healthcheck -p Result --value) = success") + ''; +} diff --git a/hosts/valefar/wg-mesh.nix b/hosts/valefar/wg-mesh.nix new file mode 100644 index 0000000..ff58ad9 --- /dev/null +++ b/hosts/valefar/wg-mesh.nix @@ -0,0 +1,63 @@ +# wisp.place mesh: full-mesh WireGuard between the wisp servers, 10.88.0.0/24. +# valefar sits behind home NAT, so it dials every peer and keeps the paths open. +# Names resolve through *.mesh.wisp.place; the peer list lives in ~/fleet/mesh.md. +{ pkgs, ... }: +{ + environment.systemPackages = [ pkgs.wireguard-tools ]; + + # Containers publish ports on 10.88.0.10; start docker once wg0 exists. + systemd.services.docker = { + after = [ "wg-quick-wg0.service" ]; + wants = [ "wg-quick-wg0.service" ]; + }; + + networking.wg-quick.interfaces.wg0 = { + address = [ "10.88.0.10/24" ]; + listenPort = 51820; + privateKeyFile = "/etc/wireguard/wg0.key"; + peers = [ + { + # baal + publicKey = "ooHJ1tE5WVfrC4bAX/japIwNahg71tTSNy94k9d5A0Q="; + allowedIPs = [ "10.88.0.1/32" ]; + endpoint = "150.136.127.67:51820"; + persistentKeepalive = 25; + } + { + # stolas + publicKey = "BYA0fbXxWvgK4uYoYiNGLscki4lTGAsKn98AKlw32B8="; + allowedIPs = [ "10.88.0.2/32" ]; + endpoint = "152.53.121.97:51820"; + persistentKeepalive = 25; + } + { + # sjo1 + publicKey = "myd1WecGLdP/DtU198anvcPtEk8Iusa1CASru5kall0="; + allowedIPs = [ "10.88.0.3/32" ]; + endpoint = "152.44.44.138:51820"; + persistentKeepalive = 25; + } + { + # sin1 + publicKey = "9mAzm703TerlCQoZP61dyJNnMATGVgVuNtdi+JmKWAo="; + allowedIPs = [ "10.88.0.4/32" ]; + endpoint = "213.163.207.16:51820"; + persistentKeepalive = 25; + } + { + # sharkgirl + publicKey = "3rgSbuy6oz8ePF55yF8ZBS03MDhj3aOq83/PgO+DYng="; + allowedIPs = [ "10.88.0.5/32" ]; + endpoint = "15.204.225.63:51820"; + persistentKeepalive = 25; + } + { + # vine (status page) + publicKey = "Iucesg9GHk9AkjewUIdgOiIBMdLBVzfvrTqLQiNecRI="; + allowedIPs = [ "10.88.0.6/32" ]; + endpoint = "144.225.80.116:51820"; + persistentKeepalive = 25; + } + ]; + }; +} diff --git a/secrets/pia-wireguard-auth.env.age b/secrets/pia-wireguard-auth.env.age new file mode 100644 index 0000000..57a4e55 --- /dev/null +++ b/secrets/pia-wireguard-auth.env.age @@ -0,0 +1,22 @@ +age-encryption.org/v1 +-> ssh-ed25519 i9wBeA oUdGLb/WpJmjxW7QHnKrcoXdEaX3vlAIh3PMuEMTFXU +xM29aQByEm1QlZe0RAbNfs4w8nVFVnFnoEADqKJLZMQ +-> ssh-ed25519 rgtFBg Dh9lxSdhL5/GwesWVz6iic1oB9hSQaY9a1lJ+MzbXRI +DGVzyVeCHLz35iVwQtIQ7WWNbkOSvrtXh43JM0B62x8 +-> ssh-rsa J32+GA +TMQkZcTbx4u9aOfjrGxMRqI+yZdGJlgG1M1Zl+cOAZQxfGlZcFMRmpK+XfiD72No +k5lGctvMFe/yCjrOwoNN/NiE/JmGVjjXh93V3IoSI743C8KAmoHsgt7XgF0bwwuq ++HU2uWp6KjoraZQ8XaQCiD9w87HQqptMFhMUVRXxxDPVVGF8sO9SdN5EX5hpOb9t +eiA8MtyYOnSkwXhsep6Uwac+/OPoc7TOaMWvtIkT1NsMECB4xZ3B8FcFDjwO+jtT +1eUe0lazmGNkyUf9dSGApfA/c02IUKVqb9drlvYHw0Uj03WDHPLqnGB32i++/Ks+ +Ui/c93wJG743YpeqPadSeZ0UHWFoEPKzgG7NU1rxHq+mvpgMRx/Guc2pkBf9MSrb +9xL29TMIOxsWiJLjxsun2u9r9AfZoHU/fUJ8Lt1RAOIwV28Jh8tsmF5e+ZaN9NKM +CkIva/aJOjrGtAknbzPTeX6PY9O7D5YQkzEwUD90Qj4d0CmlLxfbTn6RdsVxsPXR +gDV77GqCOYKkScsE+QiRwcjPzWR4kv6Su2IOV77/cHewftgfdCYzMytiDSTI4uDq +1SQRB2HhjHiT9K1otbCJT2dQNPsLxgkZsKWEgmMoCVGl82IOKOT3EEDz6AFiULRD +bgcMF+S94WbtS8Q/PBdPjIpC54+HzPswlusoOSr1GWI +-> ssh-ed25519 du7llw qFql+PH0TSCLdumpFTVsTNUOsKQbJO6K7nUUXC4Vrzo +NvA2H0/6MQ63J2gPqG0mf8txq+/UNNstXdTHyXlrC/U +--- okqLzInxo2gNlLt6QaamSRGmeYwmMhu0bSTmVYfOPSs + W�͎%��� +�}�Jw���i�,bl ����-����4s�k������RˌG�&&q��j�t����a��P� \ No newline at end of file diff --git a/secrets/pocket-id-encryption-key.age b/secrets/pocket-id-encryption-key.age new file mode 100644 index 0000000000000000000000000000000000000000..23fe8e0c37cf16bfd328e0e4a9a13dd4b98d487d GIT binary patch literal 576 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCU7v@CZ@byP5_$V|_- za5Z%F^w9Px&$KYjNiFrL^vj5{tVk~iaZU5{PxUl)HOdUB2<37uO?L}VDs*!y&vi6S zEGy3}GcYs=GD`|IFe@(e3d~7Mi8A&HPjYk4jYPLCrPMqpr(7Y?KPpi_Fg>q4(Iqu2 z(xo`9q%1Vj+$+N~%rzs##5^U}C|5hk-QTn{*psW&*TcoTEGs1`$U7t}tkfmZ)WS0> z*x1L*H?c6xGQy`U!#KyK$kI2>IUC)!$jB;3^FReB=k#C$k9125SL3wIuypOpD1Czx zcf+jYvh))33{SJ-K_e3{SKc9l^#ER5{@X)fN3in*YfK2Zo@6f=g z^Z?^3%gnN%Y-7Xp99MMPjDx}p0|FH+tNhdTLkyhDqjL2V-HftBEM1BN%JR!9O@s4_ z^ov3rle~+KynI43Bg(n71I;}O!V>d}BC@A5JEfR2qg{MA->`b}K`Hv+ ahrP$&ocg9QZ*KYqksmJv=I`tFQ3U{EUc~7D literal 0 HcmV?d00001 diff --git a/secrets/pocket-id-maxmind-license-key.age b/secrets/pocket-id-maxmind-license-key.age new file mode 100644 index 0000000000000000000000000000000000000000..ee9855887226121efbc8cf50fd33246ac877c6f7 GIT binary patch literal 542 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCU7v@CZ@byP^sh)hg& zj|?xZG_`O_F3ry=3ePSyNeU=6N%Jf5Ei5p|3{J1`b2ClL3FLAMO!x6h3eq+RGD>uD z&dN_q^zwIcF(?T)3^p?>Hp)*a_A<(kbPOvC%}2K_rPMqpr(D4zE2PLZ+t<(1DL=?S zJHR}muq4MNBqJ)h%FH`2BF)pwF}cV$!oc6ju$;>v#L_9;qbwshRJ+Q@GQy;=*eNeF z$uHBrD$GMW$gMCa)Z3)oC?&|hDiqze$jB;3^FW2vu);u7ui&U)mt>dnWJ{0yBEy{G z?8>~L+{Ey-h%nzk-?Y%&tTbaMLq{$HeQyi>jEZtYXA>{er1D~y;<94TP?tbQ-vIsO z4D+O7r;;ddN2h@FyhwE0jDx}p0|FIHwR2OvLVbEAzQL9UTLce4`9={R~S})5^3f%Jaj`D$CunLMlSj((`hX!ZIQ) z4J}@1|d^4T3^Rt~jz5QJ+ pOgy>%Y?7Vu`N!i6Z|b*h?P-6O9c|NiE5jmAjb)?6l$%xzj{v(uwQv9c literal 0 HcmV?d00001 diff --git a/secrets/regent.nix b/secrets/regent.nix new file mode 100644 index 0000000..a7112ca --- /dev/null +++ b/secrets/regent.nix @@ -0,0 +1,27 @@ +let + regent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ0pU82lV9dSjkgYbdh9utZ5CDM2dPN70S5fBqN1m3Pb regent@orobas.local"; + users = [ regent ]; + + valefar = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIPu8CVFsnUxhvABEqv4+EBBOL8tva5HJFoV3hElAlD0"; + buer = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMVhjwDcO8eleSoR8a37ZGGPvkHEgV+c8SYcy07SayPB"; + focalor = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA518oTmTp5VG60/dBrLu7rlV1hh8muhMattoiGfmrei"; + systems = [ + valefar + buer + focalor + ]; +in +{ + "pocket-id-encryption-key.age".publicKeys = users ++ systems; + "pocket-id-maxmind-license-key.age".publicKeys = users ++ systems; + + "vaultwarden-oidc.env.age".publicKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIPu8CVFsnUxhvABEqv4+EBBOL8tva5HJFoV3hElAlD0 root@valefar" + ]; + "pia-wireguard-auth.env.age".publicKeys = [ + regent + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ7Y9Je7H3gC72cgdEH4wifUDsmhKMeU5Z4oL1s1WcSE niri@nekomimi.pet" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCM1KHYX8icUv9XxV0QDgsaXE05nA8gaZ0O3OwP/vH0olpeXc13FxoFKvNVYo1aWCo8xjv9G01JrWNhBRFf76LyOJhL1cm3Psorcx0V7sdbRy9gWnPRR5tA58wKr51yvz/4I+KsrO537cTAVZQky9J2wDh9jDpiNQJN80Kv9RdrFc3BVrEXUrsE3YtsNiSg4YoAPD4vhLupVohMPoh/w3dtdTQBu+YF+1rjMJ9Xf22DaVqz6l95T/Zui1smQnIZbjXRibJ8RR3Kla7K94nqVvNMVURiK71vXh+bJvFE2HyDDNsVteAv2DxpXQJpojxGQUXvR1LGQP9Lm5yhdtTqZMeuTDOKCCTy3yMGX4tX09ZyP5S0WwgTh2vE33C4+gUFa4wSeGZj7IH2t4ivgxK17fkGIeMcPpPuwkIWbinCp5AXPBqly5OBry6Qyg8SN1jb+d6pW3sn3slsikH53B4Qx5PP1ywCSuKnboOn9pQqZt7uvs75W2oyYxdmh/SNx//dcNX16YLYJ1KfaGpxerT6AjfiHSB8Vp8n6N7bVmizsqg0nkU+SmF8gh3UKidyg2RzL+zcJcxp+APiumM0rs3fXFNBuT8VncOPbiyjSw/NUt3EiuBGIap5mnJF+zW965YqZAo+Upivqltdah0E9WJrF4t7Z5+FyuLi3N5ovyROEEAlNw== root@valefar" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIPu8CVFsnUxhvABEqv4+EBBOL8tva5HJFoV3hElAlD0 root@valefar" + ]; +} diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 4182daf..8dbf4dd 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -40,4 +40,4 @@ in yusdacra trimounts ]; -} +} // import ./regent.nix diff --git a/secrets/vaultwarden-oidc.env.age b/secrets/vaultwarden-oidc.env.age new file mode 100644 index 0000000000000000000000000000000000000000..7227ebe8af05874643acedd7e87be953a2164149 GIT binary patch literal 314 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCSnDK*c@DOU(K^7jeN zGI34y&nPf6$qlnGDKc@)j|eOC^UF;%Do-uZj||Q(&UYzH_vA7tN=>xPHYoQ<_A50A zFYz;T%5_cmHOz1duP_fPPEQOe(RMQ_@k=$xcI494)m8Ad%n31Z&#eg0tBP<8EO2(y zPW38CcMdi;3Mq>)OZW0GOwV;P%?wOWi{#q1|6RaxQR@QHKF(jxbx)NAzU?Y`aNA@0 zweM@BoE@0=G4lxBf1?m3X|UJEK;Y~Cm0tU_l|6mi-q$lux_{%td*6HB(-@pKS$=ff", builtin.find_files, { desc = "Find files" }) + vim.keymap.set("n", "fg", builtin.live_grep, { desc = "Live grep" }) + vim.keymap.set("n", "fb", builtin.buffers, { desc = "Buffers" }) + vim.keymap.set("n", "fh", builtin.help_tags, { desc = "Help" }) + + local cmp = require("cmp") + local luasnip = require("luasnip") + cmp.setup({ + snippet = { expand = function(args) luasnip.lsp_expand(args.body) end }, + mapping = cmp.mapping.preset.insert({ + [""] = cmp.mapping.complete(), + [""] = cmp.mapping.confirm({ select = true }), + [""] = cmp.mapping.select_next_item(), + [""] = cmp.mapping.select_prev_item(), + }), + sources = cmp.config.sources({ { name = "nvim_lsp" } }), + }) + + local capabilities = require("cmp_nvim_lsp").default_capabilities() + for _, server in ipairs({ "bashls", "lua_ls", "nil_ls", "pyright", "rust_analyzer", "ts_ls" }) do + vim.lsp.config(server, { capabilities = capabilities }) + vim.lsp.enable(server) + end + vim.keymap.set("n", "gd", vim.lsp.buf.definition, { desc = "Go to definition" }) + vim.keymap.set("n", "gr", vim.lsp.buf.references, { desc = "References" }) + vim.keymap.set("n", "K", vim.lsp.buf.hover, { desc = "Hover documentation" }) + vim.keymap.set("n", "rn", vim.lsp.buf.rename, { desc = "Rename" }) + vim.keymap.set("n", "ca", vim.lsp.buf.code_action, { desc = "Code action" }) + vim.keymap.set("n", "f", function() vim.lsp.buf.format({ async = true }) end, { desc = "Format" }) + ''; + }; + + home.pointerCursor = { + enable = true; + gtk.enable = true; + package = pkgs.phinger-cursors; + name = "Phinger-cursors-light"; + size = 32; + }; + + gtk = { + enable = true; + font = { + name = "Comic Neue"; + size = 11; + }; + }; + + xdg.configFile."niri/config.kdl".text = '' + input { + keyboard { + xkb { + layout "us" + } + } + touchpad { + tap + natural-scroll + } + mouse { + accel-speed -0.25 + accel-profile "flat" + } + focus-follows-mouse max-scroll-amount="0%" + } + + window-rule { + geometry-corner-radius 20 + + clip-to-geometry true + } + + window-rule { + match app-id=r#"(?i)steam_app|aoe2|wine"# + geometry-corner-radius 0 + clip-to-geometry false + } + + window-rule { + match app-id="dev.noctalia.Noctalia" + open-floating true + default-column-width { fixed 1080; } + default-window-height { fixed 920; } + } + + debug { + render-drm-device "/dev/dri/renderD129" + ignore-drm-device "/dev/dri/renderD128" + honor-xdg-activation-with-invalid-serial + } + + layer-rule { + match namespace="^noctalia-backdrop" + place-within-backdrop true + } + + layer-rule { + match namespace="^noctalia-wallpaper" + place-within-backdrop true + } + + overview { + workspace-shadow { + off + } + } + + window-rule { + exclude app-id="com\\.mitchellh\\.ghostty" + background-effect { + blur true + xray false + } + } + + layer-rule { + match namespace="^noctalia-(bar-[^\"]+|notification|dock|panel|attached-panel|osd)$" + background-effect { + xray false + } + } + + blur { + passes 2 + offset 3.0 + noise 0.03 + saturation 1.0 + } + + layout { + background-color "transparent" + gaps 12 + center-focused-column "never" + default-column-width { + proportion 0.5 + } + focus-ring { + width 2 + active-color "#89b4fa" + inactive-color "#45475a" + } + border { + off + } + } + + prefer-no-csd + screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" + spawn-at-startup "noctalia" + spawn-at-startup "xwayland-satellite" + + // Match by make/model/serial: connector names shift across GPU/driver + // updates (HDMI-A-3 to HDMI-A-1, DP-4 to DP-2) and silently orphan the blocks. + output "Microstep MAG 341C OLED 0x01010101" { + mode "3440x1440@174.962" + position x=0 y=0 + } + + output "ASUSTek COMPUTER INC ASUS PA279CV R9LMTF061509" { + mode "3840x2160@59.997" + scale 1.5 + position x=3440 y=0 + } + + binds { + Mod+Return { spawn "ghostty"; } + Mod+Space { spawn-sh "noctalia msg panel-toggle launcher"; } + Mod+D { spawn-sh "noctalia msg panel-toggle launcher"; } + Mod+S { spawn-sh "noctalia msg panel-toggle control-center"; } + Mod+Comma { spawn-sh "noctalia msg settings-toggle"; } + Mod+Alt+L { spawn "noctalia" "ipc" "call" "lockScreen" "lock"; } + Mod+Shift+Escape { quit; } + Mod+Q { close-window; } + Mod+H { focus-column-left; } + Mod+J { focus-window-down; } + Mod+K { focus-window-up; } + Mod+L { focus-column-right; } + Mod+Shift+Q { move-column-left; } + Mod+Shift+J { move-window-down; } + Mod+Shift+K { move-window-up; } + Mod+Shift+E { move-column-right; } + Mod+Left { focus-column-left; } + Mod+Down { focus-window-down; } + Mod+Up { focus-window-up; } + Mod+Right { focus-column-right; } + Mod+Shift+Left { move-column-left; } + Mod+Shift+Down { move-window-down; } + Mod+Shift+Up { move-window-up; } + Mod+Shift+Right { move-column-right; } + Mod+1 { focus-workspace 1; } + Mod+2 { focus-workspace 2; } + Mod+3 { focus-workspace 3; } + Mod+4 { focus-workspace 4; } + Mod+5 { focus-workspace 5; } + Mod+Shift+X { maximize-column; } + Mod+Shift+F { fullscreen-window; } + Mod+C { center-column; } + Mod+V { toggle-window-floating; } + Print { screenshot; } + XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "noctalia msg volume-up"; } + XF86AudioLowerVolume allow-when-locked=true { spawn-sh "noctalia msg volume-down"; } + XF86AudioMute allow-when-locked=true { spawn-sh "noctalia msg volume-mute"; } + XF86MonBrightnessUp { spawn-sh "noctalia msg brightness-up"; } + XF86MonBrightnessDown { spawn-sh "noctalia msg brightness-down"; } + } + ''; +}