Tribuchet
用于 Nix 的远程构建执行,基于实验性的
external-builders 特性:一个位于 nix-daemon 旁边的中心节点将构建任务
分派给远程工作节点,这些工作节点在各自的沙箱中执行构建,并将
日志和输出流式传输回等待的 nix build。
状态:实验性。 它依赖于 Nix 的实验性
external-builders特性(外加一个用于 uid-range 构建的小补丁) 并且协议和配置可能仍会发生变化。
为什么不是 --builders / SSH 构建钩子?
经典的远程构建协议需要能够 SSH 访问每个 构建器,在其上安装 Nix,并且在不进行调度的情况下复制闭包。 tribuchet 从 Nix 接收完整的构建环境,并自行负责 传输、调度和执行。
特性
- Workers 通过 gRPC 使用双向 TLS 与 hub 建立连接,因此它们可以位于 NAT 之后,并注册其服务的系统和功能。
- Hub 调度采用按系统划分的队列和能力匹配
(
kvm,uid-range,big-parallel, …);相同的提交共享 同一个构建。 - 仅缺失的输入路径会被传输,以 zstd 压缩的 NAR 形式;输出 作为由 worker 签名(ed25519)的 NAR 返回,且不进行 store-path 重写。
- 构建在 hub 和 worker 重启/重新加载后依然存活,因此部署不会 终止进行中的构建;当客户端断开时,它们会被取消。
- 沙箱机制与 Nix 自身的等效(Linux 命名空间 + 按构建划分的
cgroup 限制,macOS 下按构建用户运行的 Seatbelt),外加
uid-range构建以及 跨系统用户态模拟。 - 固定输出派生项通过 presto-pasta
(嵌入式用户态 NAT)在一个否则隔离的网络命名空间中获取网络,
并带有可选的允许/拒绝流策略(
fod-networkin worker.toml)。 - 跨重新加载/重启的实时构建日志,带有 max-log-size、 max-silent-time 和超时强制执行。
- 针对这两个服务的 NixOS 和 nix-darwin 模块。
快速入门
tribuchet 是一个包含四个子命令的二进制文件:hub、worker、
attach(Nix 执行的 shim)以及 ca。
1. 证书
Worker 使用来自私有 CA 的客户端证书向 hub 进行身份验证:
$ tribuchet ca init --dir ./ca
$ tribuchet ca issue hub --dir ./ca # SAN must match the hub address workers dial
$ tribuchet ca issue worker --dir ./ca # one per worker
Hub 从 <config-dir>/ca 读取 ca/{hub.crt,hub.key,ca.crt}
(默认 /etc/tribuchet/ca);每个 worker 获得 ca.crt 及其自身的
密钥对(默认 /var/lib/tribuchet/tls/)。
或者,在两端设置 auth = "tailscale" 以完全跳过 TLS:
worker 拨号 http://<hub-tailnet-name>:7437,hub
在每次连接时针对 tailscaled 的 LocalAPI 查找对等节点
(因此任何不在 tailnet 上的节点都会被拒绝),并使用节点名称
作为 worker 身份。使用
tailscale-allowed-tags = ["tag:tribuchet-worker"] 将注册限制为特定的 ACL 标签。
2. Hub(在运行 nix-daemon 的机器上)
/etc/tribuchet/hub.toml:
socket = "/run/tribuchet/hub.sock" # for tribuchet attach
listen = "0.0.0.0:7437" # for workers
config-dir = "/etc/tribuchet"
将 Nix 指向 nix.conf 中的 attach shim:
experimental-features = external-builders
external-builders = [{"systems":["x86_64-linux","aarch64-linux"],"program":"/path/to/tribuchet-attach"}]
其中 tribuchet-attach 是一个包装脚本:
#!/bin/sh
exec tribuchet attach "$1" --socket /run/tribuchet/hub.sock
可选地在
/etc/tribuchet/trusted-signing-keys 中固定 worker 签名密钥(每行一个 name:base64,语法与
trusted-public-keys 相同)。
3. Workers
Workers 需要拥有自己正在运行的 nix-daemon(输入通过它导入,并通过临时根目录从垃圾回收中保护)。 worker 以非特权方式运行,并将每个构建租约分配给按 uid 划分的 agent 服务(由 NixOS 和 nix-darwin 模块设置),该服务拥有构建器进程及其 uid 块。
/etc/tribuchet/worker.toml:
hub = "https://hub.example.org:7437"
max-jobs = 4
max-log-size = 67108864
[emulate]
aarch64-linux = "/path/to/static/qemu-aarch64"
$ tribuchet worker --config /etc/tribuchet/worker.toml
两个文件的完整选项集记录在
crates/tribuchet/src/config.rs。
工作进程的 TLS 路径可以通过 TRIBUCHET_CA_CERT、
TRIBUCHET_CERT 和 TRIBUCHET_KEY 环境变量进行覆盖,例如
指向由 systemd LoadCredential 提供的密钥(NixOS
模块的 services.tribuchet-worker.keyFile 会执行此操作)。
NixOS
导入 tribuchet.nixosModules.default(flake 输入
github:Mic92/tribuchet)并启用服务:
{
# hub machine
services.tribuchet-hub.enable = true;
# optional: route this machine's nix-daemon builds through the hub
services.tribuchet-hub.externalBuilders = {
enable = true;
systems = [ "x86_64-linux" "aarch64-linux" ];
};
# worker machines
services.tribuchet-worker = {
enable = true;
settings = {
hub = "https://hub.example.org:7437";
max-jobs = 4;
};
};
}
hub 单元采用套接字激活;worker 单元在包或配置变更时执行重载而非重启,因此正在运行的构建在部署过程中得以保留。
macOS (nix-darwin)
tribuchet.darwinModules.default 为 launchd 提供相同的两个服务:hub 从 launchd 接管其套接字,worker 守护进程通过一个稳定的符号链接执行,该链接在激活时切换并发送 SIGHUP 信号,同样确保构建在升级过程中保持存活。
容器
对于没有 Nix 的主机,flake 会构建一个 OCI 镜像,
packages.x86_64-linux.worker-image。它自带 Nix 存储,
启动 nix-daemon 和 worker,并根据 worker.toml 中的
spawn-agents 和 agent-uid-base 生成其构建代理。无需添加
任何 capabilities,但沙箱会创建命名空间和
挂载点,而默认运行时 seccomp 配置禁止这些操作。请使用
packages.x86_64-linux.seccomp-profile 中的配置并取消
/proc 的掩码:
$ podman run -d --name tribuchet-worker \
-v /etc/tribuchet:/etc/tribuchet:ro \
-v tribuchet-nix:/nix \
--security-opt seccomp=$(nix build --print-out-paths .#seccomp-profile) \
--security-opt unmask=ALL \
tribuchet-worker:latest
沙箱所需的额外 syscall 规则也可单独在 nix/seccomp-additions.json 中获取,
以便追加到您选择的任何基础配置中。
对于 docker,请将 unmask=ALL 替换为 systempaths=unconfined。在
Kubernetes 上,将配置作为 Localhost seccomp 配置文件分发,或者回退到
Unconfined,并设置 procMount: Unmasked。
与 NixOS 模块相比,代理不会获得委托的 cgroup,因此
build-memory-max 和 uid-range 构建不可用。/nix
卷仅是一个缓存。守护进程通过镜像中内置的
min-free 和 max-free 设置对其进行垃圾回收。
固定输出网络策略
在具有 /dev/net/tun 的 Linux 工作节点上,固定输出构建运行在
私有网络命名空间中,并通过嵌入式 presto-pasta 用户态 NAT 获得出站连接。
工作节点的环回服务和抽象套接字从那里永远不可达。在此基础上,
可选的 fod-network 设置会过滤此类构建可以连接的目标。它位于工作节点的自由格式设置中,因此
在使用 NixOS 模块时,它就是普通的 Nix:
services.tribuchet-worker.settings.fod-network = {
# action when no rule matches (default: "allow")
default = "allow";
# ordered rules, first match wins
rules = [
{
action = "deny";
dst = "private"; # loopback, RFC 1918, link-local, ULA, CGNAT, ...
}
{
action = "allow";
dst = "10.20.0.15"; # single IP or CIDR, IPv4 or IPv6
ports = [ "443" ];
}
{
action = "deny";
proto = "tcp"; # "tcp", "udp" or "any" (default)
dst = "any";
ports = [
"25"
"465"
"587"
"8000-8999"
];
}
];
};
如果没有该模块,相同的结构将以 [fod-network] 表的形式写入 worker.toml,其中包含 [[fod-network.rules]] 个条目。
每条规则都匹配新出站连接的目的地:
dst:"any"、"private"(所有非公共地址:环回、 RFC 1918、链路本地、ULA、CGNAT、组播)、单个 IP 或类似"192.0.2.0/24"/"2001:db8::/32"的 CIDR。proto:"tcp"、"udp"或"any"。ports: 目的地端口,单个("443")或包含范围 ("8000-8999");为空或省略表示任意端口。
在创建主机套接字之前,规则按顺序对每个新流进行评估;被拒绝的连接根本不会离开沙箱(TCP
SYN 不会收到响应)。规则基于 IP 是有意为之——基于主机名的规则仅适用于连接时名称解析到的目标,
并且可以通过自行解析名称的构建轻松绕过。DNS 查询由 presto-pasta 独立于这些规则转发到主机解析器,
因此 deny 规则不会破坏名称解析。
构建流程
- Nix 执行
tribuchet attach build.json;shim 通过 unix socket 将构建提交给 hub。 - Hub 验证并去重请求,将其排队给服务于该系统和功能集的 worker。
- 路径协商:worker 报告其已拥有的输入路径;hub 将缺失的路径作为带有 Nix db 元数据的 zstd NAR 流式传输,并通过 worker 的 nix-daemon 在 worker 上导入。
- Worker 在其沙箱中运行 builder;日志实时流式传输回客户端。
- 输出被打包、签名、由 hub 验证,并在 Nix 提供的 scratch 路径上解包;Nix 完成哈希和注册,就好像构建是在本地运行的一样。
DESIGN.md 详细描述了架构、沙箱、安全模型和故障处理。
开发
$ nix develop # rust toolchain + protobuf
$ cargo test
$ cargo clippy --all-targets
$ nix build .#checks.x86_64-linux.nixos-test # end-to-end VM test (hub + worker)
VM 测试涵盖远程构建、构建过程中的 hub/worker 重启与重新加载、 取消操作、日志限制、uid-range 和模拟构建,以及 固定输出网络。