如何搭建 ZeroTier Controller
Wed Sep 03 2025
一些关于部署 ZeroTier Controller 的经验分享,和大量吐槽 👊😡
- Q: “小白如何配置自己的 ZeroTier Controller”
- A: “别用了,直接睡吧”
# 写在前面
ZeroTier One 这东西某种意义上也算挺奇怪的,它不严格区分 Client/Server,或是所谓 LEAF/MOON/PLANET,所有功能的代码都在同一个 ZeroTierOne 可执行二进制文件里。但是**的官方,提供的 CLI 工具 zerotier-cli
,里面只有与 Client 相关的功能,而 Server (Controller) 相关的功能是一点没有。
官方文档里给出的办法是:用 curl 调用 HTTP API,麻烦死了,狗都不用。因此就有了根据 API 文档进行二次开发的工具,比如下文即将提到的 ztncui,但用起来不能说不太好用,只能说是赤石领域大神。我虽然也写了一个简单的 CLI 工具来补全官方工具这一短板,但因为 CLI 设计过于复杂,同时 usage-cli 也存在一些问题,导致命令补全有问题,所以也只能算是勉强能用。
但其他平替方案也没好到哪里去,tailscale/headscale 需要一个 443 端口,EasyTier 也对零基础知识的小白不友好,所以只能捏着鼻子用了。
# ztncli with Container
yysy,这项目都两年没有一个 commit 了,官方提供的那个 docker 镜像也不知到从哪里构建的,所以还是自己构建为妙。但很不幸,这上古项目用的是 nodejs 14。如果真严格按照 README 说的,用 nodejs 14,在拉下来docker.io/library/node:14
的镜像之后,会发现这个版本的 Debian 已经 EOL 了(悲)。
但好在经测试发现,Debian 12 的 nodejs 18 也是能运行这个项目的。所以给一些必要的依赖一装,就得到了一个能用的 docker 镜像了,吗?
FROM debian:12
WORKDIR /app
RUN mkdir -p etc/tls
RUN apt-get update && \
apt-get install -y g++ openssl nodejs npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY ./ztncui/src /app
RUN npm install -g node-gyp && npm install
COPY ./entrypoint.sh /app
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
其中 entrypoint.sh
是一个启动脚本,主要负责生成证书
#!/bin/sh
if [ ! -f etc/tls/privkey.pem ] || [ ! -f etc/tls/fullchain.pem ]; then
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
-keyout etc/tls/privkey.pem -out etc/tls/fullchain.pem \
-subj "/CN=ztncui" >/dev/null
fi
npm start
用 docker-compose 管理环境变量等配置会比较方便,但是记得手动复制一下密码文件到 data/ 下面
services:
ztncui:
image: localhost/ztncui:latest
container_name: ztncui
restart: unless-stopped
volumes:
- /var/lib/zerotier-one:/var/lib/zerotier-one:ro
- ./data:/app/etc
environment:
- HTTP_PORT=${HTTP_PORT:-3000}
- HTTP_ALL_INTERFACES=${HTTP_ALL_INTERFACES:-no}
- HTTPS_PORT=${HTTPS_PORT:-3443}
- HTTPS_HOST=${HTTPS_HOST:-}
- NODE_ENV=production
network_mode: "host"
security_opt:
- no-new-privileges:true
然后就可以在 webUI 上控制 ZeroTier 的网络控制器了(应该)
Q: “诶,这里为什么是 network_mode: "host"
?用 bridge 不应该更安全吗?”
A: “那你得问**的 ZeroTierOne 怎么想的了 😅”
他的 HTTP API 在 9993 端口下,并且这个端口是监听0.0.0.0
的,且有 authToken 保护,正常人都以为直接带 token 访问就可以远程管理了,结果 *** ZeroTierOne 对非 localhost 的请求全部 401 Unauthorized 😅。👴 调了半天,甚至怀疑是不是这个 ztncui 的请求发的不对,然后发现是 *** ZeroTierOne 的问题,你**文档也没说啊 😅。(此处省略若干脏话 & 龙图 🤬)
接着就得喷 ztncui 了,你的意思是,你的默认密码文件 (default.passwd) 放在数据文件夹下面 (etc/) 是吗?😅 这容器启动的时候一个 Volume 挂上去,去哪里找你的默认密码,但凡往外放一层呢?
写到这的时候又突然发现问题了,ztncui 你把数据放哪了 😨,我不会重启容器数据没了吧。
总之,这种上古代码最好别碰(
# ztcli: A Brand-new ZeroTier CLI WRITTEN IN RUST 🦀
嘛,自己写的东西肯定不喷啊
我承认可能设计的有些复杂,subcommand 嵌套有点多,但至少能用,实现了 HTTP API 99% 的常用功能(
虽然可能不是很友好,但总比 curl HTTP API 好用吧(逃
不要问怎么用,问就是 clap
已经把帮助文本生成好了,自己 --help
看(
首先用 cargo 安装 ztcli:
cargo install ztcli --all-features
# 或者从 git 获取最新代码
# cargo install --git https://github.com/koitococo/ztcli ztcli --all-features
以及一些常用命令:
# 查看当前 ZeroTier One 状态,以及获取 id
ztcli status
# 创建一个网络(下划线前面的替换成上一条获取到的 id)
ztcli controller network create -n "${id}______"
# 查看网络里的成员(还是记得替换网络 id)
ztcli controller network -n ${network_id} members
# 给一个成员授权
ztcli controller network -n ${network_id} member -m ${member_id} update --authorized true
说明一下,第一条获取到的 id 是 10 位 hex 字符,如果 id 是 0d0007210d
,第二条传入的参数就是 0d0007210d______
,之后会返回一个完整的网络 id,可能是0d0007210d000721
这样的 16 位 hex 字符。
好了,介绍完毕。
那接下来我就要喷 usage-cli 了
作为一个给 mise 设计的命令行补全工具,但是连 mise 的命令行都补全不好,那你是这个 👎
常见的,很多 cli 都有一些顶层的 flag,随后是一个 subcommand,比如 mise 的 mise -n exec ...
,systemctl 的 systemctl --user status ...
。但 usage-cli 可不一般,当你用这种 flag 的时候,后面的补全就全部失效了……轻则类似 mise,只是反复的补全 subcommand;重则像 ztcli 的 -n
-m
一样,直接报错。那不是成路边一条了吗
usage-cli 用的是 kdl 这个 markup language 作为输入,这个 kdl 的 rust 实现也是神人。
kdl 的规范中有如下规则:
- 值是有类型的,比如 string, number, bool;
- 并且 string 的引号是可选的;
- 而其他类型如 bool,需要加前缀表示,比如
#true
; "true"
是有效的 string 类型;- 直接使用
true
是禁止的,以防混淆,还有几个关键字也因为类似原因被静止。
到这,还算可以接受,顶多是和 yaml 这样的语言不太一样,需要适应一下这个前缀的写法。但 kdl-rs 的神人实现就不一般了
他的 parser 里面,会检测这几个关键字,检测到直接报错,这没问题,符合规范。但他的 serializer 就不一样了,遇到 string 类型的 "true"
, 他是真的一点没做检测啊,直接输出 true
😅
**这直接导致了一个合法的 KdlDocument 被序列化成字符串之后,反序列化时过不了 parser 的检测。**在 GitHub 上有相关 issue,但截至目前无人回复。
离神很近了,但离人很远了
# 总结
每一个诗人