在 Proxmox 非特权 LXC 容器中配置 Tailscale:子网路由与出口节点

Proxmox 中的非特权 LXC(Linux 容器)看不到 /dev/net/tun,因此 tailscaled 首次启动就会因 CreateTUN("tailscale0") failed 而退出。解决这个问题无需改用特权容器,也不需要第三方脚本:Proxmox 自带容器设备直通功能。执行一条 pct set 命令,或在网页界面填写一个对话框,就能将 TUN 设备交给容器,让 Tailscale 在保持非特权模式的 LXC 中运行。

本文在非特权 Debian 13 容器内安装 Tailscale,分别通过 pct set --dev0 和网页界面的 Device Passthrough 直通 TUN 设备,再将容器配置为子网路由器和出口节点。同时介绍无法修改容器配置时的用户空间替代方案,以及实验遇到的错误。以下命令来自 2026 年 11 月的实验:宿主为 Proxmox VE 9.2,Debian 13 容器内运行 Tailscale 1.104.4。

规划容器布局

实验在同一个 Proxmox 节点上使用四个容器,以便从 tailnet(Tailscale 私有网络)中的另一台机器验证各项功能。关键是第二座网桥:宿主没有在 vmbr1 上配置 IP,路由器容器是唯一同时连接两侧网络的容器,因此进入 10.20.20.0/24 的唯一通道就是 Tailscale 子网路由。

<a href=
Proxmox LXC 中的 Tailscale 子网路由、出口节点与 Headscale 实验架构" />

每个容器各司其职:

  • CT 200 ts-router:通过 pct set 接入 TUN,充当子网路由器和出口节点,地址为 10.10.10.20、10.20.20.20。
  • CT 201 ts-client:通过网页界面接入 TUN,接受子网路由,地址为 10.10.10.21。
  • CT 202 web01:在 10.20.20.30 上提供普通 HTTP 服务,只连接 vmbr1。
  • CT 203 ts-userspace:完全没有 TUN 设备,使用用户空间网络。

实验的 tailnet 由宿主上的自托管 Headscale 服务器协调,因此 tailscale up 带有 --login-server,路由批准使用 headscale 命令。使用 Tailscale 托管服务时,省去该参数,打开命令输出的登录地址,并在管理控制台批准路由即可。容器侧配置相同。

创建非特权容器

已有容器可跳过本节。否则,先在 Proxmox 宿主上刷新模板列表,查找当前 Debian 13 镜像;模板文件名会随维护版本变化:

pveam update
pveam available --section system | grep debian-13

实验当天的 amd64 镜像为 debian-13-standard_13.6-1。

system          debian-13-standard_13.6-1_amd64.tar.zst                      amd64
system          debian-13-standard_13.6-1_arm64.tar.zst                      arm64

按 pveam available 实际输出的名称,将镜像下载到 local 存储:

pveam download local debian-13-standard_13.6-1_amd64.tar.zst

随后创建容器。本文关键参数是 --unprivileged 1;nesting=1 与网页界面 Create CT 向导的默认设置一致。实验节点把根磁盘放在本地目录存储,所以使用 local:4。通过 Proxmox ISO 安装的节点通常改用 local-lvm:4(ext4 或 xfs 安装)或 local-zfs:4(ZFS 安装)。请按实际网络修改网桥、地址和网关:

pct create 200 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname ts-router --unprivileged 1 --features nesting=1 \
  --cores 1 --memory 512 --rootfs local:4 \
  --net0 name=eth0,bridge=vmbr0,ip=10.10.10.20/24,gw=10.10.10.1 \
  --nameserver 1.1.1.1 --onboot 1 --start 1

确认容器正在运行且确实为非特权模式。宿主已有 /dev/net/tun,但容器内部仍找不到:

pct config 200 | grep -E "unprivileged|features"
pct exec 200 -- ls -l /dev/net/tun

配置正确、设备却缺失,这两行输出概括了问题。除非明确列出设备,Proxmox 不会把宿主设备交给非特权容器,因此要从宿主修复,而不是在容器内修复:

features: nesting=1
unprivileged: 1
ls: cannot access '/dev/net/tun': No such file or directory

下文将容器 200 用作路由器;出现 200 的地方请替换为自己的 CTID。Debian 和 Ubuntu LXC 使用指南进一步介绍了模板和存储的选择。

在容器内安装 Tailscale

先安装软件包,再添加设备。这个顺序可以看到真实故障,也便于识别容器日后丢失设备时的同类错误。从 Proxmox 宿主进入容器 shell:

pct enter 200

此时在容器内以 root 运行,无需 sudo。安装 curl,添加 Tailscale 的签名密钥和 Debian 13(trixie)软件仓库。使用软件仓库可让 apt 在更新容器其余软件时一并维护 Tailscale:

apt update && apt install -y curl ca-certificates
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list

第二条 tee 会回显写入的仓库配置。检查发行版代号:若容器来自 Debian 12 模板,应使用 bookworm 对应文件。发行版配置错误是 apt 找不到软件包的常见原因:

# Tailscale packages for debian trixie
deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/debian trixie main

安装软件包并检查版本:

apt update && apt install -y tailscale
tailscale version

实验当天稳定通道提供 1.104.4。后续警告和健康检查均来自这个版本;旧版本的提示可能不同,甚至没有对应提示:

1.104.4
  tailscale commit: 9382131983ca16de1357091618fd903e35db3c4e
  long version: 1.104.4-t938213198-gd38c96d6a
  other commit: d38c96d6afce903d0a81e5f3e32d7048bc33b660
  go version: go1.27.3 (tailscale/go 7ef7b40364)

软件包安装后立即启用 tailscaled,但进程在一秒内失败。日志清楚指出了原因:

缺失 TUN 设备导致 tailscaled 启动失败

留意 modprobe 这一行,它容易让人误判。实验使用的 Proxmox 内核 7.0.14-26-pve 配置为 CONFIG_TUN=y,TUN 已编译进内核,没有可加载的模块,所以 modprobe tun 找不到模块。宿主已经有 /dev/net/tun,只是容器无权访问。不需要加载模块,也不需要切换到特权容器。

进入下一节前执行 exit 离开容器 shell,因为设备要在 Proxmox 宿主上添加。下面两种方法都会写入相同的 dev0 容器配置。

通过 pct set 直通 /dev/net/tun

容器选项 devN 在 pct(1) 中写为 --dev[n] [[path=]<Path>],可设置 mode、uid、gid 和 deny-write。它替代旧教程直接写进 /etc/pve/lxc/<CTID>.conf 的 lxc.cgroup2.devices.allow、lxc.mount.entry。Proxmox 会验证该选项并在网页界面显示;非 root 管理员恢复容器时也能保留它,而原始 lxc.* 配置会在非 root 恢复时被跳过。在宿主执行:

pct set 200 --dev0 /dev/net/tun

Proxmox VE 9 无需先停止容器。实验在容器运行时添加设备,设备立即出现在容器内;这与 9.0 引入的运行中设备热插拔功能一致。检查配置和容器:

pct config 200 | grep dev0
pct exec 200 -- ls -l /dev/net/tun

应看到主设备号 10、次设备号 200 的字符设备。它们与宿主 /dev/net/tun 的设备号一致,说明容器看到的是正确的宿主设备节点:

dev0: /dev/net/tun
crw-rw---- 1 root root 10, 200 Nov  2 16:27 /dev/net/tun

热插拔后权限为 crw-rw----,完整重启容器后变成 crw-rw-rw-。动态添加和全新启动时权限可能不同;tailscaled 以 root 运行,两种权限都能工作。添加设备时,systemd 通常已因五次快速失败而停止重试,先清除失败状态,再启动服务:

pct exec 200 -- systemctl reset-failed tailscaled
pct exec 200 -- systemctl restart tailscaled
pct exec 200 -- systemctl status tailscaled --no-pager

这次守护进程持续运行并显示 Needs login,容器内也出现 tailscale0 接口:

通过 pct set 添加 TUN 后服务和 tailscale0 接口正常

Tailscale 的 LXC 文档还建议 pct set <CTID> --features keyctl=1,nesting=1。实验容器只有上面显示的 features: nesting=1,未启用 keyctl,但启动、加入网络、路由和重启后恢复都正常。仅当容器内其他软件确实需要时再添加。

通过网页界面添加 TUN 设备

网页界面也写入同一个 dev0 条目,适合已在浏览器中管理节点的场景。实验将它用于 CT 201(ts-client),该容器此前与 CT 200 一样已安装 Tailscale 但启动失败。在资源树选择容器,然后:

<a href=
Proxmox 资源页中的设备直通菜单" />

  1. 打开容器菜单的 Resources(资源)。
  2. 点击资源列表上方的 Add(添加)。
  3. 选择 Device Passthrough(设备直通)。

Add: Device 对话框只需填写一个值。除非需要指定权限或所有者,否则不要勾选 Advanced(高级):

设备直通对话框填写 /dev/net/tun

  1. 在 Device Path(设备路径)中输入 /dev/net/tun。
  2. 点击 Add 保存。

容器资源列表出现 Device (dev0)。与命令行方法一样,修改立即应用到运行中的容器,无需重启:

<a href=
Proxmox 资源列表显示 TUN 设备 dev0" />

回到宿主,配置行与 pct set 写入的完全一致。清除失败状态后,tailscaled 即可启动:

pct config 201 | grep dev0
pct exec 201 -- systemctl reset-failed tailscaled
pct exec 201 -- systemctl restart tailscaled
pct exec 201 -- systemctl is-active tailscaled

CT 201 的两项检查均通过。网页界面和命令行写入同一个配置键,日后可随时换用另一种管理方法,无需清理:

dev0: /dev/net/tun
active

按节点管理习惯选择即可。使用脚本或 Ansible 构建容器时,pct set 是可重复执行的幂等命令。单个容器适合网页界面操作;下次打开 Resources 时能直接看到设备,便于其他管理员理解配置。

将容器加入 tailnet

设备就绪后连接节点。使用 Tailscale 托管服务时,普通 tailscale up 会输出登录地址。实验控制服务器是 Headscale,所以添加 --login-server;同时通过 --hostname 固定节点名。在 CT 200 内执行:

tailscale up --login-server http://10.10.10.1:8080 --hostname ts-router

命令会等待节点获批,随后输出 Success. 并返回。保留这个 shell;获批前关闭它,下一次登录会生成新地址:

To authenticate, visit:

    http://10.10.10.1:8080/register/hskey-authreq-uzGc_Z6juYT1Y_s3mtZTOj4q

Success.

使用 Headscale 时,在控制服务器执行 headscale auth register --auth-id <登录地址中的 ID> --user josphat。使用 Tailscale 服务则打开输出地址并登录。无人值守容器可以通过 --auth-key 提供认证密钥,省去浏览器步骤。确认节点获得 tailnet 地址:

tailscale status
tailscale ip -4
ip -br addr show tailscale0

地址绑定到真实的内核接口,这就是 TUN 设备带来的区别。容器内任何程序都能通过普通套接字访问 tailnet 对端,ssh、curl 无需代理设置或专用参数。

100.64.0.1  ts-router  josphat  linux  -
100.64.0.1
tailscale0       UNKNOWN        100.64.0.1/32 fd7a:115c:a1e0::1/128 fe80::7bd5:fe25:e389:c5fa/64

Tailscale 可能接管容器内的 /etc/resolv.conf。实验中它写入 nameserver 100.100.100.100,并注明由 Tailscale 生成;DNS 在容器重启后仍正常。若要保留 Proxmox 设置的解析器,可按 tailscale dns status 的提示执行 tailscale set --accept-dns=false。同样的安装和加入流程也适用于完整虚拟机或裸机,Ubuntu 26.04 和 Rocky Linux 10 的 Tailscale 指南对此有进一步说明。

将容器配置为子网路由器

子网路由器是 LXC 中运行 Tailscale 的常见用途:一个小容器就能让 tailnet 设备访问局域网、Proxmox 网页界面以及无法安装 Tailscale 的机器。普通部署只需通告容器 eth0 所在的局域网,可跳过下一条命令。实验通告另一张隔离网络,以证明流量确实经过路由器:vmbr1 是没有物理端口和宿主地址的网桥,与 vmbr0 一样定义在宿主 /etc/network/interfaces 中;CT 202 web01 地址为 10.20.20.30。在宿主上为 CT 200 添加连接该网桥的第二个接口:

pct set 200 --net1 name=eth1,bridge=vmbr1,ip=10.20.20.20/24

在容器内通告路由。实验先用 sysctl -w net.ipv4.ip_forward=0 关闭转发,以观察当前 Tailscale 的提示。自己的节点只有在转发确实关闭时才显示转发警告,GRO 警告则不受这一条件影响:

tailscale up --login-server http://10.10.10.1:8080 --hostname ts-router --advertise-routes=10.20.20.0/24

两条警告立即出现;转发问题修复前,tailscale status 会持续保留健康检查提示。相比只输出一次的 tailscale up 警告,这个状态提示更便于后续检查者发现问题。

IP 转发关闭时的 Tailscale 警告和健康检查

实验节点上的新容器默认已有 net.ipv4.ip_forward = 1,但 net.ipv6.conf.all.forwarding = 0。每个非特权容器都有独立网络命名空间和 sysctl 设置,初始值取决于宿主,也可能随宿主重启变化。应在容器内把两项配置写入文件,使其持久生效:

echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
sysctl -p /etc/sysctl.d/99-tailscale.conf

非特权容器允许修改这些参数,因此 sysctl -p 能成功回显新值。Proxmox 宿主的设置不受影响,同一节点上的其他来宾也不会因此改变。

net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

第二条警告涉及 eth0 的 UDP GRO(通用接收卸载),它限制转发吞吐量,而不是让路由失效。实验原以为非特权容器会拒绝 ethtool 修改,但 veth 属于容器自己的命名空间,修改成功。安装 ethtool,按 Tailscale 性能指南设置:

apt install -y ethtool
ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off
ethtool -k eth0 | grep -E "rx-udp-gro-forwarding|rx-gro-list"

回读时两项功能均为预期值。若自己的容器拒绝修改,路由仍能使用,可稍后再处理吞吐量优化。

rx-gro-list: off
rx-udp-gro-forwarding: on

ethtool 修改只持续到接口被重新创建;容器重启会重建 veth。在容器内添加一个 oneshot 单元,在 tailscaled 启动前重新应用设置。创建单元文件:

nano /etc/systemd/system/tailscale-gro.service

写入实验使用的配置:

[Unit]
Description=UDP GRO forwarding for Tailscale on eth0
After=network-online.target
Wants=network-online.target
Before=tailscaled.service

[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off

[Install]
WantedBy=multi-user.target

启用单元,然后从宿主重启容器,验证整套配置能自行恢复:

systemctl daemon-reload
systemctl enable tailscale-gro.service

在 Proxmox 宿主上重启 CT 200:

pct reboot 200

等待 10~15 秒完成启动,再检查设备、sysctl 和 GRO 标志。尤其注意 IPv6 转发:实验中新容器的 IPv4 转发本就开启,单独检查它无法证明配置文件生效:

pct exec 200 -- bash -c "ethtool -k eth0 | grep rx-udp-gro-forwarding; sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding; ls -l /dev/net/tun"

重启后三项设置全部恢复。设备来自容器配置的 dev0,sysctl 来自 /etc/sysctl.d 文件,GRO 来自新单元;若某项缺失,可以分别找到对应配置位置。

rx-udp-gro-forwarding: on
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
crw-rw-rw- 1 root root 10, 200 Nov  2 16:53 /dev/net/tun

随后 systemctl status tailscale-gro 显示 inactive (dead)、status=0/SUCCESS,这是未设置 RemainAfterExit 的 oneshot 单元完成工作后退出的正常状态。重新执行相同的 tailscale up 已无警告,tailscale status 中的健康检查也消失。

通告路由后仍需控制服务器批准。使用 Tailscale 服务时,在管理控制台的 Machines 中打开路由器菜单,选择 Edit route settings,勾选子网;同一个对话框还有后面要用的出口节点开关。Headscale 中,先用 headscale nodes list 查节点 ID(实验中 ts-router 为 1),再按 ID 批准路由:

headscale nodes approve-routes -i 1 -r 10.20.20.0/24
headscale nodes list-routes

留意最后三列:Available 表示节点已通告路由,Approved 表示控制服务器已接受,Serving 表示已通知对端使用。只有 Available 有值的路由仍在等待批准:

Node updated
ID | Hostname  | Approved      | Available     | Serving (Primary)
1  | ts-router | 10.20.20.0/24 | 10.20.20.0/24 | 10.20.20.0/24

Linux 客户端默认忽略子网路由,应在 CT 201 加入网络时设置 --accept-routes。此前 CT 201 到 10.20.20.30 的 curl 失败,因为它没有连接 vmbr1 的接口,宿主也没有可用于转发的该网段地址:

tailscale up --login-server http://10.10.10.1:8080 --hostname ts-client --accept-routes

批准 ts-client 后,路由出现在 Tailscale 路由表 52 中,隔离网络里的网页服务器可以经路由器访问:

客户端经 Tailscale 子网路由访问隔离网络的 web01

关键证据是 traceroute:第一跳为路由器的 tailnet 地址 100.64.0.1,第二跳为 web01。两个容器位于同一 Proxmox 节点,tailscale ping 找到经过 10.10.10.20:41641 的直接路径,没有使用中继。同样的模式也适用于 tailnet 中的 pfSense 主机,或需要通过单个节点访问的其他站点。

将容器用作出口节点

出口节点把客户端全部互联网流量经容器发出,适合在酒店或咖啡馆使用家里的网络出口。它与子网路由器需要相同的转发配置,因此 CT 200 已就绪。在容器内通告出口节点:

tailscale set --advertise-exit-node

在控制服务器批准。Tailscale 服务使用 Edit route settings 对话框中的出口节点选项。Headscale 中出口节点对应 0.0.0.0/0、::/0 两条路由;approve-routes 会替换路由列表,务必同时保留原有子网:

headscale nodes approve-routes -i 1 -r 10.20.20.0/24,0.0.0.0/0,::/0

客户端选择该路由器作为出口节点,并跟踪一个公网地址。实验客户端与路由器共用上行链路,不论是否经过出口节点,公网 IP 都相同,所以 traceroute 比查公网 IP 更有判别力。在 CT 201 内执行:

tailscale set --exit-node=ts-router
tailscale status
traceroute -n -m 3 1.1.1.1

tailscale status 将 ts-router 标记为活动出口节点;跟踪结果先经过它,再到宿主 NAT 网关:

客户端经 Tailscale 出口节点访问公网的路径

512 MB 内存的容器足以为几部手机和笔记本提供出口,但仍与节点上其他工作负载共享上行链路。若需要独立旅行设备,可参考 GL.iNet 出口节点配置及支持原生 Tailscale 的旅行路由器清单。

退回用户空间网络模式

有时无法修改容器配置,例如托管商提供的 LXC,或自己没有管理权限的节点。Tailscale 可在用户空间处理数据包,不依赖 /dev/net/tun。实验 CT 203 完全没有设备;在容器内为 Debian 软件包启动时读取的 /etc/default/tailscaled 添加参数:

sed -i 's|^FLAGS=.*|FLAGS="--tun=userspace-networking --socks5-server=localhost:1055"|' /etc/default/tailscaled
systemctl reset-failed tailscaled
systemctl restart tailscaled

以相同的 tailscale up ... --accept-routes 加入后,取舍很明显:节点已加入 tailnet,tailscale ping 正常,但内核不知道 100.64.0.0/10 的路由,普通工具只有通过 SOCKS5 代理才能访问:

无 TUN 的用户空间模式通过 SOCKS5 访问子网服务

CT 203 直接运行 curl http://10.20.20.30/ 超时,退出码为 28;添加 --socks5-hostname localhost:1055 后,同一请求能经子网路由器取得页面。因此用户空间模式适合只需被访问,或只运行一个支持代理工具的容器;对于依赖普通套接字的应用则不合适。能添加设备时,应优先添加。

修复实验遇到的错误

以下错误均出现在 Proxmox VE 9.2、Tailscale 1.104.4 的实验中,提示文字保留终端原样。

CreateTUN(“tailscale0”) failed;/dev/net/tun does not exist

容器缺少 TUN 设备。用 pct set <CTID> --dev0 /dev/net/tun,或依次选择 Resources、Add、Device Passthrough 添加,再重置失败状态并重启 tailscaled。前面的 modprobe tun 错误只是 TUN 驱动已内置的结果,并非根因。

tailscaled.service: Start request repeated too quickly

五次快速失败后 systemd 会停止重试,仅添加设备不会恢复服务。先在容器内执行 systemctl reset-failed tailscaled,再运行 systemctl restart tailscaled;也可重启容器。

failed to connect to local tailscaled; it doesn’t appear to be running

任何原因导致守护进程停止时,tailscale 命令都会显示此错误。先检查 systemctl status tailscaled 和日志;容器中的常见原因就是缺失 TUN 设备。

Warning: IP forwarding is disabled, subnet routing/exit nodes will not work

tailscale up 搭配 --advertise-routes 或 --advertise-exit-node 时输出此警告,tailscale status 也会持续保留健康检查。应在容器的 /etc/sysctl.d/99-tailscale.conf 设置 IPv4、IPv6 两项转发参数,因为容器网络命名空间拥有独立的值。

Warning: UDP GRO forwarding is suboptimally configured on eth0

不修复时仍能路由,只是在高 UDP 负载下更慢。ethtool -K 可在非特权容器内设置,上面的 oneshot 单元让它在容器重启后继续生效。普通主机的同类配置见 Fedora Tailscale 指南。

Avatar photo

关于 木子

Email: muzi@vip.rockylinux.cn 微信:VebinLee QQ: 2306867585
Founder of the Rocky Linux Chinese community, MVP、VMware vExpert、TVP, advocate for cloud native technologies, with over ten years of experience in site reliability engineering (SRE) and the DevOps field. Passionate about Cloud Computing、Microservices、CI&CD、DevOps、Kubernetes, currently dedicated to promoting and implementing Rocky Linux in Chinese-speaking regions.
用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇