前言
在 所以我放弃了旁路由 中我曾提到过,OpenClash 在 Dnsmasq 转发模式下,无法设置不走代理的设备 MAC。我非常需要这个功能,但同时我又无法放弃 Dnsmasq 转发模式。如果无法根据设备 Mac 绕过 Clash 内核,NAS 的 BT/PT 等大量 P2P 连接会进入内核,降低了连接效率,还徒增内核的负担;如果放弃 Dnsmasq 转发模式,绕过中国大陆功能会失效,实测无论国内外域名都会被解析为 Fake IP,意味着大量直连流量会进入内核。

于是我便探索如何在 Dnsmasq 转发模式下根据 Mac 地址绕过设备。思路是参考 OpenClash 的实现方式,设计在 Dnsmasq 转发模式下可用的防火墙规则,利用 OpenClash 的开发者选项自动添加这些规则。
分析 OpenClash 的实现
在防火墙转发模式下,开启黑名单模式,添加一个 MAC 地址。使用nft list ruleset查看防火墙规则如下(节选):
table inet fw4 { # === 黑名单 MAC 集合 === set lan_ac_black_macs { type ether_addr elements = { 0d:00:07:21:ca:fe } } set china_ip_route {} set localnetwork {} set china_ip6_route {} set localnetwork6 {}
# === 入站/转发/出站:OpenClash 的 ICMP/QUIC 拦截 === chain input { type filter hook input priority filter; policy drop; ip protocol icmp icmp type echo-request ip daddr 198.18.0.0/16 reject with icmp port-unreachable comment "OpenClash ICMP INPUT REJECT" iifname "pppoe-wan" ip6 saddr != @localnetwork6 jump openclash_wan6_input udp dport 443 ip6 daddr != @china_ip6_route reject with icmpv6 port-unreachable comment "OpenClash QUIC REJECT" udp dport 443 ip daddr != @china_ip_route reject with icmp port-unreachable comment "OpenClash QUIC REJECT" iifname "pppoe-wan" ip saddr != @localnetwork jump openclash_wan_input }
chain forward { type filter hook forward priority filter; policy drop; ip protocol icmp icmp type echo-request ip daddr 198.18.0.0/16 reject with icmp port-unreachable comment "OpenClash ICMP FORWARD REJECT" }
chain output { type filter hook output priority filter; policy accept; ip protocol icmp icmp type echo-request ip daddr 198.18.0.0/16 reject with icmp port-unreachable comment "OpenClash ICMP OUTPUT REJECT" }
# === DNAT 阶段:DNS 劫持 & TCP 代理 === chain dstnat { type nat hook prerouting priority dstnat; policy accept; ip6 nexthdr { tcp, udp } th dport 53 jump openclash_dns_redirect meta l4proto { tcp, udp } th dport 53 jump openclash_dns_redirect ip protocol tcp jump openclash }
# === Mangle 阶段:透明代理(TPROXY) === chain mangle_prerouting { type filter hook prerouting priority mangle; policy accept; ip protocol udp jump openclash_mangle meta nfproto ipv6 jump openclash_mangle_v6 }
# === DNS 劫持(基于 MAC 的绕过,不在集合里才劫持到 7874) === chain openclash_dns_redirect { meta l4proto { tcp, udp } th dport 53 ether saddr != @lan_ac_black_macs redirect to :7874 comment "OpenClash DNS Hijack" ip6 nexthdr { tcp, udp } th dport 53 ether saddr != @lan_ac_black_macs redirect to :7874 comment "OpenClash DNS Hijack" }
# === TCP 代理(REDIRECT 到 clash 内核;命中黑名单 MAC 直接 return) === chain openclash { ip daddr @localnetwork return ct direction reply return ip protocol tcp ip daddr 198.18.0.0/16 redirect to :7892 ether saddr @lan_ac_black_macs return ip daddr @china_ip_route return ip protocol tcp redirect to :7892 }
# === UDP 代理(IPv4 TPROXY 到 clash 内核;命中黑名单 MAC 直接 return) === chain openclash_mangle { meta nfproto ipv4 udp sport 500 return meta nfproto ipv4 udp sport 68 return ip daddr @localnetwork return ct direction reply return meta l4proto udp ip daddr 198.18.0.0/16 meta mark set 0x00000162 tproxy ip to 127.0.0.1:7895 accept ether saddr @lan_ac_black_macs return ip daddr @china_ip_route return ip protocol udp jump openclash_upnp meta l4proto udp meta mark set 0x00000162 tproxy ip to 127.0.0.1:7895 accept }
# === UDP/TCP 代理(IPv6 TPROXY 到 clash 内核;命中黑名单 MAC 直接 return) === chain openclash_mangle_v6 { meta nfproto ipv6 udp sport 500 return meta nfproto ipv6 udp sport 546 return ip6 daddr @localnetwork6 return ct direction reply return ether saddr @lan_ac_black_macs return ip6 daddr @china_ip6_route return ip6 nexthdr tcp meta mark set 0x00000162 tproxy ip6 to :7895 accept comment "OpenClash TCP Tproxy" ip6 nexthdr udp meta mark set 0x00000162 tproxy ip6 to :7895 accept comment "OpenClash UDP Tproxy" }}分析可知其根据 MAC 绕过设备的核心思路是,把需要绕过的设备 MAC 放进一个集合,随后在 DNS 劫持与 TCP/UDP 透明代理中,凡是命中该 MAC 集合的数据包都 return 早退。
先定义一个需要绕过的 MAC 集合,用于后续匹配。
set lan_ac_black_macs { type ether_addr elements = { 0d:00:07:21:ca:fe }}在 dstnat 链中将 DNS 流量劫持到 openclash_dns_redirect 链处理,只有源 MAC 不在黑名单中的流量才会被 DNS 劫持,避免被绕过的设备 DNS 解析为 Fake IP。
chain openclash_dns_redirect { meta l4proto { tcp, udp } th dport 53 ether saddr != @lan_ac_black_macs redirect to :7874 comment "OpenClash DNS Hijack" ip6 nexthdr { tcp, udp } th dport 53 ether saddr != @lan_ac_black_macs redirect to :7874 comment "OpenClash DNS Hijack"}对于 TCP 流量,在 dstnat 链中被劫持到 openclash 链处理,如果源 MAC 在黑名单集合中,直接 return 早退。
chain openclash { ip daddr @localnetwork return ct direction reply return ip protocol tcp ip daddr 198.18.0.0/16 redirect to :7892 ether saddr @lan_ac_black_macs return ip daddr @china_ip_route return ip protocol tcp redirect to :7892}对于 UDP 流量,在 mangle_prerouting 链中劫持到 OpenClash 的规则处理,如果源 MAC 在黑名单集合中,直接 return 早退。IPv6 流量同理,不再赘述。
chain openclash_mangle { meta nfproto ipv4 udp sport 500 return meta nfproto ipv4 udp sport 68 return ip daddr @localnetwork return ct direction reply return meta l4proto udp ip daddr 198.18.0.0/16 meta mark set 0x00000162 tproxy ip to 127.0.0.1:7895 accept ether saddr @lan_ac_black_macs return ip daddr @china_ip_route return ip protocol udp jump openclash_upnp meta l4proto udp meta mark set 0x00000162 tproxy ip to 127.0.0.1:7895 accept}复刻实现
可见 OpenClash 的 MAC 黑名单实现还是简单易懂的,那么按照这个思路复刻一个实现,在 Dnsmasq 转发模式下能否达到这个效果呢?实际上是可以的。如下图在被绕过的设备上,DNS 没有被劫持,流量也没有进入 Clash 内核,但仍然可以使用 OpenClash 提供的代理服务。

只需模仿官方的实现,先创建一个黑名单 MAC 集合,然后在 DNS 劫持与 TCP/UDP 透明代理中绕过这些源 MAC 地址即可。需要注意的是,在 Dnsmasq 转发模式没有 openclash_dns_redirect 链,DNS 劫持直接写在了 dstnat 链中,感兴趣的话可自行查看防火墙规则。
下面给出一份我的实现,将以下内容放到 OpenClash -> 插件设置 -> 开发者选项 中即可。
#!/bin/sh. /usr/share/openclash/log.sh. /lib/functions.sh
# This script is called by /etc/init.d/openclash# Add your custom firewall rules here, they will be added after the end of the OpenClash iptables rules
LOG_OUT "Tip: Start Add Custom Firewall Rules..."# >>> 在这里填需要绕过 Clash 的设备 MAC,空格分隔MACS="0d:00:07:21:ca:fe"
# 1) 集合:lan_bypass_macs(存在则清空,确保可重复执行)if nft list set inet fw4 lan_bypass_macs >/dev/null 2>&1; then nft flush set inet fw4 lan_bypass_macs || :else nft add set inet fw4 lan_bypass_macs '{ type ether_addr; flags interval; }' || :fi
# 填充集合for mac in $MACS; do # 正规化为小写 m=$(echo "$mac" | tr 'A-F' 'a-f') nft add element inet fw4 lan_bypass_macs "{ $m }" 2>/dev/null || :done
# 2) 删除所有旧的“ether saddr @lan_bypass_macs return”,再插到链首nft -a list chain inet fw4 dstnat 2>/dev/null | awk ' /ether saddr @lan_bypass_macs/ && / return/ { for (i=1;i<=NF;i++) if ($i=="handle") {print $(i+1)} }' | while read -r h; do [ -n "$h" ] && nft delete rule inet fw4 dstnat handle "$h" 2>/dev/null || : donenft insert rule inet fw4 dstnat ether saddr @lan_bypass_macs return || :
# 3) 对 UDP/TCP 的 tproxy/redirect 入口做早退nft insert rule inet fw4 openclash_mangle ether saddr @lan_bypass_macs return || : # IPv4 TPROXYnft insert rule inet fw4 openclash_mangle_v6 ether saddr @lan_bypass_macs return || : # IPv6 TPROXYnft insert rule inet fw4 openclash ether saddr @lan_bypass_macs return || : # TCP REDIRECTLOG_OUT "Done: MAC bypass rules loaded."
exit 0值得注意的是,OpenClash 在启动时添加的防火墙规则,在关闭时都会被清理掉,包括 openclash*的几个链和dstnat 中插入的规则。而我们手动添加的规则并不会被 OpenClash 清理,所以在第二步先清理掉旧的规则,再重新插入到链首,避免反复重启后 dstnat中有冗余的规则。
除了防火墙规则之外,可能还需要修改 Dnsmasq 的设置,让其为指定设备下发指定的DNS服务器,而不是网关自身。因为有一些系统,即使你手动指定了 DNS 服务器,如果DHCP下发了其他的DNS服务器,系统依旧会优先去请求DHCP下发的DNS服务器,导致域名被解析成 Fake IP。
dhcp-host=AA:BB:CC:DD:EE:FF,set:dnsbypass,192.168.0.123dhcp-option=tag:dnsbypass,option:dns-server,223.5.5.5,119.29.29.29dhcp-option=tag:dnsbypass,option6:dns-server,[2400:3200::1],[2400:3200:baba::1]参考资料
本文没有参考资料,我在网上找了很久,没找到解决方案,于是自己动手丰衣足食。