土制 Linux 路由器 (Chapter 2): 策略路由的使用方法

系列链接

  1. 土制 Linux 路由器(Chapter 1):设置一个简单的家用 Linux 路由器
  2. 土制 Linux 路由器(Chapter 2):策略路由
  3. Work in progress…

写在前面

传统的路由只是通过目标地址来转发,而无法依据诸如源地址,协议等转发数据包。
为了解决这个痛点,策略路由就诞生了。
策略路由则是通过匹配一系列的规则(rules),符合规则,则让数据包按照匹配规则执行对应的动作(action),装这一系列 rules 和 actions 的东西叫做 RPDB (routing policy database) 。
系统则通过 查询 RPDB -> 对应匹配条件 -> 对应 action 这个流程来决定如何把包转发出去。
例如可以让来自 192.168.1.2 查 810 号路由表,根据 810 号路由表来转发。

此篇将简短的介绍 Linux 上的策略路由,也就是 ip-rule (配合 nftables) 的使用方法,以及会附上一两个实验 (不过环境还是需要自己搭建喔) 。
因为想控制篇幅,加上 IPv6 和 IPv4 大致配置流程相同,所以只会涉及到 IPv4 的部分, IPv6 部分读者可以自行尝试。

环境则是以上篇为基础。
主角是我们全能的 iproute2 , nftables 则是负责更改规则来打标(fwmark)。

如果发行版自带的是 busybox 的 ip 命令,个人推荐单独安装一个 iproute2 , AlpineLinux 可以直接使用 apk 包管理进行安装。

废话不多嗦,咱们就开始吧。

正文

ip-rule

初始的规则和它们对应执行的动作

我们可以先看一下 ip rule 的输出

alpinerouter:~# ip rule
0:      from all lookup local
32766:  from all lookup main
32767:  from all lookup default

初始状态下我们会有这三个规则。
这三个规则是匹配全部的,优先级从高到低。
第一个 local 表优先级最高,装了local的和广播的路由。
可以通过 ip route show table local 来查看。

alpinerouter:~# ip route show table local
local 100.127.255.254 dev ppp0 proto kernel scope host src 100.127.255.254 
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1 
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1 
local 192.168.10.1 dev eth1 proto kernel scope host src 192.168.10.1 
broadcast 192.168.10.255 dev eth1 proto kernel scope link src 192.168.10.1

main 表则是我们所熟悉的默认查找到的表,输入 ip route show 或者 ip route show table main 即可看到。

alpinerouter:~# ip route show table main
default dev ppp0 scope link 
100.127.255.255 dev ppp0 proto kernel scope link src 100.127.255.254 
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.1

第三个是 default 表,默认为空,如果前面没有匹配到就会查这个表,这条规则也可以被删除。

对于 IPv6 ,可以用 ip -6 ruleip -6 route show table $table_id 来查看,和 IPv4 差不多,不过相对于 IPv4 来说,默认配置少了 default 表。

使用 ip rule 添加和删除规则

我们可以来看一下 ip rule help 来查看使用方法。

alpinerouter:~# ip rule help
Usage: ip rule { add | del } SELECTOR ACTION
       ip rule { flush | save | restore }
       ip rule [ list [ SELECTOR ]]
SELECTOR := [ not ] [ from PREFIX ] [ to PREFIX ] [ tos TOS ]
            [ fwmark FWMARK[/MASK] ]
            [ iif STRING ] [ oif STRING ] [ pref NUMBER ] [ l3mdev ]
            [ uidrange NUMBER-NUMBER ]
            [ ipproto PROTOCOL ]
            [ sport [ NUMBER | NUMBER-NUMBER ]
            [ dport [ NUMBER | NUMBER-NUMBER ] ]
ACTION := [ table TABLE_ID ]
          [ protocol PROTO ]
          [ nat ADDRESS ]
          [ realms [SRCREALM/]DSTREALM ]
          [ goto NUMBER ]
          SUPPRESSOR
SUPPRESSOR := [ suppress_prefixlength NUMBER ]
              [ suppress_ifgroup DEVGROUP ]
TABLE_ID := [ local | main | default | NUMBER ]

常用的几个 SELECTOR(筛选条件)

  • from PREFIXto PREFIX ,即利用源地址和目标地址来匹配。
  • fwmark FWMARK ,常用于防火墙打标,根据标记匹配。
  • iif STRINGoif STRING ,根据入接口和出接口匹配。
  • pref NUMBER,即设定要添加的规则的优先级,越小越优先。
  • uidrange NUMBER-NUMBER ,即通过 uid 来选择,安卓的分应用代理也是这个原理。
  • ipproto 即根据 ipv4 包中承载的协议来匹配(看的是 ip 包头部!),例如 TCP 和 UDP。
  • sportdport 则是根据源端口和目标端口来匹配。

常用的 ACTION(执行的操作)

  • table TABLE_ID ,匹配到之后走特定的路由表。

Suppress PrefixLength 的补充

给定的解释是,不匹配任何小于等于这个前缀长度的路由。
这个的应用在 wg-quick 这个脚本中有体现。
如果把 WireGuard 的配置文件中 Peer 的 AllowedIPs 设置为 0.0.0.0/0 或者 ::/0 ,抑或两者都有,并且没有设定 Table = off,wg-quick 启动它会自动给加上 from all lookup main suppress_prefixlength 0 的规则。
这个的意思就是,来自所有地方的数据包,首先查找 main 表,但是不匹配长度小于等于 0 的路由,也就是长度等于零的不匹配了,那0.0.0.0/0是什么呢?是默认路由。
也就是说,匹配了这个规则之后, main 表内的默认路由即使存在也不会对应给转发到默认网关了,而是继续匹配下一条规则。

具体可以阅读这里:StackOverflow: What does ip4 rule add table main suppress prefixlength 0 meaning.

添加与删除规则的例子

  • 添加:

    • 默认状态下,我们可以通过 ip rule add iif eth1 from 192.168.10.2/32 table 233 使从 192.168.10.2 并且入接口为 eth1 的数据包去查 233 号路由表。如果不指定优先级的话,优先级会从 32765(32766-1) 开始依次向下加入规则,读者可以自行尝试。
  • 删除:

    • 上面的规则,可以通过 ip rule del table 233 这种较模糊的指定来删除(这种是通过 action 指定,也可以通过 selector 来指定),不过如果有其他 action 也是 table 233 的话,会按优先度从高到低删除。
    • 因为每条规则的 pref(优先级) 是唯一的,也可以通过指定 ip rule del pref $pref_num 来精确删除;优先级可以通过 ip rule 列出,例如默认配置下 main 表对应优先级为 32766 。

ip-route

添加/删除特定路由表的路由条目

添加规则之后,如果对应的路由表内没有东西的话,也是行不通的。
在上面,我们添加了让 192.168.10.2/32 走路由表 233 的规则。
此处我们可以执行以下命令让来自 192.168.10.2/32 的所有包都发到 eth2 的 192.168.70.1 上:
ip route add default via 192.168.70.1 dev eth2 table 233
这样就可以啦。
如果要删除的话可以直接通过:
ip route del default via 192.168.70.1 dev eth2 table 233
务必记住要在后面指定对应 table 号,如果不指定的话就会直接动 main 表了。

nftables

在策略路由中,nftables 主要是通过更细粒度和更灵活的匹配,给包打上标记,最后配合 ip-rule 中 fwmark 这个 SELECTOR 让包对应走不同的路由表。
不过需要注意的是,如果您正在使用 fastpath bypass 这种跳过 hooks 处理的方式来降低路由器的转发负载,那么通过打路由标记来影响路由选择这种方式就行不通了。打标的操作是在 postrouting 这个钩子执行的,而跳过的时候,则是从 ingress(prerouting 之前) 直接调到了neigh_xmit(postrouting 之后)了。
如果想要让打标影响路由选择,就应该在挂载点为 prerouting 这里添加规则,即在路由选择之前打上标记(mark before routing decisions)。
例如,如果需要来自 192.0.3.1/32 的去往 192.0.2.2 的,目标为tcp端口号 22,80,443 的连接上所有数据包都打上 0x1 的标记,并且在本机可以用conntrack -L -m 1 可以列出,可以在 /etc/nftables.nft 中加入如下内容:

table inet mangle {
        chain prerouting {
                type filter hook prerouting priority mangle;
                policy accept
                ip saddr 192.0.3.1/32 ip daddr 192.0.2.2 tcp dport { 22, 80, 443 } ct mark set 0x1
                meta mark set ct mark
        }
}

之后 nft -f /etc/nftables.nft 以保存配置。

以上的思路是,先让匹配条件的数据包的连接打上标记 0x1(即连接打上connection mark),之后第二条规则则是让包打上和连接标记相同的标记(即包打上packet fwmark)。
之后的路由选择就看 ip rule 和 ip route 的功能了。

如何让三者配合使用呢?

大致思路:

  1. 使用 ip rule 命令加一条规则,例如让 路由标记(fwmark) 为 0x1 的数据包查找编号为 1 的路由表。
  2. 使用 ip route 在编号为 1 的路由表上加入路由条目。
  3. 使用 nftables 给符合条件的数据包打上 0x1 的路由标记。

(其中路由标记可以根据需要选择。)

之后符合条件( fwmark 为 0x1 )的数据包就会走编号为1的路由表了。

可能还有一个需要注意的地方, Alpine Linux 的 IPv4 Reverse Path Filtering 默认为1,如果开启策略路由有去程通但是回来不通的问题,可以通过sysctl关闭rp_filter。

echo net.ipv4.conf.default.rp_filter=0 >> /etc/sysctl.d/80-rp_filter.conf
echo net.ipv4.conf.all.rp_filter=0 >> /etc/sysctl.d/80-rp_filter.conf

就可以持久化了。

实验

讲了这么多,接下来就做实验吧!

实验一: 基于端口号的分流

小明在家整了一个实体机开虚拟机给群友们玩,但担心群友用他家的ISP网络看涩涩,于是他选择了一个匿名上网工具,这个匿名上网工具可以在 Linux 路由器上开出一个网络接口,接口名为 usb0,小明可以通过这个来匿名上网。
但是小明又不想影响到群友到虚拟机的联通性,于是他决定从虚拟机局域网来的端口为 10000 及以上的连接直接走 ISP(ppp0) ,而目标端口为 10000 以下的连接走 usb0 这个匿名上网通道出去,并且因为群友需要测试延迟,他希望 ICMP echo-request 也直接走 ISP(ppp0) 出去。
(其中 ppp0 和 usb0 接口出都需要进行 masquerade 操作)
你可以帮到他吗?
小明也想通过 conntrack 命令看到群友走 ISP 出去的时候的连接,你可以帮到他吗?(可选)
拓扑图长这样: 2.png

实验二: 基于 GeoIP 的策略路由

李华是一名 HomeLab 爱好者,他家接了两条宽带。其中一条是粘不通(ppp0),另外一条是移不冻(ppp1)。他听说粘不通到美国连接性很好,移不冻到香港连接性很好。
他决定他家的流量到美国的 IP 走粘不通,到香港的 IP 则走移不冻,剩下默认走粘不通。并且需要配置好相关的NAT规则。
你可以帮到他吗?
实验二材料: nftables GeoIP repository
如果实验二需要帮助,可以参考:nftables wiki: GeoIP Matching

参考资料与推荐阅读

末尾

此篇本来想要写实验的,但是感觉太多了,这次就没有去写它,不过基础部分应该还是讲到了。两个实验的话读者可以自行尝试去做。
不过实验只是实验,有可以上网的两个不同的接口就可以做了,接口名也不是必须是那个名称。
关于第二个上网卡,个人是使用的 4G USB Dongle 去模拟的(关于上网卡为什么是usb0的这件事)。
实验的话下次会写的, 都会有的,都会有的。(心虚
不过这大概还是基础的一部分,更高级的一些选项咱倒是没有用过了。

以及 wg-quick 的策略路由实现也推荐读者去看一下,它是一套完整的通过 ip-rule, ip-route 以及 nftables 的策略路由实现(AllowedIPs 前缀长度为0的时候)。

同样,如果有发现文章中有误,欢迎指出来喔。
评论区正在施工中~

Built with Hugo
Theme Stack designed by Jimmy