在 Steam Link 上安装 NixOS

Lobsters Hottest 工具

摘要

本文描述了如何通过使用自定义内核模块和 kexec 来绕过引导加载程序限制,在 Steam Link 设备上安装 NixOS,将 Arm 硬件重新用于一般用途。

<p><a href="https://lobste.rs/s/wpv5ed/infecting_steam_link_with_nixos">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/26 15:26

# 在 Steam Link 上移植 NixOS 来源:https://feyor.sh/blog/infecting-the-steam-link-with-nixos/ 前几天翻找壁橱时,我发现了一台 Steam Link(https://en.wikipedia.org/wiki/Steam_Link#Hardware_device)——这还是2018年我在促销时购买的,这么多年过去它居然还在默默运转。我想到,拥有一个始终在线的低功耗 Arm 设备,配备以太网、Wi-Fi、蓝牙和多个USB接口,将会非常实用,于是便开始了在 Steam Link 上运行 NixOS 的探索之旅。事实证明,一位名叫 fijam 的开发者(https://heap.ovh/getting-linux-on-valve-steam-link.html)已经解决了在 Steam Link 上运行自定义 Linux 发行版的核心难题。最显著的障碍在于引导程序只会启动由 Valve 签名的内核;为绕过这一点,我们可以启动到 Valve 认可的内核,然后通过 kexec 加载我们自己的新内核。然而,Steam Link 预装的内核并未启用 `CONFIG_KEXEC` 选项。这里便显现出真正的巧思:我们可以将相关的 kexec 源代码文件整合成一个最小化的内核模块,从而为当前系统添加 kexec 系统调用!已有多人成功运用此技术启动其他发行版1(https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#fn:1),但他们似乎都是直接复制 fijam 网站提供的 kexec 二进制文件和内核模块。fijam 看起来是个不错的人,但我对从网络上下载内核模块持谨慎态度,因此决定自行编译。 ## 启动设备 编译 NixOS 用户空间和内核/初始内存盘相当简单;只需将正确的 `system`(以及因为我使用 `__splicedPackages/crossSystem`,还需指定 `pkgs`)传递给 `lib.nixosSystem`,然后添加所需模块即可。选择目标架构则稍显复杂:Valve 的 steamlink 工具链使用 `armv7a`,但在 nixpkgs 中导入 `crossSystem.config = "armv7a-unknown-linux-gnueabihf"` 时会与 Go 构建工具链(https://github.com/NixOS/nixpkgs/blob/fbe840e7184ed15fd5b1ca8f1a8746462a38d59c/lib/systems/default.nix#L596-L599)产生冲突,因此我使用了(看似等效的)`armv7l` 代替。 Nix``` { inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; outputs = { self, nixpkgs }: let inherit (nixpkgs) lib; system = "armv7l-linux"; hostSystem = "aarch64-linux"; pkgs = (import nixpkgs { system = hostSystem; crossSystem = { config = "armv7l-unknown-linux-gnueabihf"; }; }).__splicedPackages; in { nixosConfigurations.steamlink = lib.nixosSystem { inherit system pkgs; modules = [ # ... ]; }; }; } ``` 真正的挑战在于为一个基于13年陈期内核的定制分支编译内核模块;NixOS Wiki(https://wiki.nixos.org/wiki/Linux_kernel#Packaging_out-of-tree_kernel_modules)在此很有帮助,它指出 stdenv 默认安全加固标志可能引发的一些问题。 Nix``` kexecMod = let inherit (pkgs) stdenv; inherit (self.nixosConfigurations.steamlink.config.system.build) kernel oldKernel; in stdenv.mkDerivation { pname = "kexec_mod"; version = "0.0.1"; src = ./kexec_mod; postPatch = '' for f in machine_kexec.c kexec.c relocate_kernel.S; do substituteInPlace "$f" --subst-var-by KERNEL ${oldKernel} done ''; nativeBuildInputs = kernel.moduleBuildDependencies; makeFlags = [ "ARCH=${stdenv.hostPlatform.linuxArch}" "CROSS_COMPILE=${stdenv.cc.targetPrefix}" "KDIR=${oldKernel}" "INSTALL_MOD_PATH=$(out)" ]; env.NIX_CFLAGS_COMPILE = toString [ "-std=gnu89" "-fno-pie" ]; inherit (kernel) hardeningDisable; meta = { description = "kexec functionality as a kernel module for old kernels"; homepage = "https://github.com/lukas2511/steamlink-sdk"; license = lib.licenses.gpl2; platforms = [ system ]; }; }; ``` (请参阅文件部分(https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#files)获取 kexec_mod 源代码。) 为使构建成功,需要将 Kbuild 指向已通过 `make modules`2(https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#fn:2)构建的 Linux 内核源码树。(注意:我使用了 NixOS 配置中相对较新的内核的 `moduleBuildDependencies` 属性。) Nix``` oldKernel = let inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText; inherit (self.nixosConfigurations.steamlink.config.system.build) kernel; in stdenv.mkDerivation { pname = "linux-steamlink"; version = "3.8.13"; src = fetchFromGitHub { owner = "ValveSoftware"; repo = "steamlink-sdk"; rootDir = "kernel"; rev = "62b4d098d1472c3534dd098ca2a0e0e10712f1c6"; hash = "sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs="; }; sourceRoot = "source"; patches = [ (fetchpatch { url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch"; hash = "sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI="; }) (fetchpatch { url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch"; hash = "sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw="; }) (writeText "0001-gcc-bug85745.diff" '' diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h index 74b17d0..dc64fa2 100644 --- a/arch/arm/include/asm/uaccess.h +++ b/arch/arm/include/asm/uaccess.h @@ -164,7 +164,7 @@ #define __put_user_check(x,p)''\t''\t''\t''\t''\t''\t''\t${"\\"} ''\t({''\t''\t''\t''\t''\t''\t''\t''\t${"\\"} ''\t''\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${"\\"} -''\t''\tregister const typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"} +''\t''\tregister typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"} ''\t''\tregister const typeof(*(p)) __user *__p asm("r0") = (p);${"\\"} ''\t''\tregister unsigned long __l asm("r1") = __limit;''\t''\t${"\\"} ''\t''\tregister int __e asm("r0");''\t''\t''\t''\t${"\\"} '') ]; postPatch = '' substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' "a"' substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}' substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail '${"\t"}@$(check_for_multiple_zreladdr)' '${"\t"}echo LDFLAGS_vmlinux = ''${LDFLAGS_vmlinux}${"\n\t"}@$(check_for_multiple_zreladdr)' cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h ''; inherit (kernel) nativeBuildInputs; depsBuildBuild = [ buildPackages.stdenv.cc ]; makeFlags = [ "ARCH=${stdenv.hostPlatform.linuxArch}" "LOCALVERSION=-mrvl" "CROSS_COMPILE=${stdenv.cc.targetPrefix}" ]; env.NIX_CFLAGS_COMPILE = toString [ "-std=gnu89" "-Wno-error=address" "-Wno-error=dangling-pointer" "-Wno-error=missing-attributes" ]; inherit (kernel) hardeningDisable; configurePhase = '' make bg2cd_penguin_mlc_defconfig $makeFlags echo "CONFIG_KEXEC=y" >> .config echo "CONFIG_KERNEL_XZ=y" >> .config make olddefconfig $makeFlags ''; postBuild = '' make modules $makeFlags -j$NIX_BUILD_CORES ''; installPhase = '' mkdir $out cp -r * $out/ ''; dontFixup = true; }; ``` 经过若干补丁调整,终于能在现代版本的 GCC 上成功构建 3.8.13-mrvl 内核与 kexec_mod 内核模块。现在我们已获得 `kexec_mod.ko`,接下来需要新的初始内存盘和内核(这些都来自 NixOS 配置),Steam Link 的设备树二进制文件(已合并到 Linux 主线(https://github.com/torvalds/linux/blob/6812ce4e4379ffc99c52401ec28f0d7ffbc36206/arch/arm/boot/dts/synaptics/berlin2cd-valve-steamlink.dts),可通过 `hardware.deviceTree.package` 获取),一个静态编译的 kexec 用户态工具(用 `pkgsStatic` 编译以确保在非 NixOS 系统上运行),以及一个将所有组件串联起来的小脚本: Bash``` fts-set steamlink.crashcounter 0 # 需要此操作以防止数次重启后触发恢复出厂设置 mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev mount -t proc proc /mnt/disk/proc mount -o rbind /sys /mnt/disk/sys mount -o rbind /dev /mnt/disk/dev insmod /mnt/disk/kexec_load.ko chroot /mnt/disk/ /kexec --load /zImage \ --initrd /initrd \ --dtb /berlin2cd-valve-steamlink.dtb \ --command-line "init=/init root=/dev/sda2 rootwait rw usbcore.autosuspend=-1" chroot /mnt/disk/ /kexec -e ``` 虽然可以手动整理这些文件并上传到 USB 驱动器,但使用 sd-image(https://github.com/NixOS/nixpkgs/blob/5f5458dc42bf4391dc5f85e7682decb8c78e8756/nixos/modules/installer/sd-card/sd-image.nix)NixOS 模块创建磁盘镜像会方便得多: Nix``` usb-image = { modulesPath, config, ... }: { imports = [ (modulesPath + "/installer/sd-card/sd-image.nix") ]; image.extension = lib.mkForce "img"; sdImage = let dtb = "berlin2cd-valve-steamlink.dtb"; kexecScript = ./kexec-nixos; inherit (config.system.build) kernel initialRamdisk; in { compressImage = false; firmwarePartitionName = "STEAMLINK"; rootVolumeLabel = "NIXOS"; populateFirmwareCommands = '' pushd firmware files=( ${kernel}/${config.system.boot.loader.kernelFile} ${initialRamdisk}/${config.system.boot.loader.initrdFile} ${config.hardware.deviceTree.package}/${dtb} ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko ${pkgs.pkgsStatic.kexec-tools}/bin/kexec ) for f in ''${files[@]}; do cp $f ./ done # factory_test/run.sh 将在启动时优先执行 # 若不直接启动 NixOS 可启用 SSH,需取消注释 # mkdir -p steamlink/config/system # touch steamlink/config/system/enable_ssh.txt mkdir -p steamlink/factory_test cp ${kexecScript} steamlink/factory_test/run.sh popd ''; populateRootCommands = ""; }; }; ``` 测试 kexec 交接过程异常棘手,因为我使用的内核无法输出 HDMI 信号,且决定不拆机获取 UART 接口,这使我完全处于盲操作状态。我使用了一个基于 Busybox 的精简初始内存盘(slop)进行测试,它会在可变时间后重启以表示成功。 Nix``` initramfs = pkgs.buildPackages.runCommand "build-initramfs" {} '' mkdir initramfs; cd initramfs mkdir -pv {etc,proc,sys,usr/{bin,sbin}} cp -a ${pkgs.pkgsStatic.busybox}/{bin,sbin} . chmod 755 ./{bin,sbin} cat < init #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev mkdir -p /mnt try_mount() { dev="$1" fs="$2" if [ "$fs" = auto ]; then mount -o rw "$dev" /mnt 2>/dev/null || return 1 else mount -t "$fs" -o rw "$dev" /mnt 2>/dev/null || return 1 fi marker=kexec-mounted-ok if [ -f /mnt/zImage ]; then marker=kexec-steamlink-ok fi { echo "device=$dev" echo "fs=$fs" cat /proc/partitions } > "/mnt/$marker" 2>/dev/null && sync umount /mnt sleep 10 reboot -f } for dev in /dev/mmcblk*p* /dev/sd[a-z][0-9]* /dev/vd[a-z][0-9]*; do [ -b "$dev" ] || continue try_mount "$dev" vfat try_mount "$dev" ext4 try_mount "$dev" auto done sleep 45 reboot -f EOF chmod +x init find . -print0 | ${lib.getExe pkgs.buildPackages.cpio} --null -ov --format=newc > $out ''; ``` 确认该流程有效后,我切换到启用 `boot.initrd.network.enable = true` 的 NixOS 初始内存盘,并使用基于 Netcat 的反向连接到我的笔记本 IP 以进行进一步调试。 Nix``` debugModule = { lib, ... }: { boot.initrd.systemd.enable = lib.mkForce false; boot.initrd.kernelModules = [ "pxa168_eth" ]; boot.initrd.availableKernelModules = [ "reset_berlin" ]; boot.initrd.network.enable = true; boot.initrd.network.udhcpc.enable = false; boot.kernelParams = [ "ip=192.168.2.2::192.168.2.1:255.255.255.0:stm-link:eth0:off" ]; boot.initrd.network.postCommands = '' mac_peer=192.168.2.1 stm_link_ip=192.168.2.2 echo "initrd net debug: interfaces: $(ls /sys/class/net)" > /dev/kmsg for iface_path in /sys/class/net/*; do iface="''${iface_path##*/}" [ "$iface" != lo ] || continue echo "initrd net debug: configuring $iface" > /dev/kmsg ip link set dev "$iface" up || true ip address flush dev "$iface" || true ip address add "$stm_link_ip/24" dev "$iface" || true done ( while true; do ping -c 1 -W 1 "$mac_peer" sleep 2 done ) & ( while true; do rm -f /tmp/revsh mkfifo /tmp/revsh /bin/ash -i < /tmp/revsh 2>&1 | nc "$mac_peer" 4444 > /tmp/revsh rm -f /tmp/revsh sleep 2 done ) & ''; }; ``` 此阶段我需要解决的主要问题包括:将 `reset_berlin` 添加到 `boot.initrd.availableKernelModules` 以允许读取 USB 驱动器,以及使用传统 NixOS 初始内存盘系统替代基于 systemd 的新版本(`boot.initrd.systemd.enable = lib.mkForce false`)。最终成功启动用户空间并通过 SSH 连接!🥳 不过,我启动时使用的 USB 镜像体积高达 2.3GB... 我们显然可以做得更好。 ## 精简体积 令人惊讶的是,关于缩减 NixOS 闭包大小并无权威指南;我找到一些 NixOS 论坛讨论和几篇博文,但最有用的资料是《NixOS is a good server OS, except when it isn’t》(https://sidhion.com/blog/nixos_server_issues)和《I can haz smoller NixOS ISOs?》(https://natkr.com/2026-06-19-nixos-but-smol/)。虽然这些资源很有价值,但由于我们针对的是实体硬件而非虚拟机,裁剪内容时必须更加谨慎。 Nix``` minimal = { modulesPath, pkgs, ... }: { imports = [ (modulesPath + "/profiles/minimal.nix") (modulesPath + "/profiles/headless.nix") # (modulesPath + "/profiles/perlless.nix") ]; disabledModules = [ (modulesPath + "/profiles/base.nix") ]; boot.loader = { grub.enable = false; systemd-boot.enable = false; supportsInitrdSecrets = false; }; boot.initrd.systemd.enable = lib.mkForce false; boot.initrd.availableKernelModules = lib.mkForce [ "reset_berlin" "uas" ]; boot.kernelModules = [ "pxa168_eth" "mwifiex_sdio" "btmrvl_sdio" ]; hardware.firmware = lib.mkForce (with pkgs; [ (runCommand "marvell-firmware" {} '' mkdir -p $out/lib/firmware/mrvl cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/ '') wireless-regdb ]); documentation.enable = false; programs.command-not-found.enable = lib.mkDefault false; networking.networkmanager.enable = false; networking.firewall.enable = false; xdg.icons.enable = false; xdg.mime.enable = false; xdg.sounds.enable = false; fonts.fontconfig.enable = false; programs.nano.enable = false; system.disableInstallerTools = true; system.switch.enable = false; system.nixos-init.enable = false; nix.enable = false; systemd.services.register-nix-paths = lib.mkForce {}; }; ``` 精简过程中的主要调整如下: - 我们仅需从庞大的 `linux-firmware` 包(压缩后仍达 1.8GB!)中提取一个固件二进制文件,通过仅添加所需文件到 `hardware.firmware` 可节省大量空间 - 同理,理论上应可为 Steam Link 硬件定制内核配置以构建更小的内核,但这可能得不偿失 - 移除 `i18n.glibcLocales` 或 `security.wrappers` 模块时 OpenSSH 似乎会出现异常 - kexec 引导流程使得大部分 NixOS 系统管理工具失去意义;实际上 Nix 在此类系统上用处不大,移除它也能节省空间 - 理论上应可通过禁用 `boot.initrd` 和 `boot.kernel` 来节省空间(因为我们在 kexec 时通过 FAT32 分区提供内核/初始内存盘/设备树),但我始终未能在不破坏引导过程的情况下禁用它们 - 若能切换到基于 systemd 的初始内存盘,即可启用 "perlless" NixOS 模块以从系统闭包中移除 Perl 进一步缩减体积 当优化进入收益递减阶段且大多数新改动都会导致系统崩溃时,我将当前 1.2GB 的磁盘镜像视为“足够好”的状态。 ## 文件 本文使用的 Nix flake 和 kexec_mod 内核模块源代码可在此处下载(https://feyor.sh/infecting-the-steam-link-with-nixos/steamlink-nixos.tar.gz)。相同的 `flake.nix` 文件复制如下

相似文章

Lanzaboote – NixOS 安全启动

Hacker News Top

本文介绍了 Lanzaboote,这是一个用 Rust 编写的 UEFI UKI 存根,可为 NixOS 提供安全启动支持。它将签名检查推迟到 UEFI 阶段执行,同时保持内核和 initrd 与 UKI 二进制文件分离,从而解决了 NixOS 特有的启动挑战。

将我的 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS

Michael Stapelberg

Michael Stapelberg 详细介绍了他将一台 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS 的过程,涵盖了从 Docker 容器逐步过渡到原生 NixOS 模块的步骤,并附有实际示例。

驯服 Steam arm64 客户端(在 pmOS 上)

Lobsters Hottest

一篇博客文章,详细介绍了在 postmarketOS 上运行非官方 Steam arm64 客户端的各种蹊跷与挑战,包括客户端未意识到自己是 arm64、缺少 Proton/运行时下载,以及提及 FEX 和图形提供商清单(与 Valve 即将推出的 Steam Frame 相关)。