跳过正文

OpenWRT 旁路由部署 Tailscale 实践

·3874 字
目录

OpenWRT 作为针对路由器等嵌入式设备设计的基于 Linux 的开源操作系统,因其较低的硬件配置需求和广泛的开源社区支持获得了许多技术爱好者的青睐。在家庭网络部署中,为了不修改网络原本的拓扑,可以将 OpenWRT 部署在老旧的 ARM 或者 X86 设备上作为「旁路网关」使用。

所谓「旁路网关」,也被叫做「旁路由」。实质上是在不替换网关、不改变(或较少改动)网关配置下,在同一个局域网段中额外增加一台路由设备,通过修改终端设备的指向网关地址和 DHCP 地址的方式让部分或全部流量经过这台路由设备处理的网络部署模式。实际配置中,通常让主网关实现家庭网络拨号上网,旁路由实现广告过滤、流量分流等高级功能,例如在旁路由上配置 Tailscale,可以实现在没有公网 IP 时也能在外部网络访问家庭网络。

配置预装 Tailscale 包的旁路由
#

除了拉取源代码自行编译 OpenWRT 固件外,OpenWRT 官网提供了 Firmware Selector 可以很方便快速地构建自定义固件。在网页上输入设备的名称或型号,然后选择一个稳定或快照版本,以 OpenWRT 24.05 版为例,只需要在「自定义预安装软件包和/或首次启动脚本」中添加以下软件包就能获得预装了 Tailscale 软件包的 OpenWRT 固件。

tailscale ip-full iptables-nft kmod-nft-nat kmod-tun luci-compat

我自己部署到 x86 AIO 的 OpenWRT 旁路由基于 PVE + Q35 环境,网卡使用 X550 SR-IOV 的虚拟化网卡, OpenWRT 对 VF 网卡的驱动支持很好,因此删掉了多余的网卡驱动、i915 图形驱动和 Legacy BIOS 支持,我的预安装软件包完整配置如下:

base-files ca-bundle dnsmasq dropbear e2fsprogs firewall4 fstools kmod-button-hotplug kmod-nft-offload libc libgcc libustream-mbedtls logd mkf2fs mtd netifd nftables odhcp6c odhcpd-ipv6only opkg partx-utils ppp ppp-mod-pppoe procd-ujail uci uclient-fetch urandom-seed urngd  kmod-fs-vfat kmod-ixgbe kmod-ixgbevf luci qemu-ga openssl-util tailscale ip-full iptables-nft kmod-nft-nat kmod-tun luci-compat

接下来点击「请求构建软件包」,等待构建完成后即可根据需要下载 EXT4 或者 SQUASHFS 格式的固件,安装并启动后,在控制台输入:

vi /etc/congfig/network

按 i 键进入编辑模式修改 OpenWRT 旁路由 IP 地址,输入一个不和内网其他设备冲突的 IP 地址,按 Esc 键退出编辑,按冒号键并输入 wq 回车保存编辑即可,重启后就能在浏览器中输入修改后的旁路由 IP 进入 Web 页。

Tailscale 在旁路由模式下的部署
#

Tailscale 使用场景
#

为了能在公网访问家庭网络设备,常见办法是开启内网设备的公网端口映射或者搭建内网穿透,但如果不想把家庭网络设备暴露到公网,或者不想使用付费的中转服务,那么借助 Tailscale 也能轻松实现公网回家需求。Tailscale 是基于 WireGuard 协议的异地组网工具,简单来说,不论设备是否在本地,只要设备加入了 Tailscale 组建的虚拟局域网(Tailnet),就能像在本地局域网中使用,比如最常见的需求就是异地访问家里的 NAS,通过 Tailscale 甚至能在 Windows 上正常使用通过 SMB 映射的网络硬盘,极大地解决了异地访问 NAS 文件需要依赖厂商软件和速度慢的痛点。

尽管 Tailscale 可以在很多平台上直接安装对应客户端一键组网,但如果不想安装 Tailscale 客户端或者实在没有可供安装的客户端时怎么办呢?Tailscale 还提供了 Subnet routersExit nodes 这两件神器。简而言之,是将部署了 Tailscale 的节点作为「跳板」中转来自 Tailnet 设备的访问,前者用来访问家庭局域网内部设备,后者用来代理访问公网。我在实际使用中把安装了 Tailscale 的 OpenWRT 作为访问家里群晖的「中转」节点,而不需要在群晖上安装 Tailscale,这样我就能在笔记本上随时随地使用内网的 NAS IP 地址映射 SMB 网络磁盘,而不需要繁琐地回家也要打开笔记本的 Tailscale 客户端,在外需要修改网络映射地址了。

旁路由接口配置
#

  • 在「网络」→「接口」→「lan」配置中,配置网关地址和 DNS 服务器地址指向主网关,子网掩码为 255.255.255.0,DHCP服务器勾选忽略接口。
  • 在「网络」→「接口」→「添加新接口」,名称输入 「tailscale」,协议选择「不配置协议」,设备选择「tailscale0」
  • 终端设备根据需要修改网络配置为指向主网关或旁路由。实际上不论终端设备是否把 OpenWRT 作为默认网关都不影响使用 Tailscale 在公网环境下访问家庭内网设备,区别在于内网设备能否访问外部的 Tailscale 设备
    旁路由部署模式

旁路由防火墙配置
#

OpenWRT 提供了部署 Tailscale 的官方教程,但教程针对的是 OpenWRT 作为主网关部署的情况,此时局域网的所有设备默认路由都指向 OpenWRT。但在旁路由模式下终端设备按需指向主网关或者旁路由,例如当 NAS 网关地址指向主网关而非旁路由,通过 OpenWRT 「跳板」访问 NAS 就会出现类似局域网公网 IP 访问内部服务器的 NAT 回流问题。此时有三种解决方法:

  1. 让终端设备默认路由指向旁路由或者开启 OpenWRT 的「强制使用 DHCP」功能强行让所有设备的网络流量都将通过旁路由;
  2. Tailscale 有一条默认启动参数「snat-subnet-routes=true」。意思是当远程 Tailscale 设备通过 OpenWRT 的 Subnet routers 功能访问内网没有安装 Tailscale 的设备时,Tailscale 会对 tailscale0 接口来的流量做 SNAT,让防火墙把远程设备的源 IP 修改为流量发出去的接口的 IP,一般就是 OpenWRT 物理接口的 IP,这样一来内网设备会认为是 OpenWRT 在访问它。这和下面方法三在 lan zone 上配置 Masquerade 的行为很类似;
  3. 手动配置防火墙。如果 OpenWRT 还安装了其他插件,Tailscale 对防火墙的改动很可能会引起一连串连锁反应,为了拿掉 Tailscale 的配置黑箱,我们也可以手动修改防火墙配置,将外部设备的 Tailnet IP 替换为能被内网设备认识的旁路由物理接口 IP。要实现这一点,只需要将官方教程中对 tailscale zone 配置的 Masquerading 修改为绑定物理接口的 lan zone 即可。在旁路由模式下,来自 Tailnet 的外部设备通过「跳板」访问内网设备的流量路径如下:
    tailscale网络流量路径
    在 lan zone 防火墙没有启用 Masquerade 时,来自 tailscale0 接口的请求包记录了正确的内网 IP,能被正确转发到 lan zone 并走 eth0 物理接口到达内网设备,但这个请求包的源 IP 地址仍然为 Tailnet 的 IP 地址而非 OpenWRT 旁路由物理接口的 IP 地址,因此在内网设备网关地址没有指向旁路由的情况下,来自内网设备目的 IP 地址为 Tailnet IP 的回包,会被内网设备转发到默认的主网关并被主网关丢弃或者转到公网。当启用 lan zone 的 Masquerade 后,此时内网设备识别到请求包来自 OpenWRT 并正确发送回包从而建立连接。
    openwrt防火墙配置

一点遗憾
#

那么 OpenWRT 作为旁路由的代价是什么呢?实际上依照官方教程将 OpenWRT 作为主网关,并配置好 Subnet routers 后,不仅能实现 Tailnet 设备访问内网设备,而且内网设备能正确记录到访问 IP 来自于 Tailnet 设备而非笼统的旁路由物理接口 IP。另外,不在 Tailnet 的内网设备还能通过直接输入 Tailnet IP 反向访问 Tailscale 设备。典型的应用场景就是在家里的一台没有安装 Tailscale 的电脑也能访问办公室里的 Tailscale 节点设备。

依照官方教程,当 OpenWRT 作为主网关,且开启 tailscale zone 防火墙的 Masquerade 时,来自没有安装 Tailscale 的局域网设备对远程 Tailscale 设备发起的请求流量路径如下:

tailscale内网设备流量转发路径
如图所示,OpenWRT 作为局域网设备的默认路由时,对于设备发出的目的 IP 不在内网网段的请求包,局域网设备会将包转到 OpenWRT 上。而当 tailscale0 接口启动时,tailscaled 守护进程会向 OpenWRT 的内核路由表写入路由,规定所有指向 100.64.0.0/10(Tailnet 网段)的包都经过 tailscale0 接口,因此请求包能被内核从 eth0 接口正确转发到 tailscale0 接口,内核再根据 tailscale zone 防火墙的 Masquerade 配置执行 SNAT,修改数据包源 IP 地址为 tailnet 的 IP,然后被 tun 网卡送到 tailscaled 用户态进程经加密后发送到公网。

而在旁路由部署模式下,只要 OpenWRT 不被内网设备指为默认网关,除非在主网关上配置 Tailnet 网段的静态路由,让主网关将来自内网设备目的地址为 Tailnet 网段的包又转发给 OpenWRT,否则无论是方法二还是方法三,OpenWRT 的防火墙都不会对来自 eth0 接口并转发到 tailscale0 接口的包做任何 SNAT 处理,因而在如上配置中只能「痛失」这个 feature。

启动 Tailscale
#

解决了防火墙配置,接下来就可以启动 Tailscale 了,由于 Tailscale 官方并没有提供官方的 LuCI 网页插件,因此需要在 OpenWRT 控制台手动输入命令:

tailscale up --advertise-routes=192.168.x.0/24 --advertise-exit-node --netfilter-mode=off --snat-subnet-routes=false --accept-dns=false
  • 「netfilter-mode=off」为了防止 Tailscale 自作主张地修改我们配置好的防火墙参数,禁用 Tailscale 对 OpenWRT 防火墙的管理,让 Tailscale 完全不碰防火墙。
  • 「accept-dns=false」不接受 Tailscale 下发的 DNS 设置(MagicDNS),不让 Tailscale 接管 OpenWRT 原有的 DNS(通常是 dnsmasq),避免 Tailscale 影响 OpenWRT 其他插件正常工作。
  • 「snat-subnet-routes=false」由于在 lan zone 防火墙中已经启用了 Masquerade,而且禁用了 Tailscale 对防火墙的管理,因此没有必要启用 Tailscale 作为子网路由时对通过 tailscale0 接口数据包的 SNAT。当然,如果使用方法二,不需要打开 lan zone 或者 tailscale zone 的Masquerade,Tailscale 启动命令也应当修改为:
    tailscale up --advertise-routes=192.168.x.0/24 --advertise-exit-node --netfilter-mode=on --snat-subnet-routes=true --accept-dns=false

接着在 Tailscale 网页管理界面批准 OpenWRT 设备加入 Tailnet,批准设备开启 Subnet routers 和 Exit nodes,这样我们就成功上线了 Tailscale!

再做点优化
#

在 Tailscale 版本 1.54 及以上版本与 OpenWrt 24.10 及以上版本一起使用时,在网卡硬件支持的前提下,可以通过传输层卸载提高 UDP 吞吐量,优化 OpenWRT 性能,降低 CPU 负载。

临时开启传输层卸载
#

opkg update 
opkg install ethtool
# eth0 替换为实际的网卡名
ethtool -K eth0 rx-gro-list off 
ethtool -K eth0 rx-udp-gro-forwarding on

查询参数是否启用

ethtool -k eth0 | grep generic-receive-offload

永久生效
#

在 Shell 中直接输入:

# 1. 创建配置文件
touch /etc/config/ethtool
# 2. 定义针对 eth0 的配置块
uci set ethtool.eth0=device
# 3. 设置具体的参数
uci set ethtool.eth0.rx_gro_list='off'
uci set ethtool.eth0.rx_udp_gro_forwarding='on' 
# 4. 将更改从内存保存到硬盘文件
uci commit

在 Shell 中直接输入:

cat <<'EOF' > /etc/hotplug.d/iface/99-ethtool
#!/bin/sh
# shellcheck disable=SC3043

# 只有在接口启动时运行
[ "${ACTION}" = "ifup" ] || exit 0

. /lib/functions.sh

config_load ethtool

log_crit() { logger -t "ethtool-cfg" -p crit "$1"; }
log_info() { logger -t "ethtool-cfg" -p info "$1"; }

apply_settings() {
    local config feature ifname option value
    ifname="$1"
    
    # 获取当前配置节下的所有参数名
    config=$(uci show ethtool."${ifname}" | sed -n "s/^ethtool.${ifname}\.\([^=]*\)=.*/\1/p")

    for option in ${config}; do
        config_get value "${ifname}" "${option}"
        # 将下划线转为中划线以符合 ethtool 参数格式
        feature=$(echo "${option}" | tr '_' '-')
        
        if [ -n "${value}" ]; then
            {
                ethtool -K "${ifname}" "${feature}" "${value}" \
                && log_info "${feature} set to ${value} on ${ifname}";
            } || log_crit "Failed to set ${feature} to ${value} on ${ifname}"
        fi
    done
}

# 遍历配置文件中所有的 device 节
config_foreach apply_settings device
EOF

# 赋予可执行权限
chmod +x /etc/hotplug.d/iface/99-ethtool

重启 OpenWRT 验证配置是否生效。

其他
#

  • 当需要修改 Tailscale 配置或者 Tailscale 配置出错需要清理防火墙时,可以先重置防火墙再重新上线 Tailscale:
    tailscale down # 停止 Tailscale
    iptables -L -v -n | grep tailscale # 检查防火墙残留
    /etc/init.d/firewall restart # 重置防火墙状态,抹除 Tailscale 注入的非持久化规则
    tailscale up --reset # 重置 Tailscale 启动参数
  • 由于 Tailscale 加密包存在头开销,因此 Tailscale 将 MSS 设置为比较保守的 1280 字节,在 OpenWRT 的防火墙设置中应该开启 MSS 钳制以防止 MTU 过大被路由丢弃。

大功告成
#

这样就完成了 Tailscale 部署在 OpenWRT 旁路由上从固件到安装再到优化的全流程工作!在折腾过程中碰到了不少问题,把前后都写出来,希望能对后来人有一点帮助。