使用 Gentoo Linux 手搓路由器之三 -- 国内访问加速

本文的做法同样适用 OpenWRT

继之前的三篇文章

文章中的 iptables 指令中包含了一些和ipset相关的,虽然做了一些注释,但是还是发文解释一下为何要这样做,好处是什么? 原理又是什么?

之前文章中的 iptables 指令. 区分来源IP的原因是,我希望有些场景下不走直连。

1## 国内IP直连
2iptables -t mangle -A XRAY -p TCP -s 192.168.88.0/24 -m set --match-set ${IPSET_CN_IP} dst -j RETURN
3iptables -t mangle -A XRAY -p TCP -s 192.168.1.0/24 -m set --match-set ${IPSET_CN_IP} dst -j RETURN
4iptables -t mangle -A XRAY -p TCP -s 10.8.9.0/24 -m set --match-set ${IPSET_CN_IP} dst -j RETURN
5iptables -t mangle -A XRAY -p UDP -s 192.168.88.0/24 -m set --match-set ${IPSET_CN_UDP_IP} dst -j RETURN
6iptables -t mangle -A XRAY -p UDP -s 192.168.1.0/24 -m set --match-set ${IPSET_CN_UDP_IP} dst -j RETURN

当你按之前的三篇文章做完,你的路由器大概的工作原理是,将所有的流量(内网,本机。排除掉目标地址是内网地址的部分),发住 xray, 在xray里配置了国内走 direct, 国外走 proxy 。也就是说把对域名和IP的判断交给了 Xray 。 对 Linux 有了解的人都知道, iptables 本质修改的是netfilter, 而netfilter是在内核空间里的,Xray是工作在用户空间的。对 Linux 路由器来说,转发流量只需要走内核就可以完成。而访问google必须要走xray,也就是用户空间。那么有没有一种办法让国内流量直接走内核空间转发?

以上只条 iptables 的指令大概的意思就是如果 dst 目标IP是在 ${IPSET_CN_IP} 里,就不走 tproxy了。这个 ${IPSET_CN_IP} 就是一个保存了国内 IP 地址的 IPSET。

ipset是iptables的扩展,它允许你创建 匹配整个地址集合的规则。而不像普通的iptables链只能单IP匹配, ip集合存储在带索引的数据结构中,这种结构即时集合比较大也可以进行高效的查找,除了一些常用的情况,比如阻止一些危险主机访问本机,从而减少系统资源占用或网络拥塞,IPsets也具备一些新防火墙设计方法,并简化了配置.

那也就是说, 我们只要把国内的IP地址写入这个 ${IPSET_CN_IP} IPSET中就可以了。那如何实现比较好呢?

简单做法:直接使用 APNIC 的国内的IP段列表

简单做法就是直接把 APNIC 的国内IP网段全部直接转发

这个IP段的列表非常容易拿到 在亚太互联网络信息中心官网直接就下载的. 过滤一下里面的 CN, IPv4, 就得到了 中国的 IPv4的IP网段, 一般不会发生变化,全球的IPv4地址基本上都分配完了。

把这个 IP 段加到 ${IPSET_CN_IP} 就可以了

创建一个 ipset

1ipset -N chnroute hash:net maxelem 65536
2ipset add chnroute xxxx/xx

写一个脚本,直接导入

 1#!/bin/bash
 2CHNROUTE_TMP_FILE="/home/zmhu/tmp/chnroute.txt"
 3
 4rm -f "${CHNROUTE_TMP_FILE}"
 5echo "${CHNROUTE_TMP_FILE}"
 6curl -s 'https://ftp.apnic.net/stats/apnic/delegated-apnic-latest' | \
 7   awk -F '|' '{if($2=="CN"&&$3=="ipv4"){printf "%s/%d\n",$4,32-log($5)/log(2)}}' \
 8   > "${CHNROUTE_TMP_FILE}"
 9
10# 清空一下ipset
11ipset flush chnroute
12
13for ip in $(cat $CHNROUTE_TMP_FILE); do
14  ipset add chnroute $ip
15done

进阶做法,可防广告,可防DNS泄漏

我的广告拦截是配置在 xray里面的,geoip里有广告地址

在 xray 的配置文件里 routing -> rules里增加配置

1// 广告流量 拦截
2{
3    "type": "field",
4    "domain": [
5        "geosite:category-ads-all"
6    ],
7    "outboundTag": "out-block"
8},

故,如果国内的广告IP直接被转发了,这里就启不到作用了。所以我想了一个取巧的办法: 监听 xray的access logdirectout-proxy的IP地址拿出来判断一下是否是国内的IP,如果是创建一个CN的IPSET,写进去。在 iptables 里仅针对这个CNIP的IPSET进行判断, 不仅可以排除广告的IP,还可以排除掉 DNS服务器的IP(排除掉DNS服务器的IP对防 DNS 泄漏有好处) 这里针对是否是国内IP的判断,使用了上面写的简单做法中的 APNIC的IPSET判断就好了,简单直接。

创建国内IP列表. 注意这里是IP地址,不是IP段了,所以创建的是 hash:ip 而不是 hash:net

1ipset -N cntcpip hash:ip maxelem 65536

开启 xray的log

1{
2    "log": {
3        "loglevel": "info",
4        "access": "/mnt/tmp/xray.acess.log"
5        //"error": "/var/log/xray.error.log"
6    }
7}

监听 log 并写入 ipset 的脚本, 脚本中区分了tcp和udp(排除了53端口),区分 udp 是为了给 bittorrent 协议单独做规则用的。

cniprouter.py

 1import socket
 2import re
 3from async_tail import tail
 4from pr2modules.ipset import IPSet, PortRange, PortEntry
 5from dotenv import dotenv_values
 6# https://github.com/svinota/pyroute2/blob/master/examples/ipset.py
 7
 8def ipset_save(name, value, ipset):
 9    if (not ipset.test(name, value)):
10        ipset.add(name, value)
11
12def parser_log(log_str: str):
13    if len(log_str) <=0:
14        return None
15    pattern = r"accepted (tcp|udp):([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}):([\d]+) \[([\w|-]+) (>>|\->) ([\w|-]+)\]"
16    matches = re.search(pattern, log_str)
17    if matches:
18        group = matches.groups()
19        return {
20            'proto': group[0],
21            'remote_ip': group[1],
22            'port': group[2],
23            'in': group[3],
24            'out': group[5]
25        }
26    else:
27        return None
28def is_cn_ip(remote_ip, ipset_cn_pool_name, ipset):
29    return ipset.test(ipset_cn_pool_name, remote_ip)
30
31def save_to_ipset_dispatcher(results, config, ipset: IPSet):
32    if results['proto'] == "tcp":
33        ipset_save(config['IPSET_CN_TCP_IP'], results['remote_ip'], ipset)
34    elif results['proto'] == "udp" and results['port'] != '53':
35        ipset_save(config['IPSET_CN_UDP_IP'], results['remote_ip'], ipset)
36
37def need_to_save(results):
38    need_save = False
39    match results['out']:
40        case 'direct':
41            need_save = True
42        case 'out-kiwi':
43            need_save = True
44        case 'out-bt':
45            need_save = True
46    if (results['proto'] == 'udp' and results['port'] == '53'):
47        need_save = False
48    return need_save
49
50if __name__ == '__main__':
51    config = dotenv_values(".env")
52    XRAY_LOG_FILE = config['XRAY_LOG_FILE']
53    IPSET_APNIC_CN_IP = config['IPSET_APNIC_CN_IP']
54    IPSET_CN_UDP_IP = config['IPSET_CN_UDP_IP']
55    IPSET_CN_TCP_IP = config['IPSET_CN_TCP_IP']
56    ipset = IPSet()
57
58    for log_line in tail(XRAY_LOG_FILE):
59        for line in log_line:
60            log_str = line[0] if len(line) > 0 else ""
61            is_cn = False
62            need_save = False
63            results = parser_log(log_str)
64            if (results is not None):
65                ## 如果是direct流量可以跳过IP是否是中国的判断
66                if results['out'] in ['direct', 'out-bt']:
67                    need_save = need_to_save(results)
68                    if (need_save):
69                        save_to_ipset_dispatcher(results, config, ipset)
70                ## 非direct流量判断一下是否是中国, 再判断是否需要写入ipset
71                else:
72                    is_cn = is_cn_ip(results['remote_ip'], IPSET_APNIC_CN_IP, ipset)
73                    if is_cn:
74                        need_save = need_to_save(results)
75                        if (need_save):
76                            save_to_ipset_dispatcher(results, config, ipset)
77                print("Dst ip: {}, is_cn: {}, out: {}, proto: {}, port: {}, save: {}".format(
78                    results['remote_ip'], is_cn, results['out'], results['proto'], results['port'], need_save))

.env

1XRAY_LOG_FILE=/mnt/tmp/xray.acess.log
2IPSET_APNIC_CN_IP=chnroute
3IPSET_CN_UDP_IP=cnudpip
4IPSET_CN_TCP_IP=cntcpip

requirements.txt

1async-tail==0.2.0
2pyroute2.core==0.6.13
3pyroute2.ipset==0.6.13
4python-dotenv==1.0.1

启动这个脚本,一直监听 xray的log,实时修改 IPSET. 随着你访问的越来越多, 这个IPSET就会越来越全面,你访问国内站点就会越来越快,不会发生DNS泄漏,而且广告拦截也不会受到影响。

Posts in this series