DarkNode

Life, the Universe and Everything

用 ACL 代替 Pac 进行更底层的智能分流

本文发表于:
最后修改于:
分类:network
合计信息量:4.75kb

相关说明

之前我曾经使用 Pac 进行 智能分流,但这一方案存在几个缺陷:

  1. Pac 运作于浏览器的 js 引擎中,而各个系统支持的 js 版本不同,例如 Win­dows 下就不支持 re­duce 函数。
  2. Pac 只能控制 HTTP 协议相关的代理,比如 Mail.app 就不会受 Pac 的控制。

后来发现 shad­ow­socks-libev 原生提供了 ACL 功能,可以直接使用 ACL 代替 Pac 完成智能分流。

ACL 相关配置

可以直接参考项目中的 ACL 配置范例,目前可以使用白名单模式或者是黑名单模式。

白名单模式的基本语法如下:

[proxy_all]

[by­pass_list]
1.0.1.0/24
1.0.2.0/23
1.0.8.0/21
(^|\.)baidu\.com$

黑名单模式的基本语法如下:

[by­pass_all]

[proxy_list]
91.108.4.0/22
91.108.56.0/22
(^|\.)google(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
(^|\.)googleapis(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$

支持使用正则匹配域名或者是使用 IP 段匹配 IP,看起来很美好,但是 socks 代理有一个特性:DNS 解析是由客户端程序控制的:

  1. 客户端程序可以通过 socks5 代理连接一个域名,在 socks5 服务器上解析域名再连接 IP。
  2. 客户端程序也可以在本地进行 DNS 解析,然后通过 socks5 代理直接连接一个 IP。

而目前的 shad­ow­socks-libev 只能直接处理第一种方式下的域名,或者第二种方式下的 IP,也就是说,我们必须指明域名在白名单中还是黑名单中,如果域名不在名单里,无法根据域名解析出来的 IP 进行进一步 GEOIP 分流。

增加 DNS 本地解析

ACL 相关的处理代码在 lo­cal.c 中的如下段落:

int by­pass = 0;
if (host_match > 0)
    by­pass = 1;                 // by­pass host­names in black list
else if (host_match < 0)
    by­pass = 0;                 // proxy host­names in white list
else {
    int ip_match = acl_match_host(ip);
    switch (get_acl_mode()) {
        case BLACK_LIST:
            if (ip_match > 0)
                by­pass = 1;               // by­pass IPs in black list
            break;
        case WHITE_LIST:
            by­pass = 1;
            if (ip_match < 0)
                by­pass = 0;               // proxy IPs in white list
            break;
    }
}

在处理完对域名的匹配之后,会直接进行对 IP 的匹配,但是直接连接域名的时候 IP 是空的,所以需要追加 DNS 解析的代码:

int by­pass = 0;
int re­solved = 0;
struct sock­addr_stor­age stor­age;
mem­set(&stor­age, 0, sizeof(struct sock­addr_stor­age));
int err;

if (host_match > 0)
    by­pass = 1;                 // by­pass host­names in black list
else if (host_match < 0)
    by­pass = 0;                 // proxy host­names in white list
else {
#ifn­def AN­DROID
    if (atyp == 3) {            // re­solve do­main so we can by­pass do­main with geoip
        err = get_sock­addr(host, port, &stor­age, 0, ipv6first);
        if ( err != -1) {
            re­solved = 1;
            switch(((struct sock­addr*)&stor­age)->sa_fam­ily) {
                case AF_INET: {
                    struct sock­addr_in *addr_in = (struct sock­addr_in *)&stor­age;
                    dns_ntop(AF_INET, &(addr_in->sin_addr), ip, INET_AD­DRSTRLEN);
                    break;
                }
                case AF_INET6: {
                    struct sock­addr_in6 *addr_in6 = (struct sock­addr_in6 *)&stor­age;
                    dns_ntop(AF_INET6, &(addr_in6->sin6_addr), ip, INET6_AD­DRSTRLEN);
                    break;
                }
                de­fault:
                    break;
            }
        }
    }
#en­dif
    int ip_match = acl_match_host(ip);
    switch (get_acl_mode()) {
        case BLACK_LIST:
            if (ip_match > 0)
                by­pass = 1;               // by­pass IPs in black list
            break;
        case WHITE_LIST:
            by­pass = 1;
            if (ip_match < 0)
                by­pass = 0;               // proxy IPs in white list
            break;
    }
}

这样就能进行一次本地 DNS 解析了,之后进入对 IP 的匹配处理,就能实现当域名匹配失败之后继续对 IP 进行匹配的功能了。

目前这一改动已经被 merge 进主分支了,所以下一个版本 3.0.6 就无需再改动代码并重新编译了。

DNS 污染问题

在目前的逻辑下,优先匹配域名,随后匹配 IP,所以只要使用黑名单模式,将被污染的域名加入黑名单,不进行解析就直接走代理:

[by­pass_all]

[proxy_list]
google
(^|\.)twit­ter\.com$
(^|\.)twimg\.com$
(^|\.)face­book\.com$

参考现在流行的 surge 中的 force-re­mote-re­solve 配置项,一般来说只有少数几个域名会享受这个待遇,比如 Google、Face­book、Twit­ter 家的几个域名遭遇了这一问题,比如 google 这一关键字,twimg.com 这一域名等。

随后我们需要将国外 IP 也放入 proxy_list 中,从 AP­NIC 获得 IP 表再进行处理就好,首先提取出中国的 IP 段,随后求其补集,再去除保留 IP 段即可。若直接提取非中国的 IP 段,那么很多分配不明的 IP 段会被排除,导致无法连接 telegram 等情况发生。建议直接使用 此项目,使用其生成的 out­wall.txt