From 5a7fcbe926862f5692e47631b89022052dd60dc1 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sun, 15 Feb 2026 02:14:48 +0100 Subject: [PATCH 1/2] Fix Nix flake checks and passt networking --- .gitignore | 17 ++++ flake.nix | 226 ++++++++++++++++++++++++++++++++++++++++++++++ nix/feos.nix | 110 ++++++++++++++++++++++ nix/initramfs.nix | 99 ++++++++++++++++++++ nix/module.nix | 223 +++++++++++++++++++++++++++++++++++++++++++++ nix/vm.nix | 162 +++++++++++++++++++++++++++++++++ 6 files changed, 837 insertions(+) create mode 100644 flake.nix create mode 100644 nix/feos.nix create mode 100644 nix/initramfs.nix create mode 100644 nix/module.nix create mode 100644 nix/vm.nix diff --git a/.gitignore b/.gitignore index 808f462..2bbbc65 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,20 @@ /.idea /.vscode /.zed +/result +/result-* + +# Added by Spec Kitty CLI (auto-managed) +.claude/ +.codex/ +.opencode/ +.windsurf/ +.gemini/ +.cursor/ +.qwen/ +.kilocode/ +.augment/ +.roo/ +.amazonq/ +.github/copilot/ +.kittify/.dashboard diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9580860 --- /dev/null +++ b/flake.nix @@ -0,0 +1,226 @@ +{ + description = "FeOS - A minimal Linux init system for hypervisors and container hosts"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + crane.url = "github:ipetkov/crane"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + crane, + rust-overlay, + flake-utils, + ... + }: + let + # FeOS only targets x86_64-linux (musl static binary) + supportedSystems = [ "x86_64-linux" ]; + + # Version metadata + version = "0.5.0"; + kernelVersion = "6.12.63"; + in + flake-utils.lib.eachSystem supportedSystems ( + system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + # Rust toolchain with musl target for static linking + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + targets = [ "x86_64-unknown-linux-musl" ]; + extensions = [ + "rust-src" + "clippy" + "rustfmt" + ]; + }; + + # Crane lib configured with our custom toolchain + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + # --- Package derivations --- + + feosPackage = pkgs.callPackage ./nix/feos.nix { + inherit craneLib version; + inherit (pkgs) pkgsCross; + }; + + feosCliPackage = pkgs.callPackage ./nix/feos.nix { + inherit craneLib version; + inherit (pkgs) pkgsCross; + buildCli = true; + }; + + feosKernel = pkgs.callPackage ./nix/kernel.nix { + inherit kernelVersion; + kernelConfig = ./hack/kernel/config/feos-linux-${kernelVersion}.config; + }; + + # Pre-built hypervisor firmware (cross-compiled to x86_64-none, + # not directly buildable as a regular x86_64-linux package) + hypervisorFirmware = pkgs.fetchurl { + url = "https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.4.2/hypervisor-fw"; + hash = "sha256-WMFGE7xmBnI/GBJNAPujRk+vMx1ssGp//lbeYtgHEkA="; + }; + + feosInitramfs = pkgs.callPackage ./nix/initramfs.nix { + feos = feosPackage; + kernel = feosKernel; + cloud-hypervisor = pkgs.cloud-hypervisor; + youki = pkgs.youki; + hypervisor-firmware = hypervisorFirmware; + }; + + feosUki = pkgs.callPackage ./nix/uki.nix { + kernel = feosKernel; + initramfs = feosInitramfs; + osRelease = ./hack/uki/os-release.txt; + cmdline = ./hack/kernel/cmdline.txt; + }; + + feosVm = pkgs.callPackage ./nix/vm.nix { + kernel = feosKernel; + initramfs = feosInitramfs; + }; + + in + { + packages = { + default = feosPackage; + feos = feosPackage; + feos-cli = feosCliPackage; + feos-kernel = feosKernel; + feos-initramfs = feosInitramfs; + feos-uki = feosUki; + + # Convenience: build everything + all = pkgs.symlinkJoin { + name = "feos-all"; + paths = [ + feosPackage + feosCliPackage + feosKernel + feosInitramfs + feosUki + ]; + }; + }; + + apps = { + default = flake-utils.lib.mkApp { + drv = feosVm; + name = "feos-vm"; + }; + vm = flake-utils.lib.mkApp { + drv = feosVm; + name = "feos-vm"; + }; + feos-cli = flake-utils.lib.mkApp { + drv = feosCliPackage; + name = "feos-cli"; + }; + }; + + # --- Checks (run via `nix flake check`) --- + checks = { + # Verify the main packages build + feos = feosPackage; + feos-cli = feosCliPackage; + feos-kernel = feosKernel; + feos-initramfs = feosInitramfs; + feos-uki = feosUki; + + # Cargo fmt check + feos-fmt = craneLib.cargoFmt { + src = craneLib.path ./.; + }; + + # Cargo clippy + feos-clippy = craneLib.cargoClippy ( + feosPackage.passthru.commonArgs + // { + inherit (feosPackage.passthru) cargoArtifacts src; + pname = "feos-clippy"; + cargoClippyExtraArgs = "--all-targets -- -D warnings"; + doCheck = false; + } + ); + }; + + # Formatter (run via `nix fmt`) + formatter = pkgs.nixfmt; + + devShells.default = pkgs.mkShell { + inputsFrom = [ ]; + + nativeBuildInputs = [ + rustToolchain + pkgs.protobuf + pkgs.pkg-config + pkgs.perl + pkgs.openssl + pkgs.sqlite + + # Development tools + pkgs.cargo-watch + pkgs.cargo-edit + + # VM / testing + pkgs.qemu + pkgs.passt + + # gRPC testing + pkgs.grpcurl + + # Nix tools + pkgs.nixpkgs-fmt + ]; + + # For openssl-sys vendored build + OPENSSL_NO_VENDOR = "0"; + PROTOC = "${pkgs.protobuf}/bin/protoc"; + + shellHook = '' + echo "FeOS development shell" + echo " cargo build --target=x86_64-unknown-linux-musl --all" + echo " nix build .#feos -- build FeOS binary" + echo " nix build .#feos-kernel -- build custom kernel" + echo " nix build .#feos-initramfs -- build initramfs" + echo " nix build .#feos-uki -- build Unified Kernel Image" + echo " nix run .#vm -- launch test VM" + ''; + }; + } + ) + // { + # System-independent outputs + + nixosModules = { + default = self.nixosModules.feos; + feos = import ./nix/module.nix self; + }; + + # Overlay for use in other flakes + overlays.default = final: prev: { + feos = self.packages.${final.system}.feos; + feos-cli = self.packages.${final.system}.feos-cli; + feos-kernel = self.packages.${final.system}.feos-kernel; + feos-initramfs = self.packages.${final.system}.feos-initramfs; + feos-uki = self.packages.${final.system}.feos-uki; + }; + }; +} diff --git a/nix/feos.nix b/nix/feos.nix new file mode 100644 index 0000000..f82e160 --- /dev/null +++ b/nix/feos.nix @@ -0,0 +1,110 @@ +{ + lib, + craneLib, + version, + buildCli ? false, + protobuf, + perl, + pkg-config, + stdenv, + git, + pkgsCross, +}: + +let + # Musl cross-compilation toolchain for static linking + muslCC = pkgsCross.musl64.stdenv.cc; + + # Source filtering: include Rust sources, proto files, migrations, and .sqlx + srcFilter = + path: type: + let + baseName = builtins.baseNameOf path; + relPath = lib.removePrefix (toString ./.. + "/") (toString path); + in + # Always include proto definitions (needed by tonic-build) + (lib.hasPrefix "proto/" relPath) + || + # Include .sqlx offline query cache + (lib.hasSuffix ".json" baseName && lib.hasInfix ".sqlx" relPath) + || + # Include SQL migrations + (lib.hasSuffix ".sql" baseName && lib.hasInfix "migrations" relPath) + || + # Include Cargo/Rust source files via crane's default filter + (craneLib.filterCargoSources path type); + + src = lib.cleanSourceWith { + src = craneLib.path ./..; + filter = srcFilter; + }; + + # When targeting musl, we need the musl C compiler for vendored OpenSSL. + # The env var names use the target triple with hyphens replaced by underscores. + muslEnv = lib.optionalAttrs (!buildCli) { + CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl"; + + # Tell cargo/cc-rs to use the musl C compiler for the target + CC_x86_64_unknown_linux_musl = "${muslCC}/bin/${muslCC.targetPrefix}cc"; + AR_x86_64_unknown_linux_musl = "${muslCC}/bin/${muslCC.targetPrefix}ar"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER = "${muslCC}/bin/${muslCC.targetPrefix}cc"; + + # OpenSSL vendored build needs to find the musl headers + # The cc crate will use the CC env var above, which points at musl-gcc + }; + + # Common arguments shared between deps-only and final build + commonArgs = { + inherit src version; + pname = if buildCli then "feos-cli" else "feos"; + strictDeps = true; + + nativeBuildInputs = [ + protobuf # protoc for tonic-build + perl # for openssl vendored build + pkg-config + git # for build.rs git hash + ] + ++ lib.optional (!buildCli) muslCC; + + # Environment variables + PROTOC = "${protobuf}/bin/protoc"; + SQLX_OFFLINE = "true"; + } + // muslEnv; + + # Build dependencies only (for caching) + cargoArtifacts = craneLib.buildDepsOnly ( + commonArgs + // { + doCheck = false; + } + ); + +in +craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + + cargoExtraArgs = if buildCli then "--package feos-cli" else "--package feos"; + + doCheck = false; + + passthru = { + inherit commonArgs cargoArtifacts src; + }; + + meta = { + description = + if buildCli then + "CLI client for FeOS init system" + else + "Minimal Linux init system for hypervisors and container hosts"; + homepage = "https://github.com/ironcore-dev/FeOS"; + license = lib.licenses.asl20; + platforms = [ "x86_64-linux" ]; + mainProgram = if buildCli then "feos-cli" else "feos"; + }; + } +) diff --git a/nix/initramfs.nix b/nix/initramfs.nix new file mode 100644 index 0000000..161da49 --- /dev/null +++ b/nix/initramfs.nix @@ -0,0 +1,99 @@ +# Build the FeOS initramfs (initial root filesystem). +# +# Unlike a NixOS initrd (which is a boot stage that pivots to a real rootfs), +# the FeOS initramfs IS the complete root filesystem. FeOS runs entirely from +# this tmpfs-based rootfs with no disk backing. +# +# Contents: +# /init -> symlink to /bin/feos +# /bin/feos - FeOS init binary (static musl) +# /bin/cloud-hypervisor - VM hypervisor +# /bin/youki - OCI container runtime +# /usr/share/cloud-hypervisor/hypervisor-fw - hypervisor firmware +# /usr/share/feos/vmlinuz - kernel for nested VMs +# /etc/{hosts,hostname,resolv.conf} - basic network config +# /proc, /dev, /sys, /tmp, /run, /var/lib/feos - required mount points + +{ + lib, + runCommand, + feos, + cloud-hypervisor, + youki, + hypervisor-firmware, + kernel ? null, + cpio, + zstd, + cacert, +}: + +runCommand "feos-initramfs" + { + nativeBuildInputs = [ + cpio + zstd + ]; + + passthru = { + inherit feos cloud-hypervisor youki; + }; + } + '' + # Create the rootfs directory tree + rootfs=$TMPDIR/rootfs + mkdir -p $rootfs + + # Directory structure matching FeOS expectations + mkdir -p $rootfs/{bin,etc,var,lib,run,tmp} + mkdir -p $rootfs/{proc,dev,sys} + mkdir -p $rootfs/var/lib/feos + mkdir -p $rootfs/usr/{bin,lib,sbin,local} + mkdir -p $rootfs/usr/share/cloud-hypervisor + mkdir -p $rootfs/usr/share/youki + mkdir -p $rootfs/usr/share/feos + mkdir -p $rootfs/usr/local/ssl/certs + mkdir -p $rootfs/etc/feos + + # Install FeOS binary (statically linked, no library deps) + cp ${feos}/bin/feos $rootfs/bin/feos + chmod 755 $rootfs/bin/feos + + # Create /init symlink (kernel starts /init by default) + ln -s bin/feos $rootfs/init + + # Install cloud-hypervisor + cp ${cloud-hypervisor}/bin/cloud-hypervisor $rootfs/bin/cloud-hypervisor + chmod 755 $rootfs/bin/cloud-hypervisor + + # Install hypervisor firmware + cp ${hypervisor-firmware} $rootfs/usr/share/cloud-hypervisor/hypervisor-fw + + # Install youki container runtime + cp ${youki}/bin/youki $rootfs/bin/youki + chmod 755 $rootfs/bin/youki + + ${lib.optionalString (kernel != null) '' + # Install kernel for nested VMs + cp ${kernel}/bzImage $rootfs/usr/share/feos/vmlinuz + ''} + + # SSL certificates (needed for OCI registry pulls) + cp -rL ${cacert}/etc/ssl/certs/* $rootfs/usr/local/ssl/certs/ || true + + # Basic network configuration + printf '%s\n' "127.0.0.1 localhost" "127.0.1.1 feos" "::1 localhost feos" > $rootfs/etc/hosts + echo "feos" > $rootfs/etc/hostname + echo "nameserver 2001:4860:4860::6464" > $rootfs/etc/resolv.conf + + # Create the initramfs cpio archive compressed with zstd + mkdir -p $out + + # Uncompressed version (used for nested VM initramfs inside the image) + (cd $rootfs && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null > $out/initramfs) + + # Compressed version (used for booting) + zstd -3 < $out/initramfs > $out/initramfs.zst + + # Also provide just the rootfs tree for inspection + cp -a $rootfs $out/rootfs + '' diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..d590fdf --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,223 @@ +# NixOS module for FeOS init system. +# +# This module allows using FeOS as the init system in a NixOS-based image. +# It replaces systemd/other init with FeOS as PID 1 and configures the +# necessary kernel parameters, modules, and initramfs contents. +# +# Usage in a flake: +# +# { +# inputs.feos.url = "github:ironcore-dev/FeOS"; +# +# outputs = { nixpkgs, feos, ... }: { +# nixosConfigurations.myHost = nixpkgs.lib.nixosSystem { +# system = "x86_64-linux"; +# modules = [ +# feos.nixosModules.feos +# { +# services.feos.enable = true; +# } +# ]; +# }; +# }; +# } +# +# For standalone image building (without a full NixOS system), use the +# flake's packages directly: +# nix build .#feos-initramfs # initramfs with FeOS +# nix build .#feos-uki # Unified Kernel Image +# nix build .#feos-kernel # custom kernel + +flakeSelf: + +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.feos; + + feosPackages = flakeSelf.packages.${pkgs.system}; + +in +{ + options.services.feos = { + enable = lib.mkEnableOption "FeOS init system for hypervisors and container hosts"; + + package = lib.mkOption { + type = lib.types.package; + default = feosPackages.feos; + defaultText = lib.literalExpression "feos.packages.\${system}.feos"; + description = "The FeOS binary package."; + }; + + kernel = { + useCustom = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to use the custom FeOS kernel (Linux {version} with + FeOS-specific config) instead of the NixOS default kernel. + + The custom kernel includes optimized settings for SR-IOV, + hugepages, VFIO, and other hypervisor features. + ''; + }; + + package = lib.mkOption { + type = lib.types.package; + default = feosPackages.feos-kernel; + defaultText = lib.literalExpression "feos.packages.\${system}.feos-kernel"; + description = "Custom kernel package to use when `useCustom` is true."; + }; + }; + + cloudHypervisor = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to include cloud-hypervisor in the system."; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.cloud-hypervisor; + defaultText = lib.literalExpression "pkgs.cloud-hypervisor"; + description = "The cloud-hypervisor package."; + }; + }; + + youki = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to include the youki OCI container runtime."; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.youki; + defaultText = lib.literalExpression "pkgs.youki"; + description = "The youki package."; + }; + }; + + firmware = { + package = lib.mkOption { + type = lib.types.path; + default = pkgs.fetchurl { + url = "https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.4.2/hypervisor-fw"; + hash = "sha256-WMFGE7xmBnI/GBJNAPujRk+vMx1ssGp//lbeYtgHEkA="; + }; + defaultText = lib.literalExpression "fetchurl { ... }"; + description = "The hypervisor firmware binary for cloud-hypervisor."; + }; + }; + + grpcPort = lib.mkOption { + type = lib.types.port; + default = 1337; + description = "TCP port for the FeOS gRPC API."; + }; + + extraKernelParams = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Additional kernel command line parameters."; + }; + }; + + config = lib.mkIf cfg.enable { + + # -- Kernel configuration -- + + boot.kernelPackages = lib.mkIf cfg.kernel.useCustom (pkgs.linuxPackagesFor cfg.kernel.package); + + boot.kernelParams = [ + "init=${cfg.package}/bin/feos" + "console=tty0" + ] + ++ cfg.extraKernelParams; + + # Kernel modules required by FeOS + boot.kernelModules = [ + "kvm" + "kvm_intel" + "kvm_amd" + "vfio" + "vfio_pci" + "vfio_iommu_type1" + "vhost_net" + "tun" + "bridge" + ]; + + boot.kernel.sysctl = { + # IPv6 forwarding (FeOS enables this at boot) + "net.ipv6.conf.all.forwarding" = 1; + # Hugepages (FeOS configures 1024 x 2MB pages) + "vm.nr_hugepages" = lib.mkDefault 1024; + }; + + # -- Initramfs contents -- + # Add FeOS and its runtime dependencies to the initramfs + boot.initrd.availableKernelModules = [ + "virtio_pci" + "virtio_blk" + "virtio_net" + "virtio_console" + "virtio_rng" + ]; + + # -- System packages -- + # Make FeOS tools available in the system PATH + environment.systemPackages = [ + cfg.package + ] + ++ lib.optional cfg.cloudHypervisor.enable cfg.cloudHypervisor.package + ++ lib.optional cfg.youki.enable cfg.youki.package; + + # Install hypervisor firmware where FeOS expects it + environment.etc = lib.mkIf cfg.cloudHypervisor.enable { + "feos/hypervisor-fw" = { + source = "${cfg.firmware.package}"; + }; + }; + + # Symlink firmware to the path cloud-hypervisor expects + system.activationScripts.feos-firmware = lib.mkIf cfg.cloudHypervisor.enable '' + mkdir -p /usr/share/cloud-hypervisor + ln -sfn ${cfg.firmware.package} /usr/share/cloud-hypervisor/hypervisor-fw + ''; + + # -- Required directories -- + systemd.tmpfiles.rules = [ + "d /var/lib/feos 0755 root root -" + "d /tmp/feos 0755 root root -" + "d /tmp/feos/vm_api_sockets 0755 root root -" + "d /tmp/feos/consoles 0755 root root -" + ]; + + # -- Firewall -- + networking.firewall.allowedTCPPorts = lib.mkIf config.networking.firewall.enable [ + cfg.grpcPort + ]; + + # -- Assertions -- + assertions = [ + { + assertion = pkgs.system == "x86_64-linux"; + message = "FeOS only supports x86_64-linux."; + } + ]; + + warnings = + lib.optional ( + !cfg.cloudHypervisor.enable + ) "FeOS: cloud-hypervisor is disabled. VM management will not work." + ++ lib.optional (!cfg.youki.enable) "FeOS: youki is disabled. Container management will not work."; + }; +} diff --git a/nix/vm.nix b/nix/vm.nix new file mode 100644 index 0000000..6cfeb21 --- /dev/null +++ b/nix/vm.nix @@ -0,0 +1,162 @@ +# QEMU test VM launcher for FeOS. +# +# Uses direct kernel boot (-kernel + -initrd) to launch FeOS in a VM. +# This avoids needing UEFI firmware for quick testing. +# +# Networking uses passt (Plug A Simple Socket Transport) which provides +# full IPv6 support including Router Advertisements and DHCPv6 — required +# by FeOS's network initialization. passt runs unprivileged (no root needed). +# +# Usage: +# nix run .#vm # launch with defaults +# nix run .#vm -- --memory 4G # override memory +# nix run .#vm -- --kvm # explicitly enable KVM (auto-detected) +# +# The VM exposes: +# - Serial console on the terminal (interactive) +# - gRPC API: connect to the guest's IPv6 address on port 1337 +# - All guest ports are accessible via the host's network stack (passt) + +{ + lib, + writeShellApplication, + qemu, + passt, + kernel, + initramfs, +}: + +writeShellApplication { + name = "feos-vm"; + + runtimeInputs = [ + qemu + passt + ]; + + text = '' + # Defaults + MEMORY="''${FEOS_VM_MEMORY:-2G}" + CPUS="''${FEOS_VM_CPUS:-4}" + KVM_ARGS="" + EXTRA_ARGS=() + + # Auto-detect KVM support + if [ -w /dev/kvm ]; then + KVM_ARGS="-enable-kvm -cpu host" + echo "KVM acceleration enabled" + else + KVM_ARGS="-cpu max" + echo "WARNING: KVM not available, using software emulation (slow)" + fi + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --memory) + MEMORY="$2" + shift 2 + ;; + --cpus) + CPUS="$2" + shift 2 + ;; + --kvm) + KVM_ARGS="-enable-kvm -cpu host" + shift + ;; + --no-kvm) + KVM_ARGS="-cpu max" + shift + ;; + --uefi) + # Boot via UEFI firmware instead of direct kernel boot + # Requires the UKI to be built separately + echo "UEFI boot mode not yet supported via this launcher." + echo "Build the UKI with: nix build .#feos-uki" + exit 1 + ;; + *) + EXTRA_ARGS+=("$1") + shift + ;; + esac + done + + KERNEL="${kernel}/bzImage" + INITRD="${initramfs}/initramfs.zst" + + # Create a temporary directory for the passt socket + PASST_DIR=$(mktemp -d --tmpdir feos-vm.XXXXXX) + PASST_SOCK="$PASST_DIR/passt.sock" + PASST_PID="$PASST_DIR/passt.pid" + + cleanup() { + if [ -f "$PASST_PID" ]; then + kill "$(cat "$PASST_PID")" 2>/dev/null || true + fi + rm -rf "$PASST_DIR" + } + trap cleanup EXIT + + # Start passt in the background. + # passt provides: + # - Router Advertisements (NDP) for IPv6 SLAAC + # - DHCPv6 server (assigns the host's IPv6 address to the guest) + # - Full IPv4/IPv6 connectivity without root + # - Port forwarding: all guest ports are reachable from host + # + # --tcp-ports and --udp-ports forward specific ports. + # By default, passt forwards all ports. + passt \ + --socket "$PASST_SOCK" \ + --pid "$PASST_PID" \ + --foreground & + PASST_BG_PID=$! + + # Wait for the socket to appear + for i in $(seq 1 30); do + if [ -S "$PASST_SOCK" ]; then + break + fi + sleep 0.1 + done + + if [ ! -S "$PASST_SOCK" ]; then + echo "ERROR: passt socket not created after 3s" + exit 1 + fi + + echo "Starting FeOS test VM..." + echo " Kernel: $KERNEL" + echo " Initrd: $INITRD" + echo " Memory: $MEMORY" + echo " CPUs: $CPUS" + echo " Network: passt (IPv6 with RA + DHCPv6)" + echo " gRPC: connect to guest IPv6 address on port 1337" + echo "" + echo "Press Ctrl-A X to exit QEMU" + echo "" + + # shellcheck disable=SC2086 + qemu-system-x86_64 \ + $KVM_ARGS \ + -m "$MEMORY" \ + -smp "$CPUS" \ + -kernel "$KERNEL" \ + -initrd "$INITRD" \ + -append "console=ttyS0 init=/init" \ + -nographic \ + -serial mon:stdio \ + -device virtio-net-pci,netdev=net0 \ + -netdev stream,id=net0,server=off,addr.type=unix,addr.path="$PASST_SOCK" \ + -device virtio-rng-pci \ + -no-reboot \ + "''${EXTRA_ARGS[@]}" + ''; + + meta = { + description = "Launch a FeOS test VM with QEMU"; + platforms = [ "x86_64-linux" ]; + }; +} From a9c26776b19cd6a0b3336cc046f566c01bbf2b0b Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sat, 14 Feb 2026 01:05:04 +0100 Subject: [PATCH 2/2] Add timeouts to DHCPv6 network initialization - Wrap is_dhcpv6_needed() in a channel-based 20s timeout to prevent infinite blocking on Router Advertisement wait (pnet AF_PACKET sockets don't support read_timeout on Linux) - Wrap run_dhcpv6_client() in tokio::time::timeout(30s) to prevent infinite blocking when DHCPv6 server is unavailable - Boot now continues gracefully when no IPv6 infrastructure is present --- feos/utils/src/network/dhcpv6.rs | 33 ++++++++++++++++++++++++++++++++ feos/utils/src/network/utils.rs | 15 ++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/feos/utils/src/network/dhcpv6.rs b/feos/utils/src/network/dhcpv6.rs index 09e7bd8..491e2e3 100644 --- a/feos/utils/src/network/dhcpv6.rs +++ b/feos/utils/src/network/dhcpv6.rs @@ -162,7 +162,37 @@ fn send_router_solicitation(interface: &NetworkInterface, tx: &mut dyn datalink: } } +/// Default timeout for waiting for a Router Advertisement (in seconds). +/// This covers the 5s pre-RS sleep + time waiting for the RA response. +const RA_WAIT_TIMEOUT_SECS: u64 = 20; + pub fn is_dhcpv6_needed(interface_name: String, ignore_ra_flag: bool) -> Option { + // Run the blocking RA listener in a separate thread with a channel-based + // timeout. The rx.next() call on pnet's AF_PACKET socket blocks indefinitely + // even with read_timeout set, so we use a bounded channel recv_timeout to + // enforce the deadline regardless of the socket behavior. + let (tx, rx) = std::sync::mpsc::channel(); + let iface_name = interface_name.clone(); + + std::thread::spawn(move || { + let result = is_dhcpv6_needed_inner(iface_name, ignore_ra_flag); + let _ = tx.send(result); + }); + + let timeout = Duration::from_secs(RA_WAIT_TIMEOUT_SECS); + match rx.recv_timeout(timeout) { + Ok(result) => result, + Err(_) => { + warn!( + "No Router Advertisement received within {RA_WAIT_TIMEOUT_SECS}s. \ + Skipping DHCPv6 network configuration." + ); + None + } + } +} + +fn is_dhcpv6_needed_inner(interface_name: String, ignore_ra_flag: bool) -> Option { let mut sender_ipv6_address: Option = None; let interfaces = datalink::interfaces(); let interface = interfaces @@ -187,6 +217,9 @@ pub fn is_dhcpv6_needed(interface_name: String, ignore_ra_flag: bool) -> Option< if let Some(ra_packet) = RouterAdvertPacket::new(ipv6_packet.payload()) { if (ra_packet.get_flags() & 0xC0) == 0xC0 || ignore_ra_flag { + info!( + "Received Router Advertisement from {sender_ipv6_address:?}" + ); break; } } diff --git a/feos/utils/src/network/utils.rs b/feos/utils/src/network/utils.rs index 0741d5d..60a091f 100644 --- a/feos/utils/src/network/utils.rs +++ b/feos/utils/src/network/utils.rs @@ -193,8 +193,18 @@ pub async fn configure_network_devices() -> Result { + let dhcpv6_timeout = Duration::from_secs(30); + let dhcpv6_result = + tokio::time::timeout(dhcpv6_timeout, run_dhcpv6_client(interface_name.clone())).await; + match dhcpv6_result { + Err(_elapsed) => { + warn!( + "DHCPv6 client timed out after {}s. Continuing without IPv6 address.", + dhcpv6_timeout.as_secs() + ); + } + Ok(Err(e)) => warn!("Error running DHCPv6 client: {e}"), + Ok(Ok(result)) => { send_neigh_solicitation(interface_name.clone(), &ipv6_gateway, &result.address); if let Some(prefix_info) = result.prefix { let delegated_prefix = prefix_info.prefix; @@ -224,7 +234,6 @@ pub async fn configure_network_devices() -> Result warn!("Error running DHCPv6 client: {e}"), } }