用 nerdctl + Tailscale Sidecar 让容器流量走 Exit Node:踩坑全记录
- EN
- ZH-CN
Table of Contents
在容器化部署中,有时候我们需要让容器的所有出站流量通过特定的网络出口。Tailscale 的 sidecar 模式可以做到这一点:用一个 Tailscale 容器作为 sidecar,其他容器共享它的网络命名空间,流量通过 WireGuard 隧道经由远端 exit node 出去。
这个方案在 Docker Compose 下很成熟,但迁移到 nerdctl(containerd)时,我踩了一连串的坑。记录下来,希望能帮后来人少走弯路。
#
最终目标
让任意容器通过 Tailscale sidecar 接入 tailnet,所有出站流量经由远程 exit node 转发。架构如下:
为了方便验证网络是否通畅,我用 nicolaka/netshoot 作为测试容器——它自带 curl、dig、tcpdump、iperf 等一整套网络排查工具,比临时装工具方便得多。
#
坑 1:nerdctl compose 不支持 network_mode: service:xxx
network_mode: service:xxxDocker Compose 支持 network_mode: service:<service_name> 语法,它会自动把 service 名解析为对应容器的网络命名空间。但 nerdctl compose 没有实现这个翻译层,直接报错:
unsupported network_mode: service:tailscale-sidecar
解法: 改用 network_mode: container:<container_name>,并通过 container_name 字段固定 sidecar 容器的名称。
services:
tailscale-sidecar:
container_name: tailscale-sidecar # 固定名称
# ...
netshoot:
network_mode: container:tailscale-sidecar # 用容器名而非服务名
如果不固定 container_name,nerdctl compose 会自动生成类似 blockchain-tailscale-sidecar-1 的名称,导致引用不匹配。
#
坑 2:Tailscale ACL 中未定义 tag
启动后 Tailscale 容器立即退出,日志显示:
Received error: requested tags [tag:container] are invalid or not permitted
解法: 在 Tailscale 管理后台的 ACL 策略中添加 tag 定义,并在生成 auth key 时勾选对应 tag:
"tagOwners": {
"tag:container": ["autogroup:admin"]
}
#
坑 3:缺少 SYS_MODULE capability
SYS_MODULE capability这个坑最隐蔽。容器能正常加入 tailnet,tailscale ping 也通,但开启 exit node 后 TCP 流量完全不通,只有 ICMP 能过。
原因是内核模式(TS_USERSPACE=false)需要加载 xt_mark、nf_nat 等内核模块来设置 iptables 转发规则。缺少 SYS_MODULE capability 时,这些模块无法加载,iptables 规则写入但不生效,表现为 ICMP 正常而 TCP 断流。
解法:
cap_add:
- net_admin
- SYS_MODULE # 必须加上
#
坑 4:Exit Node 模式下 DNS 失败
开启 exit node 后,容器所有出站流量都走 WireGuard 隧道到远端 exit node 再出去。但 /etc/resolv.conf 里的 DNS 服务器仍然是本地地址(如 10.0.2.3),这些地址从 exit node 那端是不可达的,DNS 查询自然失败。
症状是 curl ifconfig.me 报 Could not resolve host,而直接用 IP 访问则卡住或返回异常。
解法: 让 Tailscale 接管 DNS,查询也走隧道:
environment:
- TS_ACCEPT_DNS=true
#
最终可用的 compose.yaml
services:
tailscale-sidecar:
image: tailscale/tailscale:latest
container_name: tailscale-sidecar
hostname: tailscale-sidecar
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_EXTRA_ARGS=--advertise-tags=tag:container --exit-node=${TS_EXITNODE}
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_ACCEPT_DNS=true
volumes:
- ${PWD}/tailscale-sidecar/state:/var/lib/tailscale
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- SYS_MODULE
restart: unless-stopped
netshoot:
image: nicolaka/netshoot
depends_on:
- tailscale-sidecar
network_mode: container:tailscale-sidecar
command: sleep infinity
需要在同目录下准备 .env 文件:
TS_AUTHKEY=tskey-auth-xxxxx
TS_EXITNODE=100.x.x.x
#
验证方法
nicolaka/netshoot 自带丰富的网络工具,非常适合验证:
# 检查出口 IP,确认流量是否走了 exit node
nerdctl exec -it netshoot curl ifconfig.me
# DNS 解析测试
nerdctl exec -it netshoot dig google.com
# 查看路由表
nerdctl exec -it netshoot ip route show table all
# 抓包排查
nerdctl exec -it netshoot tcpdump -i any -n
# Tailscale 侧的状态检查
nerdctl exec -it tailscale-sidecar tailscale status
nerdctl exec -it tailscale-sidecar tailscale exit-node status
nerdctl exec -it tailscale-sidecar tailscale ping <exit-node-ip>
#
排查思路总结
这次排查的过程本身也值得回顾。面对"exit node 不工作"这样的复合问题,有效的方法是逐层剥离:
- 先确认隧道连通性:
tailscale ping验证 WireGuard 隧道本身是否通。 - 对比实验: 关掉 exit node 测试,确认问题出在 exit node 还是基础网络。
- 区分协议: ICMP 通但 TCP 不通,指向 iptables/内核模块问题。
- 区分 DNS 和连通性: 用 IP 直连绕过 DNS,分别定位。
- 对比已知可用的配置: 发现之前能用的命令多了
SYS_MODULE,一下锁定问题。
每一步缩小范围,避免在错误的方向上浪费时间。
#
写在最后
nerdctl compose 作为 Docker Compose 的替代方案已经很成熟了,但在细节兼容性上仍有差距。network_mode: service: 的缺失、容器名的自动生成规则不同,这些都是迁移时容易踩到的坑。而 Tailscale 在容器中使用 exit node 时,SYS_MODULE 和 TS_ACCEPT_DNS 是两个容易被忽略但又不可或缺的配置。希望这篇记录能帮到遇到同样问题的人。