{ inputs, lib, specialArgs, repositoryModules, registryAssembly, util, }: let mkGuest = name: { config, lib, ... }: let cfg = config.guests.${name}; directoryMounts = lib.filterAttrs (_: mount: mount.directory != null) cfg.mounts; directoryScript = lib.concatStringsSep "\n" ( lib.mapAttrsToList ( _: mount: '' ${config.system.path}/bin/install -d \ ${lib.optionalString (mount.owner != null) "-o ${lib.escapeShellArg (toString mount.owner)}"} \ ${lib.optionalString (mount.group != null) "-g ${lib.escapeShellArg mount.group}"} \ ${lib.optionalString (mount.mode != null) "-m ${lib.escapeShellArg mount.mode}"} \ ${lib.escapeShellArg mount.directory} '' ) directoryMounts ); in { options.guests.${name} = { enable = lib.mkEnableOption "the ${name} guest, run in its own nested container"; vlan = lib.mkOption { type = lib.types.ints.between 1 4094; example = 10; description = '' The tagged VLAN this guest lives on. The guest attaches to its host's `br-vlan` bridge for that VLAN. The id must be one of the host's `modules.network.vlans.ids`. ''; }; mac = lib.mkOption { type = lib.types.str; default = util.deriveMac "${config.networking.hostName}.${name}"; defaultText = lib.literalMD "a stable address derived from the host name and guest option path"; example = "bc:24:11:00:00:01"; description = '' The guest's MAC address on its VLAN, pinned inside the guest by its own networkd. Set it to reuse an existing address so a router's DHCP reservation keeps working. Left unset, a stable address is derived from the host name and guest option path in the locally-administered range. ''; }; address = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "10.23.20.126"; description = '' Expected IP address assigned outside NixOS for this guest. Left null, this guest contributes no address to the address registry. ''; }; reverseProxy = { enable = lib.mkEnableOption "a reverse-proxy route for this guest"; route = lib.mkOption { type = registryAssembly.definitions.route.type; description = "Route registry entry published for this guest."; }; }; mounts = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { directory = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "/srv/media"; description = "Host directory bind-mounted into the guest."; }; file = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "/run/secrets/service-password"; description = "Host file bind-mounted into the guest."; }; readOnly = lib.mkOption { type = lib.types.bool; default = false; description = "Mount the host path read-only inside the guest."; }; owner = lib.mkOption { type = lib.types.nullOr ( lib.types.oneOf [ lib.types.str lib.types.ints.positive ] ); default = null; description = "Owner assigned to the host directory before the guest starts."; }; group = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Group assigned to the host directory before the guest starts."; }; mode = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "0770"; description = "Mode assigned to the host directory before the guest starts."; }; }; } ); default = { }; example = lib.literalExpression '' { "/data/media" = { directory = "/srv/media"; }; "/data/config" = { directory = "/srv/config/jellyfin"; }; } ''; description = '' Host paths bind-mounted into the guest, keyed by the path they appear at inside the guest, so a guest sees exactly the data it should at any granularity — a single folder, a whole pool, or a decrypted secret. Mounts are read-write unless `readOnly` is set. ''; }; interior = lib.mkOption { type = lib.types.deferredModule; default = { }; description = "NixOS module imported into the guest interior."; }; limits = { memory = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "2G"; description = '' Cap on the guest's memory, applied to its unit as `MemoryMax`. Accepts systemd size suffixes such as `512M` or `2G`. Left null, the guest's memory is uncapped. ''; }; cpu = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "150%"; description = '' Cap on the guest's CPU, applied to its unit as `CPUQuota`, where `100%` is one full core. Left null, the guest's CPU is uncapped. ''; }; tasksMax = lib.mkOption { type = lib.types.nullOr lib.types.ints.positive; default = null; example = 512; description = '' Cap on the number of processes and threads the guest may spawn, applied to its unit as `TasksMax`. Left null, the task count is uncapped. ''; }; }; }; config = lib.mkIf cfg.enable { assertions = let hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null; hasCompleteDirectorySettings = mount: mount.owner != null && mount.group != null && mount.mode != null; hasOneSource = mount: (mount.directory != null) != (mount.file != null); mountsWithInvalidSource = lib.attrNames (lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts); fileMountsWithHostSettings = lib.attrNames ( lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts ); directoryMountsWithPartialHostSettings = lib.attrNames ( lib.filterAttrs (_: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount) cfg.mounts ); in [ { assertion = mountsWithInvalidSource == [ ]; message = '' guests.${name} mounts ${lib.concatStringsSep ", " mountsWithInvalidSource} must set exactly one of `directory` or `file`. ''; } { assertion = fileMountsWithHostSettings == [ ]; message = '' guests.${name} file mounts ${lib.concatStringsSep ", " fileMountsWithHostSettings} must not set `owner`, `group`, or `mode`. ''; } { assertion = directoryMountsWithPartialHostSettings == [ ]; message = '' guests.${name} directory mounts ${lib.concatStringsSep ", " directoryMountsWithPartialHostSettings} must set `owner`, `group`, and `mode` together. ''; } { assertion = lib.elem cfg.vlan config.modules.network.vlans.ids; message = '' guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement. ''; } ]; registry.address.${config.networking.hostName}.${name} = lib.mkIf (cfg.address != null) cfg.address; registry.route.${config.networking.hostName}.${name} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route; systemd.services."container@${name}" = let bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device"; in { after = [ bridgeDevice ]; wants = [ bridgeDevice ]; serviceConfig = lib.filterAttrs (_: v: v != null) { MemoryMax = cfg.limits.memory; CPUQuota = cfg.limits.cpu; TasksMax = cfg.limits.tasksMax; }; preStart = lib.mkIf (directoryMounts != { }) directoryScript; }; containers.${name} = { autoStart = true; privateNetwork = lib.mkDefault true; hostBridge = util.bridgeName cfg.vlan; privateUsers = lib.mkDefault "no"; bindMounts = lib.mapAttrs (_: mount: { hostPath = if mount.directory != null then mount.directory else mount.file; isReadOnly = mount.readOnly; }) cfg.mounts; inherit specialArgs; config = { imports = [ inputs.sops-nix.nixosModules.sops inputs.stylix.nixosModules.stylix repositoryModules registryAssembly.module cfg.interior { modules.guest.enable = true; networking.useNetworkd = true; networking.useHostResolvConf = false; systemd.network.networks."20-eth0" = { matchConfig.Name = "eth0"; linkConfig.MACAddress = cfg.mac; networkConfig.DHCP = "yes"; }; } ]; }; }; }; }; in { mkGuests = { root }: let files = util.collectNixFiles root; guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (builtins.readDir root); names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles); in { imports = files ++ map mkGuest names; }; }