diff --git a/boxes/colony.nix b/boxes/colony.nix index bc4f72b..d67bba0 100644 --- a/boxes/colony.nix +++ b/boxes/colony.nix @@ -1,29 +1,29 @@ { lib, pkgs, inputs, ... }: - { - fileSystems = { - "/persist" = { - device = "/dev/disk/by-label/persist"; - fsType = "ext4"; - neededForBoot = true; +{ + fileSystems = { + "/persist" = { + device = "/dev/disk/by-label/persist"; + fsType = "ext4"; + neededForBoot = true; + }; + }; + + networking = { }; + + my = { + firewall = { + trustedInterfaces = [ "blah" ]; + nat = { + externalInterface = "eth0"; + forwardPorts = [ + { + proto = "tcp"; + sourcePort = 2222; + destination = "127.0.0.1:22"; + } + ]; }; }; - - networking = {}; - - my = { - firewall = { - trustedInterfaces = [ "blah" ]; - nat = { - externalInterface = "eth0"; - forwardPorts = [ - { - proto = "tcp"; - sourcePort = 2222; - destination = "127.0.0.1:22"; - } - ]; - }; - }; - server.enable = true; - }; - } + server.enable = true; + }; +} diff --git a/flake.nix b/flake.nix index 4f61f41..9cc0948 100644 --- a/flake.nix +++ b/flake.nix @@ -55,27 +55,32 @@ lib = pkgsFlakes.unstable.lib; pkgs' = mapAttrs (_: path: lib.my.mkPkgs path { overlays = [ libOverlay ]; }) pkgsFlakes; - in { + in + { inherit lib; - nixosModules = mapAttrs (_: path: let path' = ./. + "/modules/${path}"; in { - _file = path'; - imports = [ (import path') ]; - }) { - common = "common.nix"; - build = "build.nix"; - dynamic-motd = "dynamic-motd.nix"; - tmproot = "tmproot.nix"; - firewall = "firewall.nix"; - server = "server.nix"; - }; + nixosModules = mapAttrs + (_: path: + let path' = ./. + "/modules/${path}"; in + { + _file = path'; + imports = [ (import path') ]; + }) + { + common = "common.nix"; + build = "build.nix"; + dynamic-motd = "dynamic-motd.nix"; + tmproot = "tmproot.nix"; + firewall = "firewall.nix"; + server = "server.nix"; + }; nixosConfigurations = import ./systems.nix { inherit lib pkgsFlakes inputs; modules = self.nixosModules; }; systems = mapAttrs (_: system: system.config.system.build.toplevel) self.nixosConfigurations; vms = mapAttrs (_: system: system.config.my.build.devVM) self.nixosConfigurations; apps = - let apps' = {} + let apps' = { } // addPrefix "vms/" (mapAttrs (name: vm: { type = "app"; program = "${vm}/bin/run-${name}-vm"; }) self.vms); in { x86_64-linux = apps'; }; @@ -83,12 +88,13 @@ let pkgs = pkgs'.unstable.${system}; flakePkg = f: f.defaultPackage.${system}; - in pkgs.mkShell { - packages = map flakePkg [ - agenix - deploy-rs - ]; - } - ); + in + pkgs.mkShell { + packages = map flakePkg [ + agenix + deploy-rs + ]; + } + ); }; } diff --git a/modules/build.nix b/modules/build.nix index f291ed4..4c572e1 100644 --- a/modules/build.nix +++ b/modules/build.nix @@ -1,32 +1,33 @@ { lib, extendModules, modulesPath, baseModules, options, config, ... }: - let - inherit (lib) mkOption; - inherit (lib.my) mkBoolOpt; +let + inherit (lib) mkOption; + inherit (lib.my) mkBoolOpt; - cfg = config.my.build; + cfg = config.my.build; - asDevVM = extendModules { - # TODO: Hack because this is kinda broken on 21.11 (https://github.com/NixOS/nixpkgs/issues/148343) - specialArgs = { inherit baseModules; }; - modules = [ - "${modulesPath}/virtualisation/qemu-vm.nix" - ({ ... }: { - my.boot.isDevVM = true; - }) - ]; - }; - in { - options.my = with lib.types; { - boot.isDevVM = mkBoolOpt false; - build = options.system.build; - asDevVM = mkOption { - inherit (asDevVM) type; - default = {}; - visible = "shallow"; - }; + asDevVM = extendModules { + # TODO: Hack because this is kinda broken on 21.11 (https://github.com/NixOS/nixpkgs/issues/148343) + specialArgs = { inherit baseModules; }; + modules = [ + "${modulesPath}/virtualisation/qemu-vm.nix" + ({ ... }: { + my.boot.isDevVM = true; + }) + ]; + }; +in +{ + options.my = with lib.types; { + boot.isDevVM = mkBoolOpt false; + build = options.system.build; + asDevVM = mkOption { + inherit (asDevVM) type; + default = { }; + visible = "shallow"; }; + }; - config.my.build = { - devVM = config.my.asDevVM.system.build.vm; - }; - } + config.my.build = { + devVM = config.my.asDevVM.system.build.vm; + }; +} diff --git a/modules/common.nix b/modules/common.nix index 22c81b7..d4ba558 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -1,93 +1,95 @@ { lib, pkgs, inputs, system, config, options, ... }: - let - inherit (lib) mkIf mkDefault mkAliasDefinitions; - inherit (lib.my) mkOpt; - in { - options.my = with lib.types; { - user = mkOpt (attrsOf anything) {}; - }; +let + inherit (lib) mkIf mkDefault mkAliasDefinitions; + inherit (lib.my) mkOpt; +in +{ + options.my = with lib.types; { + user = mkOpt (attrsOf anything) { }; + }; - config = - let - defaultUsername = "dev"; - uname = config.my.user.name; - in { - my = { - user = { - name = mkDefault defaultUsername; - isNormalUser = true; - uid = mkDefault 1000; - extraGroups = mkDefault [ "wheel" ]; - password = mkDefault "hunter2"; # TODO: secrets... - }; - }; - - time.timeZone = mkDefault "Europe/Dublin"; - - users = { - mutableUsers = false; - users.${uname} = mkAliasDefinitions options.my.user; - }; - - security = { - sudo.enable = mkDefault false; - doas = { - enable = mkDefault true; - wheelNeedsPassword = mkDefault false; - }; - }; - - nix = { - package = inputs.nix.defaultPackage.${system}; - extraOptions = - '' - experimental-features = nix-command flakes ca-derivations - ''; - }; - nixpkgs = { - config = { - allowUnfree = true; - }; - }; - - boot = { - # Use latest LTS release by default - kernelPackages = mkDefault pkgs.linuxKernel.packages.linux_5_15; - loader = { - efi = { - efiSysMountPoint = mkDefault "/boot"; - canTouchEfiVariables = mkDefault false; - }; - systemd-boot = { - enable = mkDefault true; - editor = mkDefault true; - consoleMode = mkDefault "max"; - configurationLimit = mkDefault 10; - memtest86.enable = mkDefault true; - }; - }; - }; - - networking = { - useDHCP = mkDefault false; - enableIPv6 = mkDefault true; - }; - - environment.systemPackages = with pkgs; [ - bash-completion - tree - vim - htop - iperf3 - ]; - - services.openssh = { - enable = true; - }; - - system = { - stateVersion = "21.11"; - configurationRevision = with inputs; mkIf (self ? rev) self.rev; + config = + let + defaultUsername = "dev"; + uname = config.my.user.name; + in + { + my = { + user = { + name = mkDefault defaultUsername; + isNormalUser = true; + uid = mkDefault 1000; + extraGroups = mkDefault [ "wheel" ]; + password = mkDefault "hunter2"; # TODO: secrets... }; }; - } + + time.timeZone = mkDefault "Europe/Dublin"; + + users = { + mutableUsers = false; + users.${uname} = mkAliasDefinitions options.my.user; + }; + + security = { + sudo.enable = mkDefault false; + doas = { + enable = mkDefault true; + wheelNeedsPassword = mkDefault false; + }; + }; + + nix = { + package = inputs.nix.defaultPackage.${system}; + extraOptions = + '' + experimental-features = nix-command flakes ca-derivations + ''; + }; + nixpkgs = { + config = { + allowUnfree = true; + }; + }; + + boot = { + # Use latest LTS release by default + kernelPackages = mkDefault pkgs.linuxKernel.packages.linux_5_15; + loader = { + efi = { + efiSysMountPoint = mkDefault "/boot"; + canTouchEfiVariables = mkDefault false; + }; + systemd-boot = { + enable = mkDefault true; + editor = mkDefault true; + consoleMode = mkDefault "max"; + configurationLimit = mkDefault 10; + memtest86.enable = mkDefault true; + }; + }; + }; + + networking = { + useDHCP = mkDefault false; + enableIPv6 = mkDefault true; + }; + + environment.systemPackages = with pkgs; [ + bash-completion + tree + vim + htop + iperf3 + ]; + + services.openssh = { + enable = true; + }; + + system = { + stateVersion = "21.11"; + configurationRevision = with inputs; mkIf (self ? rev) self.rev; + }; + }; +} diff --git a/modules/dynamic-motd.nix b/modules/dynamic-motd.nix index a05d1d6..21da418 100644 --- a/modules/dynamic-motd.nix +++ b/modules/dynamic-motd.nix @@ -1,24 +1,25 @@ { lib, pkgs, config, ... }: - let - inherit (lib) optionalAttrs filterAttrs genAttrs mkIf mkDefault; - inherit (lib.my) mkOpt mkBoolOpt; +let + inherit (lib) optionalAttrs filterAttrs genAttrs mkIf mkDefault; + inherit (lib.my) mkOpt mkBoolOpt; - cfg = config.my.dynamic-motd; + cfg = config.my.dynamic-motd; - scriptBin = pkgs.writeShellScript "dynamic-motd-script" cfg.script; - in { - options.my.dynamic-motd = with lib.types; { - enable = mkBoolOpt true; - services = mkOpt (listOf str) [ "login" "ssh" ]; - script = mkOpt (nullOr lines) null; - }; + scriptBin = pkgs.writeShellScript "dynamic-motd-script" cfg.script; +in +{ + options.my.dynamic-motd = with lib.types; { + enable = mkBoolOpt true; + services = mkOpt (listOf str) [ "login" "ssh" ]; + script = mkOpt (nullOr lines) null; + }; - config = mkIf (cfg.enable && cfg.script != null) { - security.pam.services = genAttrs cfg.services (s: { - text = mkDefault - '' - session optional ${pkgs.pam}/lib/security/pam_exec.so stdout quiet ${scriptBin} - ''; - }); - }; - } + config = mkIf (cfg.enable && cfg.script != null) { + security.pam.services = genAttrs cfg.services (s: { + text = mkDefault + '' + session optional ${pkgs.pam}/lib/security/pam_exec.so stdout quiet ${scriptBin} + ''; + }); + }; +} diff --git a/modules/firewall.nix b/modules/firewall.nix index 4310ea3..8f1dc99 100644 --- a/modules/firewall.nix +++ b/modules/firewall.nix @@ -1,166 +1,167 @@ { lib, options, config, ... }: - let - inherit (lib) optionalString concatStringsSep concatMapStringsSep optionalAttrs mkIf mkDefault mkMerge mkOverride; - inherit (lib.my) parseIPPort mkOpt mkBoolOpt dummyOption; +let + inherit (lib) optionalString concatStringsSep concatMapStringsSep optionalAttrs mkIf mkDefault mkMerge mkOverride; + inherit (lib.my) parseIPPort mkOpt mkBoolOpt dummyOption; - cfg = config.my.firewall; - in { - options.my.firewall = with lib.types; { - enable = mkBoolOpt true; - trustedInterfaces = options.networking.firewall.trustedInterfaces; - tcp = { - allowed = mkOpt (listOf (either port str)) [ "ssh" ]; - }; - udp = { - allowed = mkOpt (listOf (either port str)) []; - }; - extraRules = mkOpt lines ""; - - nat = with options.networking.nat; { - enable = mkBoolOpt true; - inherit externalInterface forwardPorts; - }; + cfg = config.my.firewall; +in +{ + options.my.firewall = with lib.types; { + enable = mkBoolOpt true; + trustedInterfaces = options.networking.firewall.trustedInterfaces; + tcp = { + allowed = mkOpt (listOf (either port str)) [ "ssh" ]; }; + udp = { + allowed = mkOpt (listOf (either port str)) [ ]; + }; + extraRules = mkOpt lines ""; - config = mkIf cfg.enable (mkMerge [ - { - networking = { - firewall.enable = false; - nftables = { - enable = true; - ruleset = - let - trusted' = "{ ${concatStringsSep ", " cfg.trustedInterfaces} }"; - in - '' - table inet filter { - chain wan-tcp { - ${concatMapStringsSep "\n " (p: "tcp dport ${toString p} accept") cfg.tcp.allowed} - } - chain wan-udp { - ${concatMapStringsSep "\n " (p: "udp dport ${toString p} accept") cfg.udp.allowed} - } + nat = with options.networking.nat; { + enable = mkBoolOpt true; + inherit externalInterface forwardPorts; + }; + }; - chain wan { - ip6 nexthdr icmpv6 icmpv6 type { - destination-unreachable, - packet-too-big, - time-exceeded, - parameter-problem, - mld-listener-query, - mld-listener-report, - mld-listener-reduction, - nd-router-solicit, - nd-router-advert, - nd-neighbor-solicit, - nd-neighbor-advert, - ind-neighbor-solicit, - ind-neighbor-advert, - mld2-listener-report, - echo-request - } accept - ip protocol icmp icmp type { - destination-unreachable, - router-solicitation, - router-advertisement, - time-exceeded, - parameter-problem, - echo-request - } accept - ip protocol igmp accept - - ip protocol tcp tcp flags & (fin|syn|rst|ack) == syn ct state new jump wan-tcp - ip protocol udp ct state new jump wan-udp - } - - chain input { - type filter hook input priority 0; policy drop; - - ct state established,related accept - ct state invalid drop - - iif lo accept - ${optionalString (cfg.trustedInterfaces != []) "iifname ${trusted'} accept\n"} - jump wan - } - chain forward { - type filter hook forward priority 0; policy drop; - ${optionalString (cfg.trustedInterfaces != []) "\n iifname ${trusted'} accept\n"} - ct state related,established accept - } - chain output { - type filter hook output priority 0; policy accept; - } + config = mkIf cfg.enable (mkMerge [ + { + networking = { + firewall.enable = false; + nftables = { + enable = true; + ruleset = + let + trusted' = "{ ${concatStringsSep ", " cfg.trustedInterfaces} }"; + in + '' + table inet filter { + chain wan-tcp { + ${concatMapStringsSep "\n " (p: "tcp dport ${toString p} accept") cfg.tcp.allowed} + } + chain wan-udp { + ${concatMapStringsSep "\n " (p: "udp dport ${toString p} accept") cfg.udp.allowed} } - table nat { - chain prerouting { - type nat hook prerouting priority 0; - } + chain wan { + ip6 nexthdr icmpv6 icmpv6 type { + destination-unreachable, + packet-too-big, + time-exceeded, + parameter-problem, + mld-listener-query, + mld-listener-report, + mld-listener-reduction, + nd-router-solicit, + nd-router-advert, + nd-neighbor-solicit, + nd-neighbor-advert, + ind-neighbor-solicit, + ind-neighbor-advert, + mld2-listener-report, + echo-request + } accept + ip protocol icmp icmp type { + destination-unreachable, + router-solicitation, + router-advertisement, + time-exceeded, + parameter-problem, + echo-request + } accept + ip protocol igmp accept - chain postrouting { - type nat hook postrouting priority 100; - } + ip protocol tcp tcp flags & (fin|syn|rst|ack) == syn ct state new jump wan-tcp + ip protocol udp ct state new jump wan-udp } - ${cfg.extraRules} - ''; - }; + chain input { + type filter hook input priority 0; policy drop; + + ct state established,related accept + ct state invalid drop + + iif lo accept + ${optionalString (cfg.trustedInterfaces != []) "iifname ${trusted'} accept\n"} + jump wan + } + chain forward { + type filter hook forward priority 0; policy drop; + ${optionalString (cfg.trustedInterfaces != []) "\n iifname ${trusted'} accept\n"} + ct state related,established accept + } + chain output { + type filter hook output priority 0; policy accept; + } + } + + table nat { + chain prerouting { + type nat hook prerouting priority 0; + } + + chain postrouting { + type nat hook postrouting priority 100; + } + } + + ${cfg.extraRules} + ''; }; - } - (mkIf cfg.nat.enable { - assertions = [ - { - assertion = (cfg.nat.forwardPorts != []) -> (cfg.nat.externalInterface != null); - message = "my.firewall.nat.forwardPorts requires my.firewall.nat.externalInterface"; + }; + } + (mkIf cfg.nat.enable { + assertions = [ + { + assertion = (cfg.nat.forwardPorts != [ ]) -> (cfg.nat.externalInterface != null); + message = "my.firewall.nat.forwardPorts requires my.firewall.nat.externalInterface"; + } + ]; + + # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix + boot = { + kernel.sysctl = { + "net.ipv4.conf.all.forwarding" = mkOverride 99 true; + "net.ipv4.conf.default.forwarding" = mkOverride 99 true; + } // optionalAttrs config.networking.enableIPv6 { + # Do not prevent IPv6 autoconfiguration. + # See . + "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; + "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; + + # Forward IPv6 packets. + "net.ipv6.conf.all.forwarding" = mkOverride 99 true; + "net.ipv6.conf.default.forwarding" = mkOverride 99 true; + }; + }; + + my.firewall.extraRules = + let + makeFilter = f: + let + ipp = parseIPPort f.destination; + in + "ip${optionalString ipp.v6 "6"} daddr ${ipp.ip} ${f.proto} dport ${toString f.sourcePort} accept"; + makeForward = f: "${f.proto} dport ${toString f.sourcePort} dnat to ${f.destination}"; + in + '' + table inet filter { + chain filter-port-forwards { + ${concatMapStringsSep "\n " makeFilter cfg.nat.forwardPorts} + } + chain forward { + iifname ${cfg.nat.externalInterface} jump filter-port-forwards + } } - ]; - # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix - boot = { - kernel.sysctl = { - "net.ipv4.conf.all.forwarding" = mkOverride 99 true; - "net.ipv4.conf.default.forwarding" = mkOverride 99 true; - } // optionalAttrs config.networking.enableIPv6 { - # Do not prevent IPv6 autoconfiguration. - # See . - "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; - "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; - - # Forward IPv6 packets. - "net.ipv6.conf.all.forwarding" = mkOverride 99 true; - "net.ipv6.conf.default.forwarding" = mkOverride 99 true; - }; - }; - - my.firewall.extraRules = - let - makeFilter = f: - let - ipp = parseIPPort f.destination; - in - "ip${optionalString ipp.v6 "6"} daddr ${ipp.ip} ${f.proto} dport ${toString f.sourcePort} accept"; - makeForward = f: "${f.proto} dport ${toString f.sourcePort} dnat to ${f.destination}"; - in - '' - table inet filter { - chain filter-port-forwards { - ${concatMapStringsSep "\n " makeFilter cfg.nat.forwardPorts} - } - chain forward { - iifname ${cfg.nat.externalInterface} jump filter-port-forwards - } + table nat { + chain port-forward { + ${concatMapStringsSep "\n " makeForward cfg.nat.forwardPorts} } - - table nat { - chain port-forward { - ${concatMapStringsSep "\n " makeForward cfg.nat.forwardPorts} - } - chain prerouting { - iifname ${cfg.nat.externalInterface} jump port-forward - } + chain prerouting { + iifname ${cfg.nat.externalInterface} jump port-forward } - ''; - }) - ]); - } + } + ''; + }) + ]); +} diff --git a/modules/server.nix b/modules/server.nix index 5e7f58e..36615e5 100644 --- a/modules/server.nix +++ b/modules/server.nix @@ -1,10 +1,11 @@ { config, lib, ... }: - let - inherit (lib) mkIf; - inherit (lib.my) mkBoolOpt; - in { - options.my.server.enable = mkBoolOpt false; - config = mkIf config.my.server.enable { - services.getty.autologinUser = config.my.user.name; - }; - } +let + inherit (lib) mkIf; + inherit (lib.my) mkBoolOpt; +in +{ + options.my.server.enable = mkBoolOpt false; + config = mkIf config.my.server.enable { + services.getty.autologinUser = config.my.user.name; + }; +} diff --git a/modules/tmproot.nix b/modules/tmproot.nix index d25bd87..8f9f46b 100644 --- a/modules/tmproot.nix +++ b/modules/tmproot.nix @@ -1,205 +1,206 @@ { lib, pkgs, inputs, config, ... }: - let - inherit (builtins) elem; - inherit (lib) concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkForce mkVMOverride; - inherit (lib.my) mkOpt mkBoolOpt mkVMOverride' dummyOption; +let + inherit (builtins) elem; + inherit (lib) concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkForce mkVMOverride; + inherit (lib.my) mkOpt mkBoolOpt mkVMOverride' dummyOption; - cfg = config.my.tmproot; + cfg = config.my.tmproot; - showUnsaved = - '' - #!${pkgs.python310}/bin/python - import stat - import sys - import os + showUnsaved = + '' + #!${pkgs.python310}/bin/python + import stat + import sys + import os - ignored = [ - ${concatStringsSep ",\n " (map (p: "'${p}'") cfg.unsaved.ignore)} - ] + ignored = [ + ${concatStringsSep ",\n " (map (p: "'${p}'") cfg.unsaved.ignore)} + ] - base = '/' - base_dev = os.stat(base).st_dev + base = '/' + base_dev = os.stat(base).st_dev - def recurse(p, link=None): - try: - for ignore in ignored: - if p.startswith(ignore): - return - - st = os.lstat(p) - if st.st_dev != base_dev: + def recurse(p, link=None): + try: + for ignore in ignored: + if p.startswith(ignore): return - if stat.S_ISLNK(st.st_mode): - target = os.path.realpath(p, strict=False) - if os.access(target, os.F_OK): - recurse(target, link=p) - return - elif stat.S_ISDIR(st.st_mode): - for e in os.listdir(p): - recurse(os.path.join(p, e)) + st = os.lstat(p) + if st.st_dev != base_dev: + return + + if stat.S_ISLNK(st.st_mode): + target = os.path.realpath(p, strict=False) + if os.access(target, os.F_OK): + recurse(target, link=p) return + elif stat.S_ISDIR(st.st_mode): + for e in os.listdir(p): + recurse(os.path.join(p, e)) + return - print(link or p) - except PermissionError as ex: - print(f'{p}: {ex.strerror}', file=sys.stderr) + print(link or p) + except PermissionError as ex: + print(f'{p}: {ex.strerror}', file=sys.stderr) - recurse(base) - ''; + recurse(base) + ''; - rootDef = { - device = "yeet"; - fsType = "tmpfs"; - options = [ "size=${cfg.size}" ]; + rootDef = { + device = "yeet"; + fsType = "tmpfs"; + options = [ "size=${cfg.size}" ]; + }; +in +{ + imports = [ inputs.impermanence.nixosModule ]; + + options = with lib.types; { + my.tmproot = { + enable = mkBoolOpt true; + persistDir = mkOpt str "/persist"; + size = mkOpt str "2G"; + unsaved = { + showMotd = mkBoolOpt true; + ignore = mkOpt (listOf str) [ ]; + }; }; - in { - imports = [ inputs.impermanence.nixosModule ]; - options = with lib.types; { - my.tmproot = { - enable = mkBoolOpt true; - persistDir = mkOpt str "/persist"; - size = mkOpt str "2G"; - unsaved = { - showMotd = mkBoolOpt true; - ignore = mkOpt (listOf str) []; - }; + # Forward declare options that won't exist until the VM module is actually imported + virtualisation = { + diskImage = dummyOption; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = config.fileSystems ? "${cfg.persistDir}"; + message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir})."; + } + { + # I mean you probably _could_, but if you're doing tmproot... come on + assertion = !config.users.mutableUsers; + message = "users.mutableUsers is incompatible with tmproot"; + } + ]; + + my.tmproot.unsaved.ignore = [ + "/tmp" + + # setup-etc.pl will create this for us + "/etc/NIXOS" + + # Once mutableUsers is disabled, we should be all clear here + "/etc/passwd" + "/etc/group" + "/etc/shadow" + "/etc/subuid" + "/etc/subgid" + + # Lock file for /etc/{passwd,shadow} + "/etc/.pwd.lock" + + # systemd last updated? I presume they'll get updated on boot... + "/etc/.updated" + "/var/.updated" + + # Specifies obsolete files that should be deleted on activation - we'll never have those! + "/etc/.clean" + ]; + + environment.systemPackages = [ + (pkgs.writeScriptBin "tmproot-unsaved" showUnsaved) + ]; + + # Catch non-existent source directories that are needed for boot (see `pathsNeededForBoot` in + # nixos/lib/util.nix). We do this by monkey-patching the `waitDevice` function that would otherwise hang. + boot.initrd.postDeviceCommands = + '' + ensurePersistSource() { + [ -e "/mnt-root$1" ] && return + echo "Persistent source directory $1 does not exist, creating..." + install -dm "$2" "/mnt-root$1" || fail + } + + _waitDevice() { + local device="$1" + + ${concatMapStringsSep " || \\\n " (d: + let + sourceDir = "${d.persistentStoragePath}${d.directory}"; + in + ''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'') + config.environment.persistence."${cfg.persistDir}".directories} + + waitDevice "$@" + } + + type waitDevice > /dev/null || (echo "waitDevice is missing!"; fail) + alias waitDevice=_waitDevice + ''; + + environment.persistence."${cfg.persistDir}" = { + hideMounts = mkDefault true; + directories = [ + "/var/log" + # In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted + # users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update". + # Also the script runs before impermanence does. + "/var/lib/nixos" + "/var/lib/systemd" + ]; + files = [ + "/etc/machine-id" + ]; }; - # Forward declare options that won't exist until the VM module is actually imported + my.dynamic-motd.script = mkIf cfg.unsaved.showMotd + '' + tmprootUnsaved() { + local count="$(tmproot-unsaved | wc -l)" + [ $count -eq 0 ] && return + + echo + echo -e "\t\e[31;1;4mWarning:\e[0m $count file(s) on / will be lost on shutdown!" + echo -e '\tTo see them, run `tmproot-unsaved` as root.' + echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!' + echo -e '\tOtherwise, they can be ignored by adding to `my.tmproot.unsaved.ignore`.' + echo + } + + tmprootUnsaved + ''; + + fileSystems."/" = rootDef; + virtualisation = { - diskImage = dummyOption; + diskImage = "./.vms/${config.system.name}-persist.qcow2"; }; - }; + } + (mkIf config.services.openssh.enable { + environment.persistence."${cfg.persistDir}".files = + concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys; + }) + (mkIf config.networking.resolvconf.enable { + my.tmproot.unsaved.ignore = [ "/etc/resolv.conf" ]; + }) + (mkIf config.security.doas.enable { + my.tmproot.unsaved.ignore = [ "/etc/doas.conf" ]; + }) + (mkIf config.my.boot.isDevVM { + my.tmproot.unsaved.ignore = [ "/nix" ]; - config = mkIf cfg.enable (mkMerge [ - { - assertions = [ - { - assertion = config.fileSystems ? "${cfg.persistDir}"; - message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir})."; - } - { - # I mean you probably _could_, but if you're doing tmproot... come on - assertion = !config.users.mutableUsers; - message = "users.mutableUsers is incompatible with tmproot"; - } - ]; - - my.tmproot.unsaved.ignore = [ - "/tmp" - - # setup-etc.pl will create this for us - "/etc/NIXOS" - - # Once mutableUsers is disabled, we should be all clear here - "/etc/passwd" - "/etc/group" - "/etc/shadow" - "/etc/subuid" - "/etc/subgid" - - # Lock file for /etc/{passwd,shadow} - "/etc/.pwd.lock" - - # systemd last updated? I presume they'll get updated on boot... - "/etc/.updated" - "/var/.updated" - - # Specifies obsolete files that should be deleted on activation - we'll never have those! - "/etc/.clean" - ]; - - environment.systemPackages = [ - (pkgs.writeScriptBin "tmproot-unsaved" showUnsaved) - ]; - - # Catch non-existent source directories that are needed for boot (see `pathsNeededForBoot` in - # nixos/lib/util.nix). We do this by monkey-patching the `waitDevice` function that would otherwise hang. - boot.initrd.postDeviceCommands = - '' - ensurePersistSource() { - [ -e "/mnt-root$1" ] && return - echo "Persistent source directory $1 does not exist, creating..." - install -dm "$2" "/mnt-root$1" || fail - } - - _waitDevice() { - local device="$1" - - ${concatMapStringsSep " || \\\n " (d: - let - sourceDir = "${d.persistentStoragePath}${d.directory}"; - in - ''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'') - config.environment.persistence."${cfg.persistDir}".directories} - - waitDevice "$@" - } - - type waitDevice > /dev/null || (echo "waitDevice is missing!"; fail) - alias waitDevice=_waitDevice - ''; - - environment.persistence."${cfg.persistDir}" = { - hideMounts = mkDefault true; - directories = [ - "/var/log" - # In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted - # users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update". - # Also the script runs before impermanence does. - "/var/lib/nixos" - "/var/lib/systemd" - ]; - files = [ - "/etc/machine-id" - ]; + fileSystems = mkVMOverride { + "/" = mkVMOverride' rootDef; + # Hijack the "root" device for persistence in the VM + "${cfg.persistDir}" = { + device = config.virtualisation.bootDevice; + neededForBoot = true; }; - - my.dynamic-motd.script = mkIf cfg.unsaved.showMotd - '' - tmprootUnsaved() { - local count="$(tmproot-unsaved | wc -l)" - [ $count -eq 0 ] && return - - echo - echo -e "\t\e[31;1;4mWarning:\e[0m $count file(s) on / will be lost on shutdown!" - echo -e '\tTo see them, run `tmproot-unsaved` as root.' - echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!' - echo -e '\tOtherwise, they can be ignored by adding to `my.tmproot.unsaved.ignore`.' - echo - } - - tmprootUnsaved - ''; - - fileSystems."/" = rootDef; - - virtualisation = { - diskImage = "./.vms/${config.system.name}-persist.qcow2"; - }; - } - (mkIf config.services.openssh.enable { - environment.persistence."${cfg.persistDir}".files = - concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys; - }) - (mkIf config.networking.resolvconf.enable { - my.tmproot.unsaved.ignore = [ "/etc/resolv.conf" ]; - }) - (mkIf config.security.doas.enable { - my.tmproot.unsaved.ignore = [ "/etc/doas.conf" ]; - }) - (mkIf config.my.boot.isDevVM { - my.tmproot.unsaved.ignore = [ "/nix" ]; - - fileSystems = mkVMOverride { - "/" = mkVMOverride' rootDef; - # Hijack the "root" device for persistence in the VM - "${cfg.persistDir}" = { - device = config.virtualisation.bootDevice; - neededForBoot = true; - }; - }; - }) - ]); - } + }; + }) + ]); +} diff --git a/systems.nix b/systems.nix index b1101a7..d8501d0 100644 --- a/systems.nix +++ b/systems.nix @@ -1,35 +1,38 @@ { lib, pkgsFlakes, inputs, modules }: - let - inherit (builtins) attrValues mapAttrs; - inherit (lib) mkDefault; +let + inherit (builtins) attrValues mapAttrs; + inherit (lib) mkDefault; - mkSystem = name: { + mkSystem = + name: { system, nixpkgs ? "unstable", config, }: - let - pkgsFlake = pkgsFlakes.${nixpkgs}; - lib = pkgsFlake.lib; - # TODO: This is mostly yoinked from nixpkgs/flake.nix master (as of 2022/02/11) since 21.11's version has hacky - # vm build stuff that breaks our impl. REMOVE WHEN 22.05 IS OUT! - nixosSystem' = args: - import "${pkgsFlake}/nixos/lib/eval-config.nix" (args // { - modules = args.modules ++ [ { - system.nixos.versionSuffix = - ".${lib.substring 0 8 pkgsFlake.lastModifiedDate}.${pkgsFlake.shortRev}"; - system.nixos.revision = pkgsFlake.rev; - } ]; - }); - in nixosSystem' { - inherit lib system; - specialArgs = { inherit inputs system; }; - modules = attrValues modules ++ [ { networking.hostName = mkDefault name; } config ]; - }; - in mapAttrs mkSystem { - colony = { - system = "x86_64-linux"; - nixpkgs = "stable"; - config = boxes/colony.nix; + let + pkgsFlake = pkgsFlakes.${nixpkgs}; + lib = pkgsFlake.lib; + # TODO: This is mostly yoinked from nixpkgs/flake.nix master (as of 2022/02/11) since 21.11's version has hacky + # vm build stuff that breaks our impl. REMOVE WHEN 22.05 IS OUT! + nixosSystem' = args: + import "${pkgsFlake}/nixos/lib/eval-config.nix" (args // { + modules = args.modules ++ [{ + system.nixos.versionSuffix = + ".${lib.substring 0 8 pkgsFlake.lastModifiedDate}.${pkgsFlake.shortRev}"; + system.nixos.revision = pkgsFlake.rev; + }]; + }); + in + nixosSystem' { + inherit lib system; + specialArgs = { inherit inputs system; }; + modules = attrValues modules ++ [ { networking.hostName = mkDefault name; } config ]; }; - } +in +mapAttrs mkSystem { + colony = { + system = "x86_64-linux"; + nixpkgs = "stable"; + config = boxes/colony.nix; + }; +} diff --git a/util.nix b/util.nix index ab41242..78cf576 100644 --- a/util.nix +++ b/util.nix @@ -1,32 +1,34 @@ { lib }: - let - inherit (builtins) replaceStrings elemAt; - inherit (lib) genAttrs mapAttrs' types mkOption mkOverride; - inherit (lib.flake) defaultSystems; - in rec { - addPrefix = prefix: mapAttrs' (n: v: { name = "${prefix}${n}"; value = v; }); - # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix - isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; - parseIPPort = ipp: - let - v6 = isIPv6 ipp; - matchIP = if v6 then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; - m = builtins.match "${matchIP}:([0-9-]+)" ipp; - checked = v: if m == null then throw "bad ip:ports `${ipp}'" else v; - in { - inherit v6; - ip = checked (elemAt m 0); - ports = checked (replaceStrings ["-"] [":"] (elemAt m 1)); - }; - - mkPkgs = path: args: genAttrs defaultSystems (system: import path (args // { inherit system; })); - - mkOpt = type: default: mkOption { inherit type default; }; - mkBoolOpt = default: mkOption { - inherit default; - type = types.bool; - example = true; +let + inherit (builtins) replaceStrings elemAt; + inherit (lib) genAttrs mapAttrs' types mkOption mkOverride; + inherit (lib.flake) defaultSystems; +in +rec { + addPrefix = prefix: mapAttrs' (n: v: { name = "${prefix}${n}"; value = v; }); + # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix + isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; + parseIPPort = ipp: + let + v6 = isIPv6 ipp; + matchIP = if v6 then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; + m = builtins.match "${matchIP}:([0-9-]+)" ipp; + checked = v: if m == null then throw "bad ip:ports `${ipp}'" else v; + in + { + inherit v6; + ip = checked (elemAt m 0); + ports = checked (replaceStrings ["-"] [":"] (elemAt m 1)); }; - mkVMOverride' = mkOverride 9; - dummyOption = mkOption {}; - } + + mkPkgs = path: args: genAttrs defaultSystems (system: import path (args // { inherit system; })); + + mkOpt = type: default: mkOption { inherit type default; }; + mkBoolOpt = default: mkOption { + inherit default; + type = types.bool; + example = true; + }; + mkVMOverride' = mkOverride 9; + dummyOption = mkOption { }; +}