diff --git a/configurations/any.nix b/configurations/any.nix index 65b2406..0cb0037 100644 --- a/configurations/any.nix +++ b/configurations/any.nix @@ -1,18 +1,15 @@ +# Host: "default" (device: minimal) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "minimal"; - modules = [ - ( - { - inputs, - ... - }: - { - imports = [ - inputs.self.nixosModules.default - ]; - - system.stateVersion = "26.05"; - } - ) + inputs, + ... +}: +{ + imports = [ + inputs.self.nixosModules.default ]; + + system.stateVersion = "26.05"; } diff --git a/configurations/default.nix b/configurations/default.nix index 7735bae..258b6f8 100644 --- a/configurations/default.nix +++ b/configurations/default.nix @@ -1,18 +1,73 @@ { inputs, ... }@flakeContext: let + lib = inputs.nixpkgs.lib; mkSystem = import ../lib/mkSystem.nix flakeContext; + xlibLib = import ../lib/xlib.nix { inherit lib; }; + + # One record per host. The attribute name IS the hostname, so it is written + # exactly once; `hostname` is only needed where the attribute name is not + # the real hostname (the `default` entry). + # + # device device type, must be a key of `devices` in lib/xlib.nix + # modules module body for this host + hosts = { + default = { + hostname = "nixos"; + device = "minimal"; + modules = [ ./any.nix ]; + }; + atoridu = { + device = "primary"; + modules = [ ./mini-pc.nix ]; + }; + rydiwo = { + device = "secondary"; + modules = [ ./mini-laptop.nix ]; + }; + otreca = { + device = "vds"; + modules = [ ./vds.nix ]; + }; + sapphira = { + device = "server"; + modules = [ ./server.nix ]; + }; + wsl = { + device = "wsl"; + modules = [ ./wsl.nix ]; + }; + }; + + mkHost = + name: + { + device, + modules, + hostname ? name, + ... + }: + let + xlib = xlibLib.mkXlib { + inherit hostname; + type = device; + }; + in + { + inherit xlib; + system = mkSystem { inherit xlib modules; }; + }; in { - nixosConfigurations = { - default = mkSystem (import ./any.nix); # default - atoridu = mkSystem (import ./mini-pc.nix); # atoridu - rydiwo = mkSystem (import ./mini-laptop.nix); # rydiwo - otreca = mkSystem (import ./vds.nix); # vds - sapphira = mkSystem (import ./server.nix); # sapphira - wsl = mkSystem (import ./wsl.nix); # wsl - }; + nixosConfigurations = lib.mapAttrs' ( + name: spec: lib.nameValuePair name (mkHost name spec).system + ) hosts; + + # Per-host xlib values, for code that lives outside the module system + # (deploy, overlays, pkgs). + xlib = lib.mapAttrs' (name: spec: lib.nameValuePair name (mkHost name spec).xlib) hosts; + nixOnDroidConfigurations = { - epral = import ./mobile.nix flakeContext; # epral (Android via nix-on-droid) + epral = import ./mobile.nix flakeContext; # epral (Android device via nix-on-droid) # Alias so a plain `nix-on-droid switch` from a local clone # (~/.config/nix-on-droid) picks up the device config without `#epral`. default = import ./mobile.nix flakeContext; diff --git a/configurations/mini-laptop.nix b/configurations/mini-laptop.nix index 847dc5a..32ca9b2 100644 --- a/configurations/mini-laptop.nix +++ b/configurations/mini-laptop.nix @@ -1,41 +1,37 @@ +# Host: "rydiwo" (device: secondary) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "secondary"; - hostname = "rydiwo"; - modules = [ - ( - { - lib, - pkgs, - xlib, - inputs, - ... - }: - { - imports = with inputs; [ - nixos-hardware.nixosModules.chuwi-minibook-x - ./hardware/mini-laptop.nix - self.nixosModules.default - ]; - - boot = { - kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable; - loader = { - systemd-boot.enable = lib.mkDefault true; - efi.canTouchEfiVariables = lib.mkDefault true; - }; - }; - - fileSystems = xlib.helpers.mkNtfsMount { - path = xlib.dirs.lamet-drive; - uuid = "DC76BD3576BD116E"; - mask = "0000"; - }; - - xlib.ssh.enable = true; - hardware.intel-gpu-tools.enable = true; - - system.stateVersion = "26.05"; - } - ) + lib, + pkgs, + xlib, + inputs, + ... +}: +{ + imports = with inputs; [ + nixos-hardware.nixosModules.chuwi-minibook-x + ./hardware/mini-laptop.nix + self.nixosModules.default ]; + + boot = { + kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable; + loader = { + systemd-boot.enable = lib.mkDefault true; + efi.canTouchEfiVariables = lib.mkDefault true; + }; + }; + + fileSystems = xlib.helpers.mkNtfsMount { + path = xlib.dirs.lamet-drive; + uuid = "DC76BD3576BD116E"; + mask = "0000"; + }; + + host.ssh.enable = true; + hardware.intel-gpu-tools.enable = true; + + system.stateVersion = "26.05"; } diff --git a/configurations/mini-pc.nix b/configurations/mini-pc.nix index ba535ba..b31f811 100644 --- a/configurations/mini-pc.nix +++ b/configurations/mini-pc.nix @@ -1,83 +1,84 @@ +# Host: "atoridu" (device: primary) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "primary"; - hostname = "atoridu"; - modules = [ - ( - { - lib, - pkgs, - xlib, - inputs, - ... - }: - { - imports = with inputs; [ - ./hardware/mini-pc.nix - ./disko/mini-pc.nix - ./hardware/logitech.nix - self.nixosModules.default - ]; - - fileSystems = lib.listToAttrs ( - map (xlib.helpers.mkNtfsMount) [ - { - path = xlib.dirs.therima-drive; - uuid = "C0A2DDEFA2DDEA44"; - enable = false; - } - { - path = xlib.dirs.vetymae-drive; - uuid = "6408433908430A0E"; - enable = false; - } - { - path = xlib.dirs.soptur-drive; - uuid = "C00C56E40C56D54E"; - enable = false; - } - ] - ); - - boot = { - kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable; - loader = { - systemd-boot.enable = lib.mkDefault true; - efi.canTouchEfiVariables = lib.mkDefault true; - }; - }; - - services.xserver = { - videoDrivers = [ - "amdgpu" - ]; - }; - services.pipewire = { - enable = lib.mkDefault true; - systemWide = true; - alsa.enable = false; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - extraConfig.pipewire = { - "99-default.conf" = { - "context.properties" = { - "default.clock.rate" = 96000; - "default.clock.allowed-rates" = [ - 44100 - 48000 - 96000 - ]; - "default.clock.quantum" = 1024; - "default.clock.min-quantum" = 256; - "default.clock.max-quantum" = 2048; - }; - }; - }; - }; - nixpkgs.config.pulseaudio = true; - - system.stateVersion = "26.05"; - } - ) + lib, + pkgs, + xlib, + inputs, + ... +}: +{ + imports = with inputs; [ + ./hardware/mini-pc.nix + ./disko/mini-pc.nix + ./hardware/logitech.nix + self.nixosModules.default ]; + + # mkNtfsMount returns a `{ "" = { ... }; }` attrset (the shape + # fileSystems itself wants), so several mounts are combined with + # mergeAttrsList — not listToAttrs, which would demand `name`/`value`. + # + # These three ntfs3 drives are intentionally left unmounted. The entries + # are kept commented out rather than deleted, so restoring a drive is a + # matter of uncommenting its block. `enable = false` would declare a drive + # without mounting it; dropping the field mounts it. + fileSystems = lib.mergeAttrsList ( + map (xlib.helpers.mkNtfsMount) [ + # { + # path = xlib.dirs.therima-drive; + # uuid = "C0A2DDEFA2DDEA44"; + # } + # { + # path = xlib.dirs.vetymae-drive; + # uuid = "6408433908430A0E"; + # } + # { + # path = xlib.dirs.soptur-drive; + # uuid = "C00C56E40C56D54E"; + # } + ] + ); + + boot = { + kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable; + loader = { + systemd-boot.enable = lib.mkDefault true; + efi.canTouchEfiVariables = lib.mkDefault true; + }; + }; + + services.xserver = { + videoDrivers = [ + "amdgpu" + ]; + }; + services.pipewire = { + enable = lib.mkDefault true; + systemWide = true; + alsa.enable = false; + alsa.support32Bit = true; + pulse.enable = true; + jack.enable = true; + extraConfig.pipewire = { + "99-default.conf" = { + "context.properties" = { + "default.clock.rate" = 96000; + "default.clock.allowed-rates" = [ + 44100 + 48000 + 96000 + ]; + "default.clock.quantum" = 1024; + "default.clock.min-quantum" = 256; + "default.clock.max-quantum" = 2048; + }; + }; + }; + }; + nixpkgs.config.pulseaudio = true; + + system.stateVersion = "26.05"; } diff --git a/configurations/mobile.nix b/configurations/mobile.nix index bce7a5b..a271b18 100644 --- a/configurations/mobile.nix +++ b/configurations/mobile.nix @@ -9,6 +9,7 @@ let # (essentials, users.nix, home-manager, sops-nix, disko, grub2-themes) # and nixpkgs.overlays are skipped so it evaluates under nix-on-droid's # module system (class = "nixOnDroid"). + xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; }; nixOnDroidModule = { lib, @@ -21,11 +22,6 @@ let inputs.self.nixosModules.strict ]; - xlib.device = { - type = "termux"; - hostname = "epral"; - }; - # Login shell. nix-on-droid writes /etc/passwd from user.shell on every # activation, so `chsh` is useless here — set it in nix instead. # (default is bashInteractive) @@ -120,6 +116,13 @@ inputs.nix-on-droid.lib.nixOnDroidConfiguration { nixOnDroidModule ]; extraSpecialArgs = { - deviceType = "termux"; + # `xlib` is the same value shape NixOS hosts get (lib/mkSystem.nix); + # the hostname lives here because nixOnDroidConfigurations is keyed by + # both "epral" and the "default" alias, so it cannot come from the + # attribute name. + xlib = xlib.mkXlib { + hostname = "epral"; + type = "termux"; + }; }; } diff --git a/configurations/server.nix b/configurations/server.nix index 330b2eb..a863502 100644 --- a/configurations/server.nix +++ b/configurations/server.nix @@ -1,83 +1,79 @@ +# Host: "sapphira" (device: server) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "server"; - hostname = "sapphira"; - modules = [ - ( - { - lib, - pkgs, - xlib, - inputs, - ... - }: - { - imports = [ - ./hardware/server.nix - inputs.self.nixosModules.default - ]; - - boot = { - # kernelPackages = pkgs.linuxPackages_xanmod_stable; - hardwareScan = true; - loader = { - systemd-boot.enable = lib.mkDefault true; - efi.canTouchEfiVariables = lib.mkDefault true; - }; - }; - - hardware = { - bluetooth.enable = true; - graphics = { - enable = true; - extraPackages = with pkgs; [ - intel-media-driver - intel-ocl - intel-vaapi-driver - ]; - }; - intel-gpu-tools.enable = true; - }; - - fileSystems = - (xlib.helpers.mkExfatMount { - path = xlib.dirs.archive-drive; - label = "archive"; - }) - // (xlib.helpers.mkExfatMount { - path = xlib.dirs.mobile-drive; - uuid = "7EB1-DC99"; - }) - // (xlib.helpers.mkBindMount { - what = xlib.dirs.services-folder; - where = xlib.dirs.services-mnt-folder; - }) - // { - # External drive - "${xlib.dirs.server-home}" = { - device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de"; - fsType = "ext4"; - }; - }; - - systemd.tmpfiles.rules = [ - "z ${xlib.dirs.services-mnt-folder} 0777 root root -" - ]; - - xlib.ssh.enable = true; - - networking = { - networkmanager.enable = true; - firewall.enable = false; - # nameservers = [ - # "192.168.1.1" - # "127.0.0.1" - # ]; - }; - - system = { - stateVersion = "25.05"; - }; - } - ) + lib, + pkgs, + xlib, + inputs, + ... +}: +{ + imports = [ + ./hardware/server.nix + inputs.self.nixosModules.default ]; + + boot = { + # kernelPackages = pkgs.linuxPackages_xanmod_stable; + hardwareScan = true; + loader = { + systemd-boot.enable = lib.mkDefault true; + efi.canTouchEfiVariables = lib.mkDefault true; + }; + }; + + hardware = { + bluetooth.enable = true; + graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-media-driver + intel-ocl + intel-vaapi-driver + ]; + }; + intel-gpu-tools.enable = true; + }; + + fileSystems = + (xlib.helpers.mkExfatMount { + path = xlib.dirs.archive-drive; + label = "archive"; + }) + // (xlib.helpers.mkExfatMount { + path = xlib.dirs.mobile-drive; + uuid = "7EB1-DC99"; + }) + // (xlib.helpers.mkBindMount { + what = xlib.dirs.services-folder; + where = xlib.dirs.services-mnt-folder; + }) + // { + # External drive + "${xlib.dirs.server-home}" = { + device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de"; + fsType = "ext4"; + }; + }; + + systemd.tmpfiles.rules = [ + "z ${xlib.dirs.services-mnt-folder} 0777 root root -" + ]; + + host.ssh.enable = true; + + networking = { + networkmanager.enable = true; + firewall.enable = false; + # nameservers = [ + # "192.168.1.1" + # "127.0.0.1" + # ]; + }; + + system = { + stateVersion = "25.05"; + }; } diff --git a/configurations/vds.nix b/configurations/vds.nix index 6972ef3..4b704e7 100644 --- a/configurations/vds.nix +++ b/configurations/vds.nix @@ -1,122 +1,117 @@ +# Host: "otreca" (device: vds) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "vds"; - hostname = "otreca"; - modules = [ - ( - { - config, - lib, - modulesPath, - pkgs, - xlib, - inputs, - ... - }: - { - imports = [ - (modulesPath + "/installer/scan/not-detected.nix") - (modulesPath + "/profiles/qemu-guest.nix") + lib, + modulesPath, + pkgs, + xlib, + inputs, + ... +}: +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + (modulesPath + "/profiles/qemu-guest.nix") - ./disko/vds.nix - ./hardware/vds.nix + ./disko/vds.nix + ./hardware/vds.nix - inputs.self.nixosModules.default - ]; - - boot = { - # kernelPackages = pkgs.linuxPackages_xanmod_stable; - hardwareScan = true; - loader = { - grub = { - enable = true; - device = "nodev"; - useOSProber = false; - efiSupport = false; - }; - systemd-boot.enable = lib.mkDefault false; - }; - kernel.sysctl = { - "net.ipv4.tcp_syncookies" = 1; - "net.ipv4.tcp_max_syn_backlog" = 4096; - "net.ipv4.tcp_synack_retries" = 3; - "net.ipv4.tcp_syn_retries" = 3; - }; - }; - - xlib.ssh.enable = true; - services.openssh.openFirewall = true; - - services.tailscale = { - enable = true; - openFirewall = true; - }; - networking = { - nameservers = [ - "1.1.1.1" - "8.8.8.8" - ]; - networkmanager.enable = true; - tempAddresses = "disabled"; - dhcpcd = { - enable = true; - IPv6rs = false; - }; - firewall = { - enable = true; - allowPing = true; - }; - nftables = { - enable = true; - ruleset = '' - table inet filter { - chain input { - type filter hook input priority 0; - - # loopback - iif lo accept - - # уже установленные - ct state established,related accept - - # РЕЖЕМ SYN СРАЗУ - tcp flags syn tcp dport {80,443} limit rate 20/second burst 40 packets accept - tcp flags syn tcp dport {80,443} drop - - # остальное по необходимости - } - } - ''; - }; - enableIPv6 = false; - interfaces.ens3 = { - useDHCP = true; - # ipv4.addresses = [ - # { - # address = "31.57.158.109"; - # prefixLength = 24; - # } - # ]; - # ipv6.addresses = [ - # { - # address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e"; - # prefixLength = 64; - # } - # ]; - }; - # defaultGateway = { - # address = "31.57.158.1"; - # interface = "ens3"; - # }; - # defaultGateway6 = { - # address = "2a13:7c00:6:102::1"; - # interface = "ens3"; - # }; - }; - - system = { - stateVersion = "25.05"; - }; - } - ) + inputs.self.nixosModules.default ]; + + boot = { + # kernelPackages = pkgs.linuxPackages_xanmod_stable; + hardwareScan = true; + loader = { + grub = { + enable = true; + device = "nodev"; + useOSProber = false; + efiSupport = false; + }; + systemd-boot.enable = lib.mkDefault false; + }; + kernel.sysctl = { + "net.ipv4.tcp_syncookies" = 1; + "net.ipv4.tcp_max_syn_backlog" = 4096; + "net.ipv4.tcp_synack_retries" = 3; + "net.ipv4.tcp_syn_retries" = 3; + }; + }; + + host.ssh.enable = true; + services.openssh.openFirewall = true; + + services.tailscale = { + enable = true; + openFirewall = true; + }; + networking = { + nameservers = [ + "1.1.1.1" + "8.8.8.8" + ]; + networkmanager.enable = true; + tempAddresses = "disabled"; + dhcpcd = { + enable = true; + IPv6rs = false; + }; + firewall = { + enable = true; + allowPing = true; + }; + nftables = { + enable = true; + ruleset = '' + table inet filter { + chain input { + type filter hook input priority 0; + + # loopback + iif lo accept + + # уже установленные + ct state established,related accept + + # РЕЖЕМ SYN СРАЗУ + tcp flags syn tcp dport {80,443} limit rate 20/second burst 40 packets accept + tcp flags syn tcp dport {80,443} drop + + # остальное по необходимости + } + } + ''; + }; + enableIPv6 = false; + interfaces.ens3 = { + useDHCP = true; + # ipv4.addresses = [ + # { + # address = "31.57.158.109"; + # prefixLength = 24; + # } + # ]; + # ipv6.addresses = [ + # { + # address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e"; + # prefixLength = 64; + # } + # ]; + }; + # defaultGateway = { + # address = "31.57.158.1"; + # interface = "ens3"; + # }; + # defaultGateway6 = { + # address = "2a13:7c00:6:102::1"; + # interface = "ens3"; + # }; + }; + + system = { + stateVersion = "25.05"; + }; } diff --git a/configurations/wsl.nix b/configurations/wsl.nix index 321ee73..9cae7a6 100644 --- a/configurations/wsl.nix +++ b/configurations/wsl.nix @@ -1,44 +1,39 @@ +# Host: "wsl" (device: wsl) +# +# The host record lives in configurations/default.nix; this file is only the +# module body. `xlib` (identity, dirs, helpers) arrives as a module argument. { - deviceType = "wsl"; - hostname = "wsl"; - modules = [ - ( - { - config, - lib, - pkgs, - modulesPath, - xlib, - inputs, - ... - }: - { - imports = [ - inputs.nixos-wsl.nixosModules.default - inputs.self.nixosModules.default - ]; - - hardware = { - graphics.enable = true; - }; - - networking = { - firewall = { - enable = false; - allowPing = true; - }; - enableIPv6 = true; - }; - - wsl = { - enable = true; - startMenuLaunchers = true; - useWindowsDriver = true; - defaultUser = config.xlib.device.username; - }; - - system.stateVersion = "24.11"; - } - ) + lib, + modulesPath, + pkgs, + xlib, + inputs, + ... +}: +{ + imports = [ + inputs.nixos-wsl.nixosModules.default + inputs.self.nixosModules.default ]; + + hardware = { + graphics.enable = true; + }; + + networking = { + firewall = { + enable = false; + allowPing = true; + }; + enableIPv6 = true; + }; + + wsl = { + enable = true; + startMenuLaunchers = true; + useWindowsDriver = true; + defaultUser = xlib.device.username; + }; + + system.stateVersion = "24.11"; } diff --git a/deploy/default.nix b/deploy/default.nix index 68661f9..b1bf654 100644 --- a/deploy/default.nix +++ b/deploy/default.nix @@ -6,7 +6,9 @@ let path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos inputs.self.nixosConfigurations.${hostname}; }; }; - user = "${inputs.self.nixosConfigurations.default.config.xlib.device.username}"; + # Login user for every deploy target. Read from the hoisted xlib instead of + # digging through a built NixOS configuration. + user = "${inputs.self.xlib.default.device.username}"; server = "sapphira"; vds = "otreca"; mini-laptop = "rydiwo"; diff --git a/home/home.nix b/home/home.nix index 87c01df..b446347 100644 --- a/home/home.nix +++ b/home/home.nix @@ -53,11 +53,7 @@ let imports = [ (./. + "/${xlib.device.type}.nix") ]; - headless = builtins.elem xlib.device.type [ - "server" - "vds" - "wsl" - ]; + headless = xlib.isHeadless; }; }; sharedModules = [ diff --git a/lib/mkSystem.nix b/lib/mkSystem.nix index c12bcf2..d4b17d3 100644 --- a/lib/mkSystem.nix +++ b/lib/mkSystem.nix @@ -2,26 +2,28 @@ inputs, ... }: +# Builds a NixOS system from a host record. +# +# `xlib` is the pure host value (lib/xlib.nix `mkXlib`) built in +# configurations/default.nix. It is handed to every module as the `xlib` +# argument, so modules read plain `xlib.*` data instead of `config.xlib.*` +# and the host record stays the single source of truth. { - deviceType, - hostname ? null, + xlib, modules ? [ ], system ? "x86_64-linux", + ... }: let lib = inputs.nixpkgs.lib; in lib.nixosSystem { - inherit system; - modules = modules ++ [ - { - xlib.device = { - type = deviceType; - } - // lib.optionalAttrs (hostname != null) { inherit hostname; }; - } - ]; + inherit + system + modules + ; specialArgs = { - inherit deviceType inputs; + inherit inputs; + inherit xlib; }; } diff --git a/lib/xlib.nix b/lib/xlib.nix index 99c3266..9efb020 100644 --- a/lib/xlib.nix +++ b/lib/xlib.nix @@ -2,9 +2,49 @@ lib, ... }: -# Shared pure helper functions for module definitions. -# Injected into every module via `xlib.helpers` (see options.nix). +# Pure host library: no module system involved. +# +# `mkXlib` derives everything a host needs to know about itself (identity, +# well-known paths, capability flags, shared helpers) from a single record. +# It is built in flake-level code (configurations/default.nix) and handed to +# every module as the `xlib` argument via lib/mkSystem.nix, so modules read +# plain `xlib.*` values instead of `config.xlib.*` and nothing in xlib can be +# overridden per host — the host record is the only place to change it. let + # Every supported device type and its capabilities. Single source of truth: + # replaces the old `lib.types.enum` in modules/options.nix and the + # hand-written type lists in modules/default.nix and home/home.nix. + devices = { + minimal = { + desktop = false; + headless = false; + }; + primary = { + desktop = true; + headless = false; + }; + secondary = { + desktop = true; + headless = false; + }; + server = { + desktop = false; + headless = true; + }; + vds = { + desktop = false; + headless = true; + }; + wsl = { + desktop = false; + headless = true; + }; + termux = { + desktop = false; + headless = true; + }; + }; + # tmpfiles rule: "type dir mode user group -" mkTmpfile = type: dir: mode: user: group: @@ -138,16 +178,85 @@ let name = targetPath; value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}"; }) paths; + + helpers = { + inherit + mkTmpfile + mkTmpDirs + mkBindMount + mkSystemdBind + mkServiceStorage + mkNtfsMount + mkExfatMount + mkSymlinks + ; + }; + + # Well-known paths. Everything derives from `username`, which is why the + # whole set can be computed outside the module system. + mkDirs = + username: + let + user-home = "/home/${username}"; + wsl-home = "/mnt/c/Users/${username}"; + server-home = "${user-home}/External"; + services-mnt-folder = "/mnt/services"; + in + { + inherit + user-home + wsl-home + server-home + services-mnt-folder + ; + + user-storage = "${user-home}/Storage"; + wsl-storage = "${wsl-home}/Storage"; + server-credentials = "${server-home}/Credentials/server"; + storage = "${server-home}/Storage"; + calibre-library = "${server-home}/Books-Library"; + services-folder = "${server-home}/Services"; + services-nodes-folder = "${services-mnt-folder}/nodes"; + postgresql-folder = "${services-mnt-folder}/postgresql"; + music-library = "${user-home}/Music"; + + archive-drive = "/mnt/archive"; + lamet-drive = "/mnt/lamet"; + mobile-drive = "/mnt/mobile"; + therima-drive = "/mnt/therima"; + vetymae-drive = "/mnt/vetymae"; + soptur-drive = "/mnt/soptur"; + }; + + mkXlib = + { + hostname, + type, + username ? "oqyude", + }: + let + # Unknown device type fails here, at flake level, with the valid list. + capabilities = devices.${type} or (throw "xlib: unknown device type '${type}', expected one of ${lib.concatStringsSep ", " (builtins.attrNames devices)}"); + in + { + device = { + inherit + hostname + type + username + ; + }; + isDesktop = capabilities.desktop; + isHeadless = capabilities.headless; + dirs = mkDirs username; + inherit helpers; + }; in { inherit - mkTmpfile - mkTmpDirs - mkBindMount - mkSystemdBind - mkServiceStorage - mkNtfsMount - mkExfatMount - mkSymlinks + devices + helpers + mkDirs + mkXlib ; } diff --git a/modules/containers/3x-ui.nix b/modules/containers/3x-ui.nix index 45dcdf9..20dc7bd 100644 --- a/modules/containers/3x-ui.nix +++ b/modules/containers/3x-ui.nix @@ -7,7 +7,11 @@ }: let panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/3x-ui"; - certDomain = xlib.services."3x-ui".certDomain or null; + # Domain whose Let's Encrypt cert (at /var/lib/acme//) gets mounted + # read-only into the 3x-ui container so the panel can terminate TLS itself. + # Null when 3x-ui serves plain HTTP and TLS is terminated by an upstream + # nginx. + certDomain = config.host."3x-ui".certDomain; certMounts = if certDomain == null then [ ] @@ -28,94 +32,125 @@ let ]; # VDS-only: nginx stream forwards host:443 → 127.0.0.1:15380 → # container:443, so Xray sees its REALITY inbound on port 443. - realityPorts = lib.optional xlib.services."3x-ui".reality443Forwarding "127.0.0.1:15380:443/tcp"; + realityPorts = lib.optional config.host."3x-ui".reality443Forwarding "127.0.0.1:15380:443/tcp"; in { - virtualisation = { - podman = { - enable = true; - autoPrune = { + options.host."3x-ui" = { + # Domain whose LE cert should be mounted into the 3x-ui container at + # /root/cert/fullchain.pem and key.pem. Set null if 3x-ui serves plain + # HTTP and TLS is terminated by an upstream nginx. + certDomain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "pubray1.zeroq.su"; + description = '' + Domain whose LE cert should be mounted into the 3x-ui + container at /root/cert/fullchain.pem and key.pem. + ''; + }; + # Publish host:15380 → container:443. Only nodes that host an Xray + # REALITY inbound on container:443 need this (so nginx stream can + # forward TLS to Xray via 127.0.0.1:15380 while Xray itself sees + # incoming connections on its configured port 443). Set false on nodes + # that only run the 3x-ui panel. + reality443Forwarding = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + When true, publish host:15380 → container:443 so Xray + inside the container can serve REALITY on its real + configured port 443 (nginx stream forwards 443 → 15380). + ''; + }; + }; + + config = { + virtualisation = { + podman = { enable = true; - flags = [ "--all" ]; - }; - dockerCompat = true; - }; - oci-containers = { - backend = "podman"; - containers."3xui_app" = { - image = "ghcr.io/mhsanaei/3x-ui:latest"; - environment = { - "XRAY_VMESS_AEAD_FORCED" = "false"; - "XUI_ENABLE_FAIL2BAN" = "true"; - "TZ" = "Europe/Moscow"; + autoPrune = { + enable = true; + flags = [ "--all" ]; }; - volumes = [ - "${panel}/cert/:/root/cert:rw" - "${panel}/db/:/etc/x-ui:rw" - ] - ++ certMounts; - log-driver = "journald"; - # Adding a new inbound through the 3x-ui panel on a port outside - # the 14380-15379 range requires extending basePorts and rebuilding. - ports = basePorts ++ realityPorts; + dockerCompat = true; }; - }; - }; - - systemd = { - services = { - "podman-3xui_app" = { - serviceConfig.Restart = lib.mkOverride 90 "always"; - partOf = [ "podman-compose-3x-ui-root.target" ]; - wantedBy = [ "podman-compose-3x-ui-root.target" ]; - }; - "podman-update-3xui_app" = { - path = [ pkgs.podman ]; - serviceConfig = { - Type = "oneshot"; - TimeoutSec = 300; + oci-containers = { + backend = "podman"; + containers."3xui_app" = { + image = "ghcr.io/mhsanaei/3x-ui:latest"; + environment = { + "XRAY_VMESS_AEAD_FORCED" = "false"; + "XUI_ENABLE_FAIL2BAN" = "true"; + "TZ" = "Europe/Moscow"; + }; + volumes = [ + "${panel}/cert/:/root/cert:rw" + "${panel}/db/:/etc/x-ui:rw" + ] + ++ certMounts; + log-driver = "journald"; + # Adding a new inbound through the 3x-ui panel on a port outside + # the 14380-15379 range requires extending basePorts and rebuilding. + ports = basePorts ++ realityPorts; }; - script = '' - podman pull ghcr.io/mhsanaei/3x-ui:latest - systemctl restart podman-3xui_app.service - ''; }; }; - # Starts/stops together with all 3x-ui compose resources. - targets."podman-compose-3x-ui-root" = { - unitConfig.Description = "Root target generated by compose2nix."; - wantedBy = [ "multi-user.target" ]; - }; - # timers."podman-update-3xui_app" = { - # wantedBy = [ "timers.target" ]; - # timerConfig = { - # OnCalendar = "weekly"; - # Persistent = true; - # }; - # }; - tmpfiles.rules = [ - (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") - (xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root") - (xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" - "root" - "root" - ) - (xlib.helpers.mkTmpfile "d" panel "0755" "root" "root") - (xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root") - (xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root") - # Relabel panel dir for SELinux so containers can access it. - (xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root") - ]; - }; - # Enable container name DNS for all Podman networks. - networking.firewall = { - interfaces = - let - matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; - in - { - "${matchAll}".allowedUDPPorts = [ 53 ]; + systemd = { + services = { + "podman-3xui_app" = { + serviceConfig.Restart = lib.mkOverride 90 "always"; + partOf = [ "podman-compose-3x-ui-root.target" ]; + wantedBy = [ "podman-compose-3x-ui-root.target" ]; + }; + "podman-update-3xui_app" = { + path = [ pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + TimeoutSec = 300; + }; + script = '' + podman pull ghcr.io/mhsanaei/3x-ui:latest + systemctl restart podman-3xui_app.service + ''; + }; }; + # Starts/stops together with all 3x-ui compose resources. + targets."podman-compose-3x-ui-root" = { + unitConfig.Description = "Root target generated by compose2nix."; + wantedBy = [ "multi-user.target" ]; + }; + # timers."podman-update-3xui_app" = { + # wantedBy = [ "timers.target" ]; + # timerConfig = { + # OnCalendar = "weekly"; + # Persistent = true; + # }; + # }; + tmpfiles.rules = [ + (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" + "root" + "root" + ) + (xlib.helpers.mkTmpfile "d" panel "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root") + # Relabel panel dir for SELinux so containers can access it. + (xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root") + ]; + }; + + # Enable container name DNS for all Podman networks. + networking.firewall = { + interfaces = + let + matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; + in + { + "${matchAll}".allowedUDPPorts = [ 53 ]; + }; + }; }; } diff --git a/modules/default.nix b/modules/default.nix index 5ee489c..a39b866 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -3,30 +3,20 @@ let # NixOS-only modules. termux runs nix-on-droid (its own module system, # class = "nixOnDroid"): options like services.*, users.*, sops.*, disko.* # and nixpkgs.overlays (flake assertion) do not exist there. - moduleArgs = config: { - inherit inputs; - xlib = config.xlib; - }; + # + # `xlib` arrives as a module argument (see lib/mkSystem.nix) and is plain + # data, not a module option, so nothing here has to declare or set it. defaultModule = { - config, - deviceType, lib, xlib, ... }: - let - isDesktop = builtins.elem deviceType [ - "primary" - "secondary" - ]; - in { imports = with inputs; [ ./essentials - ./options.nix ./users.nix home-manager.nixosModules.home-manager # home-manager module @@ -37,33 +27,27 @@ let self.homeConfigurations.default.nixosModule # default homeConfigurations disko.nixosModules.disko # disko module ] - ++ lib.optional isDesktop ./desktop # desktop class: primary/secondary + # desktop class: primary/secondary + ++ lib.optional xlib.isDesktop ./desktop # device-type module dir; "minimal" has no extra modules - ++ lib.optional (!isDesktop && deviceType != "minimal") (./. + "/${deviceType}"); + ++ lib.optional (!xlib.isDesktop && xlib.device.type != "minimal") (./. + "/${xlib.device.type}"); nixpkgs.overlays = with inputs; [ self.nixosOverlays.default ]; - networking.hostName = lib.mkDefault config.xlib.device.hostname; - _module.args = moduleArgs config; + networking.hostName = lib.mkDefault xlib.device.hostname; }; strictModule = { - config, - deviceType, - lib, xlib, ... }: { - imports = with inputs; [ + imports = [ # ./essentials # ./users.nix - ./options.nix - (./. + "/${deviceType}") + (./. + "/${xlib.device.type}") # sops-nix.nixosModules.sops ]; - - _module.args = moduleArgs config; }; in { diff --git a/modules/essentials/ssh.nix b/modules/essentials/ssh.nix index 4069fbb..be703f4 100644 --- a/modules/essentials/ssh.nix +++ b/modules/essentials/ssh.nix @@ -3,21 +3,31 @@ lib, ... }: -lib.mkIf config.xlib.ssh.enable { - services.openssh = { - enable = true; - allowSFTP = true; - openFirewall = lib.mkDefault false; - hostKeys = [ - { - path = "/etc/ssh/id_ed25519"; - type = "ed25519"; - } - ]; - settings = { - PasswordAuthentication = false; - PermitRootLogin = "yes"; - UsePAM = true; +{ + options.host.ssh = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable the SSH server with the shared config below."; + }; + }; + + config = lib.mkIf config.host.ssh.enable { + services.openssh = { + enable = true; + allowSFTP = true; + openFirewall = lib.mkDefault false; + hostKeys = [ + { + path = "/etc/ssh/id_ed25519"; + type = "ed25519"; + } + ]; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "yes"; + UsePAM = true; + }; }; }; } diff --git a/modules/options.nix b/modules/options.nix deleted file mode 100644 index 2d7a3c5..0000000 --- a/modules/options.nix +++ /dev/null @@ -1,109 +0,0 @@ -{ - config, - lib, - ... -}: -let - # Option factory for the xlib.dirs namespace - mkDir = - default: description: - lib.mkOption { - type = lib.types.str; - inherit default description; - }; - - helpers = import ../lib/xlib.nix { inherit lib; }; -in -{ - options = { - xlib = { - device = { - type = lib.mkOption { - type = lib.types.enum [ - "minimal" - "primary" - "secondary" - "server" - "vds" - "wsl" - "termux" - ]; - default = "minimal"; - description = "Type of device for this host."; - }; - username = lib.mkOption { - type = lib.types.str; - default = "oqyude"; - description = "Username for host."; - }; - hostname = lib.mkOption { - type = lib.types.str; - default = "nixos"; - description = "Hostname..."; - }; - }; - ssh = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Enable SSH server with the standard config."; - }; - }; - dirs = { - user-home = mkDir "/home/${config.xlib.device.username}" "User home directory."; - user-storage = mkDir "${config.xlib.dirs.user-home}/Storage" "User storage directory."; - archive-drive = mkDir "/mnt/archive" "Archive drive mount point."; - lamet-drive = mkDir "/mnt/lamet" "Lamet drive mount point."; - mobile-drive = mkDir "/mnt/mobile" "Mobile drive mount point."; - therima-drive = mkDir "/mnt/therima" "Therima drive mount point."; - vetymae-drive = mkDir "/mnt/vetymae" "Vetymae drive mount point."; - soptur-drive = mkDir "/mnt/soptur" "Soptur drive mount point."; - wsl-home = mkDir "/mnt/c/Users/${config.xlib.device.username}" "WSL home directory."; - wsl-storage = mkDir "${config.xlib.dirs.wsl-home}/Storage" "WSL storage directory."; - server-home = mkDir "/home/${config.xlib.device.username}/External" "Server home directory."; - server-credentials = mkDir "${config.xlib.dirs.server-home}/Credentials/server" "Server credentials directory."; - storage = mkDir "${config.xlib.dirs.server-home}/Storage" "General storage directory."; - calibre-library = mkDir "${config.xlib.dirs.server-home}/Books-Library" "Calibre library directory."; - music-library = mkDir "${config.xlib.dirs.user-home}/Music" "Music library directory."; - services-folder = mkDir "${config.xlib.dirs.server-home}/Services" "All services folder."; - services-mnt-folder = mkDir "/mnt/services" "All services folder."; - services-nodes-folder = mkDir "${config.xlib.dirs.services-mnt-folder}/nodes" "All nodes folder."; - postgresql-folder = mkDir "${config.xlib.dirs.services-mnt-folder}/postgresql" "PostgreSQL service folder."; - }; - helpers = lib.mkOption { - type = lib.types.anything; - default = helpers; - description = "Shared helper functions (see lib/xlib.nix)."; - }; - services."3x-ui" = { - # Domain whose Let's Encrypt cert (at /var/lib/acme//) - # gets mounted read-only into the 3x-ui container so the panel - # can terminate TLS itself. Set null if 3x-ui serves plain HTTP - # and TLS is terminated by an upstream nginx. - certDomain = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "pubray1.zeroq.su"; - description = '' - Domain whose LE cert should be mounted into the 3x-ui - container at /root/cert/fullchain.pem and key.pem. - ''; - }; - # Publish host:15380 → container:443. Only nodes that host an - # Xray REALITY inbound on container:443 need this (so nginx - # stream can forward TLS to Xray via 127.0.0.1:15380 while Xray - # itself sees incoming connections on its configured port 443). - # Set false on nodes that only run the 3x-ui panel. - reality443Forwarding = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - When true, publish host:15380 → container:443 so Xray - inside the container can serve REALITY on its real - configured port 443 (nginx stream forwards 443 → 15380). - ''; - }; - }; - }; - }; -} diff --git a/modules/server/default.nix b/modules/server/default.nix index 2758179..53e0d37 100644 --- a/modules/server/default.nix +++ b/modules/server/default.nix @@ -50,7 +50,7 @@ # there are other vhosts on the same port). Cert is still mounted in # case 3x-ui is later reconfigured to terminate TLS itself (e.g. for # direct node-API access); nginx doesn't have to use it. - xlib.services."3x-ui".certDomain = "x.zeroq.su"; + host."3x-ui".certDomain = "x.zeroq.su"; systemd.tmpfiles.rules = [ (xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root") (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") diff --git a/modules/users.nix b/modules/users.nix index 28981a0..d2697ca 100644 --- a/modules/users.nix +++ b/modules/users.nix @@ -37,8 +37,6 @@ let ); in { - xlib.device.username = "oqyude"; - users = { mutableUsers = false; users = { diff --git a/modules/vds/default.nix b/modules/vds/default.nix index be108aa..1b7fc1e 100644 --- a/modules/vds/default.nix +++ b/modules/vds/default.nix @@ -14,7 +14,7 @@ ]; # VDS hosts the public-facing Xray REALITY inbound on container:443, # fronted by nginx stream on host:443 → host:15380 → container:443. - xlib.services."3x-ui" = { + host."3x-ui" = { certDomain = "pubray1.zeroq.su"; reality443Forwarding = true; };