家里的 PVE 上跑着导航页、代码仓库、音乐服务等应用。在局域网里,它们都能通过各自的 IP 和端口访问;离开家以后,我也希望继续使用同样的地址。

这次实践采用了一个相对简单的结构:在 PVE 中新建一个 Debian LXC,运行 Tailscale 子网路由器,让手机和电脑通过它访问家庭网络。

基础部署很顺利,但第一次使用手机流量访问时,网页加载非常慢。检查后发现,连接没有直连成功,而是绕到了海外 DERP 中继。于是,我又利用已有的公网服务器搭建了 Tailscale Peer Relay。期间还遇到了“规则正确、中继可见,却一直不被使用”的情况,最终通过重启家庭节点的 Tailscale 服务恢复了中继握手。

本文记录完整的部署、排查和验证过程。修复后,连续 10 次测试均走自建中继,没有超时,延迟中位数约为 73 ms,手机上的网页也恢复到正常可用的速度。

脱敏说明:本文不包含实际公网 IP、账号、密码、登录授权链接或设备密钥。公网地址统一用文档示例地址 203.0.113.10 表示,设备名、Tailscale 地址和应用内网地址也已替换。示例地址不能直接用于部署,请按自己的环境修改。性能数据保留本次实测结果。

一、先明确:要访问家庭服务,需要什么角色?

这套方案涉及三个容易混淆的概念。

角色 作用 本次是否使用
Subnet Router,子网路由器 让 Tailscale 客户端访问某个局域网网段 使用,由家庭 LXC 承担
Peer Relay,专用中继 在设备无法直连时,转发它们之间的加密流量 使用,由公网服务器承担
Exit Node,出口节点 让客户端的互联网流量经指定设备出网 不使用

我要解决的是“访问家里的服务”。因此,手机不需要选择出口节点,公网服务器也不需要接管手机的日常上网流量。

家庭服务本身无需逐个安装 Tailscale。只要 LXC 能访问这些服务,就可以通过子网路由把访问路径接起来。这也是选择子网路由器的原因。Tailscale 子网路由文档

二、最终网络结构

flowchart LR
    Phone["手机 / 笔记本<br/>Tailscale 客户端"]
    Relay["公网服务器<br/>Peer Relay · UDP 40000"]
    Official["官方 DERP<br/>后备中继"]

    subgraph Home["家庭网络 · 192.168.1.0/24"]
        Gateway["PVE LXC<br/>Tailscale 子网路由器"]
        PVE["PVE 管理页面"]
        Apps["Homepage / Forgejo / Navidrome"]
        Gateway --> PVE
        Gateway --> Apps
    end

    Phone -->|"优先尝试直连"| Gateway
    Phone -.->|"无法直连时"| Relay
    Relay -.-> Gateway
    Phone -.->|"专用中继不可用时"| Official
    Official -.-> Gateway

图中表示可选路径,不是每次访问都会经过所有节点。实际走哪条路径,要以 tailscale status 和 tailscale ping 的结果为准。

Peer Relay 是同一个 Tailscale 网络内的中继能力,无法直连时会优先尝试可用的专用中继,再回退到 DERP。它没有替代 Tailscale 官方的协调服务,本次也没有部署 Headscale 或自建 DERP。Peer Relay 官方文档

三、环境与示例地址

本次使用的环境如下,版本号记录的是实践时的状态。

项目 配置
虚拟化平台 Proxmox VE 9.2
家庭 LXC Debian 13,非特权容器
LXC 资源 1 核 CPU、512 MB 内存、256 MB Swap、4 GB 磁盘
网络桥接 vmbr0,通过 DHCP 获取地址
家庭网段 192.168.1.0/24
公网服务器 Rocky Linux 9.7
Linux Tailscale 1.102.4
Android Tailscale 1.102.3

为了方便对照,后面的命令统一使用这些示例值:

对象 示例值
LXC 编号 113
家庭 Tailscale 节点 home-router / 100.100.10.10
公网中继节点 public-relay / 100.100.10.20
手机 Tailscale 地址 100.100.10.30
公网服务器地址 203.0.113.10,仅为文档占位地址
家庭路由器 / DNS 192.168.1.1
Homepage http://192.168.1.20:3000

Tailscale 地址由平台分配。这里列出的地址用于解释配置,并不意味着要手动给设备设置这些 IP。

四、在 PVE 创建一个专用 LXC

4.1 检查资源与模板

以下命令在 PVE 宿主机执行:

pveversion
pvesm status
pct list
qm list
pveam list local
ip -4 route
free -m

主要确认四件事:容器编号没有占用、存储空间足够、已有可用的 Debian 模板,以及家庭网段和桥接接口符合预期。

本次 PVE 已缓存 Debian 13 模板,因此直接使用它创建容器。下面的模板文件名和存储名需要按实际环境调整。

pct create 113 \
  local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname tailscale-gateway \
  --cores 1 \
  --memory 512 \
  --swap 256 \
  --rootfs local:4 \
  --unprivileged 1 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --nameserver 192.168.1.1 \
  --onboot 1 \
  --startup order=2,up=10 \
  --timezone Asia/Shanghai

这台容器只负责网络转发,初始资源占用很低。上述配置足以作为本次家庭访问的起点,不代表所有带宽场景都只需要这些资源。

4.2 给容器提供 TUN 设备

Tailscale 的内核网络模式需要 /dev/net/tun。在支持设备直通的 PVE 版本上,可以通过 pct set 配置:

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

也可以在 PVE 界面的容器资源页添加 Device Passthrough,设备路径填写 /dev/net/tun。Tailscale 非特权 LXC 部署说明

本次 Debian 13 模板启动后还出现了 systemd 挂载单元失败。根据 PVE 的提示,为这个新容器开启 nesting 后,失败单元消失:

pct set 113 --features nesting=1
pct start 113

如果容器已经运行,修改设备直通和 nesting 后,需要先关闭再启动容器才能应用。

检查结果:

pct exec 113 -- ls -l /dev/net/tun
pct exec 113 -- ip -br addr
pct exec 113 -- ip route
pct exec 113 -- systemctl --failed --no-pager

这里仍然使用非特权容器。nesting 是为本次模板的实际启动问题添加的配置,不应把它当作所有 Tailscale 安装都必须具备的条件。

五、把 LXC 配置成家庭子网路由器

5.1 从官方仓库安装 Tailscale

先在 PVE 执行:

pct enter 113

以下命令都在 Debian LXC 内执行:

apt-get update
apt-get install -y curl ca-certificates iptables ethtool

install -d -m 0755 /usr/share/keyrings

curl -fsSL \
  https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg \
  -o /usr/share/keyrings/tailscale-archive-keyring.gpg

curl -fsSL \
  https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list \
  -o /etc/apt/sources.list.d/tailscale.list

apt-get update
apt-get install -y tailscale

systemctl enable --now tailscaled
tailscale version

这套安装方式使用官方 stable 仓库,后续可以通过系统包管理器更新。其他发行版应使用对应的软件源,不要直接照搬 Debian 的配置。Tailscale 官方软件包仓库

5.2 开启 IP 转发

cat > /etc/sysctl.d/99-tailscale.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF

sysctl -p /etc/sysctl.d/99-tailscale.conf

本次只发布家庭 IPv4 网段。开启 IPv6 转发本身不会让客户端获得家庭 IPv6 路由,也不会把设备变成出口节点。

5.3 持久化转发策略与网卡设置

在这个新建、专用的路由容器里,我把普通转发的默认策略设为 DROP,由 Tailscale 管理它需要的转发和 SNAT 规则。同时启用网卡支持的 UDP GRO 转发优化。

为保证重启后仍然生效,创建一个 systemd 单元:

cat > /etc/systemd/system/tailscale-router-setup.service <<'EOF'
[Unit]
Description=Tailscale subnet router forwarding policy and UDP offload
Wants=network-online.target
After=network-online.target
Before=tailscaled.service

[Service]
Type=oneshot
ExecStart=/usr/sbin/iptables -P FORWARD DROP
ExecStart=/usr/sbin/ip6tables -P FORWARD DROP
ExecStart=-/usr/sbin/ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now tailscale-router-setup.service

ethtool 命令前的 - 表示网卡不支持该设置时,不让整个单元启动失败。这个优化不能解决“流量绕海外中继”的问题,两者属于不同层面的因素。

这组 FORWARD 策略只应用在新建的专用 LXC 中。已有 Docker、其他 VPN 或转发业务的主机,应先核对其防火墙规则。

5.4 登录并发布家庭网段

tailscale up \
  --hostname=home-router \
  --advertise-routes=192.168.1.0/24 \
  --accept-dns=false \
  --timeout=30s

打开命令输出的授权链接,使用自己的 Tailscale 账号绑定设备。

参数含义如下:

参数 用途
--hostname=home-router 设置 Tailscale 设备名
--advertise-routes=192.168.1.0/24 发布家庭网段
--accept-dns=false 保留容器现有 DNS 配置
--timeout=30s 限制这次命令等待登录完成的时间

如果浏览器登录花了超过 30 秒,命令可能报告等待超时。此时先通过 tailscale status 检查真实状态,不必马上重装或重复创建容器。

随后进入 Tailscale 管理后台:

  1. 在 Machines 中找到 home-router。
  2. 打开子网设置,批准 192.168.1.0/24。
  3. 确认访问控制允许自己的客户端访问家庭网段。
  4. 对需要长期在线的节点,按使用需求配置密钥到期策略。

发布路由、批准路由、允许访问是三个不同的环节。 路由获批不等于访问控制自动放行。子网路由配置说明

5.5 客户端连接与首次验证

手机和电脑安装 Tailscale,登录同一个网络并连接。Linux 客户端还需要接受子网路由:

sudo tailscale set --accept-routes=true

Android、iOS、macOS 和 Windows 通常会自动使用获批的子网路由。客户端路由行为

手机测试时关闭 Wi-Fi,使用移动网络,然后打开原来的家庭服务地址,例如:

http://192.168.1.20:3000

记得使用服务原有的账号密码。Tailscale 负责把网络连起来,不会替应用完成身份验证。

六、网页能打开,却非常慢

第一次通过手机流量访问时,网页已经能打开,但加载明显拖沓。

我先把问题拆成两段:服务本身是否慢,以及手机到家里的网络路径是否慢。

6.1 先排除家庭应用响应缓慢

在 LXC 内访问 Homepage,记录响应时间:

curl -sS --max-time 10 \
  -o /dev/null \
  -w 'HTTP=%{http_code} total=%{time_total}s bytes=%{size_download}\n' \
  http://192.168.1.20:3000

本次结果为 HTTP 200,约 70 KB 的响应只用了 2.7 ms。LXC 的内存也很充足,因此暂时没有证据指向容器资源不足或 Homepage 后端缓慢。

这个测试测的是一次 HTTP 响应,不包括浏览器加载所有脚本、图片和接口的总耗时。

6.2 看实际路径,而不只看“Connected”

tailscale status
tailscale ping --c 5 --timeout=3s 100.100.10.30
tailscale netcheck

下面是脱敏后的代表性输出:

100.100.10.30  android-phone  ...  active; relay "lax"

pong from android-phone (100.100.10.30) via DERP(lax) in 2.598s
pong from android-phone (100.100.10.30) via DERP(lax) in 406ms
pong from android-phone (100.100.10.30) via DERP(lax) in 417ms
direct connection not established

这说明手机与家庭节点没有直连成功,而是通过洛杉矶 DERP 中继通信。后续还观察到了旧金山中继路径。

netcheck 中,家庭节点到部分海外 DERP 的延迟约为 190 ms。但手机到家庭节点的实际往返还包含另一段路径,因此不能用这个数字代替端到端延迟。

本次手机到家庭节点的实测是 约 400–2600 ms。和局域网内 2.7 ms 的应用响应相比,网络路径是当时最明显的瓶颈。

七、利用已有公网服务器部署 Peer Relay

家里到已有公网服务器的 ICMP 延迟约为 26 ms,比海外中继低得多,所以接下来尝试把这台服务器配置成 Peer Relay。

本方案不需要新增二级域名,也不需要给中继申请 HTTPS 证书。这里配置的是 Tailscale 内置的 UDP Peer Relay,不能直接套用自建 DERP 的部署步骤。

7.1 在 Rocky Linux 安装 Tailscale

以下命令在 公网服务器执行:

curl -fsSL \
  https://pkgs.tailscale.com/stable/rhel/9/tailscale.repo \
  -o /etc/yum.repos.d/tailscale.repo

dnf install -y tailscale
systemctl enable --now tailscaled

tailscale up \
  --hostname=public-relay \
  --accept-dns=false \
  --timeout=30s

使用与家庭节点相同的 Tailscale 网络完成绑定,确认 tailscale status 显示在线。

7.2 登录后启用中继功能

把下面的示例地址替换为服务器真实公网地址:

tailscale set \
  --relay-server-port=40000 \
  --relay-server-static-endpoints=203.0.113.10:40000

先完成 tailscale up 登录,再执行中继设置,有助于避免首次初始化过程中配置没有按预期保留下来。设置后应再次检查,而不是只看命令是否退出成功。

ss -lunp | grep 40000
tailscale debug peer-relay-sessions

公网中继只负责这项工作,不发布家庭网段,不开启出口节点,也不需要为了中继功能设置 --accept-routes=true。

7.3 放行 UDP 40000,并验证实际可达

需要同时检查云平台的安全组和服务器本机防火墙,允许客户端访问 UDP 40000。仅放行 TCP 40000 没有效果。

本次公网服务器的防火墙由 1Panel 维护。虽然 INPUT 链默认策略是 ACCEPT,但它跳转到的自定义链中存在 UDP DROP 规则,因此仍然需要在这些规则之前增加放行。

以下是本次环境采用的幂等规则:

iptables -C INPUT -p udp --dport 40000 \
  -m comment --comment tailscale-peer-relay -j ACCEPT 2>/dev/null || \
iptables -I INPUT 1 -p udp --dport 40000 \
  -m comment --comment tailscale-peer-relay -j ACCEPT

为了让 tailscaled 每次启动时补齐规则,添加 systemd drop-in:

install -d /etc/systemd/system/tailscaled.service.d

cat > /etc/systemd/system/tailscaled.service.d/peer-relay-firewall.conf <<'EOF'
[Service]
ExecStartPre=/bin/sh -c '/usr/sbin/iptables -C INPUT -p udp --dport 40000 -m comment --comment tailscale-peer-relay -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I INPUT 1 -p udp --dport 40000 -m comment --comment tailscale-peer-relay -j ACCEPT'
EOF

systemctl daemon-reload

这段配置针对本次 iptables 环境。使用 firewalld、UFW 或原生 nftables 的主机,应通过对应管理工具配置规则。1Panel 如果在服务运行期间重写防火墙,这个 drop-in 不会持续监控规则,需要重新检查或重启 tailscaled 补齐。

本次还从家庭容器发送了带标记的 UDP 探测包,在公网服务器确认收到,并检查到 ACCEPT 计数增加。这样验证的是实际网络路径,不只是“配置页面里看起来已经放行”。

7.4 在 Tailscale 后台授权使用中继

进入 Access controls → JSON editor,在现有 grants 数组中追加下面这个对象:

{
  "src": ["100.100.10.10"],
  "dst": ["100.100.10.20"],
  "app": {
    "tailscale.com/cap/relay": []
  }
}

其中:

  • src 是家庭子网路由器的 Tailscale IP。
  • dst 是公网中继节点的 Tailscale IP,不是它的公网 IP。
  • tailscale.com/cap/relay 授予家庭节点使用这台中继的能力。

这是数组中新增的一个规则对象,不是让人把整个策略文件替换成上面的内容。原来的家庭服务访问规则仍然需要保留。

这条授权允许其他节点与家庭节点通信时建立相应的中继路径,不需要把所有手机都改成中继服务器。中继节点和参与通信的客户端都需要支持 Peer Relay;官方要求版本为 1.86 或更新。中继授权与客户端要求

八、这次真正卡住的地方:中继可见,却没有被使用

规则保存后,网页仍然很慢。

首先检查家庭节点能否识别中继:

tailscale debug peer-relay-servers

结果已经包含了中继地址:

[
  "100.100.10.20"
]

但公网服务器上看到的会话数一直是 0:

Server port: 40000
Sessions count: 0

与此同时,手机访问仍然显示 relay "sfo"。

这些结果只能说明“中继已被发现”,不能说明“数据正在走中继”。

8.1 逐层确认,而不是反复修改同一条规则

这次按下面的顺序排查:

flowchart TD
    A["外网访问慢"] --> B{"家庭服务本地响应正常?"}
    B -->|"否"| C["检查应用、资源和家庭网络"]
    B -->|"是"| D["检查 tailscale status / ping"]
    D --> E{"实际连接路径"}
    E -->|"direct"| F["检查带宽、丢包和浏览器资源加载"]
    E -->|"海外 relay"| G["检查专用中继候选、授权和 UDP 端口"]
    E -->|"peer-relay"| H["检查中继延迟、流量和应用响应"]
    G --> I{"是否建立中继会话?"}
    I -->|"是"| H
    I -->|"否"| J["检查在线状态、版本、握手计数和日志"]
    J --> K["针对异常修复后重新测实际路径"]

手机一度在控制端显示离线,探测全部超时。重新连接后恢复在线,但仍然走海外 DERP,所以离线只是排查过程中遇到的一个状态,不能解释后续持续不使用中继的问题。

随后确认了以下事实:

检查项 结果
手机客户端版本 支持 Peer Relay
家庭与公网节点版本 支持 Peer Relay
中继 UDP 40000 已监听,家庭侧探测可达
管理后台授权 已下发到节点
家庭节点的候选中继列表 包含公网中继
公网节点中继会话数 持续为 0
家庭节点中继分配请求计数 为 0

这些证据说明不能再简单地归因于“没有保存规则”或“只要打开端口就好了”。

需要更细的日志时,可以短暂启用调试:

tailscale debug component-logs --for=3m magicsock
journalctl -u tailscaled --since '5 minutes ago' --no-pager

tailscale debug metrics | grep \
  '^magicsock_disco_sent_alloc_udp_relay_endpoint_request '

tailscale debug 属于调试接口,输出字段和子命令可能随版本变化。日志也可能带有地址和设备标识,对外分享前应脱敏。

8.2 重启家庭节点的 Tailscale 后恢复

确认上述条件后,我在 PVE 宿主机执行:

pct exec 113 -- systemctl restart tailscaled

这只重启该容器内的 Tailscale 服务,会短暂中断远程连接,不会重启其他虚拟机和家庭应用。

等待 tailscale status 恢复正常后,再测手机:

pct exec 113 -- tailscale ping \
  --c 10 --timeout=2s 100.100.10.30

随后出现了预期输出:

pong from android-phone (100.100.10.30) via DERP(sfo) in 623ms
pong from android-phone (100.100.10.30) via DERP(sfo) in 595ms
pong from android-phone (100.100.10.30) via peer-relay(203.0.113.10:40000:vni:1) in 127ms
pong from android-phone (100.100.10.30) via peer-relay(203.0.113.10:40000:vni:1) in 61ms

以上节选保留了实测时延,地址和设备名已经替换。公网服务器上也出现了双向转发的会话,包数和字节数持续增长。

这才构成了完整的验证链:候选中继可见 → 握手完成 → 会话建立 → 实际数据经中继转发。

这里能确定的是,重启服务后,本次环境恢复了正常的中继协商。没有进一步证据证明其底层原因,不能把它写成某个已经确认的版本缺陷,也不应据此配置定时重启。

另外,tailscale ping 在走专用中继时,结尾仍可能出现 direct connection not established。这表示没有建立直连,不代表专用中继失败;应该结合前面的 peer-relay(...) 结果判断。

九、修复前后,究竟改善了多少?

不同测试覆盖的路径不同,下面分开记录。

测试 本次结果 说明
家庭 LXC → Homepage 约 2.7 ms,HTTP 200 局域网内一次 HTTP 响应
家庭网络 → 公网服务器 ICMP 平均约 26 ms 只测这一段网络
公网服务器 → 家庭 Tailscale 节点 直连约 28 ms 两个 Tailscale 节点间探测
手机 → 家庭节点,修复前 约 400–2600 ms 经过海外 DERP
手机 → 家庭节点,修复后第二轮 64–280 ms,中位数约 73 ms 10 次均走 Peer Relay,无超时
公网服务器 → 家庭 Homepage 约 114–145 ms,HTTP 200 经子网路由访问应用,3 次测试

修复后的第二轮 10 次探测数据为:

169, 278, 280, 273, 66, 75, 67, 70, 64, 66 ms

排序后,中间两个数是 70 和 75,因此中位数为:

\operatorname{median}(RTT)=\frac{70+75}{2}=72.5\,\mathrm{ms}

移动网络仍然有明显的延迟波动,但已消除了这次连接持续绕海外中继的问题。手机端再次刷新网页后,实际反馈是“明显变快,能正常使用”,公网中继的转发字节也持续增加。

本次没有进行标准化吞吐量压测,也没有记录浏览器完整页面加载时间。因此,这些结果证明的是连接路径和交互体验改善,不能据此宣称带宽提升了多少倍。

十、邀请家人朋友时,访问范围和中继要分开看

这套方案使用子网路由。如果希望其他人也访问家庭网段,应在 Users 页面邀请他们加入自己的 Tailscale 网络。只分享 home-router 这个设备,不会把它后面的整个子网一起分享出去。邀请用户与分享设备的区别

接受邀请后,对方用自己的账号登录客户端,并选择加入的网络。角色通常选择普通 Member 即可。

邀请之前还要检查原有策略。本次初始策略包含全部放行规则,适合先完成自己的连通性测试,但如果直接邀请其他人,他们也会获得广泛的网络访问权限。

如果只想分享音乐服务,就应先把权限收敛到对应服务的地址和端口。保留一条全放行规则,再额外添加一条限制更窄的允许规则,并不能限制原来的权限。

现有的中继能力规则是围绕家庭网关配置的。新成员访问家庭服务时,也可以使用这条中继路径,不必按人复制一遍相同的中继授权;前提是其客户端版本、网络可达性和服务访问权限都符合要求。是否实际使用中继,仍需在对方连接后检查。

访问家庭服务不会自动让对方的全部互联网流量经过公网服务器,因为这里没有启用出口节点。

十一、日常维护与复查

平时最有用的几条命令如下。

在 PVE 宿主机检查家庭节点:

pct status 113
pct exec 113 -- tailscale status
pct exec 113 -- tailscale debug peer-relay-servers
pct exec 113 -- systemctl --failed --no-pager

在 公网中继检查会话与端口:

tailscale debug peer-relay-sessions
ss -lunp | grep 40000
iptables -nvL INPUT --line-numbers

如果以后再次变慢,先看实际路径是 direct、relay 还是 peer-relay,再针对那条路径排查。不要只根据客户端显示 Connected,或者后台设备旁边的绿点,判断访问一定正常。

还有几个与长期使用直接相关的细节:

  • 自动启动:PVE 容器和两端 tailscaled 都要设置开机启动。
  • 地址稳定性:家庭服务使用 DHCP 时,建议在路由器中设置地址保留,避免收藏的服务地址变化。
  • 密钥到期:家庭网关与公网中继是两台独立设备,要分别确认到期策略。关闭到期后仍需管理好设备撤销和账号安全。
  • 网段冲突:外部 Wi-Fi 若也使用 192.168.1.0/24,可能产生路由冲突。先用移动网络验证,再针对冲突调整。
  • 防火墙变更:升级面板或重写规则后,重新确认 UDP 40000 的可达性。
  • 公网服务器资源:中继会消耗服务器的带宽和流量额度,多人同时使用时需要观察实际负载。

这次实践里,LXC 部署只是第一步。真正影响体验的是连接最后走了哪条路径。把服务响应、子网路由、连接协商和中继转发分别验证,才能从“能访问”走到“能稳定地使用”。