├── .gitignore ├── .git-blame-ignore-revs ├── micros ├── modules │ ├── system │ │ ├── boot │ │ │ ├── kernel.nix │ │ │ ├── containers.nix │ │ │ ├── getty.nix │ │ │ ├── stage-2.nix │ │ │ ├── runit │ │ │ │ ├── services.nix │ │ │ │ └── stages.nix │ │ │ ├── stage-2-init.sh │ │ │ ├── stage-1-init.sh │ │ │ └── stage-1.nix │ │ ├── name.nix │ │ ├── activation.nix │ │ └── build.nix │ ├── systemd-compat.nix │ ├── nixpkgs.nix │ ├── hardware │ │ └── firmware.nix │ ├── profiles │ │ └── virtualization │ │ │ ├── qemu-guest.nix │ │ │ └── iso-image.nix │ ├── services │ │ ├── getty.nix │ │ ├── rngd.nix │ │ ├── nix-daemon.nix │ │ └── sshd.nix │ ├── all-modules.nix │ ├── environment.nix │ ├── security │ │ └── pam.nix │ ├── config │ │ ├── system-path.nix │ │ └── users.nix │ ├── nixpkgs-flake.nix │ ├── nix.nix │ ├── virtualisation │ │ └── qemu.nix │ ├── networking │ │ ├── networking.nix │ │ ├── nftables.nix │ │ └── firewall.nix │ └── tasks │ │ └── filesystems.nix └── lib │ └── eval-config.nix ├── .editorconfig ├── flake.lock ├── pkgs ├── ifupdown-ng.nix └── ifupdown-fix-path.patch ├── lib └── default.nix ├── LICENSE.md ├── flake.nix └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Nix build artifacts 2 | result* 3 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Run this command to always ignore formatting commits in `git blame` 2 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 3 | 4 | # Treewide format (2025-01-29) 5 | 0bd66dd455e74328082cfb79d438505cbd1d3095 6 | 7 | -------------------------------------------------------------------------------- /micros/modules/system/boot/kernel.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: { 7 | boot.kernelParams = ["systemConfig=${config.system.build.toplevel}"]; 8 | boot.kernelPackages = lib.mkDefault pkgs.linuxPackages; 9 | } 10 | -------------------------------------------------------------------------------- /micros/modules/system/boot/containers.nix: -------------------------------------------------------------------------------- 1 | {lib, ...}: let 2 | inherit (lib) mkOption; 3 | inherit (lib) types; 4 | in { 5 | options = { 6 | boot.isContainer = mkOption { 7 | type = types.bool; 8 | default = false; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /micros/modules/system/boot/getty.nix: -------------------------------------------------------------------------------- 1 | { 2 | environment.etc = { 3 | "sv/getty-5/run".text = '' 4 | #!/bin/sh 5 | exec /sbin/getty 38400 tty5 linux 6 | ''; 7 | 8 | "sv/getty-5/finish".text = '' 9 | #!/bin/sh 10 | exec utmpset -w tty5 11 | ''; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /micros/modules/systemd-compat.nix: -------------------------------------------------------------------------------- 1 | {lib, ...}: let 2 | inherit (lib) mkOption; 3 | in { 4 | options = { 5 | # TODO, it just silently ignores all systemd services 6 | systemd.services = mkOption {}; 7 | systemd.user = mkOption {}; 8 | systemd.tmpfiles = mkOption {}; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | tab_width = 2 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{lock,diff,patch}] 16 | indent_style = unset 17 | indent_size = unset 18 | insert_final_newline = unset 19 | trim_trailing_whitespace = unset 20 | end_of_line = unset 21 | 22 | -------------------------------------------------------------------------------- /micros/modules/nixpkgs.nix: -------------------------------------------------------------------------------- 1 | { 2 | nixpkgs.overlays = [ 3 | (_final: prev: { 4 | dhcpcd = prev.dhcpcd.override {withUdev = false;}; 5 | procps = prev.procps.override {withSystemd = false;}; 6 | pcslite = prev.pcslite.override {systemdSupport = false;}; 7 | openssh = prev.openssh.override {withFIDO = false;}; 8 | util-linux = prev.util-linux.override { 9 | systemd = null; 10 | systemdSupport = false; 11 | }; 12 | }) 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /micros/modules/hardware/firmware.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | lib, 4 | ... 5 | }: let 6 | inherit (lib) mkOption; 7 | inherit (lib) types; 8 | in { 9 | options = { 10 | hardware.firmware = mkOption { 11 | type = types.listOf types.package; 12 | default = []; 13 | apply = list: 14 | pkgs.buildEnv { 15 | name = "firmware"; 16 | paths = list; 17 | pathsToLink = ["/lib/firmware"]; 18 | ignoreCollisions = true; 19 | }; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /micros/modules/profiles/virtualization/qemu-guest.nix: -------------------------------------------------------------------------------- 1 | { 2 | boot.initrd.kernelModules = ["virtio" "virtio_pci" "virtio_net" "virtio_rng" "virtio_blk" "virtio_console" "af_packet"]; 3 | fileSystems."/" = { 4 | device = "none"; 5 | fsType = "tmpfs"; 6 | neededForBoot = true; 7 | }; 8 | fileSystems."/nix/store" = { 9 | device = "/dev/vda"; 10 | fsType = "auto"; 11 | neededForBoot = true; 12 | }; 13 | services.getty.enable = true; 14 | networking.interfaces = [ 15 | { 16 | name = "eth0"; 17 | } 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /micros/modules/services/getty.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkIf mkEnableOption; 8 | 9 | cfg = config.services.getty; 10 | in { 11 | options = { 12 | services.getty = { 13 | enable = mkEnableOption "getty"; 14 | }; 15 | }; 16 | 17 | config = mkIf cfg.enable { 18 | security.pam.enable = true; 19 | runit.services = { 20 | getty = { 21 | runScript = '' 22 | #!${pkgs.runtimeShell} 23 | echo "Starting getty" 24 | ${pkgs.busybox}/bin/busybox getty -l ${pkgs.shadow}/bin/login 0 /dev/ttyS0 25 | ''; 26 | }; 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1751011381, 6 | "narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /micros/modules/services/rngd.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkIf mkOption mkPackageOption mkEnableOption; 8 | 9 | cfg = config.services.nix-daemon; 10 | in { 11 | options = { 12 | services.rngd = { 13 | enable = mkEnableOption "rngd"; 14 | package = mkPackageOption pkgs "rng-tools" {}; 15 | }; 16 | }; 17 | 18 | config = mkIf cfg.enable { 19 | runit.services = { 20 | rngd = { 21 | runScript = '' 22 | #!${pkgs.runtimeShell} 23 | export PATH=$PATH:${lib.makeBinPath cfg.package} 24 | 25 | echo "Starting rngd" 26 | exec rngd -r /dev/hwrng 27 | ''; 28 | }; 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /micros/modules/system/name.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: let 6 | inherit (lib) mkOption literalExpression; 7 | inherit (lib) types; 8 | in { 9 | options.system.name = mkOption { 10 | type = types.str; 11 | default = 12 | if config.networking.hostName == "" 13 | then "unnamed" 14 | else config.networking.hostName; 15 | defaultText = literalExpression '' 16 | if config.networking.hostName == "" 17 | then "unnamed" 18 | else config.networking.hostName; 19 | ''; 20 | description = '' 21 | The name of the system used in the {option}`system.build.toplevel` derivation. 22 | 23 | That derivation has the following name: 24 | `"nixos-system-''${config.system.name}-''${config.system.nixos.label}"` 25 | ''; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /micros/modules/system/boot/stage-2.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: { 7 | config = { 8 | system.build.bootStage2 = pkgs.replaceVarsWith { 9 | src = ./stage-2-init.sh; 10 | isExecutable = true; 11 | 12 | replacements = { 13 | shell = "${pkgs.bash}/bin/bash"; 14 | systemConfig = null; # replaced in ../activation/top-level.nix 15 | 16 | path = lib.makeBinPath [ 17 | pkgs.coreutils 18 | pkgs.util-linuxMinimal 19 | ]; 20 | 21 | # The Runit executable to be run at the end of the script. 22 | runitExecutable = "${pkgs.runit}/bin/runit"; 23 | 24 | inherit (config.system.build) earlyMountScript; 25 | 26 | postBootCommands = pkgs.writeText "local-cmds" '' 27 | ${config.not-os.postBootCommands} 28 | ''; 29 | }; 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /micros/modules/all-modules.nix: -------------------------------------------------------------------------------- 1 | [ 2 | ./config/users.nix 3 | ./config/system-path.nix 4 | 5 | ./hardware/firmware.nix 6 | 7 | ./security/pam.nix 8 | 9 | ./system/boot/runit/services.nix 10 | ./system/boot/runit/stages.nix 11 | ./system/boot/containers.nix 12 | ./system/boot/getty.nix 13 | ./system/boot/kernel.nix 14 | ./system/boot/stage-1.nix 15 | ./system/boot/stage-2.nix 16 | ./system/activation.nix 17 | ./system/build.nix 18 | ./system/name.nix 19 | 20 | ./services/nix-daemon.nix 21 | ./services/getty.nix 22 | ./services/rngd.nix 23 | ./services/sshd.nix 24 | 25 | ./tasks/filesystems.nix 26 | 27 | ./virtualisation/qemu.nix 28 | 29 | ./environment.nix 30 | ./networking/networking.nix 31 | ./networking/firewall.nix 32 | ./networking/nftables.nix 33 | ./nix.nix 34 | ./nixpkgs.nix 35 | ./nixpkgs-flake.nix 36 | ./systemd-compat.nix 37 | ] 38 | -------------------------------------------------------------------------------- /pkgs/ifupdown-ng.nix: -------------------------------------------------------------------------------- 1 | { 2 | fetchFromGitHub, 3 | stdenv, 4 | libbsd, 5 | iproute2, 6 | ... 7 | }: 8 | stdenv.mkDerivation { 9 | pname = "ifupdown-ng"; 10 | version = "0-unstable-2025-05-31"; 11 | 12 | src = fetchFromGitHub { 13 | owner = "ifupdown-ng"; 14 | repo = "ifupdown-ng"; 15 | rev = "e305296b3d23e56bba92c504e030d8e4b91db403"; 16 | hash = "sha256-psA7HxDS9asUan7wKVeWB9fbL+da+s49wRvTqRQnwP0="; 17 | }; 18 | buildInputs = [libbsd iproute2]; 19 | patches = [ 20 | ./ifupdown-fix-path.patch 21 | ]; 22 | installPhase = '' 23 | runHook preInstall 24 | mkdir -p $out/bin 25 | ${builtins.concatStringsSep "\n" (map (x: "install -D -m755 ${x} $out/bin") ["ifupdown" "ifup" "ifdown" "ifquery" "ifparse" "ifctrstat"])} 26 | mkdir -p $out/usr/libexec/ifupdown-ng 27 | install -D -m755 executor-scripts/linux/* $out/usr/libexec/ifupdown-ng/ 28 | runHook postInstall 29 | ''; 30 | } 31 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | nixpkgs, 3 | micros-lib, 4 | ... 5 | }: 6 | nixpkgs.lib.extend (_: _: { 7 | microsSystem = args: 8 | import micros-lib ( 9 | { 10 | inherit nixpkgs; 11 | 12 | # Allow system to be set modularly in nixpkgs.system. 13 | # We set it to null, to remove the "legacy" entrypoint's 14 | # non-hermetic default. 15 | system = null; 16 | 17 | modules = 18 | args.modules 19 | ++ [ 20 | # This module is injected here since it exposes the nixpkgs self-path in as 21 | # constrained of contexts as possible to avoid more things depending on it and 22 | # introducing unnecessary potential fragility to changes in flakes itself. 23 | # 24 | # See: failed attempt to make pkgs.path not copy when using flakes: 25 | # https://github.com/NixOS/nixpkgs/pull/153594#issuecomment-1023287913 26 | ({config, ...}: { 27 | config.nixpkgs.flake.source = nixpkgs.outPath; 28 | }) 29 | ]; 30 | } 31 | // builtins.removeAttrs args ["modules"] 32 | ); 33 | }) 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | ===================== 4 | 5 | Copyright © `2016-2020` `Michael Bishop` Copyright © `2025` NotAShelf 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the “Software”), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /micros/modules/services/nix-daemon.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkIf mkOption mkPackageOption mkEnableOption; 8 | 9 | cfg = config.services.nix-daemon; 10 | in { 11 | options = { 12 | services.nix-daemon = { 13 | enable = mkEnableOption "nix-daemon"; 14 | package = mkPackageOption pkgs "nix" {}; 15 | }; 16 | }; 17 | 18 | config = mkIf cfg.enable { 19 | runit.services = { 20 | nix-daemon = { 21 | runScript = '' 22 | #!${pkgs.runtimeShell} 23 | 24 | echo "Starting nix-daemon" 25 | ${cfg.package}/bin/nix-daemon 26 | ''; 27 | }; 28 | }; 29 | 30 | environment.etc = { 31 | profile.text = lib.mkAfter '' 32 | export NIX_PATH="nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels" 33 | export CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; 34 | ''; 35 | 36 | "ssl/certs/ca-certificates.crt".source = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; 37 | "ssl/certs/ca-bundle.crt".source = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /micros/modules/system/activation.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkForce; 8 | inherit (lib) stringAfter; 9 | in { 10 | system.activationScripts.users = '' 11 | # dummy to make setup-etc happy 12 | ''; 13 | system.activationScripts.groups = '' 14 | # dummy to make setup-etc happy 15 | ''; 16 | 17 | system.activationScripts.etc = stringAfter ["users" "groups"] config.system.build.etcActivationCommands; 18 | 19 | # Re-apply deprecated var value due to systemd preference in recent nixpkgs 20 | # See https://github.com/NixOS/nixpkgs/commit/59e37267556eb917146ca3110ab7c96905b9ffbd 21 | system.activationScripts.var = mkForce '' 22 | # Various log/runtime directories. 23 | mkdir -p /var/tmp 24 | chmod 1777 /var/tmp 25 | mkdir -p /var/lib 26 | chmod 1777 /var/lib 27 | # Empty, immutable home directory of many system accounts. 28 | mkdir -p /var/empty 29 | # Make sure it's really empty 30 | ${pkgs.e2fsprogs}/bin/chattr -f -i /var/empty || true 31 | find /var/empty -mindepth 1 -delete 32 | chmod 0555 /var/empty 33 | chown root:root /var/empty 34 | ${pkgs.e2fsprogs}/bin/chattr -f +i /var/empty || true 35 | # Link /var/run to tmpfs 36 | ln -sfn /run /var/run 37 | hostname -F /etc/hostname 38 | ''; 39 | } 40 | -------------------------------------------------------------------------------- /micros/modules/environment.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption literalExpression; 8 | inherit (lib) types; 9 | in { 10 | options = { 11 | environment = { 12 | extraInit = mkOption { 13 | default = ""; 14 | type = types.lines; 15 | description = '' 16 | Shell script code called during global environment initialisation 17 | after all variables and profileVariables have been set. 18 | This code is assumed to be shell-independent, which means you should 19 | stick to pure sh without sh word split. 20 | ''; 21 | }; 22 | 23 | binsh = mkOption { 24 | default = "${pkgs.busybox}/bin/ash"; 25 | defaultText = literalExpression ''"''${config.system.build.binsh}/bin/sh"''; 26 | example = literalExpression ''"''${pkgs.dash}/bin/dash"''; 27 | type = types.path; 28 | description = '' 29 | The shell executable that is linked system-wide to `/bin/sh`. 30 | ''; 31 | }; 32 | }; 33 | }; 34 | 35 | config = { 36 | environment.etc = { 37 | bashrc.text = "export PATH=/run/current-system/sw/bin"; 38 | profile.text = "export PATH=/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin"; 39 | 40 | "services".source = pkgs.iana-etc + "/etc/services"; 41 | 42 | group.text = '' 43 | root:x:0: 44 | nixbld:x:30000:nixbld1,nixbld10,nixbld2,nixbld3,nixbld4,nixbld5,nixbld6,nixbld7,nixbld8,nixbld9 45 | ''; 46 | }; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /micros/modules/security/pam.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkEnableOption; 8 | inherit (lib) mkIf; 9 | inherit (lib) types; 10 | 11 | cfg = config.security.pam; 12 | in { 13 | options = { 14 | security = { 15 | pam = { 16 | enable = mkEnableOption "PAM"; 17 | 18 | # TODO: this is a hack. Almost not at all modular, and absolutely not sanitary. 19 | # Ideally we should generate PAM configuration dynamically, without what the 20 | # NixOS module is doing (horrible horrible time for anyone planning to look at it) 21 | loginText = mkOption { 22 | internal = true; 23 | type = types.lines; 24 | default = '' 25 | account required ${pkgs.linux-pam}/lib/security/pam_unix.so # unix (order 10900) 26 | 27 | auth sufficient ${pkgs.linux-pam}/lib/security/pam_unix.so likeauth nullok try_first_pass # unix (order 11600) 28 | password sufficient ${pkgs.linux-pam}/lib/security/pam_unix.so nullok yescrypt # unix (order 10200) 29 | 30 | session required ${pkgs.linux-pam}/lib/security/pam_unix.so # unix (order 10200) 31 | session required ${pkgs.linux-pam}/lib/security/pam_loginuid.so # loginuid (order 10300) 32 | ''; 33 | 34 | description = "PAM configuration to be written at {file}`/etc/pam.d/login."; 35 | }; 36 | }; 37 | }; 38 | }; 39 | 40 | config = mkIf config.security.pam.enable { 41 | environment.etc."pam.d/login".text = cfg.loginText; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 3 | 4 | outputs = { 5 | self, 6 | nixpkgs, 7 | }: let 8 | inherit (self) lib; 9 | 10 | forSupportedSystems = lib.genAttrs ["x86_64-linux" "aarch64-linux" "armv7l-linux"]; 11 | pkgsFor = system: nixpkgs.legacyPackages.${system}; 12 | in { 13 | # FIXME: syslinux is not supported on aarch64-linux, and this breaks 'nix flake show' 14 | # We currently don't have Hydra set up, so it's safe to comment this out for now. 15 | # We'll add this back if we can fix/replace syslinux, or if we choose to filter any 16 | # specific systems. 17 | # hydraJobs = self.packages; 18 | 19 | packages = forSupportedSystems (system: { 20 | iso = 21 | (lib.microsSystem { 22 | modules = [ 23 | ./micros/modules/profiles/virtualization/iso-image.nix 24 | 25 | { 26 | nixpkgs.hostPlatform = {inherit system;}; 27 | } 28 | ]; 29 | }).config.system.build.image; 30 | 31 | qemu = 32 | (lib.microsSystem { 33 | modules = [ 34 | ./micros/modules/profiles/virtualization/qemu-guest.nix 35 | 36 | { 37 | nixpkgs.hostPlatform = {inherit system;}; 38 | } 39 | ]; 40 | }).config.system.build.runvm; 41 | }); 42 | 43 | legacyPackages = forSupportedSystems (system: let 44 | pkgs = pkgsFor.${system}; 45 | in { 46 | ifupdown-ng = pkgs.callPackage ./pkgs/ifupdown-ng.nix {}; 47 | }); 48 | 49 | # Custom library to provide additional utilities for 3rd party consumption. 50 | # Primarily designed to expose `microsSystem` as, e.g., inputs.micros.lib.microsSystem 51 | # for when you are building non-supported platforms on your own accord. 52 | lib = import ./lib { 53 | inherit nixpkgs; 54 | 55 | micros-lib = ./micros/lib/eval-config.nix; 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /micros/lib/eval-config.nix: -------------------------------------------------------------------------------- 1 | { 2 | nixpkgs, 3 | prefix ? [], 4 | baseModules ? import ../modules/all-modules.nix, 5 | modulesLocation ? (builtins.unsafeGetAttrPos "modules" evalConfigArgs).file or null, 6 | lib ? nixpkgs.lib, 7 | modules, 8 | specialArgs ? {}, 9 | ... 10 | } @ evalConfigArgs: let 11 | # Init system agnostic modules that we import to retain compatibility. 12 | # While we could simply assimilate those into MicrOS module system, it 13 | # might be better to *simply import* the files as they are not very 14 | # likely to cause breaking changes. 15 | nixpkgsModules = map (x: "${nixpkgs}/nixos/modules/${x}") [ 16 | "system/etc/etc.nix" 17 | "system/activation/activation-script.nix" 18 | "system/boot/kernel.nix" 19 | "config/sysctl.nix" 20 | "misc/nixpkgs.nix" 21 | "misc/nixpkgs-flake.nix" 22 | "misc/assertions.nix" 23 | "misc/lib.nix" 24 | ]; 25 | 26 | evalModulesMinimal = 27 | (import (nixpkgs + "/nixos/lib") { 28 | inherit lib; 29 | # Implicit use of feature is noted in implementation. 30 | featureFlags.minimalModules = {}; 31 | }) 32 | .evalModules; 33 | 34 | allUserModules = let 35 | # Add the invoking file (or specified modulesLocation) as error message location 36 | # for modules that don't have their own locations; presumably inline modules. 37 | locatedModules = 38 | if modulesLocation == null 39 | then modules 40 | else map (lib.setDefaultModuleLocation modulesLocation) modules; 41 | in 42 | locatedModules; 43 | 44 | # Extra arguments that are useful for constructing a similar configuration. 45 | modulesModule = { 46 | config = { 47 | _module.args = { 48 | inherit noUserModules baseModules modules; 49 | }; 50 | }; 51 | }; 52 | 53 | nixosWithUserModules = noUserModules.extendModules {modules = allUserModules;}; 54 | withExtraAttrs = configuration: 55 | configuration 56 | // { 57 | inherit (configuration._module.args) pkgs; 58 | inherit lib; 59 | extendModules = args: withExtraAttrs (configuration.extendModules args); 60 | }; 61 | 62 | noUserModules = evalModulesMinimal { 63 | inherit prefix; 64 | specialArgs = 65 | {modulesPath = builtins.toString ./.;} // specialArgs; 66 | modules = baseModules ++ nixpkgsModules ++ [modulesModule]; 67 | }; 68 | in 69 | withExtraAttrs nixosWithUserModules 70 | -------------------------------------------------------------------------------- /micros/modules/config/system-path.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | # based heavily on https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/config/system-path.nix 8 | # crosser = if pkgs.stdenv ? cross then (x: if x ? crossDrv then x.crossDrv else x) else (x: x); 9 | let 10 | inherit (lib) mkOption literalExpression; 11 | inherit (lib) types; 12 | 13 | requiredPackages = map (pkg: lib.setPrio ((pkg.meta.priority or lib.meta.defaultPriority) + 3) pkg) [ 14 | pkgs.util-linuxMinimal 15 | pkgs.busybox 16 | pkgs.coreutils 17 | pkgs.iproute2 18 | pkgs.iputils 19 | pkgs.procps 20 | pkgs.bashInteractive 21 | pkgs.kmod 22 | pkgs.dhcpcd 23 | (pkgs.callPackage ../../../pkgs/ifupdown-ng.nix {}) 24 | ]; 25 | in { 26 | options = { 27 | system.path = mkOption { 28 | internal = true; 29 | }; 30 | 31 | environment = { 32 | systemPackages = mkOption { 33 | type = types.listOf types.package; 34 | default = []; 35 | example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]"; 36 | description = '' 37 | The set of packages that appear in {file}`/run/current-system/sw`. 38 | 39 | These packages are automatically available to all users, and are automatically 40 | updated every time you rebuild the system configuration. (The latter is the 41 | main difference with installing them in the default profile, {file}`/nix/var/nix/profiles/default`. 42 | ''; 43 | }; 44 | 45 | pathsToLink = mkOption { 46 | type = types.listOf types.str; 47 | default = []; 48 | example = ["/"]; 49 | description = "List of directories to be symlinked in {file}`/run/current-system/sw`"; 50 | }; 51 | 52 | extraOutputsToInstall = mkOption { 53 | type = types.listOf types.str; 54 | default = []; 55 | example = ["doc" "info" "docdev"]; 56 | description = "List of additional package outputs to be symlinked into {file}`/run/current-system/sw`."; 57 | }; 58 | 59 | extraSetup = mkOption { 60 | type = types.lines; 61 | default = ""; 62 | description = '' 63 | Shell fragments to be run after the system environment has been created. 64 | 65 | This should only be used for things that need to modify the internals of 66 | the environment, e.g. generating MIME caches. The environment being built 67 | can be accessed at `$out`. 68 | ''; 69 | }; 70 | }; 71 | }; 72 | 73 | config = { 74 | environment.systemPackages = requiredPackages; 75 | environment.pathsToLink = ["/bin"]; 76 | system.path = pkgs.buildEnv { 77 | name = "system-path"; 78 | paths = config.environment.systemPackages; 79 | inherit (config.environment) pathsToLink extraOutputsToInstall; 80 | postBuild = '' 81 | # Remove wrapped binaries, they shouldn't be accessible via PATH. 82 | find $out/bin -maxdepth 1 -name ".*-wrapped" -type l -delete 83 | 84 | ${config.environment.extraSetup} 85 | ''; 86 | }; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /micros/modules/profiles/virtualization/iso-image.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | lib, 4 | ... 5 | }: { 6 | # The initrd has to contain any module that might be necessary for 7 | # supporting the most important parts of HW like drives. 8 | boot.initrd.kernelModules = [ 9 | # SATA/PATA support. 10 | "ahci" 11 | 12 | "ata_piix" 13 | 14 | "sata_inic162x" 15 | "sata_nv" 16 | "sata_mv" 17 | "sata_promise" 18 | "sata_qstor" 19 | "sata_sil" 20 | "sata_sil24" 21 | "sata_sis" 22 | "sata_svw" 23 | "sata_sx4" 24 | "sata_uli" 25 | "sata_via" 26 | "sata_vsc" 27 | 28 | "pata_ali" 29 | "pata_amd" 30 | "pata_artop" 31 | "pata_atiixp" 32 | "pata_efar" 33 | "pata_hpt366" 34 | "pata_hpt37x" 35 | "pata_hpt3x2n" 36 | "pata_hpt3x3" 37 | "pata_it8213" 38 | "pata_it821x" 39 | "pata_jmicron" 40 | "pata_marvell" 41 | "pata_mpiix" 42 | "pata_netcell" 43 | "pata_ns87410" 44 | "pata_oldpiix" 45 | "pata_pcmcia" 46 | "pata_pdc2027x" 47 | "pata_qdi" 48 | "pata_rz1000" 49 | "pata_serverworks" 50 | "pata_sil680" 51 | "pata_sis" 52 | "pata_sl82c105" 53 | "pata_triflex" 54 | "pata_via" 55 | "pata_winbond" 56 | 57 | # SCSI support (incomplete). 58 | "3w-9xxx" 59 | "3w-xxxx" 60 | "aic79xx" 61 | "aic7xxx" 62 | "arcmsr" 63 | "hpsa" 64 | 65 | # USB support, especially for booting from USB CD-ROM 66 | # drives. 67 | "uas" 68 | "uhci-hcd" 69 | "ohci-hcd" 70 | "usb-storage" 71 | "hid" 72 | "cdrom" 73 | "sr_mod" 74 | "mc" 75 | "iso9660" 76 | "isofs" 77 | "sg" 78 | "st" 79 | "ch" 80 | "scsi_common" 81 | "scsi_mod" 82 | "ufshcd-core" 83 | "ufshcd-pci" 84 | "ufshcd-pltfrm" 85 | "usb_f_mass_storage" 86 | "g_mass_storage" 87 | "libcomposite" 88 | "mv_udc" 89 | "gr_udc" 90 | "sd_mod" 91 | # SD cards. 92 | "sdhci_pci" 93 | 94 | # NVMe drives 95 | "nvme" 96 | 97 | # Firewire support. Not tested. 98 | "ohci1394" 99 | "sbp2" 100 | 101 | # Virtio (QEMU, KVM etc.) support. 102 | "virtio_net" 103 | "virtio_pci" 104 | "virtio_mmio" 105 | "virtio_blk" 106 | "virtio" 107 | "virtio_scsi" 108 | "virtio_balloon" 109 | "virtio_console" 110 | "af_packet" 111 | # VMware support. 112 | "mptspi" 113 | "vmxnet3" 114 | "vsock" 115 | ]; 116 | fileSystems."/" = { 117 | device = "none"; 118 | fsType = "tmpfs"; 119 | neededForBoot = true; 120 | }; 121 | fileSystems."/iso" = { 122 | device = "/dev/root"; 123 | fsType = "iso9660"; 124 | neededForBoot = true; 125 | }; 126 | fileSystems."/nix/store" = { 127 | device = "/mnt-root/iso/root.squashfs"; 128 | fsType = "auto"; 129 | neededForBoot = true; 130 | }; 131 | services.getty.enable = true; 132 | networking.interfaces = [ 133 | { 134 | name = "eth0"; 135 | } 136 | ]; 137 | networking.firewall.allowedTCPPorts = [22]; 138 | networking.firewall.enable = true; 139 | networking.nftables.enable = true; 140 | services.sshd.enable = true; 141 | } 142 | -------------------------------------------------------------------------------- /micros/modules/config/users.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption types mkMerge mkDefault; 8 | userOpts = { 9 | name, 10 | config, 11 | ... 12 | }: { 13 | options = { 14 | name = mkOption { 15 | type = types.str; 16 | default = ""; 17 | description = "Account Username"; 18 | }; 19 | 20 | uid = mkOption { 21 | type = with types; nullOr int; 22 | default = null; 23 | description = "Account User ID"; 24 | }; 25 | 26 | gid = mkOption { 27 | type = with types; nullOr int; 28 | default = config.uid; 29 | description = "Account group ID"; 30 | }; 31 | 32 | home = mkOption { 33 | type = types.path; 34 | default = "/home/${name}"; 35 | description = "Account home directory"; 36 | }; 37 | 38 | password = mkOption { 39 | type = types.str; 40 | default = "x"; 41 | description = "Hashed account password"; 42 | }; 43 | 44 | shell = mkOption { 45 | type = with types; nullOr (either shellPackage path); 46 | default = "/run/current-system/sw/bin/bash"; 47 | description = "Account login shell"; 48 | }; 49 | 50 | packages = mkOption { 51 | type = types.listOf types.package; 52 | default = []; 53 | description = "User-wide package list"; 54 | }; 55 | }; 56 | 57 | config = mkMerge [ 58 | {name = mkDefault name;} 59 | ]; 60 | }; 61 | in { 62 | options = { 63 | users = mkOption { 64 | default = {}; 65 | type = with types; attrsOf (submodule userOpts); 66 | }; 67 | }; 68 | config = { 69 | users = { 70 | root = { 71 | uid = 0; 72 | password = ""; 73 | home = "/root"; 74 | }; 75 | 76 | micros = { 77 | uid = 1000; 78 | password = ""; 79 | }; 80 | }; 81 | 82 | runit.services = { 83 | user-init = { 84 | runScript = '' 85 | #!${pkgs.runtimeShell} 86 | # Make home directories 87 | ${lib.concatLines (builtins.attrValues (builtins.mapAttrs (name: value: "mkdir -p ${value.home}") config.users))} 88 | ${lib.concatLines (builtins.attrValues (builtins.mapAttrs (name: value: "chown ${toString value.uid}:${toString value.gid} -f -R ${value.home}") config.users))} 89 | exec ${pkgs.runit}/bin/sv pause /etc/service/user-init 90 | ''; 91 | }; 92 | }; 93 | 94 | environment.etc = mkMerge [ 95 | { 96 | passwd.text = lib.concatLines (builtins.attrValues (builtins.mapAttrs (name: value: "${name}:${value.password}:${toString value.uid}:${toString value.gid}::${value.home}:${value.shell}") config.users)); 97 | } 98 | 99 | (lib.mapAttrs' (_: { 100 | packages, 101 | name, 102 | ... 103 | }: { 104 | name = "profiles/per-user/${name}"; 105 | value.source = pkgs.buildEnv { 106 | name = "user-env"; 107 | paths = packages; 108 | }; 109 | }) 110 | config.users) 111 | ]; 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /micros/modules/services/sshd.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkPackageOption mkEnableOption; 8 | inherit (lib) mkIf; 9 | inherit (lib) types; 10 | 11 | # TODO: this needs to be integrated into the systemd module as `services.sshd.settings` 12 | # and generated dynamically. This is neither a good solution, nor a long-term one. We 13 | # would like this to be modular alongside hostKeys options. 14 | sshd_config = pkgs.writeText "sshd_config" '' 15 | Port 22 16 | PidFile /run/sshd.pid 17 | Protocol 2 18 | PermitRootLogin yes 19 | PasswordAuthentication yes 20 | AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} 21 | ${lib.flip lib.concatMapStrings cfg.hostKeys (k: '' 22 | HostKey ${k.path} 23 | '')} 24 | ''; 25 | 26 | cfg = config.services.sshd; 27 | in { 28 | options = { 29 | services.sshd = { 30 | enable = mkEnableOption "sshd"; 31 | package = mkPackageOption pkgs "openssh" {}; 32 | 33 | hostKeys = mkOption { 34 | type = with types; listOf attrs; 35 | default = [ 36 | { 37 | type = "rsa"; 38 | bits = 4096; 39 | path = "/etc/ssh/ssh_host_rsa_key"; 40 | } 41 | { 42 | type = "ed25519"; 43 | path = "/etc/ssh/ssh_host_ed25519_key"; 44 | } 45 | ]; 46 | 47 | description = '' 48 | MicrOS can automatically generate SSH host keys. This option 49 | specifies the path, type and size of each key. See 50 | {manpage}`ssh-keygen(1)` for supported types and sizes. 51 | ''; 52 | }; 53 | 54 | authorizedKeysFiles = mkOption { 55 | type = with types; listOf str; 56 | default = ["%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u"]; 57 | description = '' 58 | Specify the rules for which files to read on the host. 59 | 60 | These are paths relative to the host root file system or home 61 | directories and they are subject to certain token expansion rules. 62 | See `AuthorizedKeysFile` in man `sshd_config` for details. 63 | ''; 64 | }; 65 | }; 66 | }; 67 | 68 | config = mkIf cfg.enable { 69 | runit.services = { 70 | sshd = { 71 | runScript = '' 72 | #!${pkgs.runtimeShell} 73 | echo "Generating Host Keys" 74 | ${lib.strings.concatLines ( 75 | lib.lists.forEach config.services.sshd.hostKeys (value: '' 76 | if [ ! -f ${value.path} ]; 77 | then 78 | ${cfg.package}/bin/ssh-keygen -f ${value.path} -t ${value.type} ${ 79 | if value ? value.bits 80 | then "-b ${builtins.toString value.bits}" 81 | else "" 82 | } 83 | fi 84 | '') 85 | )} 86 | 87 | 88 | echo "Starting sshd" 89 | ${cfg.package}/bin/sshd -D -f ${sshd_config} 90 | ''; 91 | }; 92 | }; 93 | users.sshd = { 94 | shell = "/bin/false"; 95 | uid = 25600; 96 | }; 97 | environment.etc = mkIf cfg.enable { 98 | # TODO: this should be a module option. user = {key = ...; rounds = ...; } or 99 | # something similar 100 | "ssh/authorized_keys.d/root" = { 101 | text = ""; 102 | mode = "0444"; 103 | }; 104 | }; 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /micros/modules/system/boot/runit/services.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkEnableOption; 8 | inherit (lib) mkIf mkMerge mkDefault; 9 | inherit (lib) mapAttrs'; 10 | inherit (lib) types; 11 | 12 | serviceOpts = types.submodule ({ 13 | name, 14 | config, 15 | ... 16 | }: { 17 | options = { 18 | enable = 19 | mkEnableOption '' 20 | Whether to enable the service. If set to `false`, then the service files 21 | in {file}`/etc/service` will not be created. 22 | '' 23 | // {default = true;}; 24 | 25 | name = mkOption { 26 | type = types.str; 27 | description = '' 28 | Name of the service. This will determine the final path of the script 29 | in {file}`/etc/service`. For example, `name = "openssh"` would create 30 | the directory {file}`/etc/openssh` and place appropriate scripts in 31 | the created directory. 32 | ''; 33 | }; 34 | 35 | # TODO: those need descriptions. We should link relevant runit documentation 36 | # if any, and describe the process of execution. For example, can any one of 37 | # those options be omitted? Should be documented. 38 | runScript = mkOption { 39 | type = types.nullOr types.str; 40 | default = null; 41 | description = '' 42 | Script ran on service startup. Creates the {file}`/etc/service//run` file. 43 | Services are ran constantly by default. Use `sv pause ` in the run 44 | script to make the script act as a one-shot. 45 | ''; 46 | }; 47 | 48 | finishScript = mkOption { 49 | type = types.nullOr types.str; 50 | default = null; 51 | description = '' 52 | Script ran on service shutdown. Creates the {file}`/etc/service//finish` file. 53 | Can be undefined. 54 | ''; 55 | }; 56 | 57 | confScript = mkOption { 58 | type = types.nullOr types.str; 59 | default = null; 60 | description = '' 61 | Script which can be sourced by the run script to define variables. 62 | Not used by default, and can be undefined. 63 | ''; 64 | }; 65 | }; 66 | 67 | config = mkMerge [ 68 | {name = mkDefault name;} 69 | ]; 70 | }); 71 | in { 72 | options = { 73 | runit.services = mkOption { 74 | type = types.attrsOf serviceOpts; 75 | default = {}; 76 | }; 77 | }; 78 | 79 | config = { 80 | environment.etc = mkMerge [ 81 | (mapAttrs' (name: value: { 82 | inherit (value) enable; 83 | name = "service/${name}/run"; 84 | value = mkIf (value.runScript != null) { 85 | text = ''${value.runScript}''; 86 | mode = "0755"; 87 | }; 88 | }) 89 | config.runit.services) 90 | 91 | (mapAttrs' (name: value: { 92 | inherit (value) enable; 93 | name = "service/${name}/finish"; 94 | 95 | value = mkIf (value.finishScript != null) { 96 | text = ''${value.finishScript}''; 97 | mode = "0755"; 98 | }; 99 | }) 100 | config.runit.services) 101 | 102 | (mapAttrs' (name: value: { 103 | inherit (value) enable; 104 | name = "service/${name}/conf"; 105 | value = mkIf (value.confScript != null) { 106 | text = ''${value.confScript}''; 107 | mode = "0755"; 108 | }; 109 | }) 110 | config.runit.services) 111 | ]; 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /micros/modules/system/boot/stage-2-init.sh: -------------------------------------------------------------------------------- 1 | #! @shell@ 2 | 3 | systemConfig=@systemConfig@ 4 | 5 | export HOME=/root PATH="@path@" 6 | 7 | if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then 8 | # Process the kernel command line. 9 | for o in $(>>\e[0m" 21 | echo 22 | 23 | # Normally, stage 1 mounts the root filesystem read/writable. 24 | # However, in some environments, stage 2 is executed directly, and the 25 | # root is read-only. So make it writable here. 26 | if [ -z "$container" ]; then 27 | mount -n -o remount,rw none / 28 | fi 29 | fi 30 | 31 | # Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a 32 | # stage 1, we need to do that here. 33 | if [ ! -e /proc/1 ]; then 34 | specialMount() { 35 | local device="$1" 36 | local mountPoint="$2" 37 | local options="$3" 38 | local fsType="$4" 39 | 40 | # We must not overwrite this mount because it's bind-mounted 41 | # from stage 1's /run 42 | if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] && [ "${mountPoint}" = /run ]; then 43 | return 44 | fi 45 | 46 | install -m 0755 -d "$mountPoint" 47 | mount -n -t "$fsType" -o "$options" "$device" "$mountPoint" 48 | } 49 | source @earlyMountScript@ 50 | fi 51 | 52 | if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] || [ ! -c /dev/kmsg ]; then 53 | echo "booting system configuration ${systemConfig}" 54 | else 55 | echo "booting system configuration $systemConfig" >/dev/kmsg 56 | fi 57 | 58 | # Make /nix/store a read-only bind mount to enforce immutability of 59 | # the Nix store. Note that we can't use "chown root:nixbld" here 60 | # because users/groups might not exist yet. 61 | # Silence chown/chmod to fail gracefully on a readonly filesystem 62 | # like squashfs. 63 | chown -f 0:30000 /nix/store 64 | chmod -f 1775 /nix/store 65 | if ! [[ "$(findmnt --noheadings --output OPTIONS /nix/store)" =~ ro(,|$) ]]; then 66 | if [ -z "$container" ]; then 67 | mount --bind /nix/store /nix/store 68 | else 69 | mount --rbind /nix/store /nix/store 70 | fi 71 | mount -o remount,ro,bind /nix/store 72 | fi 73 | 74 | if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then 75 | # Log the script output to /dev/kmsg or /run/log/stage-2-init.log. 76 | # Only at this point are all the necessary prerequisites ready for these commands. 77 | exec {logOutFd}>&1 {logErrFd}>&2 78 | if test -w /dev/kmsg; then 79 | exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do 80 | if test -n "$line"; then 81 | echo "<7>stage-2-init: $line" >/dev/kmsg 82 | fi 83 | done) 2>&1 84 | else 85 | mkdir -p /run/log 86 | exec > >(tee -i /run/log/stage-2-init.log) 2>&1 87 | fi 88 | fi 89 | 90 | # Required by the activation script 91 | install -m 0755 -d /etc 92 | if [ ! -h "/etc/nixos" ]; then 93 | install -m 0755 -d /etc/nixos 94 | fi 95 | 96 | install -m 01777 -d /tmp 97 | 98 | # FIXME: in stage-2.nix systemConfig is set to `null`. This is based on NixOS' 99 | # stage-2 init, which replaces it in top-level.nix. Since we do not (yet) configure 100 | # something akin to top-level.nix in nixpkgs, running this script will halt everything 101 | # and kill the init process. 102 | # 103 | # Run the script that performs all configuration activation that does 104 | # not have to be done at boot time. 105 | echo "running activation script..." 106 | $systemConfig/activate 107 | 108 | # Record the boot configuration. 109 | # ln -sfn "$systemConfig" /run/booted-system 110 | 111 | # Run any user-specified commands. 112 | @shell@ @postBootCommands@ 113 | # Start runit in a clean environment. 114 | echo "starting runit..." 115 | exec @runitExecutable@ 116 | -------------------------------------------------------------------------------- /micros/modules/system/boot/runit/stages.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkPackageOption; 8 | inherit (lib) optionalString; 9 | inherit (lib) types; 10 | 11 | runit-compat = pkgs.symlinkJoin { 12 | name = "runit-compat"; 13 | paths = [ 14 | # Join runit with some additional utility scripts 15 | pkgs.runit 16 | 17 | # Poweroff 18 | (pkgs.writeShellScriptBin "poweroff" '' 19 | exec runit-init 0 20 | '') 21 | 22 | # Reboot 23 | (pkgs.writeShellScriptBin "reboot" '' 24 | exec runit-init 6 25 | '') 26 | ]; 27 | }; 28 | 29 | cfg = config.runit; 30 | in { 31 | options = { 32 | runit = { 33 | package = mkPackageOption pkgs "runit"; 34 | stage-1.script = mkOption { 35 | type = types.lines; 36 | default = '' 37 | #!${pkgs.runtimeShell} 38 | PATH=/run/current-system/sw/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin 39 | 40 | # If /etc/ssh is missing, create it. 41 | [ ! -d /etc/ssh ] && mkdir -p /etc/ssh 42 | 43 | # Link /bin/sh from environment.binsh, defaults to ash from buxybox. 44 | mkdir /bin 45 | ln -s ${config.environment.binsh} /bin/sh 46 | 47 | # Bring network interfaces up 48 | ifup -v -a -E ${(pkgs.callPackage ../../../../../pkgs/ifupdown-ng.nix {})}/usr/libexec/ifupdown-ng 49 | 50 | ${optionalString (config.networking.timeServers != []) '' 51 | # Configure timeservers 52 | ${pkgs.ntp}/bin/ntpdate ${toString config.networking.timeServers} 53 | ''} 54 | 55 | # disable DPMS on tty's 56 | echo -ne "\033[9;0]" > /dev/tty0 57 | 58 | touch /etc/runit/stopit 59 | chmod 0 /etc/runit/stopit 60 | ''; 61 | }; 62 | 63 | stage-2.script = mkOption { 64 | type = types.lines; 65 | default = '' 66 | #!${pkgs.runtimeShell} 67 | cat /proc/uptime 68 | 69 | # Watch the /etc/service directory for files 70 | # used to configure a monitored service. 71 | mkdir -p /etc/service 72 | 73 | PATH=/run/current-system/sw/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin 74 | exec env - PATH=$PATH ${pkgs.runit}/bin/runsvdir -P /etc/service 75 | ''; 76 | }; 77 | 78 | stage-3.script = mkOption { 79 | type = types.lines; 80 | default = '' 81 | #!${pkgs.runtimeShell} 82 | 83 | echo Waiting for services to stop... 84 | ${pkgs.runit}/bin/sv force-stop /etc/service/* 85 | ${pkgs.runit}/bin/sv exit /etc/service/* 86 | 87 | echo Sending TERM signal to processes... 88 | ${pkgs.procps}/bin/pkill --inverse -s0,1 -TERM 89 | sleep 1 90 | 91 | echo Sending KILL signal to processes... 92 | ${pkgs.procps}/bin/pkill --inverse -s0,1 -KILL 93 | 94 | echo Unmounting filesystems, disabling swap... 95 | swapoff -a 96 | umount -r -a -t nosysfs,noproc,nodevtmpfs,notmpfs 97 | 98 | echo Remounting rootfs read-only... 99 | mount -o remount,ro / 100 | sync 101 | ''; 102 | }; 103 | }; 104 | }; 105 | 106 | config = { 107 | environment.systemPackages = [runit-compat]; 108 | environment.etc = { 109 | # Runit has three stages: booting, running and shutdown in runit/ 1,2 and 3 respectively. 110 | # We create each stage manually and link them here. 111 | "runit/1".source = pkgs.writeScript "runit-stage-1" cfg.stage-1.script; 112 | "runit/2".source = pkgs.writeScript "runit-stage-2" cfg.stage-2.script; 113 | "runit/3".source = pkgs.writeScript "runit-stage-3" cfg.stage-3.script; 114 | }; 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /pkgs/ifupdown-fix-path.patch: -------------------------------------------------------------------------------- 1 | diff --git a/executor-scripts/linux/dhcp b/executor-scripts/linux/dhcp 2 | index aff637b..feb6673 100755 3 | --- a/executor-scripts/linux/dhcp 4 | +++ b/executor-scripts/linux/dhcp 5 | @@ -21,13 +21,13 @@ start() { 6 | [ -n "$IF_DHCP_LEASETIME" ] && optargs="$optargs -l $IF_DHCP_LEASETIME" 7 | [ -n "$IF_DHCP_CONFIG" ] && optargs="$optargs -f $IF_DHCP_CONFIG" 8 | [ -n "$IF_DHCP_SCRIPT" ] && optargs="$optargs -c $IF_DHCP_SCRIPT" 9 | - ${MOCK} /sbin/dhcpcd $optargs $IFACE 10 | + ${MOCK} dhcpcd $optargs $IFACE 11 | ;; 12 | dhclient) 13 | optargs=$(eval echo $IF_DHCP_OPTS) 14 | [ -n "$IF_DHCP_CONFIG" ] && optargs="$optargs -cf $IF_DHCP_CONFIG" 15 | [ -n "$IF_DHCP_SCRIPT" ] && optargs="$optargs -sf $IF_DHCP_SCRIPT" 16 | - ${MOCK} /usr/sbin/dhclient -pf /var/run/dhclient.$IFACE.pid $optargs $IFACE 17 | + ${MOCK} dhclient -pf /var/run/dhclient.$IFACE.pid $optargs $IFACE 18 | ;; 19 | udhcpc) 20 | optargs=$(eval echo $IF_UDHCPC_OPTS $IF_DHCP_OPTS) 21 | @@ -36,7 +36,7 @@ start() { 22 | [ -n "$IF_DHCP_CLIENT_ID" ] && optargs="$optargs -x 0x3d:${IF_DHCP_CLIENT_ID}" 23 | [ -n "$IF_DHCP_LEASETIME" ] && optargs="$optargs -x lease:${IF_DHCP_LEASETIME}" 24 | [ -n "$IF_DHCP_SCRIPT" ] && optargs="$optargs -s $IF_DHCP_SCRIPT" 25 | - ${MOCK} /sbin/udhcpc -b -R -p /var/run/udhcpc.$IFACE.pid -i $IFACE $optargs 26 | + ${MOCK} udhcpc -b -R -p /var/run/udhcpc.$IFACE.pid -i $IFACE $optargs 27 | ;; 28 | *) 29 | ;; 30 | diff --git a/executor-scripts/linux/vrf b/executor-scripts/linux/vrf 31 | index ac3668a..3803996 100755 32 | --- a/executor-scripts/linux/vrf 33 | +++ b/executor-scripts/linux/vrf 34 | @@ -11,19 +11,19 @@ kernel_version_48() { 35 | } 36 | 37 | handle_init() { 38 | - ${MOCK} /sbin/ip link $1 $IFACE type vrf table $IF_VRF_TABLE 39 | + ${MOCK} ip link $1 $IFACE type vrf table $IF_VRF_TABLE 40 | if kernel_version_48; then 41 | - ${MOCK} /sbin/ip rule $1 iif $IFACE table $IF_VRF_TABLE 42 | - ${MOCK} /sbin/ip rule $1 oif $IFACE table $IF_VRF_TABLE 43 | + ${MOCK} ip rule $1 iif $IFACE table $IF_VRF_TABLE 44 | + ${MOCK} ip rule $1 oif $IFACE table $IF_VRF_TABLE 45 | fi 46 | } 47 | 48 | handle_member() { 49 | - ${MOCK} /sbin/ip link set $IFACE master $IF_VRF_MEMBER 50 | + ${MOCK} ip link set $IFACE master $IF_VRF_MEMBER 51 | } 52 | 53 | handle_member_off() { 54 | - ${MOCK} /sbin/ip link set $IFACE nomaster 55 | + ${MOCK} ip link set $IFACE nomaster 56 | } 57 | 58 | [ -n "$VERBOSE" ] && set -x 59 | diff --git a/executor-scripts/linux/wifi b/executor-scripts/linux/wifi 60 | index 398c2ac..8cce340 100755 61 | --- a/executor-scripts/linux/wifi 62 | +++ b/executor-scripts/linux/wifi 63 | @@ -46,7 +46,7 @@ generate_config() { 64 | [ -z "$IF_WIFI_SSID" ] && die "wifi-ssid not set" 65 | [ -z "$IF_WIFI_PSK" ] && die "wifi-psk not set" 66 | 67 | - /sbin/wpa_passphrase "$IF_WIFI_SSID" "$IF_WIFI_PSK" >$WIFI_CONFIG_PATH 68 | + wpa_passphrase "$IF_WIFI_SSID" "$IF_WIFI_PSK" >$WIFI_CONFIG_PATH 69 | 70 | [ ! -e "$WIFI_CONFIG_PATH" ] && die "failed to write temporary config: $WIFI_CONFIG_PATH" 71 | } 72 | @@ -66,9 +66,9 @@ start() { 73 | # If there is no config file located at $WIFI_CONFIG_PATH, generate one. 74 | [ ! -e "$WIFI_CONFIG_PATH" ] && generate_config 75 | 76 | - /sbin/wpa_supplicant $WPA_SUPPLICANT_OPTS 77 | + wpa_supplicant $WPA_SUPPLICANT_OPTS 78 | else 79 | - /usr/sbin/iwconfig $IFACE essid -- "$IF_WIFI_SSID" ap any 80 | + iwconfig $IFACE essid -- "$IF_WIFI_SSID" ap any 81 | fi 82 | } 83 | 84 | @@ -102,7 +102,7 @@ stop() { 85 | if use_supplicant; then 86 | stop_wpa_supplicant 87 | else 88 | - /usr/sbin/iwconfig $IFACE essid any 89 | + iwconfig $IFACE essid any 90 | fi 91 | } 92 | 93 | diff --git a/libifupdown/lifecycle.c b/libifupdown/lifecycle.c 94 | index 8bc9526..998c8ee 100644 95 | --- a/libifupdown/lifecycle.c 96 | +++ b/libifupdown/lifecycle.c 97 | @@ -166,7 +166,7 @@ build_environment(char **envp[], const struct lif_execute_opts *opts, const stru 98 | 99 | /* Use sane defaults for PATH */ 100 | if (geteuid() == 0) 101 | - lif_environment_push(envp, "PATH", _PATH_STDPATH); 102 | + lif_environment_push(envp, "PATH", getenv("PATH")); 103 | else 104 | lif_environment_push(envp, "PATH", _PATH_DEFPATH); 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicrOS 2 | 3 | MicrOS is a small, experimental operating system designed for embedded 4 | situations. It is based heavily on NixOS, but compiles down to a microscopic 5 | kernel[^1], an initrd and a ~50mb squashfs. 6 | 7 | [Runit](https://smarden.org/runit/) is used instead of systemd, with some degree 8 | of abstraction over services. This is not as robust as NixOS systemd module, nor 9 | is it in any way portable (i.e., additional init systems are not yet possible) 10 | but it results in a small and fast image for low-resource scenarios, e.g., 11 | embedded development. 12 | 13 | ## Why 14 | 15 | NixOS is great, it fits under most use-cases and it is extremely flexible on 16 | what you might use it for. Unfortunately for us, it is quite _large_ even after 17 | you go out of your way to "debloat" it by applying various overlays that create 18 | a butterfly effect in the module system, breaking things you did not even know 19 | were called by your system. This is not unexpected, as Nixpkgs is quite large, 20 | but it is also very annoying. 21 | 22 | MicrOS aims to be a robust solution to building small, minimal, _and functional_ 23 | systems for various use cases. More specifically, embedded Linux and 24 | containerization. 25 | 26 | ## Building 27 | 28 | Micros is a build system more so than it is a collection of hardware modules. It 29 | should be utilized _primarily_ as a module system to reduce friction in building 30 | images for embedded systems. 31 | 32 | Construct your own system with `lib.microsSystem`. This functions almost 33 | identical to `lib.nixosSystem` that you might be familiar with. 34 | 35 | ```nix 36 | lib.microsSystem { 37 | modules = [ 38 | { 39 | not-os.rpi1 = true; 40 | not-os.rpi2 = true; 41 | 42 | system.build.rpi-firmware = raspi-firmware; 43 | 44 | nixpkgs.hostPlatform = {system = "armv7l-linux";}; 45 | nixpkgs.buildPlatform = {system = "x86_64-linux";}; 46 | 47 | } 48 | ]; 49 | }; 50 | ``` 51 | 52 | Above configuration enough to construct a basic system. You may build different 53 | components of the configuration, available under `config.system.build` to create 54 | different build artifacts for different workflows. 55 | 56 | ## Contributing 57 | 58 | Contributions are always welcome. If you have anything you'd like to see 59 | implemented, and would like to work on implementing it then please create an 60 | issue so that we may figure out next. Please remember to write comments in 61 | particularly complex areas if adding new modules, or refactoring existing 62 | modules. 63 | 64 | You may visit [runit/runscripts](https://smarden.org/runit/runscripts) for 65 | additional services that could be upstreamed to MicrOS. 66 | 67 | ## License 68 | 69 | [not-os]: https://github.com/cleverca22/not-os 70 | 71 | > [!NOTE] 72 | > Work here is based _heavily_ on the awesome [not-os]. I worked on something 73 | > similar until I realized I was duplicating effort for no reason. This 74 | > repository diverges (and will continue to diverge from not-os) in terms of 75 | > module structure, coding conventions and goals on what should and should not 76 | > be provided. In addition to the aggressive repository restructure, I will be 77 | > working to provide more _idiomatic_ Nix code that follows best practices and 78 | > focuses on purity. 79 | 80 | MicrOS is a _soft_-fork of not-os, plain and simple. Any and all work here is 81 | available under the MIT license, following upstream. I do not make any claims on 82 | the code provided here. Please support the original author and contributors. 83 | 84 | The nature of MicrOS does not quite allow for a dependency on nixpkgs due to its 85 | tight integration with systemd. Although, we borrow modules from nixpkgs at 86 | times to avoid duplicating work, or to avoid reinventing the wheel as as square. 87 | A copyright notice is hereby provided that _some_ modules in MicrOS are directly 88 | copied from nixpkgs, also available under the MIT license. 89 | 90 | Please see [LICENSE](LICENSE.md) for details. 91 | 92 | [^1]: For the time being MicrOS attempts to boot the mainline kernel as 93 | development is most steadily pacing on `x86_64-linux` and we would like 94 | minimum amount of rebuilds possible. In the future, likely after there is 95 | some CI infrastructure, alternative kernels aiming at achieving smaller 96 | kernels might become a priority. 97 | -------------------------------------------------------------------------------- /micros/modules/nixpkgs-flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | options, 4 | lib, 5 | pkgs, 6 | ... 7 | }: let 8 | cfg = config.nixpkgs.flake; 9 | in { 10 | options.nixpkgs.flake = { 11 | source = lib.mkOption { 12 | # In newer Nix versions, particularly with lazy trees, outPath of 13 | # flakes becomes a Nix-language path object. We deliberately allow this 14 | # to gracefully come through the interface in discussion with @roberth. 15 | # 16 | # See: https://github.com/NixOS/nixpkgs/pull/278522#discussion_r1460292639 17 | type = lib.types.nullOr (lib.types.either lib.types.str lib.types.path); 18 | 19 | default = null; 20 | defaultText = "if (using nixpkgsFlake.lib.nixosSystem) then self.outPath else null"; 21 | 22 | example = ''builtins.fetchTarball { name = "source"; sha256 = "${lib.fakeHash}"; url = "https://github.com/nixos/nixpkgs/archive/somecommit.tar.gz"; }''; 23 | 24 | description = '' 25 | The path to the nixpkgs sources used to build the system. This is automatically set up to be 26 | the store path of the nixpkgs flake used to build the system if using 27 | `nixpkgs.lib.nixosSystem`, and is otherwise null by default. 28 | 29 | This can also be optionally set if the NixOS system is not built with a flake but still uses 30 | pinned sources: set this to the store path for the nixpkgs sources used to build the system, 31 | as may be obtained by `builtins.fetchTarball`, for example. 32 | 33 | Note: the name of the store path must be "source" due to 34 | . 35 | ''; 36 | }; 37 | 38 | setNixPath = lib.mkOption { 39 | type = lib.types.bool; 40 | 41 | default = cfg.source != null; 42 | defaultText = "config.nixpkgs.flake.source != null"; 43 | 44 | description = '' 45 | Whether to set {env}`NIX_PATH` to include `nixpkgs=flake:nixpkgs` such that `` 46 | lookups receive the version of nixpkgs that the system was built with, in concert with 47 | {option}`nixpkgs.flake.setFlakeRegistry`. 48 | 49 | This is on by default for NixOS configurations built with flakes. 50 | 51 | This makes {command}`nix-build '' -A hello` work out of the box on flake systems. 52 | 53 | Note that this option makes the NixOS closure depend on the nixpkgs sources, which may add 54 | undesired closure size if the system will not have any nix commands run on it. 55 | ''; 56 | }; 57 | 58 | setFlakeRegistry = lib.mkOption { 59 | type = lib.types.bool; 60 | 61 | default = cfg.source != null; 62 | defaultText = "config.nixpkgs.flake.source != null"; 63 | 64 | description = '' 65 | Whether to pin nixpkgs in the system-wide flake registry (`/etc/nix/registry.json`) to the 66 | store path of the sources of nixpkgs used to build the NixOS system. 67 | 68 | This is on by default for NixOS configurations built with flakes. 69 | 70 | This option makes {command}`nix run nixpkgs#hello` reuse dependencies from the system, avoid 71 | refetching nixpkgs, and have a consistent result every time. 72 | 73 | Note that this option makes the NixOS closure depend on the nixpkgs sources, which may add 74 | undesired closure size if the system will not have any nix commands run on it. 75 | ''; 76 | }; 77 | }; 78 | 79 | config = lib.mkIf (cfg.source != null) ( 80 | lib.mkMerge [ 81 | { 82 | assertions = [ 83 | { 84 | assertion = cfg.setNixPath -> cfg.setFlakeRegistry; 85 | message = '' 86 | Setting `nixpkgs.flake.setNixPath` requires that `nixpkgs.flake.setFlakeRegistry` also 87 | be set, since it is implemented in terms of indirection through the flake registry. 88 | ''; 89 | } 90 | ]; 91 | } 92 | 93 | (lib.mkIf cfg.setFlakeRegistry { 94 | nix.registry.nixpkgs.to = lib.mkDefault { 95 | type = "path"; 96 | path = cfg.source; 97 | }; 98 | }) 99 | 100 | (lib.mkIf cfg.setNixPath { 101 | # N.B. This does not include nixos-config in NIX_PATH unlike modules/config/nix-channel.nix 102 | # because we would need some kind of evil shim taking the *calling* flake's self path, 103 | # perhaps, to ever make that work (in order to know where the Nix expr for the system came 104 | # from and how to call it). 105 | nix.nixPath = lib.mkDefault ["nixpkgs=flake:nixpkgs"]; 106 | }) 107 | ] 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /micros/modules/nix.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption literalExpression; 8 | inherit (lib) mkIf mkDefault; 9 | inherit (lib) filterAttrs mapAttrsToList; 10 | inherit (lib) types; 11 | 12 | cfg = config.nix; 13 | in { 14 | options = { 15 | nix = { 16 | enable = mkOption { 17 | type = types.bool; 18 | default = false; # strip Nix from the final closure, we are not anticipating rebuilds 19 | description = '' 20 | Whether to enable Nix. 21 | 22 | Disabling Nix makes the system hard to modify and the Nix programs and configuration 23 | will not be made available by NixOS itself. 24 | ''; 25 | }; 26 | 27 | package = mkOption { 28 | type = types.package; 29 | default = pkgs.nix; 30 | defaultText = literalExpression "pkgs.nix"; 31 | description = '' 32 | This option specifies the Nix package instance to use throughout the system. 33 | ''; 34 | }; 35 | 36 | nixPath = mkOption { 37 | type = types.listOf types.str; 38 | default = []; 39 | description = '' 40 | The default Nix expression search path, used by the Nix 41 | evaluator to look up paths enclosed in angle brackets 42 | (e.g. ``). 43 | ''; 44 | }; 45 | 46 | registry = mkOption { 47 | default = {}; 48 | type = types.attrsOf ( 49 | types.submodule ( 50 | let 51 | referenceAttrs = with types; 52 | attrsOf (oneOf [ 53 | str 54 | int 55 | bool 56 | path 57 | package 58 | ]); 59 | in 60 | { 61 | config, 62 | name, 63 | ... 64 | }: { 65 | options = { 66 | from = mkOption { 67 | type = referenceAttrs; 68 | example = { 69 | type = "indirect"; 70 | id = "nixpkgs"; 71 | }; 72 | description = "The flake reference to be rewritten."; 73 | }; 74 | 75 | to = mkOption { 76 | type = referenceAttrs; 77 | example = { 78 | type = "github"; 79 | owner = "my-org"; 80 | repo = "my-nixpkgs"; 81 | }; 82 | description = "The flake reference {option}`from` is rewritten to."; 83 | }; 84 | 85 | flake = mkOption { 86 | type = types.nullOr types.attrs; 87 | default = null; 88 | example = literalExpression "nixpkgs"; 89 | description = '' 90 | The flake input {option}`from` is rewritten to. 91 | ''; 92 | }; 93 | 94 | exact = mkOption { 95 | type = types.bool; 96 | default = true; 97 | description = '' 98 | Whether the {option}`from` reference needs to match exactly. If set, 99 | a {option}`from` reference like `nixpkgs` does not 100 | match with a reference like `nixpkgs/nixos-20.03`. 101 | ''; 102 | }; 103 | }; 104 | 105 | config = { 106 | from = mkDefault { 107 | type = "indirect"; 108 | id = name; 109 | }; 110 | 111 | to = mkIf (config.flake != null) ( 112 | mkDefault ( 113 | { 114 | type = "path"; 115 | path = config.flake.outPath; 116 | } 117 | // filterAttrs (n: _: n == "lastModified" || n == "rev" || n == "narHash") config.flake 118 | ) 119 | ); 120 | }; 121 | } 122 | ) 123 | ); 124 | description = "A system-wide flake registry."; 125 | }; 126 | }; 127 | }; 128 | 129 | config = mkIf cfg.enable { 130 | environment = { 131 | systemPackages = [cfg.package]; 132 | etc = { 133 | "nix/registry.json".text = builtins.toJSON { 134 | version = 2; 135 | flakes = mapAttrsToList (_: v: {inherit (v) from to exact;}) cfg.registry; 136 | }; 137 | 138 | "nix/nix.conf".source = pkgs.runCommand "nix.conf" {} '' 139 | extraPaths=$(for i in $(cat ${pkgs.writeClosure pkgs.runtimeShell}); do if test -d $i; then echo $i; fi; done) 140 | cat > $out << EOF 141 | build-use-sandbox = true 142 | build-users-group = nixbld 143 | build-sandbox-paths = /bin/sh=${pkgs.runtimeShell} $(echo $extraPaths) 144 | build-max-jobs = 1 145 | build-cores = 4 146 | EOF 147 | ''; 148 | }; 149 | }; 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /micros/modules/virtualisation/qemu.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption literalExpression; 8 | inherit (lib) flip concatMapStrings; 9 | inherit (lib) types; 10 | 11 | cfg = config.virtualisation; 12 | in { 13 | options = { 14 | virtualisation = { 15 | memorySize = mkOption { 16 | type = types.ints.positive; 17 | default = 1024; 18 | description = '' 19 | The memory size in megabytes of the virtual machine. 20 | ''; 21 | }; 22 | 23 | cores = mkOption { 24 | type = types.ints.positive; 25 | default = 1; 26 | description = '' 27 | Specify the number of cores the guest is permitted to use. 28 | The number can be higher than the available cores on the 29 | host system. 30 | ''; 31 | }; 32 | 33 | forwardPorts = mkOption { 34 | type = types.listOf ( 35 | types.submodule { 36 | options.from = mkOption { 37 | type = types.enum [ 38 | "host" 39 | "guest" 40 | ]; 41 | default = "host"; 42 | description = '' 43 | Controls the direction in which the ports are mapped: 44 | 45 | - `"host"` means traffic from the host ports 46 | is forwarded to the given guest port. 47 | - `"guest"` means traffic from the guest ports 48 | is forwarded to the given host port. 49 | ''; 50 | }; 51 | 52 | options.proto = mkOption { 53 | type = types.enum [ 54 | "tcp" 55 | "udp" 56 | ]; 57 | default = "tcp"; 58 | description = "The protocol to forward."; 59 | }; 60 | 61 | options.host.address = mkOption { 62 | type = types.str; 63 | default = ""; 64 | description = "The IPv4 address of the host."; 65 | }; 66 | 67 | options.host.port = mkOption { 68 | type = types.port; 69 | description = "The host port to be mapped."; 70 | }; 71 | 72 | options.guest.address = mkOption { 73 | type = types.str; 74 | default = ""; 75 | description = "The IPv4 address on the guest VLAN."; 76 | }; 77 | 78 | options.guest.port = mkOption { 79 | type = types.port; 80 | description = "The guest port to be mapped."; 81 | }; 82 | } 83 | ); 84 | default = []; 85 | example = literalExpression '' 86 | [ 87 | # forward local port 2222 -> 22, to ssh into the VM 88 | { from = "host"; host.port = 2222; guest.port = 22; } 89 | 90 | # forward local port 80 -> 10.0.2.10:80 in the VLAN 91 | { from = "guest"; 92 | guest.address = "10.0.2.10"; guest.port = 80; 93 | host.address = "127.0.0.1"; host.port = 80; 94 | } 95 | ] 96 | ''; 97 | description = '' 98 | When using the SLiRP user networking (default), this option allows to 99 | forward ports to/from the host/guest. 100 | 101 | ::: {.warning} 102 | If the NixOS firewall on the virtual machine is enabled, you also 103 | have to open the guest ports to enable the traffic between host and 104 | guest. 105 | ::: 106 | 107 | ::: {.note} 108 | Currently QEMU supports only IPv4 forwarding. 109 | ::: 110 | ''; 111 | }; 112 | 113 | networkingOptions = mkOption { 114 | type = types.listOf types.str; 115 | default = []; 116 | example = [ 117 | "-net nic,netdev=user.0,model=virtio" 118 | "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" 119 | ]; 120 | description = '' 121 | Networking-related command-line options that should be passed to qemu. 122 | The default is to use userspace networking (SLiRP). 123 | See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details. 124 | 125 | If you override this option, be advised to keep 126 | `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example) 127 | to keep the default runtime behaviour. 128 | ''; 129 | }; 130 | }; 131 | }; 132 | 133 | config = let 134 | networkOpts = let 135 | forwardingOptions = flip concatMapStrings cfg.forwardPorts ( 136 | { 137 | proto, 138 | from, 139 | host, 140 | guest, 141 | }: 142 | if from == "host" 143 | then 144 | "hostfwd=${proto}:${host.address}:${toString host.port}-" 145 | + "${guest.address}:${toString guest.port}," 146 | else 147 | "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" 148 | + "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}'," 149 | ); 150 | in [ 151 | "-net nic,netdev=user.0,model=virtio" 152 | "-netdev user,id=user.0,${forwardingOptions}" 153 | ]; 154 | 155 | startVM = '' 156 | #! ${pkgs.runtimeShell} 157 | 158 | set -e 159 | 160 | exec ${pkgs.qemu}/bin/qemu-kvm \ 161 | -name ${config.system.name} \ 162 | -m ${toString cfg.memorySize} \ 163 | -smp ${toString cfg.cores} \ 164 | -no-reboot \ 165 | -device virtio-rng-pci \ 166 | -drive index=0,id=drive1,file=${config.system.build.squashfs},readonly=on,media=cdrom,format=raw,if=virtio \ 167 | -kernel ${config.system.build.kernel}/bzImage \ 168 | -initrd ${config.system.build.initialRamdisk}/initrd \ 169 | -nographic \ 170 | ${lib.concatStringsSep " " networkOpts} \ 171 | -append "console=ttyS0 ${toString config.boot.kernelParams} quiet panic=-1" 172 | ''; 173 | in { 174 | system.build.runvm = 175 | pkgs.runCommand "micros-vm" 176 | { 177 | preferLocalBuild = true; 178 | meta.mainProgram = "run-${config.system.name}-vm"; 179 | } 180 | '' 181 | mkdir -p $out/bin 182 | ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm 183 | ''; 184 | }; 185 | } 186 | -------------------------------------------------------------------------------- /micros/modules/system/boot/stage-1-init.sh: -------------------------------------------------------------------------------- 1 | #! @shell@ 2 | 3 | targetRoot=/mnt-root 4 | 5 | info() { 6 | if [[ -n "$verbose" ]]; then 7 | echo "$@" 8 | fi 9 | } 10 | 11 | echo 12 | echo "<<< MicroOS Stage 1 >>>" 13 | echo 14 | 15 | extraUtils="@extraUtils@" 16 | export LD_LIBRARY_PATH=@extraUtils@/lib 17 | export PATH=@extraUtils@/bin 18 | 19 | ln -s @extraUtils@/bin /bin 20 | # hardcoded in util-linux's mount helper search path `/run/wrappers/bin:/run/current-system/sw/bin:/sbin` 21 | ln -s @extraUtils@/bin /sbin 22 | 23 | # Make important directories needed for booting, and mount dev, sys, and proc. 24 | mkdir -p $targetRoot 25 | mkdir -p /proc /sys /dev /etc/udev /tmp /run/ /var/log 26 | echo -n >/etc/fstab 27 | mount -t proc proc /proc 28 | mount -t sysfs none /sys 29 | mount -t devtmpfs devtmpfs /dev/ 30 | 31 | ln -s @modulesClosure@/lib/modules /lib/modules 32 | 33 | # Log the script output to /dev/kmsg or /run/log/stage-1-init.log. 34 | mkdir -p /tmp 35 | mkfifo /tmp/stage-1-init.log.fifo 36 | logOutFd=8 && logErrFd=9 37 | eval "exec $logOutFd>&1 $logErrFd>&2" 38 | if test -w /dev/kmsg; then 39 | tee -i /proc/self/fd/"$logOutFd" stage-1-init: [$(date)] $line" >/dev/kmsg 42 | fi 43 | done & 44 | else 45 | mkdir -p /run/log 46 | tee -i /run/log/stage-1-init.log /tmp/stage-1-init.log.fifo 2>&1 49 | 50 | export sysconfig=/init 51 | for o in $(cat /proc/cmdline); do 52 | case $o in 53 | systemConfig=*) 54 | set -- $( 55 | IFS== 56 | echo $o 57 | ) 58 | sysconfig=$2 59 | ;; 60 | 61 | root=*) 62 | # If a root device is specified on the kernel command 63 | # line, make it available through the symlink /dev/root. 64 | # Recognise LABEL= and UUID= to support UNetbootin. 65 | set -- $( 66 | IFS== 67 | echo $o 68 | ) 69 | if [ $2 = "LABEL" ]; then 70 | root="/dev/disk/by-label/$3" 71 | elif [ $2 = "UUID" ]; then 72 | root="/dev/disk/by-uuid/$3" 73 | else 74 | root=$2 75 | fi 76 | ln -s "$root" /dev/root 77 | ;; 78 | 79 | init=*) 80 | set -- $( 81 | IFS== 82 | echo $o 83 | ) 84 | sysconfig=$2 85 | ;; 86 | 87 | netroot=*) 88 | set -- $( 89 | IFS== 90 | echo $o 91 | ) 92 | mkdir -pv /var/run /var/db 93 | sleep 5 94 | dhcpcd eth0 -c @dhcpHook@ 95 | tftp -g -r "$3" "$2" 96 | root=/root.squashfs 97 | ;; 98 | 99 | boot.trace | debugtrace) 100 | # Show each command. 101 | set -x 102 | ;; 103 | 104 | boot.shell_on_fail) 105 | allowShell=1 106 | ;; 107 | 108 | boot.debug1 | debug1) # stop right away 109 | allowShell=1 110 | fail 111 | ;; 112 | 113 | boot.debug1devices) # stop after loading modules and creating device nodes 114 | allowShell=1 115 | debug1devices=1 116 | ;; 117 | 118 | boot.debug1mounts) # stop after mounting file systems 119 | allowShell=1 120 | debug1mounts=1 121 | ;; 122 | 123 | boot.panic_on_fail | stage1panic=1) 124 | panicOnFail=1 125 | ;; 126 | 127 | copytoram) 128 | copytoram=1 129 | ;; 130 | 131 | findiso=*) 132 | # if an iso name is supplied, try to find the device where 133 | # the iso resides on 134 | set -- $( 135 | IFS== 136 | echo $o 137 | ) 138 | isoPath=$2 139 | ;; 140 | 141 | esac 142 | done 143 | 144 | # Mount special file systems. 145 | specialMount() { 146 | local device="$1" 147 | local mountPoint="$2" 148 | local options="$3" 149 | local fsType="$4" 150 | 151 | mkdir -p "$mountPoint" 152 | mount -n -t "$fsType" -o "$options" "$device" "$mountPoint" 153 | } 154 | 155 | echo "Sourcing early mount script" 156 | source @earlyMountScript@ 157 | 158 | # Set hostid before modules are loaded. 159 | # This is needed by the spl/zfs modules 160 | echo "Setting host ID" 161 | @setHostId@ 162 | 163 | # Load the required kernel modules. 164 | echo @extraUtils@/bin/modprobe >/proc/sys/kernel/modprobe 165 | for i in @kernelModules@; do 166 | info "loading module $(basename $i)..." 167 | modprobe $i 168 | done 169 | 170 | echo "Reached mount script" 171 | @mountScript@ 172 | 173 | mkdir -p $targetRoot/dev 174 | mount -o bind /dev $targetRoot/dev 175 | 176 | mkdir -p /opt/mdev/helpers 177 | 178 | touch /opt/mdev/helpers/storage-device 179 | 180 | chmod 0755 /opt/mdev/helpers/storage-device 181 | 182 | cat @mdevHelper@ >/opt/mdev/helpers/storage-device 183 | 184 | touch /etc/mdev.conf 185 | 186 | cat @mdevRules@ >/etc/mdev.conf 187 | 188 | mdev -d 189 | 190 | echo "Mounting Nix store" 191 | 192 | mkdir -p /mnt/tmp /mnt/run /mnt/var 193 | mount -t tmpfs -o "mode=1777" none /mnt/tmp 194 | mount -t tmpfs -o "mode=755" none /mnt/run 195 | ln -sfn /run /mnt/var/run 196 | 197 | # If we have a path to an iso file, find the iso and link it to /dev/root 198 | if [ -n "$isoPath" ]; then 199 | mkdir -p /findiso 200 | 201 | for delay in 5 10; do 202 | blkid | while read -r line; do 203 | device=$(echo "$line" | sed 's/:.*//') 204 | type=$(echo "$line" | sed 's/.*TYPE="\([^"]*\)".*/\1/') 205 | 206 | mount -t "$type" "$device" /findiso 207 | if [ -e "/findiso$isoPath" ]; then 208 | ln -sf "/findiso$isoPath" /dev/root 209 | break 2 210 | else 211 | umount /findiso 212 | fi 213 | done 214 | 215 | sleep "$delay" 216 | done 217 | fi 218 | # Try to find and mount the root device. 219 | echo "Creating \$targetRoot" 220 | mkdir -p "$targetRoot" || echo "Failed to create target root" 221 | blkid 222 | exec 3<@fsInfo@ 223 | 224 | while read -u 3 mountPoint; do 225 | read -u 3 device 226 | read -u 3 fsType 227 | read -u 3 options 228 | # TODO: Add checks for bind mounts 229 | mkdir -p "$targetRoot$mountPoint" 230 | mount -t "$fsType" "$device" "$targetRoot$mountPoint" 231 | done 232 | 233 | exec 3>&- 234 | # Reset the logging file descriptors. 235 | # Do this just before pkill, which will kill the tee process. 236 | echo "Resetting logging file descriptors." 237 | exec 1>&$logOutFd 2>&$logErrFd 238 | eval "exec $logOutFd>&- $logErrFd>&-" 239 | 240 | # Kill any remaining processes, just to be sure we're not taking any 241 | # with us into stage 2. But keep storage daemons like unionfs-fuse. 242 | # 243 | # Storage daemons are distinguished by an @ in front of their command line: 244 | # https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/ 245 | for pid in $(pgrep -v -f '^@'); do 246 | # Make sure we don't kill kernel processes, see #15226 and: 247 | # http://stackoverflow.com/questions/12213445/identifying-kernel-threads 248 | readlink "/proc/$pid/exe" &>/dev/null || continue 249 | # Try to avoid killing ourselves. 250 | [ $pid -eq $$ ] && continue 251 | kill -9 "$pid" 252 | done 253 | 254 | if test -n "$debug1mounts"; then fail; fi 255 | 256 | # Restore /proc/sys/kernel/modprobe to its original value. 257 | echo @modprobe@ >/proc/sys/kernel/modprobe 258 | 259 | # Defines fail function, giving user a shell in case of emergency. 260 | 261 | fail() { 262 | if [ -n "$panicOnFail" ]; then exit 1; fi 263 | 264 | # If starting stage 2 failed, allow the user to repair the problem 265 | # in an interactive shell. 266 | cat </dev/$console 2>/dev/$console" 290 | elif [ -n "$allowShell" -a "$reply" = i ]; then 291 | echo "Starting interactive shell..." 292 | setsid @shell@ -c "exec @shell@ < /dev/$console >/dev/$console 2>/dev/$console" || fail 293 | elif [ "$reply" = r ]; then 294 | echo "Rebooting..." 295 | reboot -f 296 | else 297 | info "Continuing..." 298 | fi 299 | } 300 | 301 | # Check if stage 2 exists 302 | if [ ! -e "$targetRoot$sysconfig" ]; then 303 | stage2Check=${sysconfig} 304 | while [ "$stage2Check" != "${stage2Check%/*}" ] && [ ! -L "$targetRoot$stage2Check" ]; do 305 | stage2Check=${stage2Check%/*} 306 | done 307 | if [ ! -L "$targetRoot$stage2Check" ]; then 308 | echo "stage 2 init script ($targetRoot$sysconfig) not found" 309 | fail 310 | fi 311 | fi 312 | 313 | # Prepare mountpoints for stage 2 314 | echo "Creating special filesystems in \$targetRoot" 315 | mkdir -m 0755 -p $targetRoot/proc $targetRoot/sys $targetRoot/dev $targetRoot/run 316 | 317 | mount --move /proc $targetRoot/proc 318 | mount --move /sys $targetRoot/sys 319 | mount --move /dev $targetRoot/dev 320 | mount --move /run $targetRoot/run 321 | 322 | # Start stage 2. `switch_root' deletes all files in the ramfs on the 323 | # current root. The path has to be valid in the chroot not outside. 324 | 325 | echo "Stage 1 complete: staging to stage 2" 326 | exec env -i $(type -P switch_root) "$targetRoot" "$sysconfig/init" 327 | 328 | trap 'fail' 0 329 | 330 | fail # should never be reached 331 | -------------------------------------------------------------------------------- /micros/modules/networking/networking.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: let 6 | inherit (lib) mkIf mkOption mkDefault mkMerge; 7 | inherit (lib) types; 8 | interfaceOpts = types.submodule { 9 | options = { 10 | enable = mkOption { 11 | type = types.bool; 12 | default = true; 13 | description = '' 14 | Whether to enable the interface. This determines if the interface will have an entry in the /etc/network/interfaces file. 15 | ''; 16 | }; 17 | name = mkOption { 18 | type = types.str; 19 | description = '' 20 | Name of the interface. This is used in the /etc/network/interfaces file and needs to be set to a valid network interface, e.g. eth0, ens1p0, wlp2s0, etc. 21 | ''; 22 | }; 23 | dhcp = mkOption { 24 | type = types.nullOr types.bool; 25 | description = '' 26 | Whether to use DHCP for the interface. When null (default), DHCP is used if ipv4 has no manually configured addresses. 27 | ''; 28 | default = null; 29 | }; 30 | slaac = mkOption { 31 | type = types.nullOr types.bool; 32 | description = '' 33 | Whether to use SLAAC for configuring IPV6 on the interface. When null (default), SLAAC is used if ipv6 has no manually configured addresses. 34 | ''; 35 | default = null; 36 | }; 37 | ipv4 = { 38 | address = mkOption { 39 | type = with types; nullOr (strMatching "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/(3[0-2]|[1-2]?\d)$"); 40 | description = '' 41 | IPV4 address given to the interface, with the subnet mask. Given as "x.x.x.x/xx". 42 | ''; 43 | default = null; 44 | }; 45 | gateway = mkOption { 46 | type = with types; nullOr (strMatching "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/(3[0-2]|[1-2]?\d)$"); 47 | description = '' 48 | IPV4 address used as the network gateway. Given as "x.x.x.x/xx". 49 | ''; 50 | default = null; 51 | }; 52 | }; 53 | ipv6 = { 54 | address = mkOption { 55 | type = with types; nullOr (strMatching "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/([1-9]|[1-9][0-9]|1[01][0-9]|12[0-8])"); 56 | description = '' 57 | IPV6 address given to the interface, with the subnet mask. 58 | ''; 59 | default = null; 60 | }; 61 | gateway = mkOption { 62 | type = with types; 63 | nullOr (strMatching "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); 64 | description = '' 65 | IPV6 address used as the network gateway. 66 | ''; 67 | default = null; 68 | }; 69 | }; 70 | }; 71 | }; 72 | in { 73 | options = { 74 | networking = { 75 | hostName = mkOption { 76 | default = "micros"; # this defaults to distroId in nixpkgs, which we do not have. 77 | 78 | # Only allow hostnames without the domain name part (i.e. no FQDNs, see 79 | # e.g. "man 5 hostname") and require valid DNS labels (recommended 80 | # syntax). Note: We also allow underscores for compatibility/legacy 81 | # reasons (as undocumented feature): 82 | # https://github.com/NixOS/nixpkgs/pull/138978 83 | type = 84 | types.strMatching 85 | "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$"; 86 | description = '' 87 | The name of the machine. Leave it empty if you want to obtain it from a 88 | DHCP server (if using DHCP). The hostname must be a valid DNS label (see 89 | RFC 1035 section 2.3.1: "Preferred name syntax", RFC 1123 section 2.1: 90 | "Host Names and Numbers") and as such must not contain the domain part. 91 | This means that the hostname must start with a letter or digit, 92 | end with a letter or digit, and have as interior characters only 93 | letters, digits, and hyphen. The maximum length is 63 characters. 94 | Additionally it is recommended to only use lower-case characters. 95 | If (e.g. for legacy reasons) a FQDN is required as the Linux kernel 96 | network node hostname (uname --nodename) the option 97 | boot.kernel.sysctl."kernel.hostname" can be used as a workaround (but 98 | the 64 character limit still applies). 99 | 100 | WARNING: Do not use underscores (_) or you may run into unexpected issues. 101 | ''; 102 | }; 103 | interfaces = mkOption { 104 | type = with types; listOf interfaceOpts; 105 | description = '' 106 | The list of interfaces to configure. By default, all network interfaces detected on startup are brought up with DHCP. Use this to manually configure interfaces and set static IPs. 107 | ''; 108 | default = []; 109 | }; 110 | nameservers = mkOption { 111 | type = with types; listOf (strMatching "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"); 112 | description = '' 113 | The list of nameservers used. This can be overrided by DHCP. Defaults to cloudflare DNS (1.1.1.1, 1.0.0.1). 114 | ''; 115 | default = ["1.1.1.1" "1.0.0.1"]; 116 | }; 117 | hostId = mkOption { 118 | default = null; 119 | example = "4e98920d"; 120 | type = types.nullOr types.str; 121 | description = '' 122 | The 32-bit host ID of the machine, formatted as 8 hexadecimal characters. 123 | 124 | You should try to make this ID unique among your machines. You can 125 | generate a random 32-bit ID using the following commands: 126 | 127 | `head -c 8 /etc/machine-id` 128 | 129 | (this derives it from the machine-id that systemd generates) or 130 | 131 | `head -c4 /dev/urandom | od -A none -t x4` 132 | 133 | The primary use case is to ensure when using ZFS that a pool isn't imported 134 | accidentally on a wrong machine. 135 | ''; 136 | }; 137 | 138 | dhcp = { 139 | enable = mkOption { 140 | type = types.bool; 141 | description = ''Whether to enable DHCP globally. This is overrided by individual interface settings. Defaults to true''; 142 | default = true; 143 | }; 144 | overrideNameservers = mkOption { 145 | type = types.bool; 146 | description = ''Whether to use DHCP nameservers over configured ones. Defaults to false.''; 147 | default = false; 148 | }; 149 | }; 150 | timeServers = mkOption { 151 | type = types.listOf types.str; 152 | default = [ 153 | "0.nixos.pool.ntp.org" 154 | "1.nixos.pool.ntp.org" 155 | "2.nixos.pool.ntp.org" 156 | "3.nixos.pool.ntp.org" 157 | ]; 158 | description = '' 159 | The set of NTP servers from which to synchronise. 160 | ''; 161 | }; 162 | }; 163 | }; 164 | 165 | config = { 166 | environment.etc = { 167 | hostname = mkIf (config.networking.hostName != "") { 168 | text = config.networking.hostName + "\n"; 169 | }; 170 | "resolv.conf".text = ''${lib.strings.concatLines (lib.lists.forEach config.networking.nameservers ( 171 | x: '' 172 | nameserver ${x} 173 | '' 174 | ))}''; 175 | "udhcpc/udhcpc.conf".text = "${ 176 | if config.networking.dhcp.overrideNameservers == false 177 | then "RESOLV_CONF = no" 178 | else "RESOLV_CONF = /etc/resolv.conf" 179 | }"; 180 | "nsswitch.conf".text = '' 181 | hosts: files dns 182 | networks: files dns 183 | ''; 184 | hosts.text = ""; 185 | "network/interfaces".text = '' 186 | auto lo 187 | iface lo inet loopback 188 | ${lib.strings.concatLines (lib.lists.forEach config.networking.interfaces ( 189 | x: '' 190 | auto ${x.name} 191 | ${ 192 | if x.dhcp == true || x.dhcp == null && x.ipv4.address == null 193 | then '' 194 | iface ${x.name} 195 | use dhcp 196 | dhcp-program dhcpcd 197 | '' 198 | else '' 199 | iface ${x.name} inet static 200 | address ${x.ipv4.address} 201 | gateway ${x.ipv4.gateway} 202 | '' 203 | } 204 | ${ 205 | if x.slaac == true || x.slaac == null && x.ipv6.address == null 206 | then "iface ${x.name} inet6 auto" 207 | else '' 208 | iface ${x.name} inet6 static 209 | address ${x.ipv6.address} 210 | gateway ${x.ipv6.gateway} 211 | '' 212 | } 213 | '' 214 | ))} 215 | ''; 216 | }; 217 | }; 218 | } 219 | -------------------------------------------------------------------------------- /micros/modules/system/build.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption; 8 | inherit (lib) types; 9 | inherit (config.boot.kernelPackages) kernel; 10 | in { 11 | options = { 12 | system = { 13 | systemBuilderCommands = mkOption { 14 | type = types.lines; 15 | internal = true; 16 | default = ""; 17 | description = '' 18 | This code will be added to the builder creating the system store path. 19 | ''; 20 | }; 21 | 22 | systemBuilderArgs = mkOption { 23 | type = types.attrsOf types.unspecified; 24 | internal = true; 25 | default = {}; 26 | description = '' 27 | `lib.mkDerivation` attributes that will be passed to the top level system builder. 28 | ''; 29 | }; 30 | }; 31 | }; 32 | 33 | config = { 34 | boot.kernelParams = ["init=${config.system.build.initialRamdisk}/initrd"]; 35 | system.build = { 36 | image = let 37 | efiDir = 38 | pkgs.runCommand "efi-directory" { 39 | nativeBuildInputs = [pkgs.buildPackages.grub2_efi]; 40 | strictDeps = true; 41 | } '' 42 | mkdir -p $out/EFI/BOOT 43 | 44 | # Add a marker so GRUB can find the filesystem. 45 | touch $out/EFI/image 46 | 47 | # ALWAYS required modules. 48 | MODULES=( 49 | # Basic modules for filesystems and partition schemes 50 | "fat" 51 | "iso9660" 52 | "part_gpt" 53 | "part_msdos" 54 | 55 | # Basic stuff 56 | "normal" 57 | "boot" 58 | "linux" 59 | "configfile" 60 | "loopback" 61 | "chain" 62 | "halt" 63 | 64 | # Allows rebooting into firmware setup interface 65 | "efifwsetup" 66 | 67 | # EFI Graphics Output Protocol 68 | "efi_gop" 69 | 70 | # User commands 71 | "ls" 72 | 73 | # System commands 74 | "search" 75 | "search_label" 76 | "search_fs_uuid" 77 | "search_fs_file" 78 | "echo" 79 | 80 | # We're not using it anymore, but we'll leave it in so it can be used 81 | # by user, with the console using "C" 82 | "serial" 83 | 84 | # Graphical mode stuff 85 | "gfxmenu" 86 | "gfxterm" 87 | "gfxterm_background" 88 | "gfxterm_menu" 89 | "test" 90 | "loadenv" 91 | "all_video" 92 | "videoinfo" 93 | 94 | # File types for graphical mode 95 | "png" 96 | ) 97 | 98 | echo "Building GRUB with modules:" 99 | for mod in ''${MODULES[@]}; do 100 | echo " - $mod" 101 | done 102 | 103 | # Modules that may or may not be available per-platform. 104 | echo "Adding additional modules:" 105 | for mod in efi_uga; do 106 | if [ -f ${pkgs.grub2_efi}/lib/grub/${pkgs.grub2_efi.grubTarget}/$mod.mod ]; then 107 | echo " - $mod" 108 | MODULES+=("$mod") 109 | fi 110 | done 111 | 112 | # Make our own efi program, we can't rely on "grub-install" since it seems to 113 | # probe for devices, even with --skip-fs-probe. 114 | grub-mkimage \ 115 | --directory=${pkgs.grub2_efi}/lib/grub/${pkgs.grub2_efi.grubTarget} \ 116 | -o $out/EFI/BOOT/BOOTx64.EFI \ 117 | -p /EFI/BOOT \ 118 | -O ${pkgs.grub2_efi.grubTarget} \ 119 | ''${MODULES[@]} 120 | cp ${pkgs.grub2_efi}/share/grub/unicode.pf2 $out/EFI/BOOT/ 121 | 122 | cat < $out/EFI/BOOT/grub.cfg 123 | 124 | set timeout=-1 125 | search --set=root --file /EFI/image 126 | 127 | insmod gfxterm 128 | insmod png 129 | set gfxpayload=keep 130 | set gfxmode=${lib.concatStringsSep "," [ 131 | "1920x1200" 132 | "1920x1080" 133 | "1366x768" 134 | "1280x800" 135 | "1280x720" 136 | "1200x1920" 137 | "1024x768" 138 | "800x1280" 139 | "800x600" 140 | "auto" 141 | ]} 142 | 143 | if [ "\$textmode" == "false" ]; then 144 | terminal_output gfxterm 145 | terminal_input console 146 | else 147 | terminal_output console 148 | terminal_input console 149 | # Sets colors for console term. 150 | set menu_color_normal=cyan/blue 151 | set menu_color_highlight=white/blue 152 | fi 153 | 154 | clear 155 | # This message will only be viewable on the default (UEFI) console. 156 | echo "" 157 | echo "Loading graphical boot menu..." 158 | echo "" 159 | echo "Press 't' to use the text boot menu on this console..." 160 | echo "" 161 | 162 | 163 | hiddenentry 'Text mode' --hotkey 't' { 164 | loadfont (\$root)/EFI/BOOT/unicode.pf2 165 | set textmode=true 166 | terminal_output console 167 | } 168 | 169 | 170 | # If the parameter iso_path is set, append the findiso parameter to the kernel 171 | # line. We need this to allow the nixos iso to be booted from grub directly. 172 | if [ \''${iso_path} ] ; then 173 | set isoboot="findiso=\''${iso_path}" 174 | fi 175 | 176 | # 177 | # Menu entries 178 | # 179 | 180 | menuentry 'boot' { 181 | terminal_output console 182 | linux /boot/bzImage console=ttyS0 ${toString config.boot.kernelParams} root=LABEL=micros quiet panic=-1 183 | initrd /boot/initrd 184 | } 185 | 186 | menuentry 'Firmware Setup' --class settings { 187 | fwsetup 188 | clear 189 | echo "" 190 | echo "If you see this message, your EFI system doesn't support this feature." 191 | echo "" 192 | } 193 | menuentry 'Shutdown' --class shutdown { 194 | halt 195 | } 196 | EOF 197 | 198 | grub-script-check $out/EFI/BOOT/grub.cfg 199 | 200 | ''; 201 | 202 | efiImg = 203 | pkgs.runCommand "efi-image_eltorito" { 204 | nativeBuildInputs = [pkgs.buildPackages.mtools pkgs.buildPackages.libfaketime pkgs.buildPackages.dosfstools]; 205 | strictDeps = true; 206 | } 207 | # Be careful about determinism: du --apparent-size, 208 | # dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i) 209 | '' 210 | mkdir ./contents && cd ./contents 211 | mkdir -p ./EFI/BOOT 212 | cp -rp "${efiDir}"/EFI/BOOT/{grub.cfg,*.EFI,*.efi} ./EFI/BOOT 213 | 214 | # Rewrite dates for everything in the FS 215 | find . -exec touch --date=2000-01-01 {} + 216 | 217 | # Round up to the nearest multiple of 1MB, for more deterministic du output 218 | usage_size=$(( $(du -s --block-size=1M --apparent-size . | tr -cd '[:digit:]') * 1024 * 1024 )) 219 | # Make the image 110% as big as the files need to make up for FAT overhead 220 | image_size=$(( ($usage_size * 110) / 100 )) 221 | # Make the image fit blocks of 1M 222 | block_size=$((1024*1024)) 223 | image_size=$(( ($image_size / $block_size + 1) * $block_size )) 224 | echo "Usage size: $usage_size" 225 | echo "Image size: $image_size" 226 | truncate --size=$image_size "$out" 227 | mkfs.vfat --invariant -i 12345678 -n EFIBOOT "$out" 228 | 229 | # Force a fixed order in mcopy for better determinism, and avoid file globbing 230 | for d in $(find EFI -type d | sort); do 231 | faketime "2000-01-01 00:00:00" mmd -i "$out" "::/$d" 232 | done 233 | 234 | for f in $(find EFI -type f | sort); do 235 | mcopy -pvm -i "$out" "$f" "::/$f" 236 | done 237 | 238 | # Verify the FAT partition. 239 | fsck.vfat -vn "$out" 240 | ''; # */ 241 | in 242 | pkgs.callPackage (pkgs.path + "/nixos/lib/make-iso9660-image.nix") { 243 | contents = [ 244 | { 245 | source = config.system.build.kernel + "/bzImage"; 246 | target = "/boot/bzImage"; 247 | } 248 | { 249 | source = config.system.build.initialRamdisk + "/initrd"; 250 | target = "/boot/initrd"; 251 | } 252 | { 253 | source = config.system.build.squashfs; 254 | target = "/root.squashfs"; 255 | } 256 | { 257 | source = "${pkgs.syslinux}/share/syslinux"; 258 | target = "/isolinux"; 259 | } 260 | { 261 | source = pkgs.writeText "isolinux.cfg" '' 262 | SERIAL 0 115200 263 | TIMEOUT 35996 264 | 265 | DEFAULT boot 266 | 267 | LABEL boot 268 | MENU LABEL Boot Micros 269 | LINUX /boot/bzImage 270 | APPEND console=ttyS0 root=LABEL=micros init=${config.system.build.initialRamdisk}/initrd ${toString config.boot.kernelParams} 271 | INITRD /boot/initrd 272 | ''; 273 | target = "/isolinux/isolinux.cfg"; 274 | } 275 | { 276 | source = "${efiDir}/EFI"; 277 | target = "/EFI"; 278 | } 279 | { 280 | source = (pkgs.writeTextDir "grub/loopback.cfg" "source /EFI/BOOT/grub.cfg") + "/grub"; 281 | target = "/boot/grub"; 282 | } 283 | { 284 | source = "${efiImg}"; 285 | target = "/boot/efi.img"; 286 | } 287 | ]; 288 | isoName = "micros-image.iso"; 289 | volumeID = "micros"; 290 | bootable = true; 291 | bootImage = "/isolinux/isolinux.bin"; 292 | usbBootable = true; 293 | isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin"; 294 | efiBootable = true; 295 | efiBootImage = "boot/efi.img"; 296 | syslinux = pkgs.syslinux; 297 | }; 298 | 299 | # nix-build -A system.build.toplevel && du -h $(nix-store -qR result) --max=0 -BM|sort -n 300 | toplevel = 301 | pkgs.runCommand "micros-toplevel" { 302 | activationScript = config.system.activationScripts.script; 303 | } '' 304 | mkdir $out 305 | 306 | cp ${config.system.build.bootStage2} $out/init 307 | substituteInPlace $out/init --subst-var-by systemConfig $out 308 | ln -s ${config.system.path} $out/sw 309 | ln -s ${kernel} $out/kernel-modules 310 | echo "$activationScript" > $out/activate 311 | substituteInPlace $out/activate --subst-var out 312 | chmod u+x $out/activate 313 | 314 | unset activationScript 315 | ''; 316 | 317 | squashfs = pkgs.callPackage (pkgs.path + "/nixos/lib/make-squashfs.nix") { 318 | storeContents = [config.system.build.toplevel]; 319 | }; 320 | }; 321 | }; 322 | } 323 | -------------------------------------------------------------------------------- /micros/modules/networking/nftables.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | cfg = config.networking.nftables; 8 | 9 | tableSubmodule = {name, ...}: { 10 | options = { 11 | enable = lib.mkOption { 12 | type = lib.types.bool; 13 | default = true; 14 | description = "Enable this table."; 15 | }; 16 | 17 | name = lib.mkOption { 18 | type = lib.types.str; 19 | description = "Table name."; 20 | }; 21 | 22 | content = lib.mkOption { 23 | type = lib.types.lines; 24 | description = "The table content."; 25 | }; 26 | 27 | family = lib.mkOption { 28 | description = "Table family."; 29 | type = lib.types.enum [ 30 | "ip" 31 | "ip6" 32 | "inet" 33 | "arp" 34 | "bridge" 35 | "netdev" 36 | ]; 37 | }; 38 | }; 39 | 40 | config = { 41 | name = lib.mkDefault name; 42 | }; 43 | }; 44 | in { 45 | ###### interface 46 | 47 | options = { 48 | networking.nftables.enable = lib.mkOption { 49 | type = lib.types.bool; 50 | default = false; 51 | description = '' 52 | Whether to enable nftables and use nftables based firewall if enabled. 53 | nftables is a Linux-based packet filtering framework intended to 54 | replace frameworks like iptables. 55 | 56 | Note that if you have Docker enabled you will not be able to use 57 | nftables without intervention. Docker uses iptables internally to 58 | setup NAT for containers. This module disables the ip_tables kernel 59 | module, however Docker automatically loads the module. Please see 60 | 61 | for more information. 62 | 63 | There are other programs that use iptables internally too, such as 64 | libvirt. For information on how the two firewalls interact, see 65 | . 66 | ''; 67 | }; 68 | 69 | networking.nftables.checkRuleset = lib.mkOption { 70 | type = lib.types.bool; 71 | default = true; 72 | description = '' 73 | Run `nft check` on the ruleset to spot syntax errors during build. 74 | Because this is executed in a sandbox, the check might fail if it requires 75 | access to any environmental factors or paths outside the Nix store. 76 | To circumvent this, the ruleset file can be edited using the preCheckRuleset 77 | option to work in the sandbox environment. 78 | ''; 79 | }; 80 | 81 | networking.nftables.checkRulesetRedirects = lib.mkOption { 82 | type = lib.types.addCheck (lib.types.attrsOf lib.types.path) ( 83 | attrs: lib.all lib.types.path.check (lib.attrNames attrs) 84 | ); 85 | default = { 86 | # "/etc/hosts" = config.environment.etc.hosts.source; 87 | #"/etc/protocols" = config.environment.etc.protocols.source; 88 | #"/etc/services" = config.environment.etc.services.source; 89 | }; 90 | # defaultText = lib.literalExpression '' 91 | # { 92 | # "/etc/hosts" = config.environment.etc.hosts.source; 93 | # "/etc/protocols" = config.environment.etc.protocols.source; 94 | # "/etc/services" = config.environment.etc.services.source; 95 | # } 96 | # ''; 97 | description = '' 98 | Set of paths that should be intercepted and rewritten while checking the ruleset 99 | using `pkgs.buildPackages.libredirect`. 100 | ''; 101 | }; 102 | 103 | networking.nftables.preCheckRuleset = lib.mkOption { 104 | type = lib.types.lines; 105 | default = ""; 106 | example = lib.literalExpression '' 107 | sed 's/skgid meadow/skgid nogroup/g' -i ruleset.conf 108 | ''; 109 | description = '' 110 | This script gets run before the ruleset is checked. It can be used to 111 | create additional files needed for the ruleset check to work, or modify 112 | the ruleset for cases the build environment cannot cover. 113 | ''; 114 | }; 115 | 116 | networking.nftables.flushRuleset = lib.mkEnableOption "flushing the entire ruleset on each reload"; 117 | 118 | networking.nftables.extraDeletions = lib.mkOption { 119 | type = lib.types.lines; 120 | default = ""; 121 | example = '' 122 | # this makes deleting a non-existing table a no-op instead of an error 123 | table inet some-table; 124 | 125 | delete table inet some-table; 126 | ''; 127 | description = '' 128 | Extra deletion commands to be run on every firewall start, reload 129 | and after stopping the firewall. 130 | ''; 131 | }; 132 | 133 | networking.nftables.ruleset = lib.mkOption { 134 | type = lib.types.lines; 135 | default = ""; 136 | example = '' 137 | # Check out https://wiki.nftables.org/ for better documentation. 138 | # Table for both IPv4 and IPv6. 139 | table inet filter { 140 | # Block all incoming connections traffic except SSH and "ping". 141 | chain input { 142 | type filter hook input priority 0; 143 | 144 | # accept any localhost traffic 145 | iifname lo accept 146 | 147 | # accept traffic originated from us 148 | ct state {established, related} accept 149 | 150 | # ICMP 151 | # routers may also want: mld-listener-query, nd-router-solicit 152 | ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept 153 | ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept 154 | 155 | # allow "ping" 156 | ip6 nexthdr icmpv6 icmpv6 type echo-request accept 157 | ip protocol icmp icmp type echo-request accept 158 | 159 | # accept SSH connections (required for a server) 160 | tcp dport 22 accept 161 | 162 | # count and drop any other traffic 163 | counter drop 164 | } 165 | 166 | # Allow all outgoing connections. 167 | chain output { 168 | type filter hook output priority 0; 169 | accept 170 | } 171 | 172 | chain forward { 173 | type filter hook forward priority 0; 174 | accept 175 | } 176 | } 177 | ''; 178 | description = '' 179 | The ruleset to be used with nftables. Should be in a format that 180 | can be loaded using "/bin/nft -f". The ruleset is updated atomically. 181 | Note that if the tables should be cleaned first, either: 182 | - networking.nftables.flushRuleset = true; needs to be set (flushes all tables) 183 | - networking.nftables.extraDeletions needs to be set 184 | - or networking.nftables.tables can be used, which will clean up the table automatically 185 | ''; 186 | }; 187 | networking.nftables.rulesetFile = lib.mkOption { 188 | type = lib.types.nullOr lib.types.path; 189 | default = null; 190 | description = '' 191 | The ruleset file to be used with nftables. Should be in a format that 192 | can be loaded using "nft -f". The ruleset is updated atomically. 193 | ''; 194 | }; 195 | 196 | networking.nftables.flattenRulesetFile = lib.mkOption { 197 | type = lib.types.bool; 198 | default = false; 199 | description = '' 200 | Use `builtins.readFile` rather than `include` to handle {option}`networking.nftables.rulesetFile`. It is useful when you want to apply {option}`networking.nftables.preCheckRuleset` to {option}`networking.nftables.rulesetFile`. 201 | 202 | ::: {.note} 203 | It is expected that {option}`networking.nftables.rulesetFile` can be accessed from the build sandbox. 204 | ::: 205 | ''; 206 | }; 207 | 208 | networking.nftables.tables = lib.mkOption { 209 | type = lib.types.attrsOf (lib.types.submodule tableSubmodule); 210 | 211 | default = {}; 212 | 213 | description = '' 214 | Tables to be added to ruleset. 215 | Tables will be added together with delete statements to clean up the table before every update. 216 | ''; 217 | 218 | example = { 219 | filter = { 220 | family = "inet"; 221 | content = '' 222 | # Check out https://wiki.nftables.org/ for better documentation. 223 | # Table for both IPv4 and IPv6. 224 | # Block all incoming connections traffic except SSH and "ping". 225 | chain input { 226 | type filter hook input priority 0; 227 | 228 | # accept any localhost traffic 229 | iifname lo accept 230 | 231 | # accept traffic originated from us 232 | ct state {established, related} accept 233 | 234 | # ICMP 235 | # routers may also want: mld-listener-query, nd-router-solicit 236 | ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept 237 | ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept 238 | 239 | # allow "ping" 240 | ip6 nexthdr icmpv6 icmpv6 type echo-request accept 241 | ip protocol icmp icmp type echo-request accept 242 | 243 | # accept SSH connections (required for a server) 244 | tcp dport 22 accept 245 | 246 | # count and drop any other traffic 247 | counter drop 248 | } 249 | 250 | # Allow all outgoing connections. 251 | chain output { 252 | type filter hook output priority 0; 253 | accept 254 | } 255 | 256 | chain forward { 257 | type filter hook forward priority 0; 258 | accept 259 | } 260 | ''; 261 | }; 262 | }; 263 | }; 264 | }; 265 | 266 | ###### implementation 267 | 268 | config = lib.mkIf cfg.enable { 269 | # TODO: Add blacklistedKernelModules to block ip_tables module when using nftables 270 | # boot.blacklistedKernelModules = ["ip_tables"]; 271 | environment.systemPackages = [pkgs.nftables]; 272 | runit.services = { 273 | nftables = let 274 | enabledTables = lib.filterAttrs (_: table: table.enable) cfg.tables; 275 | deletionsScript = pkgs.writeScript "nftables-deletions" '' 276 | #! ${pkgs.nftables}/bin/nft -f 277 | ${ 278 | if cfg.flushRuleset 279 | then "flush ruleset" 280 | else 281 | lib.concatStringsSep "\n" ( 282 | lib.mapAttrsToList (_: table: '' 283 | table ${table.family} ${table.name} 284 | delete table ${table.family} ${table.name} 285 | '') 286 | enabledTables 287 | ) 288 | } 289 | ${cfg.extraDeletions} 290 | ''; 291 | deletionsScriptVar = "/var/lib/nftables/deletions.nft"; 292 | ensureDeletions = pkgs.writeShellScript "nftables-ensure-deletions" '' 293 | touch ${deletionsScriptVar} 294 | chmod +x ${deletionsScriptVar} 295 | ''; 296 | saveDeletionsScript = pkgs.writeShellScript "nftables-save-deletions" '' 297 | cp ${deletionsScript} ${deletionsScriptVar} 298 | ''; 299 | cleanupDeletionsScript = pkgs.writeShellScript "nftables-cleanup-deletions" '' 300 | rm ${deletionsScriptVar} 301 | ''; 302 | rulesScript = pkgs.writeTextFile { 303 | name = "nftables-rules"; 304 | executable = true; 305 | text = '' 306 | #! ${pkgs.nftables}/bin/nft -f 307 | # previous deletions, if any 308 | include "${deletionsScriptVar}" 309 | # current deletions 310 | include "${deletionsScript}" 311 | ${lib.concatStringsSep "\n" ( 312 | lib.mapAttrsToList (_: table: '' 313 | table ${table.family} ${table.name} { 314 | ${table.content} 315 | } 316 | '') 317 | enabledTables 318 | )} 319 | ${cfg.ruleset} 320 | ${ 321 | if cfg.rulesetFile != null 322 | then 323 | if cfg.flattenRulesetFile 324 | then builtins.readFile cfg.rulesetFile 325 | else '' 326 | include "${cfg.rulesetFile}" 327 | '' 328 | else "" 329 | } 330 | ''; 331 | checkPhase = lib.optionalString cfg.checkRuleset '' 332 | cp $out ruleset.conf 333 | sed 's|include "${deletionsScriptVar}"||' -i ruleset.conf 334 | ${cfg.preCheckRuleset} 335 | export NIX_REDIRECTS=${ 336 | lib.escapeShellArg ( 337 | lib.concatStringsSep ":" (lib.mapAttrsToList (n: v: "${n}=${v}") cfg.checkRulesetRedirects) 338 | ) 339 | } 340 | LD_PRELOAD="${pkgs.buildPackages.libredirect}/lib/libredirect.so ${pkgs.buildPackages.lklWithFirewall.lib}/lib/liblkl-hijack.so" \ 341 | ${pkgs.buildPackages.nftables}/bin/nft --check --file ruleset.conf 342 | ''; 343 | }; 344 | in { 345 | name = "nftables"; 346 | runScript = '' 347 | #!${pkgs.runtimeShell} 348 | mkdir /var/lib/nftables 349 | ${ensureDeletions} 350 | ${rulesScript} 351 | ${saveDeletionsScript} 352 | exec sv down /etc/service/nftables 353 | ''; 354 | }; 355 | }; 356 | }; 357 | } 358 | -------------------------------------------------------------------------------- /micros/modules/networking/firewall.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkEnableOption; 8 | inherit (lib) mkIf; 9 | inherit (lib) types; 10 | ifaceSet = lib.concatStringsSep ", " (map (x: ''"${x}"'') cfg.trustedInterfaces); 11 | 12 | portsToNftSet = ports: portRanges: 13 | lib.concatStringsSep ", " ( 14 | map (x: toString x) ports ++ map (x: "${toString x.from}-${toString x.to}") portRanges 15 | ); 16 | cfg = config.networking.firewall; 17 | interfaceOpts = { 18 | allowedTCPPorts = lib.mkOption { 19 | type = lib.types.listOf lib.types.port; 20 | default = []; 21 | example = [22]; 22 | description = '' 23 | List of open TCP ports. 24 | ''; 25 | }; 26 | allowedUDPPorts = lib.mkOption { 27 | type = lib.types.listOf lib.types.port; 28 | default = []; 29 | example = [53]; 30 | description = '' 31 | List of open UDP ports. 32 | ''; 33 | }; 34 | allowedTCPPortRanges = lib.mkOption { 35 | type = lib.types.listOf (lib.types.attrsOf lib.types.port); 36 | default = []; 37 | example = [ 38 | { 39 | from = 32000; 40 | to = 32768; 41 | } 42 | ]; 43 | description = '' 44 | Range of open TCP ports. 45 | ''; 46 | }; 47 | allowedUDPPortRanges = lib.mkOption { 48 | type = lib.types.listOf (lib.types.attrsOf lib.types.port); 49 | default = []; 50 | example = [ 51 | { 52 | from = 32000; 53 | to = 32768; 54 | } 55 | ]; 56 | description = '' 57 | Range of open UDP ports. 58 | ''; 59 | }; 60 | }; 61 | in { 62 | options.networking.firewall = 63 | { 64 | enable = lib.mkEnableOption ''firewall''; 65 | logRefusedConnections = lib.mkOption { 66 | type = lib.types.bool; 67 | default = true; 68 | description = '' 69 | Whether to log rejected or dropped incoming connections. 70 | Note: The logs are found in the kernel logs, i.e. dmesg 71 | or journalctl -k. 72 | ''; 73 | }; 74 | 75 | logRefusedPackets = lib.mkOption { 76 | type = lib.types.bool; 77 | default = false; 78 | description = '' 79 | Whether to log all rejected or dropped incoming packets. 80 | This tends to give a lot of log messages, so it's mostly 81 | useful for debugging. 82 | Note: The logs are found in the kernel logs, i.e. dmesg 83 | or journalctl -k. 84 | ''; 85 | }; 86 | 87 | logRefusedUnicastsOnly = lib.mkOption { 88 | type = lib.types.bool; 89 | default = true; 90 | description = '' 91 | If {option}`networking.firewall.logRefusedPackets` 92 | and this option are enabled, then only log packets 93 | specifically directed at this machine, i.e., not broadcasts 94 | or multicasts. 95 | ''; 96 | }; 97 | 98 | rejectPackets = lib.mkOption { 99 | type = lib.types.bool; 100 | default = false; 101 | description = '' 102 | If set, refused packets are rejected rather than dropped 103 | (ignored). This means that an ICMP "port unreachable" error 104 | message is sent back to the client (or a TCP RST packet in 105 | case of an existing connection). Rejecting packets makes 106 | port scanning somewhat easier. 107 | ''; 108 | }; 109 | 110 | trustedInterfaces = lib.mkOption { 111 | type = lib.types.listOf lib.types.str; 112 | default = []; 113 | example = ["enp0s2"]; 114 | description = '' 115 | Traffic coming in from these interfaces will be accepted 116 | unconditionally. Traffic from the loopback (lo) interface 117 | will always be accepted. 118 | ''; 119 | }; 120 | 121 | allowPing = lib.mkOption { 122 | type = lib.types.bool; 123 | default = true; 124 | description = '' 125 | Whether to respond to incoming ICMPv4 echo requests 126 | ("pings"). ICMPv6 pings are always allowed because the 127 | larger address space of IPv6 makes network scanning much 128 | less effective. 129 | ''; 130 | }; 131 | 132 | pingLimit = lib.mkOption { 133 | type = lib.types.nullOr (lib.types.separatedString " "); 134 | default = null; 135 | example = "--limit 1/minute --limit-burst 5"; 136 | description = '' 137 | If pings are allowed, this allows setting rate limits on them. 138 | 139 | For the iptables based firewall, it should be set like 140 | "--limit 1/minute --limit-burst 5". 141 | 142 | For the nftables based firewall, it should be set like 143 | "2/second" or "1/minute burst 5 packets". 144 | ''; 145 | }; 146 | 147 | checkReversePath = lib.mkOption { 148 | type = lib.types.either lib.types.bool ( 149 | lib.types.enum [ 150 | "strict" 151 | "loose" 152 | ] 153 | ); 154 | default = true; 155 | defaultText = lib.literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support"; 156 | example = "loose"; 157 | description = '' 158 | Performs a reverse path filter test on a packet. If a reply 159 | to the packet would not be sent via the same interface that 160 | the packet arrived on, it is refused. 161 | 162 | If using asymmetric routing or other complicated routing, set 163 | this option to loose mode or disable it and setup your own 164 | counter-measures. 165 | 166 | This option can be either true (or "strict"), "loose" (only 167 | drop the packet if the source address is not reachable via any 168 | interface) or false. 169 | ''; 170 | }; 171 | 172 | logReversePathDrops = lib.mkOption { 173 | type = lib.types.bool; 174 | default = false; 175 | description = '' 176 | Logs dropped packets failing the reverse path filter test if 177 | the option networking.firewall.checkReversePath is enabled. 178 | ''; 179 | }; 180 | 181 | filterForward = lib.mkOption { 182 | type = lib.types.bool; 183 | default = false; 184 | description = '' 185 | Enable filtering in IP forwarding. 186 | 187 | This option only works with the nftables based firewall. 188 | ''; 189 | }; 190 | 191 | connectionTrackingModules = lib.mkOption { 192 | type = lib.types.listOf lib.types.str; 193 | default = []; 194 | example = [ 195 | "ftp" 196 | "irc" 197 | "sane" 198 | "sip" 199 | "tftp" 200 | "amanda" 201 | "h323" 202 | "netbios_sn" 203 | "pptp" 204 | "snmp" 205 | ]; 206 | description = '' 207 | List of connection-tracking helpers that are auto-loaded. 208 | The complete list of possible values is given in the example. 209 | 210 | As helpers can pose as a security risk, it is advised to 211 | set this to an empty list and disable the setting 212 | networking.firewall.autoLoadConntrackHelpers unless you 213 | know what you are doing. Connection tracking is disabled 214 | by default. 215 | 216 | Loading of helpers is recommended to be done through the 217 | CT target. More info: 218 | 219 | ''; 220 | }; 221 | 222 | autoLoadConntrackHelpers = lib.mkOption { 223 | type = lib.types.bool; 224 | default = false; 225 | description = '' 226 | Whether to auto-load connection-tracking helpers. 227 | See the description at networking.firewall.connectionTrackingModules 228 | 229 | (needs kernel 3.5+) 230 | ''; 231 | }; 232 | extraInputRules = lib.mkOption { 233 | type = lib.types.lines; 234 | default = ""; 235 | example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept"; 236 | description = '' 237 | Additional nftables rules to be appended to the input-allow 238 | chain. 239 | 240 | This option only works with the nftables based firewall. 241 | ''; 242 | }; 243 | 244 | extraForwardRules = lib.mkOption { 245 | type = lib.types.lines; 246 | default = ""; 247 | example = "iifname wg0 accept"; 248 | description = '' 249 | Additional nftables rules to be appended to the forward-allow 250 | chain. 251 | 252 | This option only works with the nftables based firewall. 253 | ''; 254 | }; 255 | 256 | extraReversePathFilterRules = lib.mkOption { 257 | type = lib.types.lines; 258 | default = ""; 259 | example = "fib daddr . mark . iif type local accept"; 260 | description = '' 261 | Additional nftables rules to be appended to the rpfilter-allow 262 | chain. 263 | 264 | This option only works with the nftables based firewall. 265 | ''; 266 | }; 267 | interfaces = lib.mkOption { 268 | default = {}; 269 | type = with lib.types; attrsOf (submodule [{options = interfaceOpts;}]); 270 | description = '' 271 | Interface-specific open ports. 272 | ''; 273 | }; 274 | 275 | allInterfaces = lib.mkOption { 276 | internal = true; 277 | visible = false; 278 | default = 279 | { 280 | default = lib.mapAttrs (name: value: cfg.${name}) interfaceOpts; 281 | } 282 | // cfg.interfaces; 283 | type = with lib.types; attrsOf (submodule [{options = interfaceOpts;}]); 284 | description = '' 285 | All open ports. 286 | ''; 287 | }; 288 | } 289 | // interfaceOpts; 290 | config = lib.mkIf cfg.enable { 291 | assertions = [ 292 | { 293 | assertion = cfg.extraCommands == ""; 294 | message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}"; 295 | } 296 | { 297 | assertion = cfg.extraStopCommands == ""; 298 | message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}"; 299 | } 300 | { 301 | assertion = cfg.pingLimit == null || !(lib.hasPrefix "--" cfg.pingLimit); 302 | message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit"; 303 | } 304 | { 305 | assertion = config.networking.nftables.rulesetFile == null; 306 | message = "networking.nftables.rulesetFile conflicts with the firewall"; 307 | } 308 | ]; 309 | 310 | networking.nftables.tables."micros-fw".family = "inet"; 311 | networking.nftables.tables."micros-fw".content = '' 312 | ${lib.optionalString (cfg.checkReversePath != false) '' 313 | chain rpfilter { 314 | type filter hook prerouting priority mangle + 10; policy drop; 315 | 316 | meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server" 317 | fib saddr . mark ${lib.optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept 318 | 319 | jump rpfilter-allow 320 | 321 | ${lib.optionalString cfg.logReversePathDrops '' 322 | log level info prefix "rpfilter drop: " 323 | ''} 324 | 325 | } 326 | ''} 327 | 328 | chain rpfilter-allow { 329 | ${cfg.extraReversePathFilterRules} 330 | } 331 | 332 | chain input { 333 | type filter hook input priority filter; policy drop; 334 | 335 | ${lib.optionalString ( 336 | ifaceSet != "" 337 | ) ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''} 338 | 339 | # Some ICMPv6 types like NDP is untracked 340 | ct state vmap { 341 | invalid : drop, 342 | established : accept, 343 | related : accept, 344 | new : jump input-allow, 345 | untracked: jump input-allow, 346 | } 347 | 348 | ${lib.optionalString cfg.logRefusedConnections '' 349 | tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: " 350 | ''} 351 | ${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 352 | pkttype broadcast log level info prefix "refused broadcast: " 353 | pkttype multicast log level info prefix "refused multicast: " 354 | ''} 355 | ${lib.optionalString cfg.logRefusedPackets '' 356 | pkttype host log level info prefix "refused packet: " 357 | ''} 358 | 359 | ${lib.optionalString cfg.rejectPackets '' 360 | meta l4proto tcp reject with tcp reset 361 | reject 362 | ''} 363 | 364 | } 365 | 366 | chain input-allow { 367 | 368 | ${lib.concatStrings ( 369 | lib.mapAttrsToList ( 370 | iface: cfg: let 371 | ifaceExpr = lib.optionalString (iface != "default") "iifname ${iface}"; 372 | tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges; 373 | udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges; 374 | in '' 375 | ${lib.optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"} 376 | ${lib.optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"} 377 | '' 378 | ) 379 | cfg.allInterfaces 380 | )} 381 | 382 | ${lib.optionalString cfg.allowPing '' 383 | icmp type echo-request ${ 384 | lib.optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}" 385 | } accept comment "allow ping" 386 | ''} 387 | 388 | icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139). See RFC 4890, section 4.4." 389 | ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client" 390 | 391 | ${cfg.extraInputRules} 392 | 393 | } 394 | 395 | ${lib.optionalString cfg.filterForward '' 396 | chain forward { 397 | type filter hook forward priority filter; policy drop; 398 | 399 | ct state vmap { 400 | invalid : drop, 401 | established : accept, 402 | related : accept, 403 | new : jump forward-allow, 404 | untracked : jump forward-allow, 405 | } 406 | 407 | } 408 | 409 | chain forward-allow { 410 | 411 | icmpv6 type != { router-renumbering, 139 } accept comment "Accept all ICMPv6 messages except renumbering and node information queries (type 139). See RFC 4890, section 4.3." 412 | 413 | ct status dnat accept comment "allow port forward" 414 | 415 | ${cfg.extraForwardRules} 416 | 417 | } 418 | ''} 419 | ''; 420 | }; 421 | } 422 | -------------------------------------------------------------------------------- /micros/modules/tasks/filesystems.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | } @ moduleArgs: 7 | with lib; let 8 | # Check whenever `b` depends on `a` as a fileSystem 9 | fsBefore = a: b: let 10 | # normalisePath adds a slash at the end of the path if it didn't already 11 | # have one. 12 | # 13 | # The reason slashes are added at the end of each path is to prevent `b` 14 | # from accidentally depending on `a` in cases like 15 | # a = { mountPoint = "/aaa"; ... } 16 | # b = { device = "/aaaa"; ... } 17 | # Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is 18 | # *not* a parent of b.device. If we add a slash at the end of each string, 19 | # though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/". 20 | normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}"; 21 | normalise = mount: 22 | mount 23 | // { 24 | device = normalisePath (toString mount.device); 25 | mountPoint = normalisePath mount.mountPoint; 26 | depends = map normalisePath mount.depends; 27 | }; 28 | 29 | a' = normalise a; 30 | b' = normalise b; 31 | in 32 | hasPrefix a'.mountPoint b'.device 33 | || hasPrefix a'.mountPoint b'.mountPoint 34 | || any (hasPrefix a'.mountPoint) b'.depends; 35 | # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces 36 | escape = string: builtins.replaceStrings [" " "\t"] ["\\040" "\\011"] string; 37 | 38 | addCheckDesc = desc: elemType: check: 39 | types.addCheck elemType check 40 | // {description = "${elemType.description} (with check: ${desc})";}; 41 | 42 | isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null; 43 | nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty; 44 | 45 | fileSystems' = toposort fsBefore (attrValues config.fileSystems); 46 | 47 | fileSystems = 48 | if fileSystems' ? result 49 | then # use topologically sorted fileSystems everywhere 50 | fileSystems'.result 51 | else # the assertion below will catch this, 52 | # but we fall back to the original order 53 | # anyway so that other modules could check 54 | # their assertions too 55 | (attrValues config.fileSystems); 56 | 57 | specialFSTypes = ["proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts"]; 58 | 59 | nonEmptyWithoutTrailingSlash = 60 | addCheckDesc "non-empty without trailing slash" types.str 61 | (s: isNonEmpty s && (builtins.match ".+/" s) == null); 62 | 63 | coreFileSystemOpts = { 64 | name, 65 | config, 66 | ... 67 | }: { 68 | options = { 69 | enable = 70 | mkEnableOption "the filesystem mount" 71 | // { 72 | default = true; 73 | }; 74 | 75 | mountPoint = mkOption { 76 | example = "/mnt/usb"; 77 | type = nonEmptyWithoutTrailingSlash; 78 | description = "Location of the mounted file system."; 79 | }; 80 | 81 | stratis.poolUuid = lib.mkOption { 82 | type = types.uniq (types.nullOr types.str); 83 | description = '' 84 | UUID of the stratis pool that the fs is located in 85 | ''; 86 | example = "04c68063-90a5-4235-b9dd-6180098a20d9"; 87 | default = null; 88 | }; 89 | 90 | device = mkOption { 91 | default = null; 92 | example = "/dev/sda"; 93 | type = types.nullOr nonEmptyStr; 94 | description = "Location of the device."; 95 | }; 96 | 97 | fsType = mkOption { 98 | default = "auto"; 99 | example = "ext3"; 100 | type = nonEmptyStr; 101 | description = "Type of the file system."; 102 | }; 103 | neededForBoot = mkOption { 104 | default = "false"; 105 | example = "true"; 106 | type = types.bool; 107 | description = "Whether to mount filesystem in Stage 1"; 108 | }; 109 | 110 | options = mkOption { 111 | default = ["defaults"]; 112 | example = ["data=journal"]; 113 | description = "Options used to mount the file system."; 114 | type = types.nonEmptyListOf nonEmptyStr; 115 | }; 116 | 117 | depends = mkOption { 118 | default = []; 119 | example = ["/persist"]; 120 | type = types.listOf nonEmptyWithoutTrailingSlash; 121 | description = '' 122 | List of paths that should be mounted before this one. This filesystem's 123 | {option}`device` and {option}`mountPoint` are always 124 | checked and do not need to be included explicitly. If a path is added 125 | to this list, any other filesystem whose mount point is a parent of 126 | the path will be mounted before this filesystem. The paths do not need 127 | to actually be the {option}`mountPoint` of some other filesystem. 128 | ''; 129 | }; 130 | }; 131 | 132 | config = { 133 | mountPoint = mkDefault name; 134 | device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType); 135 | }; 136 | }; 137 | 138 | fileSystemOpts = {config, ...}: { 139 | options = { 140 | label = mkOption { 141 | default = null; 142 | example = "root-partition"; 143 | type = types.nullOr nonEmptyStr; 144 | description = "Label of the device (if any)."; 145 | }; 146 | 147 | autoFormat = mkOption { 148 | default = false; 149 | type = types.bool; 150 | description = '' 151 | If the device does not currently contain a filesystem (as 152 | determined by {command}`blkid`), then automatically 153 | format it with the filesystem type specified in 154 | {option}`fsType`. Use with caution. 155 | ''; 156 | }; 157 | 158 | formatOptions = mkOption { 159 | visible = false; 160 | type = types.unspecified; 161 | default = null; 162 | }; 163 | 164 | autoResize = mkOption { 165 | default = false; 166 | type = types.bool; 167 | description = '' 168 | If set, the filesystem is grown to its maximum size before 169 | being mounted. (This is typically the size of the containing 170 | partition.) This is currently only supported for ext2/3/4 171 | filesystems that are mounted during early boot. 172 | ''; 173 | }; 174 | 175 | noCheck = mkOption { 176 | default = false; 177 | type = types.bool; 178 | description = "Disable running fsck on this filesystem."; 179 | }; 180 | }; 181 | 182 | config.device = lib.mkIf (config.label != null) (lib.mkDefault "/dev/disk/by-label/${escape config.label}"); 183 | }; 184 | 185 | # Makes sequence of `specialMount device mountPoint options fsType` commands. 186 | # `systemMount` should be defined in the sourcing script. 187 | makeSpecialMounts = mounts: 188 | pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' 189 | specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" 190 | '') 191 | mounts); 192 | 193 | makeFstabEntries = let 194 | fsToSkipCheck = [ 195 | "none" 196 | "auto" 197 | "overlay" 198 | "iso9660" 199 | "bindfs" 200 | "udf" 201 | "btrfs" 202 | "zfs" 203 | "tmpfs" 204 | "bcachefs" 205 | "nfs" 206 | "nfs4" 207 | "nilfs2" 208 | "vboxsf" 209 | "squashfs" 210 | "glusterfs" 211 | "apfs" 212 | "9p" 213 | "cifs" 214 | "prl_fs" 215 | "vmhgfs" 216 | ]; 217 | isBindMount = fs: builtins.elem "bind" fs.options; 218 | skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs; 219 | in 220 | fstabFileSystems: {}: 221 | concatMapStrings ( 222 | fs: 223 | ( 224 | if fs.device != null 225 | then escape fs.device 226 | else throw "No device specified for mount point ‘${fs.mountPoint}’." 227 | ) 228 | + " " 229 | + escape fs.mountPoint 230 | + " " 231 | + fs.fsType 232 | + " " 233 | + escape (builtins.concatStringsSep "," fs.options) 234 | + " 0 " 235 | + ( 236 | if skipCheck fs 237 | then "0" 238 | else if fs.mountPoint == "/" 239 | then "1" 240 | else "2" 241 | ) 242 | + "\n" 243 | ) 244 | fstabFileSystems; 245 | in { 246 | ###### interface 247 | 248 | options = { 249 | fileSystems = mkOption { 250 | default = {}; 251 | example = literalExpression '' 252 | { 253 | "/".device = "/dev/hda1"; 254 | "/data" = { 255 | device = "/dev/hda2"; 256 | fsType = "ext3"; 257 | options = [ "data=journal" ]; 258 | }; 259 | "/bigdisk".label = "bigdisk"; 260 | } 261 | ''; 262 | type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); 263 | apply = lib.filterAttrs (_: fs: fs.enable); 264 | description = '' 265 | The file systems to be mounted. It must include an entry for 266 | the root directory (`mountPoint = "/"`). Each 267 | entry in the list is an attribute set with the following fields: 268 | `mountPoint`, `device`, 269 | `fsType` (a file system type recognised by 270 | {command}`mount`; defaults to 271 | `"auto"`), and `options` 272 | (the mount options passed to {command}`mount` using the 273 | {option}`-o` flag; defaults to `[ "defaults" ]`). 274 | 275 | Instead of specifying `device`, you can also 276 | specify a volume label (`label`) for file 277 | systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`). 278 | ''; 279 | }; 280 | 281 | system.fsPackages = mkOption { 282 | internal = true; 283 | default = []; 284 | description = "Packages supplying file system mounters and checkers."; 285 | }; 286 | 287 | boot.supportedFilesystems = mkOption { 288 | default = {}; 289 | example = lib.literalExpression '' 290 | { 291 | btrfs = true; 292 | zfs = lib.mkForce false; 293 | } 294 | ''; 295 | type = 296 | types.coercedTo 297 | (types.listOf types.str) 298 | (enabled: lib.listToAttrs (map (fs: lib.nameValuePair fs true) enabled)) 299 | (types.attrsOf types.bool); 300 | description = '' 301 | Names of supported filesystem types, or an attribute set of file system types 302 | and their state. The set form may be used together with `lib.mkForce` to 303 | explicitly disable support for specific filesystems, e.g. to disable ZFS 304 | with an unsupported kernel. 305 | ''; 306 | }; 307 | 308 | boot.specialFileSystems = mkOption { 309 | default = {}; 310 | type = types.attrsOf (types.submodule coreFileSystemOpts); 311 | apply = lib.filterAttrs (_: fs: fs.enable); 312 | internal = true; 313 | description = '' 314 | Special filesystems that are mounted very early during boot. 315 | ''; 316 | }; 317 | 318 | boot.devSize = mkOption { 319 | default = "5%"; 320 | example = "32m"; 321 | type = types.str; 322 | description = '' 323 | Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, 324 | for the accepted syntax. 325 | ''; 326 | }; 327 | 328 | boot.devShmSize = mkOption { 329 | default = "50%"; 330 | example = "256m"; 331 | type = types.str; 332 | description = '' 333 | Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, 334 | for the accepted syntax. 335 | ''; 336 | }; 337 | 338 | boot.runSize = mkOption { 339 | default = "25%"; 340 | example = "256m"; 341 | type = types.str; 342 | description = '' 343 | Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, 344 | for the accepted syntax. 345 | ''; 346 | }; 347 | }; 348 | 349 | ###### implementation 350 | 351 | config = { 352 | assertions = let 353 | ls = sep: concatMapStringsSep sep (x: x.mountPoint); 354 | resizableFSes = [ 355 | "ext3" 356 | "ext4" 357 | "btrfs" 358 | "xfs" 359 | ]; 360 | notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes); 361 | in 362 | [ 363 | { 364 | assertion = ! (fileSystems' ? cycle); 365 | message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 366 | } 367 | { 368 | assertion = ! (any notAutoResizable fileSystems); 369 | message = let 370 | fs = head (filter notAutoResizable fileSystems); 371 | in '' 372 | Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"' 373 | ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"} 374 | only the following support it: ${lib.concatStringsSep ", " resizableFSes}. 375 | ''; 376 | } 377 | { 378 | assertion = ! (any (fs: fs.formatOptions != null) fileSystems); 379 | message = let 380 | fs = head (filter (fs: fs.formatOptions != null) fileSystems); 381 | in '' 382 | 'fileSystems..formatOptions' has been removed, since 383 | systemd-makefs does not support any way to provide formatting 384 | options. 385 | ''; 386 | } 387 | ] 388 | ++ lib.map (fs: { 389 | assertion = fs.label != null -> fs.device == "/dev/disk/by-label/${escape fs.label}"; 390 | message = '' 391 | The filesystem with mount point ${fs.mountPoint} has its label and device set to inconsistent values: 392 | label: ${toString fs.label} 393 | device: ${toString fs.device} 394 | 'filesystems..label' and 'filesystems..device' are mutually exclusive. Please set only one. 395 | ''; 396 | }) 397 | fileSystems; 398 | 399 | # Export for use in other modules 400 | system.build.fileSystems = fileSystems; 401 | system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; 402 | 403 | boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 404 | 405 | # Add the mount helpers to the system path so that `mount' can find them. 406 | system.fsPackages = [pkgs.dosfstools]; 407 | 408 | environment.systemPackages = with pkgs; [fuse3 fuse] ++ config.system.fsPackages; 409 | 410 | environment.etc.fstab.text = let 411 | swapOptions = sw: 412 | concatStringsSep "," ( 413 | sw.options 414 | ++ optional (sw.priority != null) "pri=${toString sw.priority}" 415 | ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" 416 | ); 417 | in '' 418 | # This is a generated file. Do not edit! 419 | # 420 | 421 | # Filesystems. 422 | ${makeFstabEntries fileSystems {}} 423 | ''; 424 | 425 | # Sync mount options with systemd's src/core/mount-setup.c: mount_table. 426 | boot.specialFileSystems = 427 | { 428 | "/proc" = { 429 | fsType = "proc"; 430 | options = ["nosuid" "noexec" "nodev"]; 431 | }; 432 | 433 | "/run" = { 434 | fsType = "tmpfs"; 435 | options = ["nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}"]; 436 | }; 437 | 438 | "/dev" = { 439 | fsType = "devtmpfs"; 440 | options = ["nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}"]; 441 | }; 442 | 443 | "/dev/shm" = { 444 | fsType = "tmpfs"; 445 | options = ["nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}"]; 446 | }; 447 | 448 | "/dev/pts" = { 449 | fsType = "devpts"; 450 | options = ["nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=3"]; 451 | }; 452 | 453 | # To hold secrets that shouldn't be written to disk 454 | "/run/keys" = { 455 | fsType = "ramfs"; 456 | options = ["nosuid" "nodev" "mode=750"]; 457 | }; 458 | } 459 | // optionalAttrs (!config.boot.isContainer) { 460 | # systemd-nspawn populates /sys by itself, and remounting it causes all 461 | # kinds of weird issues (most noticeably, waiting for host disk device 462 | # nodes). 463 | "/sys" = { 464 | fsType = "sysfs"; 465 | options = ["nosuid" "noexec" "nodev"]; 466 | }; 467 | }; 468 | }; 469 | } 470 | -------------------------------------------------------------------------------- /micros/modules/system/boot/stage-1.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: let 7 | inherit (lib) mkOption mkEnableOption literalMD; 8 | inherit (lib) optionalString; 9 | inherit (lib) types; 10 | 11 | kernel-name = config.boot.kernelPackages.kernel.name or "kernel"; 12 | 13 | # Determine the set of modules that we need to mount the root FS. 14 | modulesClosure = pkgs.makeModulesClosure { 15 | kernel = config.system.build.kernel; 16 | firmware = config.hardware.firmware; 17 | rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules; 18 | allowMissing = false; 19 | }; 20 | 21 | # The initrd only has to mount `/` or any FS marked as necessary for 22 | # booting (such as the FS containing `/nix/store`, or an FS needed for 23 | # mounting `/`, like `/` on a loopback). 24 | # Check whenever fileSystem is needed for boot. NOTE: Make sure 25 | # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c 26 | # is in the list, put /a and /a/b in as well. 27 | pathsNeededForBoot = [ 28 | "/" 29 | "/nix" 30 | "/nix/store" 31 | "/var" 32 | "/var/log" 33 | "/var/lib" 34 | "/var/lib/nixos" 35 | "/var/run" 36 | "/etc" 37 | "/usr" 38 | ]; 39 | 40 | fsNeededForBoot = fs: fs.neededForBoot || lib.elem fs.mountPoint pathsNeededForBoot; 41 | fileSystems = lib.filter fsNeededForBoot config.system.build.fileSystems; 42 | 43 | # A utility for enumerating the shared-library dependencies of a program 44 | findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" '' 45 | set -euo pipefail 46 | 47 | declare -A seen 48 | left=() 49 | 50 | patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf" 51 | 52 | function add_needed { 53 | rpath="$($patchelf --print-rpath $1)" 54 | dir="$(dirname $1)" 55 | for lib in $($patchelf --print-needed $1); do 56 | left+=("$lib" "$rpath" "$dir") 57 | done 58 | } 59 | 60 | add_needed "$1" 61 | 62 | while [ ''${#left[@]} -ne 0 ]; do 63 | next=''${left[0]} 64 | rpath=''${left[1]} 65 | ORIGIN=''${left[2]} 66 | left=("''${left[@]:3}") 67 | if [ -z ''${seen[$next]+x} ]; then 68 | seen[$next]=1 69 | 70 | # Ignore the dynamic linker which for some reason appears as a DT_NEEDED of glibc but isn't in glibc's RPATH. 71 | case "$next" in 72 | ld*.so.?) continue;; 73 | esac 74 | 75 | IFS=: read -ra paths <<< $rpath 76 | res= 77 | for path in "''${paths[@]}"; do 78 | path=$(eval "echo $path") 79 | if [ -f "$path/$next" ]; then 80 | res="$path/$next" 81 | echo "$res" 82 | add_needed "$res" 83 | break 84 | fi 85 | done 86 | if [ -z "$res" ]; then 87 | echo "Couldn't satisfy dependency $next" >&2 88 | exit 1 89 | fi 90 | fi 91 | done 92 | ''; 93 | 94 | extraUtils = 95 | pkgs.runCommand "extra-utils" { 96 | nativeBuildInputs = with pkgs.buildPackages; [nukeReferences bintools]; 97 | allowedReferences = ["out"]; 98 | } '' 99 | set +o pipefail 100 | 101 | mkdir -p $out/bin $out/lib 102 | ln -s $out/bin $out/sbin 103 | 104 | copy_bin_and_libs () { 105 | [ -f "$out/bin/$(basename $1)" ] && rm "$out/bin/$(basename $1)" 106 | cp -pdv $1 $out/bin 107 | } 108 | 109 | # Copy BusyBox. 110 | for BIN in ${pkgs.busybox}/{s,}bin/*; do 111 | copy_bin_and_libs $BIN 112 | done 113 | 114 | copy_bin_and_libs ${pkgs.dhcpcd}/bin/dhcpcd 115 | 116 | # Copy ld manually since it isn't detected correctly 117 | cp -pv ${pkgs.glibc.out}/lib/ld*.so.? $out/lib 118 | 119 | # Copy all of the needed libraries in a consistent order so 120 | find $out/bin $out/lib -type f | sort | while read BIN; do 121 | echo "Copying libs for executable $BIN" 122 | for LIB in $(${findLibs}/bin/find-libs $BIN); do 123 | TGT="$out/lib/$(basename $LIB)" 124 | if [ ! -f "$TGT" ]; then 125 | SRC="$(readlink -e $LIB)" 126 | cp -pdv "$SRC" "$TGT" 127 | fi 128 | done 129 | done 130 | 131 | # Strip binaries further than normal. 132 | chmod -R u+w $out 133 | stripDirs "$STRIP" "$RANLIB" "lib bin" "-s" 134 | 135 | # Run patchelf to make the programs refer to the copied libraries. 136 | find $out/bin $out/lib -type f | while read i; do 137 | nuke-refs -e $out $i 138 | done 139 | 140 | find $out/bin -type f | while read i; do 141 | echo "patching $i..." 142 | patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true 143 | done 144 | 145 | find $out/lib -type f \! -name 'ld*.so.?' | while read i; do 146 | echo "patching $i..." 147 | patchelf --set-rpath $out/lib $i 148 | done 149 | 150 | # Make sure that the patchelf'ed binaries still work. 151 | echo "testing patched programs..." 152 | $out/bin/ash -c 'echo hello world' | grep "hello world" 153 | 154 | export LD_LIBRARY_PATH=$out/lib 155 | $out/bin/mount --help 2>&1 | grep -q "BusyBox" 156 | ''; 157 | 158 | shell = "${extraUtils}/bin/sh"; 159 | 160 | dhcpHook = pkgs.writeScript "dhcpHook" '' 161 | #!${shell} 162 | ''; 163 | 164 | bootStage1 = pkgs.replaceVarsWith { 165 | src = ./stage-1-init.sh; 166 | isExecutable = true; 167 | 168 | replacements = { 169 | shell = "${extraUtils}/bin/ash"; 170 | modprobe = "${pkgs.kmod}/bin/modprobe"; 171 | # Expects $targetRoot to be set in the stage-1 script. 172 | mountScript = '' 173 | ${config.not-os.preMount} 174 | 175 | # TODO: this should be handled better 176 | realroot=tmpfs 177 | 178 | if [ "$realroot" = tmpfs ]; then 179 | mount -t tmpfs root "$targetRoot" -o size=1G 180 | else 181 | mount "$realroot" "$targetRoot" 182 | fi 183 | 184 | chmod 755 $targetRoot 185 | 186 | ${config.not-os.postMount} 187 | ''; 188 | 189 | mdevRules = pkgs.writeText "mdev-conf" '' 190 | # 191 | # This is a sample mdev.conf. 192 | # 193 | 194 | # Devices: 195 | # Syntax: %s %d:%d %s 196 | # devices user:group mode 197 | 198 | $MODALIAS=.* 0:0 0660 @modprobe -q -b "$MODALIAS" 199 | 200 | # null does already exist; therefore ownership has to be changed with command 201 | null 0:0 0666 @chmod 666 $MDEV 202 | zero 0:0 0666 203 | full 0:0 0666 204 | 205 | random 0:0 0666 206 | urandom 0:0 0444 207 | hwrandom 0:0 0660 208 | 209 | console 0:0 0600 210 | 211 | # load frambuffer console when first frambuffer is found 212 | fb0 0:0 0660 @modprobe -q -b fbcon 213 | vchiq 0:0 0660 214 | 215 | fd0 0:0 0660 216 | kmem 0:0 0640 217 | mem 0:0 0640 218 | port 0:0 0640 219 | ptmx 0:0 0666 220 | 221 | # Kernel-based Virtual Machine. 222 | kvm 0:0 660 223 | 224 | # ram.* 225 | ram([0-9]*) 0:0 0660 >rd/%1 226 | loop([0-9]+) 0:0 0660 >loop/%1 227 | 228 | # persistent storage 229 | dasd.* 0:0 0660 */opt/mdev/helpers/storage-device 230 | mmcblk.* 0:0 0660 */opt/mdev/helpers/storage-device 231 | nbd.* 0:0 0660 */opt/mdev/helpers/storage-device 232 | nvme.* 0:0 0660 */opt/mdev/helpers/storage-device 233 | sd[a-z].* 0:0 0660 */opt/mdev/helpers/storage-device 234 | sr[0-9]+ 0:0 0660 */opt/mdev/helpers/storage-device 235 | vd[a-z].* 0:0 0660 */opt/mdev/helpers/storage-device 236 | xvd[a-z].* 0:0 0660 */opt/mdev/helpers/storage-device 237 | 238 | md[0-9] 0:0 0660 239 | 240 | tty 0:0 0666 241 | tty[0-9] 0:0 0600 242 | tty[0-9][0-9] 0:0 0660 243 | ttyS[0-9]* 0:0 0660 244 | pty.* 0:0 0660 245 | vcs[0-9]* 0:0 0660 246 | vcsa[0-9]* 0:0 0660 247 | 248 | # rpi bluetooth 249 | #ttyAMA0 0:0 660 @btattach -B /dev/$MDEV -P bcm -S 115200 -N & 250 | 251 | ttyACM[0-9] 0:0 0660 @ln -sf $MDEV modem 252 | ttyUSB[0-9] 0:0 0660 @ln -sf $MDEV modem 253 | ttyLTM[0-9] 0:0 0660 @ln -sf $MDEV modem 254 | ttySHSF[0-9] 0:0 0660 @ln -sf $MDEV modem 255 | slamr 0:0 0660 @ln -sf $MDEV slamr0 256 | slusb 0:0 0660 @ln -sf $MDEV slusb0 257 | fuse 0:0 0666 258 | 259 | # dri device 260 | dri/.* 0:0 0660 261 | card[0-9] 0:0 0660 =dri/ 262 | 263 | # alsa sound devices and audio stuff 264 | pcm.* 0:0 0660 =snd/ 265 | control.* 0:0 0660 =snd/ 266 | midi.* 0:0 0660 =snd/ 267 | seq 0:0 0660 =snd/ 268 | timer 0:0 0660 =snd/ 269 | 270 | adsp 0:0 0660 >sound/ 271 | audio 0:0 0660 >sound/ 272 | dsp 0:0 0660 >sound/ 273 | mixer 0:0 0660 >sound/ 274 | sequencer.* 0:0 0660 >sound/ 275 | 276 | SUBSYSTEM=sound;.* 0:0 0660 277 | 278 | # PTP devices 279 | ptp[0-9] 0:0 0660 */lib/mdev/ptpdev 280 | 281 | # virtio-ports 282 | SUBSYSTEM=virtio-ports;vport.* 0:0 0600 @mkdir -p virtio-ports; ln -sf ../$MDEV virtio-ports/$(cat /sys/class/virtio-ports/$MDEV/name) 283 | 284 | # misc stuff 285 | agpgart 0:0 0660 >misc/ 286 | psaux 0:0 0660 >misc/ 287 | rtc 0:0 0664 >misc/ 288 | 289 | # input stuff 290 | SUBSYSTEM=input;.* 0:0 0660 291 | 292 | # v4l stuff 293 | vbi[0-9] 0:0 0660 >v4l/ 294 | 0[0-9]+ 0:0 0660 >v4l/ 295 | 296 | # dvb stuff 297 | dvb.* 0:0 0660 */lib/mdev/dvbdev 298 | 299 | # load drivers for usb devices 300 | usb[0-9]+ 0:0 0660 */lib/mdev/usbdev 301 | 302 | # net devices 303 | # 666 is fine: https://www.kernel.org/doc/Documentation/networking/tuntap.txt 304 | net/tun[0-9]* 0:0 0666 305 | net/tap[0-9]* 0:0 0666 306 | 307 | # zaptel devices 308 | zap(.*) 0:0 0660 =zap/%1 309 | dahdi!(.*) 0:0 0660 =dahdi/%1 310 | dahdi/(.*) 0:0 0660 =dahdi/%1 311 | 312 | # raid controllers 313 | cciss!(.*) 0:0 0660 =cciss/%1 314 | cciss/(.*) 0:0 0660 =cciss/%1 315 | ida!(.*) 0:0 0660 =ida/%1 316 | ida/(.*) 0:0 0660 =ida/%1 317 | rd!(.*) 0:0 0660 =rd/%1 318 | rd/(.*) 0:0 0660 =rd/%1 319 | 320 | # tape devices 321 | nst[0-9]+.* 0:0 0660 322 | st[0-9]+.* 0:0 0660 323 | 324 | # fallback for any!device -> any/device 325 | (.*)!(.*) 0:0 0660 =%1/%2 326 | 327 | ''; 328 | 329 | mdevHelper = pkgs.writeText "mdev-helper" '' 330 | #!/bin/sh 331 | 332 | symlink_action() { 333 | case "$ACTION" in 334 | add) ln -sf "$1" "$2";; 335 | remove) rm -f "$2";; 336 | esac 337 | } 338 | 339 | sanitise_file() { 340 | sed -E -e 's/^\s+//' -e 's/\s+$//' -e 's/ /_/g' "$@" 2>/dev/null 341 | } 342 | 343 | sanitise_string() { 344 | echo "$@" | sanitise_file 345 | } 346 | 347 | blkid_encode_string() { 348 | # Rewrites string similar to libblk's blkid_encode_string 349 | # function which is used by udev/eudev. 350 | echo "$@" | sed -e 's| |\\x20|g' 351 | } 352 | 353 | : ''${SYSFS:=/sys} 354 | 355 | # cdrom symlink 356 | case "$MDEV" in 357 | sr*|xvd*) 358 | caps="$(cat $SYSFS/block/$MDEV/capability 2>/dev/null)" 359 | if [ $(( 0x''${caps:-0} & 8 )) -gt 0 ]; then 360 | symlink_action $MDEV cdrom 361 | fi 362 | esac 363 | 364 | 365 | # /dev/block symlinks 366 | mkdir -p block 367 | if [ -f "$SYSFS/class/block/$MDEV/dev" ]; then 368 | maj_min=$(sanitise_file "$SYSFS/class/block/$MDEV/dev") 369 | symlink_action ../$MDEV block/''${maj_min} 370 | fi 371 | 372 | 373 | # by-id symlinks 374 | mkdir -p disk/by-id 375 | 376 | if [ -f "$SYSFS/class/block/$MDEV/partition" ]; then 377 | # This is a partition of a device, find out its parent device 378 | _parent_dev="$(basename $(''${SBINDIR:-/usr/bin}/readlink -f "$SYSFS/class/block/$MDEV/.."))" 379 | 380 | partition=$(cat $SYSFS/class/block/$MDEV/partition 2>/dev/null) 381 | case "$partition" in 382 | [0-9]*) partsuffix="-part$partition";; 383 | esac 384 | # Get name, model, serial, wwid from parent device of the partition 385 | _check_dev="$_parent_dev" 386 | else 387 | _check_dev="$MDEV" 388 | fi 389 | 390 | model=$(sanitise_file "$SYSFS/class/block/$_check_dev/device/model") 391 | name=$(sanitise_file "$SYSFS/class/block/$_check_dev/device/name") 392 | serial=$(sanitise_file "$SYSFS/class/block/$_check_dev/device/serial") 393 | wwid=$(sanitise_file "$SYSFS/class/block/$_check_dev/wwid") 394 | : ''${wwid:=$(sanitise_file "$SYSFS/class/block/$_check_dev/device/wwid")} 395 | 396 | # Sets variables LABEL, PARTLABEL, PARTUUID, TYPE, UUID depending on 397 | # blkid output (busybox blkid will not provide PARTLABEL or PARTUUID) 398 | eval $(blkid /dev/$MDEV | cut -d: -f2-) 399 | 400 | if [ -n "$wwid" ]; then 401 | case "$MDEV" in 402 | nvme*) symlink_action ../../$MDEV disk/by-id/nvme-''${wwid}''${partsuffix};; 403 | esac 404 | case "$wwid" in 405 | naa.*) symlink_action ../../$MDEV disk/by-id/wwn-0x''${wwid#naa.};; 406 | esac 407 | fi 408 | 409 | if [ -n "$serial" ]; then 410 | if [ -n "$model" ]; then 411 | case "$MDEV" in 412 | nvme*) symlink_action ../../$MDEV disk/by-id/nvme-''${model}_''${serial}''${partsuffix};; 413 | sd*) symlink_action ../../$MDEV disk/by-id/ata-''${model}_''${serial}''${partsuffix};; 414 | esac 415 | fi 416 | if [ -n "$name" ]; then 417 | case "$MDEV" in 418 | mmcblk*) symlink_action ../../$MDEV disk/by-id/mmc-''${name}_''${serial}''${partsuffix};; 419 | esac 420 | fi 421 | 422 | # virtio-blk 423 | case "$MDEV" in 424 | vd*) symlink_action ../../$MDEV disk/by-id/virtio-''${serial}''${partsuffix};; 425 | esac 426 | fi 427 | 428 | # by-label, by-partlabel, by-partuuid, by-uuid symlinks 429 | if [ -n "$LABEL" ]; then 430 | mkdir -p disk/by-label 431 | symlink_action ../../$MDEV disk/by-label/"$(blkid_encode_string "$LABEL")" 432 | fi 433 | if [ -n "$PARTLABEL" ]; then 434 | mkdir -p disk/by-partlabel 435 | symlink_action ../../$MDEV disk/by-partlabel/"$(blkid_encode_string "$PARTLABEL")" 436 | fi 437 | if [ -n "$PARTUUID" ]; then 438 | mkdir -p disk/by-partuuid 439 | symlink_action ../../$MDEV disk/by-partuuid/"$PARTUUID" 440 | fi 441 | if [ -n "$UUID" ]; then 442 | mkdir -p disk/by-uuid 443 | symlink_action ../../$MDEV disk/by-uuid/"$UUID" 444 | fi 445 | 446 | # backwards compatibility with /dev/usbdisk for /dev/sd* 447 | if [ "''${MDEV#sd}" != "$MDEV" ]; then 448 | sysdev=$(readlink $SYSFS/class/block/$MDEV) 449 | case "$sysdev" in 450 | *usb[0-9]*) 451 | # require vfat for devices without partition 452 | if ! [ -e $SYSFS/block/$MDEV ] || [ TYPE="vfat" ]; then 453 | symlink_action $MDEV usbdisk 454 | fi 455 | ;; 456 | esac 457 | fi 458 | ''; 459 | 460 | fsInfo = let 461 | f = fs: [ 462 | fs.mountPoint 463 | ( 464 | if fs.device != null 465 | then fs.device 466 | else "/dev/disk/by-label/${fs.label}" 467 | ) 468 | fs.fsType 469 | (builtins.concatStringsSep "," fs.options) 470 | ]; 471 | in 472 | pkgs.writeText "initrd-fsinfo" (lib.concatStringsSep "\n" (lib.concatMap f fileSystems)); 473 | 474 | setHostId = optionalString (config.networking.hostId != null) '' 475 | hi="${config.networking.hostId}" 476 | ${ 477 | if pkgs.stdenv.hostPlatform.isBigEndian 478 | then '' 479 | echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > /etc/hostid 480 | '' 481 | else '' 482 | echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > /etc/hostid 483 | '' 484 | } 485 | ''; 486 | 487 | inherit extraUtils dhcpHook modulesClosure; 488 | 489 | inherit (config.boot.initrd) kernelModules; 490 | inherit (config.system.build) earlyMountScript; 491 | }; 492 | 493 | postInstall = '' 494 | echo checking syntax 495 | # check both with bash 496 | ${pkgs.buildPackages.bash}/bin/sh -n $target 497 | # and with ash shell, just in case 498 | ${pkgs.buildPackages.busybox}/bin/ash -n $target 499 | ''; 500 | }; 501 | 502 | initialRamdisk = pkgs.makeInitrd { 503 | name = "initrd-${kernel-name}"; 504 | 505 | contents = [ 506 | { 507 | object = bootStage1; 508 | symlink = "/init"; 509 | } 510 | { 511 | object = "${modulesClosure}/lib"; 512 | symlink = "/lib"; 513 | } 514 | ]; 515 | 516 | inherit (config.boot.initrd) compressor compressorArgs; 517 | }; 518 | 519 | netbootRamdisk = pkgs.makeInitrd { 520 | name = "initrd-${kernel-name}-netboot"; 521 | prepend = ["${config.system.build.initialRamdisk}/initrd"]; 522 | 523 | contents = [ 524 | { 525 | object = config.system.build.squashfsStore; 526 | symlink = "/nix-store.squashfs"; 527 | } 528 | ]; 529 | 530 | inherit (config.boot.initrd) compressor compressorArgs; 531 | }; 532 | in { 533 | options = { 534 | not-os = { 535 | preMount = mkOption { 536 | type = types.lines; 537 | default = ""; 538 | }; 539 | 540 | postMount = mkOption { 541 | type = types.lines; 542 | default = ""; 543 | }; 544 | 545 | # There is no preBootCommands. Trust me, I've checked. 546 | postBootCommands = mkOption { 547 | default = ""; 548 | example = "rm -f /var/log/messages"; 549 | type = types.lines; 550 | description = '' 551 | Shell commands to be executed just before runit is started. 552 | ''; 553 | }; 554 | 555 | readOnlyNixStore = mkOption { 556 | type = types.bool; 557 | default = true; 558 | description = '' 559 | If set, NixOS will enforce the immutability of the Nix store 560 | by making {file}`/nix/store` a read-only bind 561 | mount. Nix will automatically make the store writable when 562 | needed. 563 | ''; 564 | }; 565 | }; 566 | 567 | boot.initrd = { 568 | enable = mkEnableOption "initrd" // {default = true;}; 569 | 570 | compressor = mkOption { 571 | type = with types; either str (functionTo str); 572 | default = 573 | if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9" 574 | then "zstd" 575 | else "gzip"; 576 | 577 | defaultText = literalMD "`zstd` if the kernel supports it (5.9+), `gzip` if not"; 578 | description = '' 579 | The compressor to use on the initrd image. May be any of: 580 | 581 | - The name of one of the predefined compressors, see {file}`pkgs/build-support/kernel/initrd-compressor-meta.nix` for the definitions. 582 | - A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. `pkgs: "''${pkgs.pigz}/bin/pigz"` 583 | - (not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. `"''${pkgs.pigz}/bin/pigz"` 584 | 585 | The given program should read data from stdin and write it to stdout compressed. 586 | ''; 587 | example = "xz"; 588 | }; 589 | 590 | compressorArgs = mkOption { 591 | default = null; 592 | type = types.nullOr (types.listOf types.str); 593 | description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults."; 594 | }; 595 | }; 596 | }; 597 | 598 | config = { 599 | system.build = { 600 | inherit bootStage1; 601 | inherit initialRamdisk netbootRamdisk; 602 | inherit extraUtils; 603 | }; 604 | 605 | boot.initrd.availableKernelModules = []; 606 | boot.initrd.kernelModules = 607 | ["tun" "loop" "squashfs"] 608 | ++ (lib.optional config.nix.enable "overlay"); 609 | }; 610 | } 611 | --------------------------------------------------------------------------------