NixOS 镜像中的 Secret 管理
给 NixOS 的 Installer Image 加入 Secret 管理支持。 背景 玩 NixOS 玩到一定程度,都将面对 Secret 管理这一经典问题,当然对于一般的 NixOS Setup 来说,这并不是非常复杂的问题,有非常多的工具可以解决,如 sops-nix、agenix、vaultix 等。 但是,我们的 NixOS 系统还包括 Installer,要是 Installer 中也需要使用一些 Secret 怎么办呢?最近我就想定制一个 Installer ISO 包括一个完整的透明代理服务,以方便安装过程中访问某些不存在的网站。透明代理的节点和订阅链接都是需要保密的。然而这些 Secret 管理方案都不能直接用在 ISO 上,这也是本文需要解决的问题。 方案设计 Secret 管理基本机制 首先我们回顾一下在普通的 NixOS 系统中如何管理 Secret。 Secret 的内容需要保密,不可以直接内联在 NixOS 配置中,也不可以在 NixOS 构建阶段直接引用,否则会被复制到 /nix/store 中。这些内容需要首先被加密后再用于构建和部署,在系统真正开始运行时再动态地解密。 每个 Secret 都是以非对称加密的方式被保护的。Secret 需要可以被维护者修改,也需要可以被需要的机器解密。前者的加密解密使用用户所有的密钥对,一般称为 Identity,后者的加解密则使用机器所有的密钥对,一般就称为 Key,大多数情况下会使用 SSH key /etc/ssh/ssh_host_{rsa,ed25519}_key{,.pub}。用户使用 Identity 的 Public Key 加密明文,后续可以使用 Identity Private Key 查看已加密的内容。同时,对于需要使用这个 Secret 的机器,需要使用该机器的 Public Key 重加密内容,以便该机器可以用自己的 Private Key 在运行时解密内容。重加密过程需要用 Identity Private Key 解密获取原始内容。 ...
现代 Linux 桌面会话管理
最近尝试了使用 Wayfire 作为 Wayland 混成器,整体体验后,Wayfire 的功能是不错的,可惜其没有 systemd 支持,对于依赖 systemd 的我来说,这显然是不能够满足日常需求的。因此,我研究了如何为其添加基于 systemd 的会话管理机制,记录在此文中。 桌面会话的生命周期 传统方式 在 Linux 中,本没有什么非常精细的会话管理机制,无论是 Shell 登录还是图形界面登录,一切都可以是非常简洁的流程完成。 Shell 登录时,Login Manager 以登录用户身份启动 Login Interactive Shell 进程,Shell 则运行相应的 Profile 脚本,如 bash(1) 加载 ~/.bash_profile 或 ~/.profile,作为最简单的会话管理机制,运行一些后台任务。对于图形界面来说,可以直接 startx、加载 ~/.xinitrc 、启动 X 服务器和 Window Manager 等进程,或者运行 Wayland 混成器,或者由 Display Manager 负责创建 X 服务器或 Wayland 混成器进程,而图形环境所需要的程序要么通过 ~/.xinitrc 这样的脚本启动,要么在 Window Manager 的配置文件中指定。 无论是什么方法,都是原始的机制。尽管它们原理十分简单,却存在诸多弊端,比如配置服务启动的方式不统一,Shell 登录、X 环境登录和 Wayland 环境登录需要各种不同的配置文件、需要写脚本以命令式的方式完成;比如这种方式没有充分利用 systemd 的支持,集成差、体验割裂。在 Linux 桌面系统中 systemd 早已普及的今天,这种原始方式显然跟不上时代。 基于 systemd 的现代方式 systemd 早已为 Linux 的各种应用场景设计好了一套服务和会话管理的框架,其关键点在于各种预定义的 target。target 是系统的生命周期的各个阶段起始或终止的标志,只要为每个程序写好 service 文件,并设置好其与 target 的关系,systemd 即可完全自动地在特定时间节点做好规划的工作,运行各种服务。 ...
为 EndeavourOS 构建 Linux 内核
简单记录一下在 EndeavourOS 上手动编译 Linux 内核的过程。 环境准备 整个过程在虚拟机上完成,具体配置参数如下: Hypervisor: VMware Workstation Pro 17 OS: EndeavourOS Mercury Neo Bootloader: systemd-boot Old Kernel: 6.16.8-arch3-1 New Kernel: 6.17.1 CPU: 8 x 13th Gen Intel(R) Core(TM) i7-13700HX @ 2.30 GHz RAM: 8 GB IP:VMnet8 (NAT) 192.168.255.11 为了操作方便,通过 SSH 从宿主机连接到虚拟机,运行以下命令连接,其中 starryreverie 为已创建用户: ssh starryreverie@192.168.255.11 获取内核源码 内核源码可以从 kernel.org 下载。选择 linux-6.17.1.tar.xz 源码包,并使用以下命令下载: wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.1.tar.xz 在本地新建目录 build,并解压源码包: mkdir build tar -xvf linux-6.17.1.tar.xz -C build 进入源码目录并查看目录: cd build/linux-6.17.1 ls -alh 配置编译环境 在编译内核前,需要安装必要的工具和依赖包。使用以下命令安装: ...
结合 SPPM 的 Path Tracing
随机渐进式光子映射(Stochastic Progressive Photon Mapping, SPPM)是光子映射系列算法的进阶算法,本文介绍如何实现 SPPM 以及如何将 SPPM 与路径追踪(Path Tracing, PT)结合。 光子映射系列算法 光子映射 光子映射(Photon Mapping, PM)是光子映射系列算法中的初代算法,是一个两阶段算法,包括光子追踪和光子映射。其思想就是首先从光源发射光子,光子在场景中不断反弹,模拟现实世界的过程,然后再使用 Path Tracing,过程中在合适的地方使用光子来估计 radiance,加速渲染。 光子的发射 光子是光源的通量/功率的载体,光源实质上是通过发射光子来实现照明。PM 中的光子与物理中的光子不同,物理学中的光子是光能传递的最小单元,即量子化的性质,单个光子的能量是 $E = h\nu$,而 PM 中的光子实际上是一堆物理中的光子,是更符合应用的模型。所以,接下来所讨论的光子都是 PM 意义下的光子。 如何计算单个光子的通量呢?发射光子的过程实际上就类似于对光源的总通量进行 Monte Carlo 估计的过程,每个光子都是一个样本,每一份通量相加就得到了总通量。所以我们就对光源表面和出射方向进行采样,按照类似的过程计算每一个光子的通量。总通量按照以下积分计算 $$ \begin{aligned} \Phi & = \int_M \int_{\Omega^+} L_e(x, \omega) (n \cdot \omega) \mathrm d\omega \mathrm dA \\ & \approx \dfrac{1}{N} \sum_{i = 1}^N \dfrac{L_e(x, \omega) (n \cdot \omega)}{p(x)p(\omega)} \end{aligned} $$所以单个光子的通量就是 $\dfrac{1}{N} \dfrac{L_e(x, \omega) (n \cdot \omega)}{p(x)p(\omega)}$,$N$ 表示被发射光子的总数。严格意义上来说,通量的定义是不考虑立体角和照明面积的,所以所谓的单个光子的通量实际上是通量的二阶差分 $\Delta^2 \Phi$。在实际实现中,我们在计算通量时并不会除以 $N$,这是因为在 SPPM 中光子数量会随着迭代次数增加而增加,因此我们也将其 $\dfrac{L_e(x, \omega) (n \cdot \omega)}{p(x)p(\omega)}$ 视为一种没有放缩的通量,直到最后再除以 $N$ 得到真正的通量。 ...
光线追踪中的 Multiple Importance Sampling
本文主要介绍多重重要性采样(Multiple Importance Sampling)及其在光线追踪中的典型应用。 Multiple Importance Sampling Monte Carlo Path Tracing 在 Ray Tracing 中,最核心的技术就是 Monte Carlo Estimation。通过使用 Monte Carlo Estimation,我们可以求解 Rendering Equation。对于 Monte Carlo Estimation 来说,一个合适的采样方法可以极大提升收敛的速度,具体来说,对于以下积分及其 Estimator $$ I = \int_D f(x) \mathrm dx \approx \dfrac{1}{N} \sum_{i = 1}^N \dfrac{f(X_i)}{p(X_i)} $$$X_i$ 的最佳的采样分布应该满足 $p(X_i) \propto f(X_i)$。 然而对于 Rendering Equation 来说,这个条件难以满足,因为其被积函数包含了 cosine-weighted BSDF 和 radiance 两部分因子,后者更是需要递归求解的未知项,显然没有办法进行完美的采样。 $$ L_o(x, \omega_o) = L_e(x, \omega_o) + \int_{\Omega^+} f_s(x, \omega_o, \omega_i) L_i(x, \omega_i) (n \cdot \omega_i) \mathrm d\omega_i $$一般情况下,我们只能选择让采样分布于其中一部分因子的形状近似,也就产生了两种采样——BSDF 采样(包括了余弦项)和光源采样(光源贡献的 radiance 应该比非直接的更大)。 ...
无需宏为 Trait Objects 实现 Any
std::any::Any 是 Rust 在运行时进行类型擦除和转换的工具,所有 'static 类型都实现了 Any,因此装箱为 Box<dyn Any> 后可以借助 Any::type_id() 获取 TypeId,还可以通过 dyn Any 的 downcast_*() 等方法再转换回具体类型。 然而问题也出在 dyn Any 的 downcast_*() 方法。这些方法是 dyn Any 的方法,而不是 Any trait 中的方法,所以其他任何 dyn Trait 都不会拥有这些方法,即使有 Trait: Any。另一方面,由于 trait upcasting 直到最近才成为稳定特性,且还没进入 stable 版本,所以依赖语言支持的 trait upcasting 来转换为 dyn Any 对于较早版本的项目并不合适。 因此,本文则通过纯 Rust 语法来扩展 trait object,以实现 Any 的所有功能。这些实现都可以在 better-as-any 找到。 现有解决方案 downcast downcast crate 通过宏来生成代码,直接为指定的 dyn Trait 添加 is()、downcast_*() 方法。这种做法在最终效果上与 dyn Any 完全一样,但是需要使用 impl_downcast!() 宏来实现。我个人则偏好能使用语言特性实现就不使用宏。 as-any 这个 crate 不使用宏实现了功能,其中的 AsAny 可以把任意类型的引用转换为对 dyn Any 的应用,即 trait upcasting。同时其有 Downcast trait 可以把引用向下转型为具体类型的引用。 ...
anyerr 上下文中的类型体操
在对 Rust 错误处理的思考和 anyerr一文中,我介绍了 anyerr 这一个错误处理库,其可以携带上下文信息,且储存上下文的数据结构是可定制的。本文则聚焦 anyerr 是如何实现这样的特性的。 上下文的核心特性 基本结构和表示 在设计之前,首先我们要明确需求。什么样的上下文数据结构是我们所需要的?携带上下文是为了能够记录某些变量所保存的值,我们需要记录变量的名称和其中的值。上下文可以有很多种类,但所有的上下文都可以表示为一个键值映射表。 所以以下是我们对一个上下文储存的基本特性的定义: pub trait AbstractContext: Default + Debug + Send + Sync + 'static { type Key; type Value; type Entry: Entry<Key = Self::Key, Value = Self::Value>; type Iter<'a>: Iter<'a, Entry = Self::Entry> where Self: 'a; fn iter(&self) -> Self::Iter<'_>; } 这样的一个 trait 定义仅仅规定了一个上下文的键值对类型和迭代其中元素的方法,却没有插入或者其他查询的方法。这是因为一个上下文不一定需要真的携带有信息,如果不需要上下文,那么一个不带有任何信息的上下文就可以非常好地适用于这种场景,这也是为什么这个 trait 叫做 AbstractContext。anyerr 针对这样的情况有特殊的优化,这些后面再说。 上下文的元素 AbstractContext::Entry 规定了上下文中每一个元素的类型,其应当实现 Entry trait。以下则是 Entry 的定义: pub trait Entry: Debug + Send + Sync + 'static { type Key: Borrow<Self::KeyBorrowed> + Debug + Send + Sync + 'static; type KeyBorrowed: Debug + Display + Eq + Hash + ?Sized + Send + Sync + 'static; type Value: Borrow<Self::ValueBorrowed> + Debug + Send + Sync + 'static; type ValueBorrowed: Debug + ?Sized + Send + Sync + 'static; fn new<Q, R>(key: Q, value: R) -> Self where Q: Into<Self::Key>, R: Into<Self::Value>; fn key(&self) -> &Self::KeyBorrowed; fn value(&self) -> &Self::ValueBorrowed; } Entry trait 具体规定了键和值的类型以及创建和访问的方法。在 AbstractContext 中,还要将 AbstractContext::Key、AbstractContext::Value 和 Entry::Key、Entry::Value 匹配。 ...
对 Rust 错误处理的思考和 anyerr
错误处理是 Rust 中核心的一部分,从标准库中的 Result<T, E> 和 Error 到社区的 anyhow、thiserror、color-eyre、snafu 等 crates,可见其重要地位。但是在我看来,这些仅仅是错误处理机制的基础,而不是一个十分完备的框架,同时某些 crate 的设计,要么不能符合实际需要,要么使用起来很麻烦。本文将阐述我对 Rust 错误处理的理解和自己的实践 anyerr。 std 中的基础设施 在讨论我对错误处理的理解之前,有必要先回顾标准库中与错误处理相关的基础设施。 截至本文写作时间,Rust 的最新版本为 1.84.1,接下来将以此版本为基础进行讨论。 错误与结果 标准库中最广为人知的一个类型就是 Result<T, E>,用来表示一个可能成功或失败的结果。这是一个非常精妙的设计,主要体现其可以编码成功或失败中的一者,而不是将失败结果糅合进成功结果的值中。C 广泛采用后一种处理方式,导致 API 的混乱,当然这很大一部分是历史原因。在 Java 等以异常为主要错误处理机制的语言中,这一问题有了很大改善,但这又引入了隐式控制流的问题,Result<T, E> 则又避开了这个问题。所以,Result<T, E> 应该是一个很不错的机制。 尽管 Result<T, E> 中的 E 代表错误,但标准库对其具体应是什么类型没有什么限制。一般情况下,E 是一个实现了 Error trait 的类型,或者是其他可以间接地访问到内部的错误的类型,如 Box<dyn Error>。 错误类型的统一契约 若某类型实现了 Error,那么其就可以以一种标准的形式来被集成的错误处理的框架中。Error 规定了如何显示错误信息(通过 Display trait)和如何溯源错误(通过 <Self as Error>::source())。 在 Rust 早期,并没有 Error,错误处理是处于一种野蛮生长的状态。直到 Error 的引入,Rust 才可以算是有了一套标准的错误处理机制。 错误的传播 Result<T, E> 虽好,但在深层函数调用中,一次次手动向上传播错误却很麻烦。? 运算符则有效解决了这个问题,实现了把错误方便的提前返回,传播给调用方。 ? 的使用不要求被传播错误类型和接受的错误类型完全相同,只需要有 From 的联系,即如果 E1: From<E2>,那么对 Result<U, E2> 使用 ? 就可以把错误传播为 Result<T, E1>。 ...
统一 Linux GUI 框架主题和外观
在 Linux 下,GUI 外观配置一直是一个复杂的话题。本文试图梳理 Qt 和 GTK 两种 GUI 框架的相关概念,并给出不同情况下的配置方案,实现外观的统一。 本文讨论的 Qt 包括 Qt 5 和 Qt 6,GTK 包括 GTK 2、GTK 3、GTK 4,并且将以 Qt 6 和 GTK 4 为重点。测试的 DE 和 WM 包括 GNOME 4.46、KDE Plamsa 6.1、Hyprland 0.41。 配置组成 基本概念 外观配置一般包括以下几个方面: 主题/Theme:这是一个比较广泛的概念,一般包括了样式、图标和鼠标指针等各配置项在内。 样式/Style:一般指程序窗口、面板、组件的外观。 图标/Icon 指针/Cursor 字体/Font 配色方案/Color Scheme:较细粒度的配置项,诸如主要颜色、强调颜色的配置都属于配置方案。 声音/Sound …… 这是一个比较广泛的定义,具体到各框架,又会产生一定的变化。 GTK GTK 中可直接配置的部分相对较少,主要是: 主题/Theme:主要与一般定义中的样式/Style 对应。 图标/Icon Theme:与一般定义中的图标/Icon 相同。 指针/Cursor Theme:与一般定义中的指针/Cursor 相同。 字体/Font:与一般定义中的字体/Font 相同。 包括 GNOME 在内的基于 GTK 开发的 DE 基本上直接使用上述概念,利用这些 DE 的工具配置外观,基本上就是对 GTK 的配置直接修改。 ...
用 NixOS 部署 Minecraft 服务器
紧张的考试周过后,终于有时间和朋友玩 Minecraft 了。为了方便进行多人游戏,我和 @whitepaperdog 决定购买一台云主机作为服务器。鉴于 NixOS 强大的 reproducibility 以及我这半年使用 NixOS 的优秀体验,我决定将服务器的系统更换为 NixOS,并在其之上部署 Minecraft 服务。 准备工作 NixOS 安装 这台服务器并不是用我的账号买的,所以我没有办法直接在控制台上传 NixOS 的镜像进行安装。等我拿到 root 密码后,系统就已经是 Ubuntu 了。在此情况下,我选择了使用 NixOS-infect。NixOS-infect 是一个 shell 脚本,其在服务器上安装 Nix,再用 Nix 构建出 NixOS,最后修改 bootloader 配置,添加 NixOS 的启动项并删除其他东西。 使用 NixOS-infect 前需要配置好访问 root 的 SSH 公钥,这是因为 NixOS-infect 并没有提供设置 root 密码的步骤,并且 NixOS 默认情况下将禁用 SSH 通过密码登录 root。NixOS-infect 脚本会将原有的 root 的公钥重新导入到新的 NixOS 里。 由于服务器在国内,所以访问 nixpkgs 的 cache 将会非常慢,在安装 NixOS 之前需要配置好 cache 镜像。编辑脚本,在完成 Nix 的安装后,配置镜像源: infect() { # ... NIX_INSTALL_URL="${NIX_INSTALL_URL:-https://nixos.org/nix/install}" curl -L "${NIX_INSTALL_URL}" | sh -s -- --no-channel-add # 添加以下 3 行 cat << EOF > /etc/nix/nix.conf substituters = https://mirror.sjtu.edu.cn/nix-channels/store https://mirrors.ustc.edu.cn/nix-channels/store https://cache.nixos.org/ EOF # shellcheck disable=SC1090 source ~/.nix-profile/etc/profile.d/nix.sh # ... } 随后就是运行脚本,等待 SSH 断开,安装完成。 ...