Skip to content

基于 DNS 的内网透明代理分流方案

Posted on:March 25, 2023 at 08:05 AM
0

从 3 年前开始 r2s 成为了我的家庭主路由,也就是那一年 Clash 社区迅速演进,OpenClash 也逐渐成为了在 OpenWRT 上使用 Clash 的标准姿势。

但当年的 OpenClash 还没有基于 iptables 的分流功能,所有流量都经 Clash 核心转发,性能损耗过大,对 r2s 小小的 CPU 造成了大大的压力,于是我魔改了 passwall,利用 passwall 的 iptables 规则将流量分流,配合 Clash 核心,这样一直凑合用了几年。只要勤快点维护着规则,其实效果也还可以。

直到一个月前我终于下定决心把主路由换成性能稍微高一点的 r4s,同时我发现 OpenClash 也已经支持了绕过大陆 IP 的基本分流功能,加上我实在懒得维护自己的固件和魔改 passwall 了,索性用 OpenClash 的 redir-host 模式运行了一段时间,这段时间我发现 redir-host 确实对于 DNS 污染力不从心1,需要非常勤奋的维护规则才能勉强维持。我也试用过 Meta 核心,Fake-IP 和 TUN 模式,都有各自的问题,于是我又走上了自研解决方案的路。

预料到自己一定会疏于维护,这次我肯定不能再造轮子了。以下配置全部基于 OpenWRT 内已有的包,使用了 AdGuardHome,mosdns,OpenClash,经过排列组合和配置优化来达到分流和扶梯的效果。

实现效果

  1. 完美保留了 AdGuardHome 的各种功能
  2. 基于 DNS 的流量分流,国内流量绕过 Clash 核心
  3. 用 Fake-IP 模式来解决 DNS 污染的问题,但限制 Fake-IP 的范围,不需要代理的域名仍返回正常 IP
  4. 不用再费心找无污染的 DNS 服务器,使用运营商提供的 DNS 也没问题
  5. 因为彻底解决了 DNS 污染,可以放心缓存 DNS 请求结果,开启 AdGuardHome 的乐观缓存后,DNS 平均处理时间降到 3ms
  6. 完美兼容 IPv6。国内流量可正常使用 IPv6 服务。只要代理有 IPv6 出口,那国外也可正常使用。(使用 IPv6 居然还有意料之外的好处,后悔没早开)
  7. 兼容 BT/PT 应用,无需特殊配置也不会消耗代理流量
  8. 可以通过 AdGuardHome 的 Web 管理页面轻松切换内网设备是否走代理

DNS 分流

iptables 劫持
需要代理的设备
无需代理的设备
国内域名
国外域名
其它
国内IP
国外IP
域名白名单
域名黑名单
DNS 请求
AdGuardHome
mosdns
dnsmasq
忽略 IPv6 请求
fallback
运营商 DNS
Clash Fake IP

经过 DNS 分流后,所有需要代理的域名都分配到了 Fake IP,无需代理的域名都是由运营商 DNS 返回的最优结果。Clash 的 DNS 在 Fake IP 模式下可以无需请求网络直接返回结果,所以整体的 DNS 响应速度非常快。在处理 Fake IP 的流量时,Clash 会把 hostname 发送到远端进行 DNS 解析,也就自然不存在 DNS 污染的问题了。

流量代理分流

经过 DNS 分流以后,我们只需要一条 iptables 规则,把所有目的地址是 Fake IP 的流量都转发到 Clash 核心,所有其他流量都不经转发正常通行。

OpenClash 在 Fake IP 模式下会自动帮我们添加对应的 iptables 规则。但它为了防止小白误操作把其它 IP 的流量也转发到 Clash 核心了,这是没必要的,我们在自定义防火墙规则里把这条删掉就可以了。

同时由于只有 Fake IP 流量会经过代理,那么无需 DNS 解析的 IP 直连流量自然就不会经过代理了,这样就不用再担心 BT/PT 跑代理的流量了。

解决个别 IP 的代理问题

有的需要代理的 App 是直连 IP,不经过 DNS 域名解析的步骤的,目前我用到的只有一个,就是 telegram。好在 telegram 提供了它所使用的 ip-cidr 列表,我们只需要为这些 IP 单独配置 iptables 规则,给它们转发到 Clash 核心。

IPv6

在这种分流方案下,可以放心开启路由器的 IPv6 支持了。因为我们在 DNS 请求阶段,把可能需要代理才能访问的 IPv6 请求已经都过滤掉了,也就不用担心代理软件对 IPv6 支持不好的问题了。

同时因为 Clash 会将 hostname 传递到远端进行解析,那么如果你的代理落地机支持 IPv6 的话,经过代理的流量就也可以走 IPv6 出口了。

内网设备分流控制

通过 AdGuardHome 的 Web 管理界面可以为局域网内不同的设备指定不同的上游 DNS 服务器。

对于需要代理的设备,把上游 DNS 服务器指定到 mosdns。对于不需代理的设备,把上游 DNS 服务器指定到 dnsmasq。这样就能做到基于设备的分流控制了。

如果内网大部分设备都需要走代理,那就把 mosdns 作为 AdGuardHome 的默认上游 DNS 服务器,然后对个别不需要走代理的设备单独配置 dnsmasq 上游。反过来也是一样的。

基于域名的黑白名单

通过 mosdns 的 query_matcher 功能,可以构建基于域名的黑白名单,保证一些你选择的域名一定通过或不通过 Clash 核心的代理。

比如开启了 IPv6 功能之后,我们就可以把 test-ipv6.com 加入到域名白名单里,这样我们就能测试自己对 IPv6 配置的怎么样了。

AdGuardHome 乐观缓存

Clash 可以在重启过程中对 Fake IP 列表进行保存,运营商 DNS 的返回结果也基本稳定,所以我们可以放心的开启 AdGuardHome 的乐观缓存功能。这时 AdGuardHome 会优先返回已过期的缓存结果然后再自己慢慢去更新缓存内容,可以大大提高内网客户端的 DNS 请求响应速度。

具体配置

上面都在讲思路,终于轮到具体的配置了。先把对应的包都安装好:luci-app-adguardhomeluci-app-mosdns(v4)OpenClash。然后我们来一个一个的配置。

OpenClash

OpenClash 配置繁多,初始配置请自行参考 OpenClash 的 wiki,下面只说换成本文方案所需的配置。

en_mode=$(uci -q get openclash.config.en_mode)
proxy_port=$(uci -q get openclash.config.proxy_port)

if [ "$en_mode" == "fake-ip" ]; then
  LOG_OUT "limit route to only fake ips with proxy port $proxy_port"
  iptables -t nat -D openclash -p tcp -j REDIRECT --to-ports $proxy_port
  LOG_OUT "update telegram ipset"
  /etc/mosdns/rule/geoip2ipset.sh /etc/openclash/GeoIP.dat telegram
  iptables -t nat -A openclash -m set --match-set telegram dst -p tcp -j REDIRECT --to-ports $proxy_port
fi

LOG_OUT "restart adguardhome"
/etc/init.d/AdGuardHome restart

其中 /etc/mosdns/rule/geoip2ipset.sh 这个脚本可以根据 GeoIP 数据库来生成对应的 ipset,我这里只处理了 IPv4 的规则。IPv6 没管,因为也用不到。内容如下,这个文件放到路由器上后,记得要执行 chmod a+x /etc/mosdns/rule/geoip2ipset.sh 给它赋予可执行权限。

#!/bin/bash

geoipfile="$1"
tag="$2"
tmpdir="/tmp/v2dat"

cd $(cd $(dirname $BASH_SOURCE) && pwd)

mkdir -p "$tmpdir"
filename=$(basename -- "$geoipfile")
filename="${filename%.*}"
filename="$tmpdir/${filename}_$tag.txt"

if [ "$tag" == "telegram" ]; then
    wget --timeout 5 -O "$filename" 'https://core.telegram.org/resources/cidr.txt'
    if [ "$?" != "0" ]; then
         /usr/bin/mosdns v2dat unpack-ip -o "$tmpdir" "$geoipfile:$tag"
    fi
else
    /usr/bin/mosdns v2dat unpack-ip -o "$tmpdir" "$geoipfile:$tag"
fi

if test -f "$filename"; then
    ipset destroy "$tag"
    ipset create "$tag" hash:net
    while read p; do
        if ! grep -q ":" <<< "$p"; then
            ipset add "$tag" "$p"
        fi
    done <"$filename"
else
    echo "$filename missing."
fi

rm -rf "$tmpdir"

mosdns

选自定义配置文件,取消 DNS 转发的勾,然后我就直接贴配置了,注意 Clash DNS 端口要改成你自己在 OpenClash 里的配置,LAN IP-CIDR 也要改成你自己的内网配置,这里 mosdns 监听了 5335 端口。

log:
  level: info
  file: "/tmp/mosdns.log"

include: []

data_providers:
  - tag: geoip
    file: "/etc/openclash/GeoIP.dat"
    auto_reload: true

  - tag: geosite
    file: "/etc/openclash/GeoSite.dat"
    auto_reload: true

  - tag: whitelist
    file: "/etc/mosdns/rule/whitelist.txt"
    auto_reload: true

  - tag: greylist
    file: "/etc/mosdns/rule/greylist.txt"
    auto_reload: true

  - tag: hosts
    file: "/etc/mosdns/rule/hosts.txt"
    auto_reload: true

  - tag: redirect
    file: "/etc/mosdns/rule/redirect.txt"
    auto_reload: true

  - tag: local_ptr
    file: "/etc/mosdns/rule/local-ptr.txt"
    auto_reload: true

plugins:
  - tag: "local_end"
    type: forward
    args:
      upstream:
        - addr: 127.0.0.1 # dnsmasq
      timeout: 15

  - tag: "remote_end"
    type: forward
    args:
      upstream:
        - addr: 127.127.127.127:7874 # clash dns 端口
      timeout: 15

  - tag: query_is_whitelist_domain
    type: query_matcher
    args:
      domain:
        - "provider:whitelist"

  - tag: query_is_greylist_domain
    type: query_matcher
    args:
      domain:
        - "provider:greylist"

  - tag: query_is_hosts_domain
    type: hosts
    args:
      hosts:
        - "provider:hosts"

  - tag: query_is_redirect_domain
    type: redirect
    args:
      rule:
        - "provider:redirect"

  - tag: query_is_local_domain
    type: query_matcher
    args:
      domain:
        - "lan"
        - "local"
        - "provider:geosite:cn,apple-cn,category-games@cn"

  - tag: query_is_non_local_domain
    type: query_matcher
    args:
      domain:
        - "provider:geosite:geolocation-!cn"

  - tag: response_has_local_ip
    type: response_matcher
    args:
      ip:
        - "provider:geoip:cn"
        - "192.168.1.0/24" # Your lan ip-cidr here
      cname:
        - "provider:whitelist"

  - tag: is_https_query
    type: query_matcher
    args:
      qtype: [65]

  - tag: response_has_lan_ip
    type: response_matcher
    args:
      ip:
        - "192.168.1.0/24" # Your lan ip-cidr here

  - tag: increase_ttl
    type: ttl
    args:
      minimal_ttl: 1800

  - tag: match_local_ptr
    type: query_matcher
    args:
      qtype: [12]
      domain:
        - "provider:local_ptr"

  - tag: is_ipv6_query
    type: query_matcher
    args:
      qtype: [28]

  - tag: forward_local
    type: sequence
    args:
      exec:
        - local_end
        - if: response_has_lan_ip
          exec:
            - increase_ttl
            - _return

  - tag: "forward_remote"
    type: "sequence"
    args:
      exec:
        - if: is_ipv6_query
          exec:
            - _new_empty_response
            - _return
        - remote_end

  - tag: "main_sequence"
    type: "sequence"
    args:
      exec:
        - _misc_optm
        - query_is_hosts_domain
        - query_is_redirect_domain

        - if: is_https_query
          exec:
            - _new_nxdomain_response
            - _return

        - if: query_is_whitelist_domain
          exec:
            - forward_local
            - _return

        - if: query_is_greylist_domain
          exec:
            - forward_remote
            - _return
	    
        - if: match_local_ptr
          exec:
            - forward_local
            - _return

        - if: query_is_local_domain
          exec:
            - forward_local
            - _return

        - if: query_is_non_local_domain
          exec:
            - forward_remote
            - _return

        - primary:
            - forward_local
            - if: "(! response_has_local_ip) && [_response_valid_answer]"
              exec:
                - _drop_response
          secondary:
            - forward_remote
          fast_fallback: 200

servers:
  - exec: main_sequence
    listeners:
      - protocol: udp
        addr: ":5335"

AdGuardHome

在 luci 页面上,开启端口重定向,选择重定向53端口到AdGuardHome,这里注意 AdGuardHome 本身不要监听 53 端口,把 53 端口留给 dnsmasq,AdGuardHome 设置一个其它的端口就可以了。

在 Web 管理页面上,设置 - DNS 设置中,上游 DNS 服务器内只填写一个 mosdns 的地址 127.0.0.1:5335 #mosdns,私人反向 DNS 服务器写上 127.0.0.1 #dnsmasq。DNS 缓存配置里面,缓存大小看你内存大小填写,乐观缓存勾上。

对于不想走代理的设备,可以在设置 - 客户端设置中添加,并且把上游 DNS 服务器设置成 127.0.0.1

私聊咨询

Subscribers Only Content Loading...

Footnotes

  1. Clash 删除 redir-host 模式