家里的 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 statustailscale 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/tunTailscale 非特权 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

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

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

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

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