梅林固件与 ShellCrash 的冲突 - 关于 iptables 与 Dnsmasq
前言
ShellCrash 按设计能够在各种基于/类似 OpenWrt 的系统上方便地配置管理 mihomo、singbox 之类的代理内核。但由于 Merlin(梅林)固件的许多组件都有自己的实现,ShellCrash 在 Merlin 上运行可能遇到各种各样的问题。
这篇指南包含对 iptables、ShellCrash、Dnsmasq 运作方式的说明,使用了很多强制覆写之类的 hack,如果你也遇到了各种奇奇怪怪的问题,希望可以提供一些参考
这篇指南应该也同样适用于 OpenClash 之类的程序,由于 Merlin 固件可能因更新导致部分特性变化,所以注意本指南特别适用于 Merlin 3004.388.8 (388.8) 版本以及 ShellCrash 1.9.0release 版本,ShellCrash 升级到 beta 后部分功能可能有改善,但也有更多不兼容(截止 1.9.1beta13)。本指南下,路由器 IP 为 192.168.50.1,如果没改过网段,这也是默认值
DNS
ShellCrash 默认的 DNS 配置是很够用的,然而你如果发现无论怎么配置,用 https://ipleak.net 或 https://browserleaks.com/dns 之类的网站仍能找到运营商 DNS 泄露,或者间歇性的泄露话,那很可能不是你 内核 DNS 配置的问题,而是 ShellCrash 没能正确将所有 DNS 请求劫持到 内核 中
内核 DNS 配置无误的情况下,除了 ShellCrash 对 DNS 请求劫持不完善可能导致问题,问题也有可能是 Windows 智能多宿主名称解析(Link-Local Multicast Name Resolution)导致的,此种情况可以自行搜索解决,不在这篇指南的讨论范围内
确认问题
如果你没修改端口,那 ShellCrash 默认会让内核 DNS 监听 1053 端口,也会通过 iptables 将 53 端口转发至 1053
你可以在设备上尝试运行 dig google.com @192.168.50.1
和 dig google.com @192.168.50.1 -p 1053
。如果后者能得到正确的 IP(或 fakeip),而前者只能得到国内污染后的 IP,那就说明 ShellCrash 对 DNS 请求的劫持存在问题,需要自己重新定向 DNS
Merlin 中的 DNS
而 Merlin 有许多处地方可以配置 DNS,配置及其复杂,说明又不甚清晰,这里就我的理解说明一下
LAN - DNS Director
这是华硕/Trend Micro 自己实现的优先级最高的 DNS 重定向工具,主要用于 家长控制 类功能,通常不应该用 DNS Director 设置 DNS
根据文档,在较老的 Merlin 固件中,DNS Director 叫做 DNSFilter
LAN - DHCP 服务器 - DNS 及 WINS 服务器设置
此处配置的 DNS 只是在分配 IP 的时候指定,默认就是 192.168.50.1,交由路由器处理,同时也可配置“Advertise router’s IP in addition to user-specified DNS”,即 在用户指定的 DNS 之外,还附上路由器的 IP。通常全部交给路由器处理即可
WAN - 互联网 DNS 设置
此处配置的 DNS 是 路由器本身 Dnsmasq 的 DNS 配置,即关于 192.168.50.1:53 的配置,默认自动使用运营商 DNS。会影响路由器自己的网络,如果设备将 DNS 请求交给路由器(如 DHCP 分配路由器 IP 作为 DNS 服务器的情况下),也会影响连接到该路由器的设备
Merlin 内置 Dnsmasq 用于处理 DNS,Dnsmasq 的配置位于 /etc/dnsmasq.conf
,其中引用了 servers-file=/tmp/resolv.dnsmasq
,而 WAN 处的 DNS 配置就正对应了 /tmp/resolv.conf
的内容
根据文档,你可以通过 /jffs/configs/dnsmasq.conf.add
在 Dnsmasq 配置末尾添加新配置。但这种方式不适用于解决 DNS 泄露问题,因为 Dnsmasq 会并发所有 DNS 服务器,即使配置为 strict-order
也会因所添加的配置在配置末尾,导致顺序上不优先使用设定的 DNS 服务器
开始操作
管理面板中以上三处都能改 DNS,然而都被限制不能自定义 DNS 服务器的端口。内核监听的是 1053 端口,肯定需要自定义端口,所以我们要自己覆写 Dnsmasq 配置:
- 在开始之前,为防止与我们自己的 DNS 处理冲突,你要在 crash 菜单的 7-6-7 处(新版可能变化)禁用 ShellCrash 自己的 DNS 劫持
- 我们要修改的是
/tmp/resolv.dnsmasq
的配置,可以先确认一下这个配置的内容,用cp /tmp/resolv.dnsmasq /tmp/resolv.dnsmasq.bak
备份原始配置 - 然后运行
echo "server=192.168.50.1#1053" > /tmp/resolv.dnsmasq
覆写原配置。用 # 表示端口 - 用
service restart_dnsmasq
重启 Dnsmasq 服务
User script 防覆盖
为防止配置重新被 Merlin 改写回去,你可以通过 Merlin 的 User script 功能在路由器启动后自动覆写配置
比如,你可以创建 /jffs/scripts/override-dnsmasq.sh
并粘贴上
1 |
|
可以自己试运行一下。注意还要 chmod +x /jffs/scripts/override-dnsmasq.sh
为 .sh 文件添加执行权限
然后在 /jffs/scripts/nat-start
或其他 User script Hook 点(大多数都可以)中添加 sh /jffs/scripts/override-dnsmasq.sh
来自动执行这个脚本
ShellCrash 默认也是在 nat-start 处初始化 ShellCrash 的,添加后 nat-start 看起来应该像这样:
1 |
|
完成
DNS 配置完成了🎉
回到 确认问题 并用 https://ipleak.net 或 https://browserleaks.com/dns 之类的网站来检查一下还有没有问题吧!
如果你有对国内网站设置国内的 DNS,那还可以用网易的 https://nstool.netease.com 这个网站来检查你在国内使用的 DNS 服务器
本机代理问题
实际上,完成以上步骤之后,设备应当就已经能够正常上网了。然而,由于对 Dnsmasq 的配置,路由器本身(本机)进行 DNS 解析时也会通过内核的 192.168.50.1#1053
DNS 进行,而如果你在使用 fakeip 模式,并没有配置本机代理的话,那便会导致 路由器自己 无法正常上网,因为路由器解析出的是 fakeip,而出站流量却不经过内核,那 fake 的 ip 肯定就连不上了
这个问题有两种解法:
- 如果你希望本机也能代理,那需要让本机流量也走内核。但 ShellCrash 目前版本(1.9.0release)对本机代理的支持并不很好,只支持以“环境变量”的方式代理。你当然可以尝试自己用 iptables 在 OUTPUT 链上设置规则,只是会比较折腾
- 如果你认为本机没必要代理,那可以只劫持本机自己的 DNS 请求,重定向到一个能正常解析出 真 IP 的 DNS 服务器。这种情况下,你需要执行这条命令来通过 iptables 设置劫持:
1 |
|
这个命令会在 nat 表的 OUTPUT 链中要求 NAT 将向 192.168.50.1:53
请求的目标地址转换为 8.8.8.8(:53)
由于这个规则在 OUTPUT 链上,所以它不会影响到连接路由器的其他设备,而只影响路由器本机,详细原理可参考以下 iptables 运作方式
当然你也可以像上面一样把这个命令添加到 User script 的 firewall-start
Hook 点上,可以使用这个脚本避免规则被重复添加
1 |
|
其中 -C
flag 即用于确认该规则是否存在, 2>/dev/null
用于处理错误输出(规则不存在即报错)
防火墙
除了 DNS 问题,Merlin 自带的防火墙配置,尤其是 防火墙 - IPv6 防火墙 也会与 ShellCrash 对 IPv6 的流量劫持产生冲突
确认问题
实际上解决这个问题最简单的方式就是取消勾选 启动 IPv6 防火墙
。如果关闭 IPv6 防火墙 后网络不存在问题,那便可以确定问题就出在这里
当然关闭整个 IPv6 防火墙肯定是存在安全风险的,Merlin 内建的防火墙其实基于一条条 iptables 规则,IPv6 防火墙则基于 ip6tables ,最好是搞清楚防火墙的运作原理,并自己正确配置它们
iptables 运作方式
iptables 内建于 Merlin 固件中,用于控制数据包的处理和转发,你可以在它各个 表 的各个 链 上设置规则
flowchart BT
subgraph 网络层输入
in(["输入"]) --> PREROUTING
PREROUTING --> routing(["路由决策"])
end
subgraph 本机
routing(["路由决策"]) --> INPUT
INPUT --> local(("本机(ShellCrash)"))
local(("本机(ShellCrash)")) --> OUTPUT
end
subgraph 转发处理
routing(["路由决策"]) --> FORWARD
end
subgraph 网络层输出
FORWARD --> POSTROUTING
OUTPUT --> POSTROUTING
POSTROUTING --> out(["输出"])
end
style 本机 fill:#f9f,stroke:#333,stroke-width:2px
style 转发处理 fill:#bbf,stroke:#333,stroke-width:2px
style 网络层输入 fill:#ff9,stroke:#333,stroke-width:2px
style 网络层输出 fill:#ff9,stroke:#333,stroke-width:2px
这个流程图中的 方框 如 PREROUTING
、 FORWARD
即属于不同 表 中的 链
主要有四张表:
- filter
- mangle
- nat
- raw
五条链:
- PREROUTING
- FORWARD
- INPUT
- OUTPUT
- POSTROUTING
即常说的“四表五链”
当然也可能有其他各种表和链,比如 security
表,还有 ShellCrash 会在 mangle 表创建的 shellcrashv6
链,但我们通常不用动那些表和链
命令行
比如,你可以通过 ip6tables -t filter -nvL INPUT
查看 filter 表中 INPUT 链上的规则,规则从上往下按顺序匹配
其中:
- t [map] 用于指定要查哪张表,实际上不填默认就是
filter
表 - n 表示不对 ip 进行域名解析,让返回快很多
- v 表示更多信息,比如
pkts
和bytes
列 - L 表示列出规则
你还可以用 ip6tables-save
查看现有的所有表,并得到 可以作为命令来添加执行 的格式
ShellCrash 也是靠 iptables 劫持流量 的。以 tproxy 模式举例,ShellCrash 会在 PREROUTING 中通过 tproxy(透明代理)和路由表将本该通过 FORWARD 到公网的流量, 重定向到本机的 INPUT ,并在 mihomo/sing-box 内核中处理流量
由于 PREROUTING 链位于 mangle 表,所以你可以通过ip6tables -t mangle -nvL
查看整个 mangle 表的内容,其中包括了 ShellCrash 在 PREROUTING 链设置的规则,也包括了它自己创建的 shellcrashv6
链。输出应该形似这样:
1 |
|
表的内容都很好理解。其中:
pkts
指通过这条规则的包数bytes
指通过这条规则的字节数
可以用来确定规则有没有在正常工作
Merlin 中的防火墙
通过对比关闭和开启 IPv6 防火墙的 ip6tables 规则,可以注意到:
- 在 FORWARD 链,多了根据面板设置的端口白名单(ACCEPT)。同时顶部的 policy(即默认策略),从 ACCEPT 变成了 DROP(即丢弃)
- 在 INPUT 链,最下面多了一条全部 DROP 的规则
相当于
flowchart BT
subgraph 网络层输入
in(["输入"]) --> PREROUTING
PREROUTING --> routing(["路由决策"])
end
subgraph 本机
routing(["路由决策"]) --x |"DROP"| INPUT
INPUT --> local(("本机"))
local(("本机(ShellCrash)")) --> OUTPUT
end
subgraph 转发处理
routing(["路由决策"]) --x |"非白名单就 DROP"| FORWARD
end
subgraph 网络层输出
FORWARD --> POSTROUTING
OUTPUT --> POSTROUTING
POSTROUTING --> out(["输出"])
end
style 本机 fill:#f9f,stroke:#333,stroke-width:2px
style 转发处理 fill:#bbf,stroke:#333,stroke-width:2px
style 网络层输入 fill:#ff9,stroke:#333,stroke-width:2px
style 网络层输出 fill:#ff9,stroke:#333,stroke-width:2px
linkStyle 5 stroke:red
linkStyle 2 stroke:red
FORWARD 链对 ShellCrash 应当没有影响
但参照此处对 ShellCrash 原理的说明,ShellCrash 劫持的流量肯定要 经过 INPUT ,到本机的内核里去处理。而本机的 INPUT 入站始终被 DROP,自然便无法正常使用 IPv6 了
开始操作
要解决这一问题,自然就是要让 ShellCrash 劫持的流量不被 DROP 掉。从问题出发,主要有两种办法
方法一:在 DROP 规则的上面插入 ACCEPT 规则
用 ip6tables -I INPUT 1 -i ppp0 -j ACCEPT
在 INPUT 链的首位插入”对所有来自 ppp0 网络接口(IPv6)的流量都 ACCEPT”规则
其中
-I [Chain] 1
即表示在指定链的第 1 位插入规则
方法二:删去原本的 DROP 规则
用 ip6tables -D INPUT -j DROP
直接删掉那条 DROP 规则
其中
-D [Chain]
即表示在指定链中删除规则
两种方法都可以,也同样需要把这个命令添加到 User script 的 firewall-start
Hook 点上,参照上文
调试
如果你在折腾的过程中遇到了很奇怪的问题,可以用这些小工具小技巧方便调试、透析流量
Entware
Entware 是一个强大的适用于嵌入式的包管理器,即使路由器固件不是 OpenWrt,也可使用 opkg
之类的命令
Merlin 固件内建了 amtm - the Asuswrt-Merlin Terminal Menu ,方便你在 Merlin 上安装 Entware 之类的程序
- 运行
amtm
打开 amtm,第一次打开可能要求配置主题,可以直接选第一个 - 在 amtm 中,输入
i
可查看所有可用的程序,参照指示输入ep
即可在一个 U 盘上安装 entware
事实上 Merlin 内建的 SSH 服务端是不支持 SFTP 的,而有了 Entware,你便可以直接输入 opkg install openssh-sftp-server
来安装 sftp。不需要任何配置,也不用折腾 FTP 之类的,随便找一个 SFTP 的客户端即可轻松管理路由器中的文件系统
用 Wireshark 在路由器上抓包
Wireshark 作为很强大的抓包工具,支持通过 SSH 连接远程设备并通过 tcpdump/dumpcap 抓取流量。自然也可以连接到路由器的 SSH 上抓包
- Merlin 固件并不内建 tcpdump,参照以上说明安装 Entware 之后,你便也可以通过
opkg
安装来安装 tcpdump
1 |
|
- 打开 Wireshark 客户端,打开
捕获 - 选项
- 选中
SSH remote capture:sshdump
,点击它的设置
- 在 Server 和 Authentication 页配置路由器的 SSH 连接信息,并在 Capture 页中设置 Remote interface 和 Remote capture filter
Remote interface 即所抓包的网络接口,主要可以为:
- ppp0 即 IPv6 的接口
- br0 即内网接口
可以通过 ip a
查看所有可用的网络接口和对应的 IP
Remote capture filter 是总的过滤器,建议用 not port [SSH_PORT]
过滤掉 Wireshark 客户端自己抓包的 SSH 数据
- 然后就可以关掉设置开始抓包了
用 iptables LOG
iptables 不仅可以设置 ACCEPT、DROP 之类的规则,还可以设置 jump 到 LOG
例如 ip6tables -I INPUT 1 -i ppp0 -m limit --limit 60/min -j LOG --log-prefix "FROM-PPP0: "
即可将所有符合该规则(从 ppp0 接口进入)的流量记录到日志中
其中:
-m limit --limit 60/min
用于限制记录日志的频率,包太多路由器很容易卡死--log-prefix [str]
即日志前缀
注意 iptables 规则从上往下匹配,用 -A
来添加会把 LOG 规则放在链的最后,如果数据在前面就已经被 DROP 了那就不会记录下来。所以这里用 -I [Chain] 1
指定插入到链的第 1 位
可以通过 dmesg | grep "FROM-PPP0: "
来查找记录下来的流量,grep 即用于查找 log 前缀