跳至内容

Junyi's Lab

用 nerdctl + Tailscale Sidecar 让容器流量走 Exit Node:踩坑全记录

Table of Contents

在容器化部署中,有时候我们需要让容器的所有出站流量通过特定的网络出口。Tailscale 的 sidecar 模式可以做到这一点:用一个 Tailscale 容器作为 sidecar,其他容器共享它的网络命名空间,流量通过 WireGuard 隧道经由远端 exit node 出去。

这个方案在 Docker Compose 下很成熟,但迁移到 nerdctl(containerd)时,我踩了一连串的坑。记录下来,希望能帮后来人少走弯路。

# 最终目标

让任意容器通过 Tailscale sidecar 接入 tailnet,所有出站流量经由远程 exit node 转发。架构如下:

nerdctl + Tailscale Sidecar 架构图

为了方便验证网络是否通畅,我用 nicolaka/netshoot 作为测试容器——它自带 curl、dig、tcpdump、iperf 等一整套网络排查工具,比临时装工具方便得多。

# 坑 1:nerdctl compose 不支持 network_mode: service:xxx

Docker 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

这个坑最隐蔽。容器能正常加入 tailnet,tailscale ping 也通,但开启 exit node 后 TCP 流量完全不通,只有 ICMP 能过。

原因是内核模式(TS_USERSPACE=false)需要加载 xt_marknf_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.meCould 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 不工作"这样的复合问题,有效的方法是逐层剥离:

  1. 先确认隧道连通性: tailscale ping 验证 WireGuard 隧道本身是否通。
  2. 对比实验: 关掉 exit node 测试,确认问题出在 exit node 还是基础网络。
  3. 区分协议: ICMP 通但 TCP 不通,指向 iptables/内核模块问题。
  4. 区分 DNS 和连通性: 用 IP 直连绕过 DNS,分别定位。
  5. 对比已知可用的配置: 发现之前能用的命令多了 SYS_MODULE,一下锁定问题。

每一步缩小范围,避免在错误的方向上浪费时间。

# 写在最后

nerdctl compose 作为 Docker Compose 的替代方案已经很成熟了,但在细节兼容性上仍有差距。network_mode: service: 的缺失、容器名的自动生成规则不同,这些都是迁移时容易踩到的坑。而 Tailscale 在容器中使用 exit node 时,SYS_MODULETS_ACCEPT_DNS 是两个容易被忽略但又不可或缺的配置。希望这篇记录能帮到遇到同样问题的人。