发 v1.0.9 前发现两个真问题,都会直接影响用户拿到的附件。 一、docker-compose.yml 里的镜像标签是硬编码的 1.0.8 这两个文件是**直接挂到 GitHub Release 上的附件**,用户照着抄命令。 早先它们硬编码具体版本号,每次发版要人工 sed 一遍 —— 这次就漏了: 附件里写着 `image: drouter:1.0.8`,而镜像 tar 加载后的标签是 `drouter:1.0.9`, `docker compose up` 直接报「找不到镜像」。DOCKER-GUIDE.md 另有 14 处同样问题。 改成 __VERSION__ 模板,由 build-docker.sh 渲染(版本从 packaging/VERSION 读)。 渲染分两步,顺序不能反:先删 TEMPLATE-NOTE 标记块(那是给维护者看的 「这是模板」提示,留着用户会看到「1.0.9 是占位符」这种自相矛盾的话), 再替换占位符。先替换的话,标记块里的 __VERSION__ 也会被换掉。 retag-docker-image.py 同样问题:NEW_TAGS 硬编码。改完参数化。 这个 bug 更隐蔽 —— 拿 1.0.9 的镜像跑重标记会打上 1.0.8 的标签, 用户 load 完看到的标签与附件说明对不上。 t-packaging.py 补 12 条判据卡住这些(4 条已做反向验证:注入即红、还原即绿)。 判据只抓「紧挨 drouter 标识或产物文件名」的版本号 —— 一刀切拦所有 x.y 会 误伤「1.0.1 起 Web 终端绕开 init」这种历史沿革说明和「GPL-3.0」许可证名。 二、t-worktree-clean.py 抓不到「注释掉整行代码」型注入 跑回归时 t-107 红了 1 项,查出来是 t-107-inject-wg.py 把 `'env': _vpn_env(),` 注释掉了没还原(SIGTERM 打断,finally 没执行)—— 和 v1.0.8 那次 `/nonexistent` 残留一模一样的坑,但**形态不同**: 不含任何哨兵字样,原判据的 4 个哨兵全都抓不到。 后果很实:_vpn_status 不返回 env,前端拿不到四态诊断, VPN 页面的三个修复按钮全废。这正是 v1.0.8 声称修好的那个 bug。 补两条判据: - 扫「被注释掉的整行代码」:去掉 # 后能 ast.parse 成功,且含调用/赋值/return/dict 项 - 扫 CRLF:Windows 工具写文件会整篇改行尾,sync.sh 预检才拦太晚 判据本身踩了三个坑,都记在注释里: - dict 项 `'env': _vpn_env(),` 单独 ast.parse 会 SyntaxError(没大括号), 真正的注入反而被放过 → 补大括号再解析 - dict 项的键**通常带引号**,正则只写 ^[A-Za-z_] 认不出来 → 改成同时认引号键 - 分隔线注释 `# ------ radvd` 里的 ----- 会被解析成负号表达式 → 按前缀排掉 双向验证:4 种注入形态(赋值/dict项/bool/列表)全部精确报到行号, 基线零误报(排掉了分隔线注释和「# module: (service, action, 中文名)」这类 长得像 dict 项的文档注释)。 三、_dev/dl-verify.py(新增) 从目标机往 Windows 拉大产物时发现的坑,记成脚本省得下次重踩: - `ssh host 'cat f' > out` 在 Windows 上做**文本模式换行转换**,二进制被破坏 (实测 deb 少 4 字节,sha256 直接对不上)→ 走 base64 再本地解码 - Windows Python 看不到 Git Bash 的 /tmp,必须给 C:/ 绝对路径 - scp 传完一定要判 sha256 —— scp/ssh 都可能「返回 0 却落地 0 字节」 - 落地走 .part + os.replace,避免写一半失败留下半个产物 顺带修了 build-docker.sh 里一处路径笔误 (packaging/docker/packaging/docker/DOCKER-GUIDE.md)和步骤编号 1/3→1/4。
13 KiB
Drouter · Docker 使用说明
⚠️ 这是模板,
__VERSION__是占位符。发版时由packaging/build-docker.sh渲染成dist/DOCKER-GUIDE.md(版本号从packaging/VERSION读)。 改版本只需改packaging/VERSION一个文件。
适用镜像:
drouter:__VERSION__(drouter-__VERSION__-docker.tar,约 169 MB)
⚠️ 先说清楚:Docker 形态的定位
这是「体验 / 调试 / CI 渲染校验」用的,不建议把容器当主路由长期跑。
原因很实在,不是谦虚:
| 限制 | 具体表现 | 影响 |
|---|---|---|
| 容器里没有 systemd | 「服务管理」页显示 active: unknown |
这是预期内的优雅降级,不是故障 |
| 无法持久化 systemd 单元 | 容器重启后 dnsmasq/radvd 等不被托管 | 托管能力缺失 |
需要 NET_ADMIN 才完全可用 |
不加时报 Operation not permitted |
防火墙页失效 |
| 网络命名空间限制 | 即使 host 网络,也没法操作宿主的内核模块、sysctl 全集 | 部分高级功能受限 |
不过 Web 终端不受 systemd 缺失影响 —— 1.0.1 起终端守护会绕开 init 系统直接启动,容器里一样能开出 root shell(见第六节第 4 条自测)。
真正当路由器跑 → 用 drouter-__VERSION__-offline-amd64.tar.gz 装到 Debian 13 物理机/虚拟机上。
那才是这个项目的主路径,也是它被设计出来的场景。
想快速看看界面长什么样、试试 API → 用 Docker,一分钟就能跑起来。
一、加载镜像
docker load -i drouter-__VERSION__-docker.tar
加载完会看到两个标签(同一个镜像):
drouter:__VERSION__
liuzhuohua/drouter:__VERSION__
验证:
docker images | grep drouter
为什么有两个标签? 带用户名的那个是给将来推 Docker Hub 用的。 日常用短的
drouter:__VERSION__就行。
如果你用的是 Podman:本镜像最初用 buildah 构建,内部标签曾为
localhost/drouter:__VERSION__。发布版已统一改成drouter:__VERSION__(另有别名liuzhuohua/drouter:__VERSION__),podman load和docker load都能直接加载。若你手上是更早期的 tar 包,引用时才需要localhost/前缀。
二、方式 A:docker compose(推荐)
我已经写好编排文件,直接用:
# 1) 把 compose 文件放到任意目录
mkdir -p ~/drouter && cd ~/drouter
# 把 docker-compose.yml 放进来
# 2) 起
docker compose up -d
# 3) 看状态(等 health 变成 healthy)
docker compose ps
访问 https://<宿主IP>:8443/
- 账号
admin/admin123 - 自签证书,浏览器会报「不安全」→ 点「高级」→「继续访问」
- 第一件事:改密码
停止 / 卸载:
docker compose down # 停容器,配置(命名卷)保留
docker compose down -v # 停容器并删除配置卷(彻底清空)
docker compose logs -f # 看日志
三、方式 B:docker run(不想用 compose)
docker run -d \
--name drouter \
--restart unless-stopped \
--network host \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
-e TZ=Asia/Shanghai \
-v drouter-etc:/etc/drouter \
-v drouter-data:/opt/drouter/data \
-v drouter-snapshots:/opt/drouter/snapshots \
-v drouter-log:/var/log/drouter \
drouter:__VERSION__
就这几行。参数含义与 compose 里完全一致,逐个对应。
别用
--privileged网上很多教程图省事都写--privileged。实际上 Drouter 只需要NET_ADMIN+NET_RAW两个能力,给privileged等于把宿主机完全敞开。 除非你在排查权限问题,否则不需要。
四、参数为什么这么配
--network host 是硬要求
面板要干的事:读宿主网卡状态、写 nftables 规则、看 conntrack 连接跟踪。
这些全都在宿主机的网络命名空间里。用 bridge 模式的话容器看到的是一个
虚拟网络栈 —— 里面只有 eth0,看不到你的 ens18/ens19,
也管不了宿主的 NAT 规则。所以必须 network_mode: host。
副作用:ports: 映射会失效(host 模式下 Docker 直接忽略它)。
端口由容器内 /etc/drouter/web-port 决定,默认 8443。
NET_ADMIN 不能省
不加的话,nft list ruleset 会返回 Operation not permitted:
「防火墙」页会显示成空的,看起来像「没有规则」,而不是像「没有权限」——
这个误导性很强,所以我在镜像里也加了 HEALTHCHECK 之外的显式提示。
实测加上 NET_ADMIN 后 /api/nft 正常返回真实规则集。
NET_RAW
给 ping / traceroute / arping 这类需要原始套接字的探测功能用。 「网络诊断」工具页会用到。
四个命名卷必须挂
docker compose down 默认保留命名卷,但如果你用了 down -v
或者直接 docker rm -f 再重建,没挂出来的目录就全没了。所以四个都挂上:
| 卷 | 挂载点 | 装什么 |
|---|---|---|
drouter-etc |
/etc/drouter |
配置核心:settings、web-port、active-theme、证书索引 |
drouter-data |
/opt/drouter/data |
SQLite 库、流量统计 |
drouter-snapshots |
/opt/drouter/snapshots |
配置快照(回滚用) |
drouter-log |
/var/log/drouter |
日志 |
DROUTER_WEB_PORT 只在首次启动生效
这是个故意的设计。容器入口脚本只在 /etc/drouter/web-port 不存在时
才把环境变量写进去,之后会 unset。
原因:后端读端口时环境变量优先级高于配置文件。如果一直留着这个环境变量, 你在界面上把端口从 8443 改成别的,看起来「改了却没生效」—— 实际的坑是环境变量把它覆盖回去了。
所以:改端口请去界面改,不要改 compose 里的环境变量。
五、常用运维命令
# 看日志(实时)
docker logs -f drouter
# 健康状态
docker inspect --format '{{.State.Health.Status}}' drouter
# 进容器(注意:dash 里很多命令查不到,要 bash -lc)
docker exec -it drouter bash -lc 'nft list ruleset'
docker exec -it drouter bash -lc 'cat /etc/drouter/web-port'
docker exec -it drouter bash -lc 'ls -la /opt/drouter/backend/'
# 手动触发一次健康检查
docker exec drouter bash -lc 'PORT=$(cat /etc/drouter/web-port||echo 8443); curl -fsSk https://127.0.0.1:$PORT/api/health'
# 备份配置(把卷打包出来)
docker run --rm -v drouter-etc:/src -v "$PWD":/backup alpine \
tar czf /backup/drouter-etc-$(date +%F).tar.gz -C /src .
# 备份日志
docker logs drouter > drouter-$(date +%F).log 2>&1
docker exec默认用/bin/sh(dash),不是 bash。 直接docker exec drouter command -v nft会说找不到东西 —— 那是因为 dash 的command -v行为不同。加bash -lc就正常了。
六、验证容器里的功能是否真的可用
我在构建时做过一轮完整验收,你可以自己复现。
1) 健康检查
docker inspect --format '{{.State.Health.Status}}' drouter
# 期望:healthy
2) 登录拿 token
TOKEN=$(curl -sk -X POST https://127.0.0.1:8443/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
echo "$TOKEN"
鉴权走
X-Token请求头,不是?token=查询参数。 这是最容易搞错的地方。
3) 打几个真实接口
for ep in sysinfo ifaces metrics ipv6 leases users; do
printf '%-10s ' "$ep"
curl -sk -o /dev/null -w '%{http_code}\n' \
-H "X-Token: $TOKEN" https://127.0.0.1:8443/api/$ep
done
# 期望:全部 200
4) Web 终端(容器形态也能用)
容器里没有 systemd,但终端不受影响 —— 1.0.1 起守护会绕开 init 系统 直接以进程方式启动。想确认它真的能开:
curl -sk -X POST -H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
-d '{"user":"root","cols":100,"rows":30}' \
https://127.0.0.1:8443/api/webshell/connect
# 期望:{"ok": true, "msg_cn": "终端已连接", "data": {"sid": "...", ...}}
# 守护进程与 socket
docker exec drouter pgrep -af shelld
docker exec drouter ls -l /run/drouter/shell.sock
5) 对比「预期差异」
这两个不是 bug,是容器形态的正常表现:
# 服务状态 → active: unknown(容器无 systemd,优雅降级)
curl -sk -H "X-Token: $TOKEN" https://127.0.0.1:8443/api/services
# nft 规则 → 不加 NET_ADMIN 会 Operation not permitted
curl -sk -H "X-Token: $TOKEN" https://127.0.0.1:8443/api/nft
七、常见问题
浏览器打不开 8443
按顺序查:
# 1) 容器活着吗
docker ps | grep drouter
# 2) 健康吗
docker inspect --format '{{.State.Health.Status}}' drouter
# 3) 端口在听吗(在宿主上跑)
ss -tlnp | grep 8443
# 4) 日志里有报错吗
docker logs --tail 50 drouter
如果容器在跑、端口也在听但访问不了 → 大概率是宿主防火墙挡了。
防火墙页是空的 / 报 Operation not permitted
镜像里 99% 是这个原因:启动时漏了 --cap-add NET_ADMIN。
# 确认当前容器有哪些能力
docker inspect --format '{{.HostConfig.CapAdd}}' drouter
# 期望:[NET_ADMIN NET_RAW]
修正:删掉容器重建(配置在命名卷里,不会丢):
docker rm -f drouter
# 然后用带 cap_add 的 compose 或 docker run 重新起
Web 终端报「终端守护未运行」
容器里没有 systemd,所以不能用 systemctl start drouter-shelld(敲不了)。
1.0.1 起面板会自动回退成直接启动守护进程,正常情况不会走到这里。 若仍报错,按顺序查:
# 1) 守护进程在不在
docker exec drouter pgrep -af shelld
# 2) socket 建出来了没(权限应为 srw------- root root)
docker exec drouter ls -l /run/drouter/shell.sock
# 3) 伪终端入口在不在 —— 少了它内核会误报 "out of pty devices"
docker exec drouter ls -l /dev/ptmx # 应指向 pts/ptmx
# 4) 守护自己的日志
docker logs --tail 50 drouter | grep -i shelld
守护进程起不来时,通常是 /opt/drouter/backend/drouter-shelld.py 被卷挂载
覆盖掉了(比如误把宿主目录挂到 /opt/drouter)。检查 docker inspect 的
Mounts 里有没有多余的 /opt/drouter 挂载。
健康检查一直 unhealthy
两个可能:
- 面板还没起来 →
start_period: 20s内是正常的,等一会 - curl 没加
-k→ 面板是 HTTPS + 自签证书,不加-k证书校验必失败
手动验一下:
docker exec drouter bash -lc \
'curl -fsSk --max-time 4 https://127.0.0.1:8443/api/health'
改了端口没生效
检查是不是 compose 里还留着 DROUTER_WEB_PORT 环境变量 ——
它的优先级高于配置文件。改端口请走界面,不要在 compose 里改这个变量。
容器重启后配置没了
确认四个卷都挂了:
docker inspect --format '{{json .Mounts}}' drouter | python3 -m json.tool
应该看到 drouter-etc → /etc/drouter 等四条。
能直接用 --privileged 吗
能,但没必要。Drouter 只需 NET_ADMIN + NET_RAW。
privileged: true 会让容器能访问宿主所有设备 —— 权限面大得多。
除非在排查权限问题,否则建议保持最小权限。
八、从镜像回退到 deb 安装
如果你用 Docker 体验完,决定正式部署:
- 备份配置(见上面「常用运维命令」的备份命令)
- 在目标 Debian 13 机器上装离线包
- 把备份的
/etc/drouter内容恢复过去(路径一致,可直接用)
# 目标机上
tar xzf drouter-__VERSION__-offline-amd64.tar.gz
cd drouter-__VERSION__-offline-amd64
sudo bash install.sh
# 恢复你在容器里试出来的配置
sudo tar xzf drouter-etc-2026-09-30.tar.gz -C /etc/drouter --strip-components=0
sudo systemctl restart drouter-web drouter-helpd drouter-shelld
附:镜像技术信息
| 项 | 值 |
|---|---|
| 基础镜像 | debian:trixie-slim |
| 架构 | amd64 / linux |
| 标签 | drouter:__VERSION__、liuzhuohua/drouter:__VERSION__ |
| 入口 | /opt/drouter/bin/docker-init.sh |
| 默认命令 | /usr/bin/python3 /opt/drouter/backend/drouter-web.py |
| 暴露端口 | 8443(HTTPS 面板)、8080(备用 HTTP,默认关) |
| 数据卷 | /etc/drouter、/opt/drouter/data、/opt/drouter/snapshots、/var/log/drouter |
| 时区 | Asia/Shanghai |
| 许可 | MIT |
docker-init.sh 做的事(因为容器里没有 systemd,这些原本由 systemd 单元负责):
- 建齐运行所需目录、设好属主(数据归低权用户
drouter,代码归 root) - 首次启动播种默认主题
active-theme=default - 播种运营商 DNS 占位文件(避免渲染时找不到文件报错)
- 首次启动把
DROUTER_WEB_PORT写进/etc/drouter/web-port,然后unset exec真正的主进程(保证信号能正确传递,docker stop能优雅退出)