梅林固件与 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.nethttps://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.1dig 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 配置:

  1. 在开始之前,为防止与我们自己的 DNS 处理冲突,你要在 crash 菜单的 7-6-7 处(新版可能变化)禁用 ShellCrash 自己的 DNS 劫持
  2. 我们要修改的是 /tmp/resolv.dnsmasq 的配置,可以先确认一下这个配置的内容,用 cp /tmp/resolv.dnsmasq /tmp/resolv.dnsmasq.bak 备份原始配置
  3. 然后运行 echo "server=192.168.50.1#1053" > /tmp/resolv.dnsmasq 覆写原配置。用 # 表示端口
  4. service restart_dnsmasq 重启 Dnsmasq 服务

User script 防覆盖

为防止配置重新被 Merlin 改写回去,你可以通过 Merlin 的 User script 功能在路由器启动后自动覆写配置

比如,你可以创建 /jffs/scripts/override-dnsmasq.sh 并粘贴上

1
2
3
4
5
6
#!/bin/sh

# 执行 Dnsmasq 复写
cp /tmp/resolv.dnsmasq /tmp/resolv.dnsmasq.bak # 备份原始
echo "server=192.168.50.1#1053" > /tmp/resolv.dnsmasq
service restart_dnsmasq

可以自己试运行一下。注意还要 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
2
3
/jffs/ShellCrash/start.sh init #ShellCrash初始化脚本

sh /jffs/scripts/start-services.sh

完成

DNS 配置完成了🎉

回到 确认问题 并用 https://ipleak.nethttps://browserleaks.com/dns 之类的网站来检查一下还有没有问题吧!

如果你有对国内网站设置国内的 DNS,那还可以用网易的 https://nstool.netease.com 这个网站来检查你在国内使用的 DNS 服务器

本机代理问题

实际上,完成以上步骤之后,设备应当就已经能够正常上网了。然而,由于对 Dnsmasq 的配置,路由器本身(本机)进行 DNS 解析时也会通过内核的 192.168.50.1#1053 DNS 进行,而如果你在使用 fakeip 模式,并没有配置本机代理的话,那便会导致 路由器自己 无法正常上网,因为路由器解析出的是 fakeip,而出站流量却不经过内核,那 fake 的 ip 肯定就连不上了

这个问题有两种解法:

  1. 如果你希望本机也能代理,那需要让本机流量也走内核。但 ShellCrash 目前版本(1.9.0release)对本机代理的支持并不很好,只支持以“环境变量”的方式代理。你当然可以尝试自己用 iptables 在 OUTPUT 链上设置规则,只是会比较折腾
  2. 如果你认为本机没必要代理,那可以只劫持本机自己的 DNS 请求,重定向到一个能正常解析出 真 IP 的 DNS 服务器。这种情况下,你需要执行这条命令来通过 iptables 设置劫持:
1
iptables -t nat -A OUTPUT -p udp --dport 53 -d 192.168.50.1 -j DNAT --to-destination 8.8.8.8

这个命令会在 nat 表的 OUTPUT 链中要求 NAT 将向 192.168.50.1:53 请求的目标地址转换为 8.8.8.8(:53)

由于这个规则在 OUTPUT 链上,所以它不会影响到连接路由器的其他设备,而只影响路由器本机,详细原理可参考以下 iptables 运作方式

当然你也可以像上面一样把这个命令添加到 User script 的 firewall-start Hook 点上,可以使用这个脚本避免规则被重复添加

1
2
3
4
5
6
# 重定向路由器 DNS
DNS_RULE="OUTPUT -p udp --dport 53 -d 192.168.50.1 -j DNAT --to-destination 8.8.8.8"

if ! iptables -t nat -C $DNS_RULE 2>/dev/null; then
iptables -t nat -A $DNS_RULE
fi

其中 -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

这个流程图中的 方框 如 PREROUTINGFORWARD 即属于不同 中的

主要有四张表:

  • 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 表示更多信息,比如 pktsbytes
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Chain PREROUTING (policy ACCEPT 1843K packets, 2274M bytes)
pkts bytes target prot opt in out source destination
2176K 2300M shellcrashv6 tcp * * ::/0 ::/0
107K 54M shellcrashv6 udp * * ::/0 ::/0

Chain INPUT (policy ACCEPT 2294K packets, 2355M bytes)
pkts bytes target prot opt in out source destination

Chain FORWARD (policy ACCEPT 40213 packets, 3977K bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 1694K packets, 2232M bytes)
pkts bytes target prot opt in out source destination

Chain POSTROUTING (policy ACCEPT 1739K packets, 2236M bytes)
pkts bytes target prot opt in out source destination

Chain shellcrashv6 (2 references)
pkts bytes target prot opt in out source destination
22957 1991K RETURN udp * * ::/0 ::/0 udp dpt:53
6309 460K RETURN all * * ::/0 240x:xxxx:xxxx:xxxx::/64
1748K 2264M RETURN all * * ::/0 240x:xxxx:xxxx:xxxx::/64
0 0 RETURN all * * ::/0 ::/128
0 0 RETURN all * * ::/0 ::1/128
0 0 RETURN all * * ::/0 ::ffff:0.0.0.0/96
13 1040 RETURN all * * ::/0 64:ff9b::/96
0 0 RETURN all * * ::/0 100::/64
1360 97998 RETURN all * * ::/0 2001::/32
0 0 RETURN all * * ::/0 2001:20::/28
0 0 RETURN all * * ::/0 2001:db8::/32
41 4670 RETURN all * * ::/0 2002::/16
4400 348K RETURN all * * ::/0 fc00::/7
25 4350 RETURN all * * ::/0 fe80::/10
5737 1554K RETURN all * * ::/0 ff00::/8
463K 79M TPROXY tcp * * 240x:xxxx:xxxx:xxxx::/64 ::/0 TPROXY redirect :::7893 mark 0x1ed4/0xffffffff
0 0 TPROXY tcp * * 240x:xxxx:xxxx:xxxx::/64 ::/0 TPROXY redirect :::7893 mark 0x1ed4/0xffffffff
32340 6248K TPROXY udp * * 240x:xxxx:xxxx:xxxx::/64 ::/0 TPROXY redirect :::7893 mark 0x1ed4/0xffffffff
0 0 TPROXY udp * * 240x:xxxx:xxxx:xxxx::/64 ::/0 TPROXY redirect :::7893 mark 0x1ed4/0xffffffff

表的内容都很好理解。其中:

  • pkts 指通过这条规则的包数
  • bytes 指通过这条规则的字节数

可以用来确定规则有没有在正常工作

Merlin 中的防火墙

FORWARD diff

INPUT diff

通过对比关闭和开启 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 之类的程序

  1. 运行 amtm打开 amtm,第一次打开可能要求配置主题,可以直接选第一个
  2. 在 amtm 中,输入 i 可查看所有可用的程序,参照指示输入 ep 即可在一个 U 盘上安装 entware

事实上 Merlin 内建的 SSH 服务端是不支持 SFTP 的,而有了 Entware,你便可以直接输入 opkg install openssh-sftp-server 来安装 sftp。不需要任何配置,也不用折腾 FTP 之类的,随便找一个 SFTP 的客户端即可轻松管理路由器中的文件系统

用 Wireshark 在路由器上抓包

Wireshark 作为很强大的抓包工具,支持通过 SSH 连接远程设备并通过 tcpdump/dumpcap 抓取流量。自然也可以连接到路由器的 SSH 上抓包

  1. Merlin 固件并不内建 tcpdump,参照以上说明安装 Entware 之后,你便也可以通过 opkg 安装来安装 tcpdump
1
opkg install tcpdump
  1. 打开 Wireshark 客户端,打开 捕获 - 选项

Wireshark 首页

  1. 选中 SSH remote capture:sshdump,点击它的设置

捕获选项

  1. 在 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 数据

  1. 然后就可以关掉设置开始抓包了

用 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 前缀