本文定义 Chimera Client 对外发布时推荐采用的 Nix package、NixOS
module、flake 输出和测试契约。目标是让用户不依赖
services.mihomo,只需导入 Chimera Client 自己的模块即可声明式运行
服务。
本文面向两个角色:
- 项目维护者:在仓库中实现 package、module 和 NixOS VM 测试;
- 下游用户:从 flake、固定版本源码或 nixpkgs 包中启用服务。
建议最终仓库结构如下:
Chimera_Client/
├── Cargo.toml
├── Cargo.lock
├── flake.nix
├── nix/
│ ├── package.nix
│ ├── module.nix
│ └── tests/
│ └── service.nix
└── NIXOS_MODULE.md
发布后的 flake 至少应提供:
packages.<system>.chimera-client
packages.<system>.default
nixosModules.chimera-client
nixosModules.default
checks.<system>.package
checks.<system>.nixos-module模块的稳定公共入口为:
services.chimera-client不要使用 services.clash-rs 作为长期接口。clash-rs 是当前二进制
名称,而 chimera-client 才是软件和模块的发布身份。
当前二进制为 clash-rs,已经具备 NixOS 服务所需的基本接口:
# 验证配置
clash-rs \
--directory /var/lib/private/chimera-client \
--config /run/credentials/chimera-client.service/config.yaml \
--test-config
# 前台运行
clash-rs \
--directory /var/lib/private/chimera-client \
--config /run/credentials/chimera-client.service/config.yaml
# 输出版本
clash-rs --version注意:
--directory是状态和资源目录,不是配置文件所在目录;--config可以传绝对路径,因此可以直接读取 systemd credential;--test-config对应 systemd 的ExecStartPre;- 程序必须保持前台运行,由 systemd 管理生命周期;
cache.db、下载的 MMDB、ASN MMDB、GeoSite 和 provider 数据应位于--directory;- 当前兼容模式默认开启。指定
--directory时,程序会把该目录设为 工作目录,并为未指定的 MMDB/GeoSite 应用兼容默认值。
将来即使增加 check、run 子命令,也应至少在一个稳定发布周期内
保留上述参数兼容性。
第一版模块建议声明以下选项:
| Option | 类型 | 默认值 | 说明 |
|---|---|---|---|
enable |
bool |
false |
启用 Chimera Client |
package |
package |
flake 默认包 | 要运行的软件包 |
configFile |
path |
无 | 包含敏感信息的 YAML 配置 |
checkConfig |
bool |
true |
启动前执行 --test-config |
tun.enable |
bool |
false |
授予 TUN 和策略路由权限 |
processInspection |
bool |
false |
允许按进程名称/路径匹配 |
extraArgs |
listOf str |
[] |
追加的 CLI 参数 |
environment |
attrsOf str |
{} |
服务环境变量 |
stateDirectory |
str |
"chimera-client" |
systemd 状态目录名 |
openFirewall |
bool |
false |
预留选项,第一版建议不自动开放端口 |
模块不应提供 settings 并把完整 YAML 生成到 Nix Store。代理节点密码、
订阅 URL、控制器 secret 和证书路径通常属于敏感配置,应通过
configFile 与 systemd credentials 加载。
tun.enable 只控制 systemd 权限和沙箱,不修改 YAML。用户仍需在
配置文件中显式设置:
tun:
enable: true
route-all: true建议将以下内容保存为 nix/module.nix。其中
self.packages.${pkgs.system}.default 的注入方式由 flake 决定;如果
模块需要脱离 flake 单独使用,可要求用户显式设置 package。
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.chimera-client;
executable = lib.getExe cfg.package;
stateDirectory = "/var/lib/private/${cfg.stateDirectory}";
credentialConfig = "\${CREDENTIALS_DIRECTORY}/config.yaml";
capabilities =
lib.optional cfg.tun.enable "CAP_NET_ADMIN"
++ lib.optionals cfg.processInspection [
"CAP_DAC_READ_SEARCH"
"CAP_SYS_PTRACE"
];
commonArgs = [
executable
"--directory"
stateDirectory
"--config"
credentialConfig
];
startCommand =
utils.escapeSystemdExecArgs
(commonArgs ++ cfg.extraArgs);
checkCommand =
utils.escapeSystemdExecArgs
(commonArgs ++ [ "--test-config" ]);
in
{
options.services.chimera-client = {
enable = lib.mkEnableOption "Chimera Client rule-based proxy service";
package = lib.mkOption {
type = lib.types.package;
description = ''
Chimera Client package containing the clash-rs executable.
The package must declare meta.mainProgram = "clash-rs".
'';
};
configFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to the Chimera Client YAML configuration.
The file may contain credentials and is loaded through systemd
credentials. Do not generate secret configuration in the Nix Store.
'';
};
checkConfig = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Validate the configuration with --test-config before starting.
'';
};
tun.enable = lib.mkEnableOption ''
the privileges and sandbox exceptions required for TUN mode
'';
processInspection = lib.mkEnableOption ''
privileges required for process-name and process-path rules
'';
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"--controller-ipc"
"/run/chimera-client/controller.sock"
];
description = ''
Additional command-line arguments passed to Chimera Client.
Do not put secrets in this option because command-line arguments are
observable through process metadata.
'';
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
RUST_LOG = "info";
};
description = "Environment variables for the service.";
};
stateDirectory = lib.mkOption {
type = lib.types.strMatching "[A-Za-z0-9_.-]+";
default = "chimera-client";
description = ''
StateDirectory name used by systemd. Runtime data is stored below
/var/lib/private/<name> when DynamicUser is enabled.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether the module may open configured listener ports.
The first module release should reject true until listener extraction
from external YAML is implemented safely.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
lib.meta.availableOn
pkgs.stdenv.hostPlatform
cfg.package;
message = ''
services.chimera-client.package is not available on
${pkgs.stdenv.hostPlatform.system}.
'';
}
{
assertion = !cfg.openFirewall;
message = ''
services.chimera-client.openFirewall is not implemented yet.
Open explicit ports with networking.firewall.allowedTCPPorts and
networking.firewall.allowedUDPPorts.
'';
}
];
systemd.services.chimera-client = {
description = "Chimera Client rule-based proxy service";
documentation = [
"https://github.com/mfsga/Chimera_Client"
];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
];
# Chimera currently invokes `ip` as a subprocess.
path = [
pkgs.iproute2
];
environment = cfg.environment;
serviceConfig = {
Type = "simple";
ExecStartPre = lib.optional cfg.checkConfig checkCommand;
ExecStart = startCommand;
Restart = "on-failure";
RestartSec = "3s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
DynamicUser = true;
StateDirectory = cfg.stateDirectory;
LoadCredential = [
"config.yaml:${cfg.configFile}"
];
UMask = "0077";
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
NoNewPrivileges = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateTmp = true;
PrivateMounts = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectHostname = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"bpf"
];
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
] ++ lib.optional cfg.tun.enable "AF_NETLINK";
PrivateDevices = !cfg.tun.enable;
PrivateUsers = !(cfg.tun.enable || cfg.processInspection);
ProtectProc =
if cfg.processInspection
then "default"
else "invisible";
ProcSubset =
if cfg.processInspection
then "all"
else "pid";
};
};
};
meta.maintainers = [ ];
}Chimera Client 当前不是只通过 Rust netlink API 管理系统,它还会调用:
ip
因此 unit 必须显式提供:
path = [
pkgs.iproute2
];否则开发环境能够运行、systemd 服务却可能报 No such file or directory。
TUN 模式需要:
CAP_NET_ADMIN;AF_NETLINK;- 访问
/dev/net/tun; - 创建、删除接口和策略路由。
Chimera 不修改 systemd-resolved 的 per-link DNS,也不需要
CAP_NET_BIND_SERVICE 或 resolve1 Polkit 授权。
因此启用 tun.enable 时至少要设置:
PrivateDevices = false;
PrivateUsers = false;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];不要仅添加 CAP_NET_ADMIN 后仍保持 PrivateDevices = true,否则服务
可能拥有 capability,却看不到宿主的 TUN 设备。
当 YAML 包含 PROCESS-NAME、PROCESS-PATH 等规则时,模块用户应设置:
services.chimera-client.processInspection = true;这会放宽 /proc 可见性,并授予:
CAP_DAC_READ_SEARCH
CAP_SYS_PTRACE
第一版发布前必须用真实进程匹配测试确认这些权限足够;如果不同内核的 Yama、hidepid 或 LSM 仍阻止检查,应记录平台限制,而不是直接把服务 改成 root 常驻。
nix/package.nix 应从源码构建,而不是包装开发机的 target/debug
二进制。包至少需要满足:
meta = {
mainProgram = "clash-rs";
platforms = lib.platforms.linux;
};建议使用 rustPlatform.buildRustPackage。示意模板:
{
lib,
rustPlatform,
pkg-config,
cmake,
protobuf,
llvmPackages,
...
}:
rustPlatform.buildRustPackage {
pname = "chimera-client";
version = "0.23.0";
src = lib.cleanSource ../.;
cargoLock = {
lockFile = ../Cargo.lock;
# Cargo.lock 中 git dependencies 需要逐项提供 outputHashes。
outputHashes = {
# "dependency-version" = "sha256-...";
};
};
nativeBuildInputs = [
pkg-config
cmake
protobuf
llvmPackages.libclang
];
LIBCLANG_PATH = "${llvmPackages.libclang.lib}/lib";
cargoBuildFlags = [
"--package"
"clash-rs"
];
cargoTestFlags = [
"--package"
"clash-rs"
];
meta = {
description = "Rust rule-based proxy client with DNS, TUN and Clash-compatible APIs";
homepage = "https://github.com/mfsga/Chimera_Client";
license = lib.licenses.gpl3Only; # 发布前按仓库真实许可证修正
mainProgram = "clash-rs";
platforms = lib.platforms.linux;
};
}上面的 license 和 outputHashes 是发布阻断项,不能保留猜测值。
发布前必须:
- 在仓库根目录加入明确的许可证文件;
- 为所有 Cargo git dependencies 固定 Nix output hash;
- 确认 release 构建启用了需要的默认 features;
- 确认最终包只安装需要发布的二进制和资源。
当前仓库的 flake.nix 只提供本机开发 shell,并把 nixpkgs 输入绑定到
本机 /nix/store 路径。这种写法不可发布。
公开发布时应将输入改为可复现的远端引用,例如:
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";推荐输出结构:
{
description = "Chimera Client";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, ... }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems =
nixpkgs.lib.genAttrs supportedSystems
(system:
let
pkgs = import nixpkgs { inherit system; };
in
pkgs);
in
{
packages = nixpkgs.lib.mapAttrs
(_: pkgs:
let
package = pkgs.callPackage ./nix/package.nix { };
in
{
chimera-client = package;
default = package;
})
forAllSystems;
nixosModules.chimera-client =
{ lib, pkgs, ... }:
{
imports = [ ./nix/module.nix ];
services.chimera-client.package =
lib.mkDefault
self.packages.${pkgs.system}.default;
};
nixosModules.default = self.nixosModules.chimera-client;
};
}这里展示的是输出契约,不应未经 nix flake check 就直接视为最终
实现。实际实现还需要把 pkgs 的作用域正确传入 module wrapper,并
加入 checks 和 devShell。
{
inputs.chimera-client.url =
"github:mfsga/Chimera_Client";
outputs = { nixpkgs, chimera-client, ... }: {
nixosConfigurations.desktop =
nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
chimera-client.nixosModules.default
({ config, lib, ... }: {
services.chimera-client = {
enable = true;
configFile =
config.age.secrets.chimera-client-config.path;
tun.enable = true;
processInspection = true;
checkConfig = true;
};
age.secrets.chimera-client-config.file =
./secrets/chimera-client.yaml.age;
networking.firewall.checkReversePath =
lib.mkDefault "loose";
})
];
};
};
}services.chimera-client = {
enable = true;
package = inputs.chimera-client.packages.${pkgs.system}.default;
configFile = "/var/lib/secrets/chimera-client/config.yaml";
tun.enable = true;
};配置文件应由 root 安装:
sudo install -Dm600 config.yaml \
/var/lib/secrets/chimera-client/config.yaml不要将含凭据的 YAML 写入:
environment.etc."chimera-client/config.yaml".text = "...";策略路由和 TUN 可能与严格 rpfilter 冲突。主机配置通常需要:
networking.firewall.checkReversePath = "loose";模块第一版不建议静默覆盖全局防火墙策略。可以:
- 在文档中要求用户设置;
- 或增加一个显式 option,由用户授权后设置
mkDefault "loose"。
推荐默认布局:
TUN link: 198.18.0.1/30
Fake-IP: 198.19.0.0/16
两个网段必须分离。Chimera Client 在 Fake-IP 模式和非
route-all 模式下会自动补充 Fake-IP 到 TUN 的路由;显式重叠配置
应在启动前验证阶段失败。
启用 TUN 可能切断当前 SSH、Codex 或构建代理连接。远程部署应:
- 先执行
nixos-rebuild build; - 使用
nixos-rebuild test或带自动回退的部署工具; - 保留物理局域网和管理端点的 main-table 绕行规则;
- 确认
SIGTERM能清理 TUN 和 policy rules; - 在另一个终端持续检查默认路由和远程连接;
- 不要在未验证备用通道时直接切换生产主机。
备用代理、SSH 跳板或带外管理地址属于部署环境信息,不应硬编码进 通用 module。下游部署者应在主机配置中显式维护这些绕行地址。
建议 nix/tests/service.nix 至少覆盖:
- module 能成功 evaluation;
- package 的
meta.mainProgram可解析; - credential 文件没有被复制成普通公开配置;
ExecStartPre会接受合法配置;- 非法配置导致 unit 启动失败;
StateDirectory可写,能够创建cache.db;- 非 TUN 模式不拥有
CAP_NET_ADMIN; - TUN 模式包含
CAP_NET_ADMIN和AF_NETLINK; - unit PATH 中能找到
ip,且不依赖resolvectl; - 服务收到
SIGTERM后正常退出; - 重启后没有重复 policy rule;
- 条件允许时,验证 TUN 和 Fake-IP 使用分离网段。
最小测试形状:
import "${pkgs.path}/nixos/tests/make-test-python.nix" ({
name = "chimera-client";
nodes.machine = { config, ... }: {
imports = [ ../module.nix ];
services.chimera-client = {
enable = true;
package = packageUnderTest;
configFile = ./fixtures/minimal.yaml;
checkConfig = true;
};
};
testScript = ''
machine.start()
machine.wait_for_unit("chimera-client.service")
machine.succeed("systemctl is-active chimera-client.service")
machine.succeed(
"systemctl show chimera-client.service "
"-p StateDirectory -p DynamicUser"
)
'';
})TUN 测试应单独建立测试项,避免普通 module evaluation 因内核权限或 网络副作用变得不稳定。
- 仓库有明确许可证;
- Cargo.lock 已提交;
- 所有 git dependencies 有固定 hash;
-
nix build .#chimera-client成功; -
nix run . -- --version成功; -
meta.mainProgram = "clash-rs"; - x86_64-linux 构建通过;
- aarch64-linux 至少完成 evaluation,最好有真实构建。
-
services.chimera-client.enable; - package 可覆盖;
- configFile 使用 credential;
- 状态文件写入 StateDirectory;
-
ExecStartPre验证配置; - TUN 权限只在明确启用时授予;
- process inspection 权限只在明确启用时授予;
-
ip位于 unit PATH,unit 不依赖resolvectl; - SIGTERM 能清理系统状态;
- 无效配置不会进入重启风暴。
-
nix flake check; - 非 TUN 服务测试;
- 无效配置测试;
- capability 断言;
- 状态目录写入测试;
- TUN 独立 VM 测试;
- restart/reload 幂等性测试。
- README 提供 flake 导入示例;
- options 有稳定名称和说明;
- 说明配置包含秘密;
- 说明 rpfilter;
- 说明 TUN/Fake-IP 网段;
- 说明远程部署回退方案;
- 标明支持的 NixOS/nixpkgs 版本。
本设计沿用 NixOS 官方推荐的 module 结构:
- 使用
options声明带类型的公共接口; - 使用
config = lib.mkIf cfg.enable生成 systemd 配置; - package 允许由用户覆盖;
- 使用
LoadCredential避免秘密进入普通命令行和公开配置; - 使用
DynamicUser与StateDirectory管理服务身份和可写状态; - 使用 NixOS VM tests 验证模块行为。
Chimera Client 当前仍会主动调用 ip 管理 Linux policy routing,但
不会调用 resolvectl 或管理 per-link DNS。该差异必须体现在 unit
PATH、TUN 沙箱和测试中。
官方参考: