February 19, 2016

使用 dnsmasq 和 ipset 的策略路由

今天试了一把 dnsmasq 的 ipset 功能,配合 iptables 和 iproute2 即可实现基于域名的策略路由。 网上已有一些文章介绍了这种方法,但多是面向 OpenWrt 在路由器上做, 我直接拿来则发现在本地跑其实有坑。

基本需求和思路

我平时一直开着 VPN,配合 chnroute 实现策略路由。但是在学校里访问各类学术数据库需要用学校 IP,过去的做法是把几个常用数据库的 IP 加进路由表里,然而最近一些数据库开始使用 CDN 了,IP 三天两头的变化…… 所以基于域名实现策略路由势在必行。

Ipset 是一个可以和 iptables 配合使用的工具,可以把一系列 ip 放进一个集合中,iptables 则根据集合名进行规则匹配。Dnsmasq 从 2.66 版本之后就支持将一些域名的查询结果放进 ipset 中,这样就可以对这些域名对应的 IP 使用 iptables 处理。集合中的数据包在 iptables mangle 表打上 mark,再使用 iproute2 的规则(rule),对该 mark 的数据包查询一个单独的路由表,从而实现策略路由。

配置流程

VPN?

这个不用说了。

IPSET

首先创建 ipset

ipset create bypass_vpn hash:ip

这个每次重启就会丢失,可以 ipset save 导出配置文件,Arch Linux 的 ipset 包还带了 systemd service,所以

sudo ipset save | sudo tee /etc/ipset.conf
sudo systemctl enable ipset.service

即可。

DNSMASQ

配置 dnsmasq,这里推荐一下 @felixonmars 的 dnsmasq-china-list,把国内 常见网站都涵盖了,自己再把常见学术数据库的域名加进去。但是这份配置中不包含 ipset 的配置,这个简单,简单的全文替换就好。

格式如下:

server=/cn/114.114.114.114  # 这里指定域名的上级 DNS 
ipset=/cn/bypass_vpn        # 这里指定查询结果要放入的 ipset
server=/sciencedirect.com/166.111.8.28
ipset=/sciencedirect.com/bypass_vpn

我自己是将server=ipset=开头的配置分别放在了 china.confchina-ipset.conf 两个文件中,/etc/dnsmasq.conf 中再使用 conf-dir=/etc/dnsmasq.d 配置。

路由表,IPTABLES, IP RULE

首先创建路由表(重启仍有效)

echo "200 bypass_vpn" | sudo tee -a /etc/iproute2/rt_tables   # 这里数字在 1 到 252 之间,不要重复

再加 iptables 规则

sudo iptables -t mangle -N fwmark
sudo iptables -t mangle -A PREROUTING -j fwmark  # 对转发数据包有效
sudo iptables -t mangle -A OUTPUT -j fwmark      # 对从本地发出的数据包有效
sudo iptables -t mangle -A fwmark -m set --match-set bypass_vpn dst -j MARK --set-mark 1  # 给目标地址在 bypass_vpn 中的数据包打上mark 1

在 VPN 启动后,执行一些操作:

  • 把默认物理路由设为 bypass_vpn 表的默认路由
  • 让 mark=1 的数据包查询 bypass_vpn 表

我是在 VPN 的 post-up 脚本中加了这么几句:

OLDGW=$(ip route show 1/0 | head -n1 | sed -e 's/^default//')

gwdev=$(echo $OLDGW|awk '{print $4}')   # 物理设备
ip route show dev $gwdev | while read gwroute; do
	ip route add $gwroute dev $gwdev table bypass_vpn
done

ip rule add fwmark 1 table bypass_vpn

填坑

按理说到这里就该好了,但是实际是跑不起来的,抓包发现,虽然 iptables 的 mark 打上了,路由查询也是对的,但是发出去的包源IP竟然是我的 VPN 网卡地址。

然后 @shankerwangmiao 和 @hexchain 帮我找到了下面这张图还有这份文档。 大致意思就是,Linux 给数据包设置的源 IP 是通过查路由表决定的,而 Routing Decision 的过程发生在进入 iptables 之前,所以这时候根据规则,查询的是默认路由表,策略 路由没有起作用。在经过 mangle OUTPUT chain 之后,有一次 reroute check 过程,如果被打上了 mark,则再决定是不是要根据别的路由表发包,但是这时源地址中央已经 决定了。

解决的办法也比较简单,通过 NAT 把源地址改了就好

sudo iptables -t nat -A POSTROUTING -m mark --mark 0x1 -j MASQUERADE
# 或者使用
sudo iptables -t nat -A POSTROUTING -m mark --mark 0x1 -j SNAT --to-source <your ip>

这时 tcpdump 发现 ICMP reply 已经收到了,但是 ping 仍然没反应。这是因为 Arch Linux 默认的 rp_filter 策略是严格路由匹配,即我收到 ICMP 包的网卡(物理网卡)并不是 最佳路径(默认路由表中VPN才是最佳路径)所以就丢掉了。所以把 rp_filter 改成宽松模式(路由可达即认为数据包合法)

sudo sysctl net.ipv4.conf.{all,enp2s0}.rp_filter=2  # 你的网卡不一定叫 enp2s0

其实默认改成宽松也行,编辑 /etc/sysctl.d/60-rp_filter.conf

net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.rp_filter = 2

以上。