使用 OpenWrt 策略路由把流量转发到旁路网关

发布:

前言🔗

最近一段时间,小山重新规划了家里的网络,更换了主路由,并把之前主路由上运行的一些软件卸载到了虚拟机运行的旁路由。

常见的使用旁路由的方法有两种,第一种是把设备的网关更改旁路由的 IP,第二种是用 DHCP 为设备下发网关,两种方法都是让设备的所有流量转发到旁路由处理。这两种方法都会导致一个问题,如果旁路由掉线,设备将无法访问互联网。

如果要解决上面提到的问题,需要用第三种方法:策略路由。什么是策略路由?通过配置规则将流量路由到不同的目标/网关,它可以在设备端实现,也可以在路由端实现,这种方法的好处在于,即使旁路由掉线,也不会影响正常上网。经过这段时间的摸索,我整理出了一套在 OpenWrt 上实现策略路由的方案,将特定的流量从主路由转发到旁路由。

开始之前,请确保你的旁路由可以正常工作并且运行模式是 Redir-Host,主路由正在运行的 OpenWrt 版本不低于 24.10.0,我没有在低版本测试过,还需要熟悉 SSH 和终端的使用,之后的大部分操作都需要运行命令。

如果你之前使用的方法是手动设置网关或 DHCP 下发网关,请先还原这些设置。

除非另有说明,否则所有操作都是在主路由上进行。

创建路由和规则🔗

首先添加一个路由表和路由规则,将特定流量转发到特定网关。

给序号 288 的路由表创建别名 byepass:echo '288 byepass' >> /etc/iproute2/rt_tables

添加一个路由到 byepass 路由表,并将网关设置为旁路由,务必将 $gateway$ 更改为你的旁路由 IP 地址:

uci set network.byepass_route=route
uci set network.byepass_route.interface='lan'
uci set network.byepass_route.target='0.0.0.0/0'
uci set network.byepass_route.gateway='$gateway$'
uci set network.byepass_route.table='byepass'
uci commit

创建路由规则,将包含特定标记的流量转发到 byepass 路由表:

uci set network.byepass_rule=rule
uci set network.byepass_rule.in='lan'
uci set network.byepass_rule.lookup='byepass'
uci set network.byepass_rule.mark='114514'
uci set network.byepass_rule.priority='30000'
uci commit

执行命令 /etc/init.d/network reload 重新加载路由,然后转到 OpenWrt 管理界面查看路由是否已加载。

OpenWrt LuCI 路由状态界面

刚刚创建的路由规则只转发包含特定标记的流量,所以要让防火墙给特定的流量写入标记。不过在此之前,我们先为防火墙创建一些地址集合。

创建防火墙地址集合🔗

下载并创建国内 IPv4 地址段集合:

wget -O /etc/luci-uploads/chnroutes.txt https://ispip.clang.cn/all_cn.txt
uci set firewall.byepass_cn_ipset=ipset
uci set firewall.byepass_cn_ipset.name='byepass_cn_ipset'
uci set firewall.byepass_cn_ipset.family='ipv4'
uci set firewall.byepass_cn_ipset.match='dest_net'
uci set firewall.byepass_cn_ipset.loadfile='/etc/luci-uploads/chnroutes.txt'
uci commit

创建局域网 IPv4 地址段集合:

uci set firewall.byepass_lan_ipset=ipset
uci set firewall.byepass_lan_ipset.name='byepass_lan_ipset'
uci set firewall.byepass_lan_ipset.family='ipv4'
uci set firewall.byepass_lan_ipset.match='dest_net'
uci add_list firewall.byepass_lan_ipset.entry='0.0.0.0/8'
uci add_list firewall.byepass_lan_ipset.entry='10.0.0.0/8'
uci add_list firewall.byepass_lan_ipset.entry='100.64.0.0/10'
uci add_list firewall.byepass_lan_ipset.entry='127.0.0.0/8'
uci add_list firewall.byepass_lan_ipset.entry='169.254.0.0/16'
uci add_list firewall.byepass_lan_ipset.entry='172.16.0.0/12'
uci add_list firewall.byepass_lan_ipset.entry='198.51.100.0/24'
uci add_list firewall.byepass_lan_ipset.entry='192.168.0.0/16'
uci add_list firewall.byepass_lan_ipset.entry='224.0.0.0/3'
uci commit

我们还需要创建一个 MAC 地址集合,用于检查流量来源,从而实现只处理特定设备的流量。

创建 MAC 地址集合:

uci set firewall.byepass_devices=ipset
uci set firewall.byepass_devices.name='byepass_devices'
uci set firewall.byepass_devices.family='ipv4'
uci set firewall.byepass_devices.match='mac'
uci commit

MAC 地址集合默认是空的,可以手动在 OpenWrt 管理界面添加地址,如下图所示:

OpenWrt LuCI 防火墙集合设置界面

比如需要让电脑和手机的流量转发到旁路由,添加这两个设备的 MAC 地址即可。需要注意的是,如果 WiFi 启用了隐私 MAC 地址,则需要添加对应的隐私 MAC 地址。

下一步也是最关键的一步,创建防火墙规则,给特定的流量写入标记,让路由规则可以工作。

创建防火墙规则🔗

创建并编辑规则文件 /etc/nftables.d/99-byepass.nft,文件内容如下:

chain byepass_postrouting {
    ether saddr != @byepass_devices return
    ip daddr @byepass_lan_ipset return
    tcp dport 53 goto byepass_mark
    udp dport 53 goto byepass_mark
    ip daddr @byepass_cn_ipset return
    goto byepass_mark
}

chain byepass_mark {
    meta mark set 114514
    return
}

创建路由预处理规则并添加到 mangle_prerouting 链:

echo 'meta nfproto ipv4 meta l4proto { tcp, udp } jump byepass_postrouting' > /etc/nftables.d/99-byepass.include
uci set firewall.byepass_include=include
uci set firewall.byepass_include.type='nftables'
uci set firewall.byepass_include.position='chain-post'
uci set firewall.byepass_include.chain='mangle_prerouting'
uci set firewall.byepass_include.path='/etc/nftables.d/99-byepass.include'
uci commit

至此,主要工作已经做完了,现在执行命令 /etc/init.d/firewall reload 重新加载防火墙规则,然后转到 OpenWrt 管理界面查看防火墙规则是否已加载。

OpenWrt LuCI 防火墙状态界面 1

OpenWrt LuCI 防火墙状态界面 2

如果确认无误,现在可以访问 https://ip.skk.moe/ 确认策略路由是否工作正常。如果不工作,可以试着重来一遍,以及确保设备的 MAC 地址已经添加到集合。

还有一种特殊情况也会导致策略路由无法正常工作,旁路由如果通过一个中继路由连接到主路由,中继路由有一些特殊设置,会导致流量无法转发,要解决这个问题,可以开启 IP 动态伪装。

添加防火墙规则为特定流量开启 IP 动态伪装:

uci set firewall.byepass_masq=nat
uci set firewall.byepass_masq.name='Byepass-MASQUERADE'
uci set firewall.byepass_masq.family='ipv4'
uci set firewall.byepass_masq.proto='all'
uci set firewall.byepass_masq.src='lan'
uci set firewall.byepass_masq.target='MASQUERADE'
uci set firewall.byepass_masq.mark='114514'
uci commit
/etc/init.d/firewall reload

创建网关状态检查脚本🔗

你可能注意到了,上面的防火墙规则跳过了目标 IP 是国内 IP 的流量,但是没有跳过目标端口是 53 的流量,原因是因为要做到无感故障转移,即使旁路由掉线了,设备的网络也不会故障,不过 DNS 的故障转移要用一种特殊的方法,下一步再细说。

首先创建一个网关状态检查脚本,这个脚本会不停的检查网关可用性,如果发现网关不可用,会临时禁用路由规则,当网关可用时重新启用规则。

创建并编辑文件 /etc/init.d/check-byepass-gateway,文件内容如下:

#!/bin/sh /etc/rc.common

START=99
STOP=01
USE_PROCD=1

_SCRIPT=/tmp/$(basename "$initscript")-helper.sh

write_script() {
    cat > $_SCRIPT <<'EOF'
#!/bin/sh
gateway=$(uci -q get network.byepass_route.gateway)
connectable=1
if [ -z "$gateway" ]; then
    exit 1
fi
while true; do
    if ping -c 1 -W 1 "$gateway" >/dev/null 2>&1; then
        if [ $connectable -eq 0 ]; then
            echo "The gateway is back online!"
            sleep 3
            uci del network.byepass_rule.disabled
            uci commit
            service network reload
        fi
        connectable=1
        sleep 3
    else
        echo "The gateway is unreachable! ($gateway)"
        if [ $connectable -eq 1 ]; then
            uci set network.byepass_rule.disabled='1'
            uci commit
            service network reload
        fi
        connectable=0
        gateway=$(uci -q get network.byepass_route.gateway)
        sleep 5
    fi
done
EOF
    chmod 700 $_SCRIPT
}
 
start_service() {
    write_script
    procd_open_instance
    procd_set_param command $_SCRIPT
    procd_set_param term_timeout 60
    procd_set_param stdout 1
    procd_set_param respawn 0 1 1
    procd_close_instance
}

添加执行权限:chmod +x /etc/init.d/check-byepass-gateway

启动脚本:/etc/init.d/check-byepass-gateway enable && /etc/init.d/check-byepass-gateway start

现在你可以重启旁路由并检查路由规则是否被禁用。

需要注意的是,这个脚本默认并不会在系统升级时保留,除此之外,其他的设置和文件默认都会在系统升级时保留。

修改 DHCP 给设备下发 DNS🔗

以下指南假设主路由的 DNS 是污染的,如果你的主路由已经配置了无污染的 DNS,那么可以略过。

刚才提到了要用特殊的方法做到 DNS 的故障转移,原因是,如果给设备下发主路由的 DNS,那么解析结果是污染的,如果给设备下发旁路由的 DNS,如果旁路由掉线,设备会无法使用 DNS 解析。

针对这个问题,我的解决方法是,通过 DHCP 给设备下发一个可用的公共 DNS,由于目标端口是 53 的所有流量会被转发到旁路由,就可以在旁路由上将其拦截到本地。如果旁路由正常工作,发送到这个公共 DNS 的查询会被拦截,如果旁路由掉线,则不会经过任何处理。

如果你使用的是 ImmortalWrt 固件,请先关闭「DNS 重定向」功能,此选项位于「服务」->「DHCP/DNS」->「常规」。

首先修改主路由的 DHCP 选项,给标签为 byepass-onlydns 的客户端下发指定的 DNS,这里用的是百度的公共 DNS:

uci add_list dhcp.lan.dhcp_option='tag:byepass-onlydns,6,180.76.76.76'
uci commit
/etc/init.d/dnsmasq restart

创建并编辑文件 /etc/nftables.d/99-byepass.sh,这个脚本的作用是给集合里的 MAC 自动设置标签,文件内容如下:

#!/bin/sh

. /lib/functions.sh

write_dnsmasq_conf_file() {
    local cfg=$1
    local dir=/tmp/dnsmasq.${cfg}.d
    local conf="$dir/byepass.conf"
    for _ in $(seq 1 10); do
        [ -d $dir ] && break
        sleep 2
    done
    [ -e $conf ] && rm "$conf"

    for mac in $(uci get firewall.byepass_devices.entry); do
        grep -E -i -q "^dhcp-host=$mac" "/tmp/etc/dnsmasq.conf.${cfg}" || echo "dhcp-host=$mac,set:byepass-onlydns" >> "$conf"
    done
}

config_load 'dhcp'
config_foreach write_dnsmasq_conf_file 'dnsmasq'

service dnsmasq restart

如果你已经为特定设备分配了静态 DHCP 租约,则需要手动为设备添加标签 byepass-onlydns

将这个脚本加入到防火墙,当防火墙启动/重新加载时,脚本会自动运行:

uci set firewall.byepass_script=include
uci set firewall.byepass_script.type='script'
uci set firewall.byepass_script.path='/etc/nftables.d/99-byepass.sh'
uci commit
/etc/init.d/firewall reload

现在重新让设备获取 DHCP 并检查 DNS 是否是 180.76.76.76

然后在旁路由上将 180.76.76.76 的 53 端口拦截到本地,在旁路由执行下面的命令:

uci set firewall.intercept_dns.target='DNAT'
uci set firewall.intercept_dns.name='Intercept-BaiduDNS'
uci set firewall.intercept_dns.src='lan'
uci set firewall.intercept_dns.src_dport='53'
uci set firewall.intercept_dns.src_dip='180.76.76.76'
uci commit
/etc/init.d/firewall reload

现在可以在设备上使用 nslookup 命令查询 DNS 结果并检查是否和旁路由返回的 DNS 结果一致。

至此,所有工作都完成了,尽情享受吧!策略路由不仅可以搭配旁路由使用,还可以做到单线 IPTV 复用等,总之是个很有用的特性。如果遇到问题,可以加入 QQ 群或在下方评论区与我探讨。


评论由 giscus 驱动,基于 GitHub Discussions。