多年来,Openwrt作为旁路由方案非常完美的契合了我的需求,使用起来非常方便,加上OpenWrt可以自行编译,在保障了一定程度上的网络安全同时还有着数量相当广的插件来实现许多有意思的功能

随着使用时间变多,对插件的依赖逐渐变小,Openwrt逐渐变得重起来了,而且在界面上去实现一些路由规则、Bash脚本有些隔靴搔痒,于是打算实现一个旁路由透明代理和分流的方案,为此查阅了一些资料,计划实现如下功能:

  1. 智能分流:对国内地址采用国内DNS解析直连访问,对国外地址则走网络代理进行访问
  2. 解决DNS污染:畅通地访问无需科学上网的网站(如GitHub)
  3. 支持旁路由:家庭网络中其他设备无需安装任何代理工具即可享受上述的一切

思路如下:

  • Alpine:最迷你的Linux发行版,非常适用Docker/虚拟机
  • 分流方案:使用iptables搭配ipset实现国内外IP分流
  • DNS污染:使用overture解除国内的DNS污染

下面将实践上述的方案,我的信息如下

  • 网络段:192.168.10.0/20
  • 路由器IP:192.168.10.1
  • 透明网关IP:192.168.10.2

最终,我希望局域网中的其他设备将网关地址和DNS设置为192.168.10.2,将可以实现自动分流(国内直连,国外代理)

透明网关:即网关的存在对网络用户来说是透明的,它们无需对其进行特殊的配置或设置(例如手动设置代理),用户可以像访问任何其他网络设备一样,与透明网关进行通信,无需关心网关做了什么代理策略

原理如下图

@startuml
skinparam monochrome false
skinparam shadowing false
skinparam nodesep 50
skinparam ranksep 70
skinparam ArrowColor Black
skinparam ArrowFontColor Black
skinparam ArrowLenght 30
skinparam ArrowThickness 2

left to right direction


package "我的网络\n192.168.10.1/20" {
  [手机/平板] #LightYellow
  [PC/电视] #LightYellow
  [其他设备] #LightYellow
  [旁路由器(192.168.10.2)] #LightGreen
}


cloud "国外网站" #LightPink

database "国内网站" #LightSkyBlue

[手机/平板] --> [旁路由器(192.168.10.2)]
[PC/电视] --> [旁路由器(192.168.10.2)]
[其他设备] --> [旁路由器(192.168.10.2)]

[旁路由器(192.168.10.2)] --> [国内网站] : 直接连接
[旁路由器(192.168.10.2)] --> [国外网站] : 代理连接

@enduml

此文建议有一定网络基础知识的同学实践,如果卡在一些简单的步骤说明可能不那么合适自定义透明网关

1. Alpine

Alpine是一个非常迷你的操作系统,docker容器大小仅为5Mb,ISO镜像也仅有100+M,非常适合用来定制Linux透明网关

1.1. Alpine安装

下载Alpine

下载使用任意虚拟机软件虚拟该系统,我的配置是 $2C4T/1024 RAM/1Gb Disk$

开启虚拟机后,输入root用户登录,无需密码,然后输入setup-Alpine开始安装系统

之后的安装过程与其他发行版安装无异,安装较为简单,此处不再赘述

1.2. Alpine网络设置

在系统安装完成后,编辑文件:/etc/network/interfaces

内容参考如下如下

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
        address 192.168.10.2
        netmask 255.255.255.0
        gateway 192.168.10.1
        nameservers 114.114.114.114 # 暂时设置DNS为114.114.114.114

修改完成后重启网络,测试网络是否畅通

service networking restart

在确保网络正常后,设置允许机器转发局域网请求

编辑文件:/etc/sysctl.conf

添加如下字段到文件尾

net.ipv4.ip_forward=1

并更新sysctl配置

sysctl -p

1.3. Alpine软件源

在重启网络之后,为了加快软件下载,将软件源重定义到清华源

编辑文件:/etc/apk/repositories

#/media/cdrom/apks
https://mirrors.tuna.tsinghua.edu.cn/Alpine/v3.16/main
https://mirrors.tuna.tsinghua.edu.cn/Alpine/v3.16/community
https://mirrors.tuna.tsinghua.edu.cn/Alpine/edge/main
https://mirrors.tuna.tsinghua.edu.cn/Alpine/edge/community
https://mirrors.tuna.tsinghua.edu.cn/Alpine/edge/testing

然后更新apk仓库

apk update

1.4. Alpine的准备工作

接下来准备相关工具,安装以下软件,并初始化运行

apk add supervisor curl iptables vim wget ipset
service iptables save
service iptables start
service supervisord start

由于Alpine没有使用systemd来管理系统,需要借助crontab手动添加以上部分软件的自启动

crontab -e

设置supervisordiptables服务在开机之后自启动

# do daily/weekly/monthly maintenance
# min   hour    day     month   weekday command
...

@reboot /sbin/service supervisord start
@reboot /sbin/service iptables start

到此,系统的初始化设置完毕

2. gost

gost是一款使用golang编写的网络安全隧道工具,支持大部分主流协议,目前更新到V3.0版本,本文使用v2.11版本

仓库地址:https://github.com/ginuerzh/gost

2.1. 服务端

在服务端下载gost

并使用gzip解压

gzip -d gost-linux-amd64-2.11.2.gz

运行最简单的代理服务

./gost-linux-amd64-2.11.2 -L=ss://chacha20:password@:8338

2.2. 客户端

回到我们本地的Alpine系统,一样先下载gost

并使用gzip解压

gzip -d gost-linux-amd64-2.11.2.gz

下载完成后先尝试运行

./gost -L=:18080 -F="ss://chacha20:password@server_ip:8338"

测试代理是否正常

~$ export http_proxy=http://127.0.0.1:18080
~$ curl cip.cc                 
IP      : 1.2.3.4
地址    : 中国  香港  keaiduo.com
数据二  : 香港 | 可爱多优先公司
数据三  : 中国香港 | 可爱多优先公司
URL     : http://www.cip.cc/1.2.3.4

可以看到成功使用了服务端的IP

2.3. 中继代理

在确认直连测试ss连接是正常后,开启gost的透明网关模式(中继代理),如下

# 仅修改了监听协议为red
./gost -L=red://:28080 -F="ss://chacha20:password@server_ip:8338"

检查以上命令运行无报错输出后结束,写入到supervisor中作为daemon程序运行

编辑文件:/etc/supervisor.d/gost.ini

[program:gost-18080]
command=/root/gost/gost -L=:18080 -F="ss://chacha20:password@server_ip:8338"
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
user=root

[program:gost-28080]
command=/root/gost/gost -L=red://:28080 -F="ss://chacha20:password@server_ip:8338"
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
user=root

上面的配置将运行2个监听,分别是代理监听18080和中继转发监听28080

更新supervisor运行

supervisorctl update

检查运行状态

supervisorctl status

3. iptables

iptables是Linux中用来管理网络包的用户态工具,大部分操作都需要超级用户权限,其路径通常位于/sbin/iptables

iptables是由table、chain、rules组成的,用不同的表来处理不同类型的数据包,用链来处理不同时期的数据包,用规则来管理数据包的行为

3.1. 基础概念(可跳过)

要使用iptables,要掌握一定的基础网络知识,这里受限于篇幅,仅查阅了一些关于透明网关相关的iptables操作

iptables的基本构成是4表5链

  • Filter表:iptables的默认表,包含INPUT/OUTPUT/FORWARD链
  • NAT表:地址转换表,包含了OUTPUT/PREROUTING/POSTROUTING链
  • Mangle表:用于管理数据包内容的表,包含了PREROUTING/OUTPUT/FORWARD/INPUT/POSTROUTING链
  • raw表:处理异常的表,包含了PREROUTING/OUTPUT链
  • security表:不常见,此处可忽略

此外还包含以下动作(即-j参数值)

  1. ACCEPT
  2. DROP
  3. REDIRECT
  4. RETURN
  5. SNAT
  6. DNAT
  7. MASQUERADE
  8. LOG
  9. SEMARK

iptables -A INPUT -p all -s 192.168.1.0/24 -j ACCEPT 为例

其中-A表示append,-p表示协议类型,-s表示来源,-j表示动作

由上可以看出iptables的基础命令形式

iptables -t 表名 <-A/I/D/R> 规则链名 [规则号] <-i/o 网络设备> -p 协议名 <-s 源IP/源子网> --sport 源端口 <-d 目标IP/目标子网> --dport 目标端口 -j 动作

相关参数解释

  • <-A/I/D/R>:A代表添加,I代表插入,D代表删除,R代表替换
  • <-i/o 网络设备>:-i表示流入设备名称,-o表示流出设备名称
  • -p:表示protocol,常见的如tcp/udp/icmp/stcp等

接下来,我们可以看看下面的例子,帮助理解iptables的命令行规则

创建与删除规则示例

# 允许192.168.1.0/24网段访问本机22端口
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
# 查看刚才创建的规则
iptables -L -n -v
# 删除“允许192.168.1.0/24网段访问本机22端口”规则
iptables -D INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT

iptables常见操作例子

# 允许访问本机22,80,443端口
iptables -A INPUT  -p tcp -m multiport --dports 22,80,443 -j ACCEPT
# 允许本机访问192.168.1.0/24网段的所有22端口
iptables -A OUTPUT -p tcp -d 192.168.1.0/24 --dport 22 -j ACCEPT
# 禁止访问192.168.1.1(注意协议类型为all)
iptables -A OUTPUT -p all -d 192.168.1.1 -j DROP
# 将本机端口12222的请求转发到22
iptables -t nat -A PREROUTING -p tcp --dport 12222 -j REDIRECT --to-port 22
# 限制本机端口80的并发数量最高为200,每分钟最高100个(应对DDOS攻击)
iptables -A INPUT -p tcp --dport 80 -m limit --limit 100/minute --limit-burst 200 -j ACCEPT
# 限制本机的ICMP请求
iptables -A INPUT -p icmp -j DROP
# 限制mac地址访问本机
iptables -A INPUT -m mac --mac-source 32:e6:37:5c:40 -j DROP
# 清除所有规则
iptables -F

下面是设置网关转发需要用到的NAT表操作

# 创建一个nat表,名为SSNAT
iptables -t nat -N SSNAT
# 为SSNAT表设置遇到192.168.1.1/20网段的目标IP网段则终止当前链返回上一个调用链
iptables -t nat -A SSNAT -d 192.168.1.1/20 -j RETURN
# 为SSNAT表设置将所有tcp请求转发到本机18080端口
iptables -t nat -A SSNAT -p tcp -j REDIRECT --to-port 18080
# 将SSNAT链中所有的规则追加到OUTPUT链中
iptables -t nat -A SSNAT -p tcp -j SHADOWSOCKS
# 查看所有NAT表规则
iptables -t nat -L -v -n

掌握了以上的iptables基础操作,就可以操作iptables转发来实现透明网关

3.2. 实现分流

iptables在进行地址集合查找时效率不高,可引入iptables的扩展插件ipset来处理地址合集

要区分国内外ip,首先需要一份国内的IP网段

有了国内ip地址合集就可以实现针对国内外ip走不同代理策略(国外走代理,国内直连)

编辑文件:/root/iptables/iptables.sh

#/bin/bash
#author:Chancel.Yang
#date:2023/09/21

remote_ip=[your_server_ip]
local_port=[your_local_port]

/usr/sbin/ipset -N china hash:net
for i in $(cat ./china_ip_list.txt ); 
do
    /usr/sbin/ipset -A china $i; 
done

# 创建一个NAT规则集`SSNAT`
/sbin/iptables -t nat -N SSNAT
# 在`SSNAT`规则中添加无需走ss流量的国内ip以及局域网ip
/sbin/iptables -t nat -A SSNAT -p all -m set --match-set china dst -j RETURN
/sbin/iptables -t nat -A SSNAT -d 0.0.0.0/8 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 10.0.0.0/8 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 127.0.0.0/8 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 169.254.0.0/16 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 172.16.0.0/12 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 192.168.0.0/16 -j RETURN
/sbin/iptables -t nat -A SSNAT -d 224.0.0.0/4 -j RETURN
/sbin/iptables -t nat -A SSNAT -d $remote_ip -j RETURN

# `SSNAT`的网络代理包括udp、icmp、udp(请按需)
/sbin/iptables -t nat -A SSNAT -p tcp -j REDIRECT --to-port $local_port
/sbin/iptables -t nat -A SSNAT -p udp -j REDIRECT --to-port $local_port
/sbin/iptables -t nat -A SSNAT -p icmp -j REDIRECT --to-port $local_port

# 将所有数据包转入`SSNAT`规则集合中
/sbin/iptables -t nat -A PREROUTING -p tcp -j SSNAT

# 将出站数据包的源地址进行NAT,否则在部分网络场景下会100%丢包
/sbin/iptables -t nat -I POSTROUTING -j MASQUERADE

脚本说明:

  1. 首先使用ipset导入china_ip_list.txt文件并命名集合名为china
  2. 创建名为SSNAT的nat链,在此链中设置国外走代理,国内直连的处理策略
  3. 最后将入网流量全部转入SSNAT链中处理并进行NAT伪装

运行上面的脚本缺少china_ip_list.txt 文件,china_ip_list.txt 文件在github.com可以下载,地址如下

wget https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt

脚本目录如下

Alpine:~# ls -l /root/iptables
total 13212
-rw-r--r--    1 root     root          1526 Aug  2 10:13 iptables.sh
-rw-r--r--    1 root     root         95316 Jul 27 11:51 china_ip_list.txt

执行脚本查看结果

sh /root/iptables/iptables.sh

没有错误输出的话,就可以将这个脚本设置为开机执行

crontab -e

设置开机执行

# do daily/weekly/monthly maintenance
# min   hour    day     month   weekday command
...

@reboot /bin/sh /root/iptables/iptables.sh

我的NAT表规则如下(可供参考)

chancel@j3455 ~$ sudo iptables -L -n -v -t nat

Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
  508 31917 SSNAT      6    --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 2283  183K MASQUERADE  0    --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     0    --  docker0 *       0.0.0.0/0            0.0.0.0/0           

Chain SSNAT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
   47  4192 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set china dst
    0     0 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/8           
    0     0 RETURN     0    --  *      *       0.0.0.0/0            10.0.0.0/8          
    0     0 RETURN     0    --  *      *       0.0.0.0/0            127.0.0.0/8         
    0     0 RETURN     0    --  *      *       0.0.0.0/0            169.254.0.0/16      
    2   185 RETURN     0    --  *      *       0.0.0.0/0            172.16.0.0/12       
    2   120 RETURN     0    --  *      *       0.0.0.0/0            192.168.0.0/16      
    0     0 RETURN     0    --  *      *       0.0.0.0/0            224.0.0.0/4         
  449 26940 REDIRECT   6    --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 28080

在局域网内,找任意机器将网关设置成192.168.10.2,此时应该可以正常访问国内网站

4. overture

到上一步为止,可以访问国内网站,但仍然无法正常访问国外网站

即使SSNAT内能根据国内外IP自动分流,由于DNS污染没有解决,google.com等域名的ip是错误的,分流是无法正常生效的

注:DNS污染指通过国内DNS解析去解析国外的IP地址时,DNS服务器返回一个错误的IP(或者根本不存在的IP)

所以我们需要自建一个正常的DNS解析服务,overture是一个基于Go语言开发的DNS解析服务程序

比起经典的ChinaDNS的优点是设置更加丰富,也更简单一些

4.1. 安装

安装方法如下

mkdir -p /root/overture && cd /root/overture
wget https://github.com/shawn1m/overture/releases/download/v1.8/overture-linux-amd64.zip
unzip overture-linux-amd64.zip

# 保留默认配置文件用于恢复(可选)
cp config.yml config.yml.bak

4.2. 配置

编辑文件:/root/overture/config.yml

bindAddress: :53
debugHTTPAddress: 127.0.0.1:55555
dohEnabled: false
primaryDNS:
  - name: DNS114
    address: 114.114.114.114:53
    protocol: udp
    socks5Address: 
    timeout: 6
    ednsClientSubnet:
      policy: disable
      externalIP:
      noCookie: true
  - name: AliDNS
    address: 223.5.5.5:53
    protocol: udp
    socks5Address:
    timeout: 6
    ednsClientSubnet:
      policy: disable
      externalIP:
      noCookie: true
onlyPrimaryDNS: false
alternativeDNS:
  - name: GoogleDNS
    address: 8.8.8.8:53
    protocol: tcp
    # 使用一开始设置的`gost`直连代理来访问Google的DNS服务
    socks5Address: 127.0.0.1:18080
    timeout: 6
    ednsClientSubnet:
      policy: disable
      externalIP:
      noCookie: true
ipv6UseAlternativeDNS: false
alternativeDNSConcurrent: false
whenPrimaryDNSAnswerNoneUse: primaryDNS
ipNetworkFile:
  # 如在`primary`2个DNS服务中匹配到`china_ip_list.txt`中的ip则直接返回
  primary: /root/overture/china_ip_list.txt
  alternative: /root/overture/ip_network_alternative_sample
domainFile: 
  primary: /root/overture/domain_primary_sample
  # 如在`alternative`2个DNS服务中匹配到`gfw_all_domain.txt`中的域名则直接返回
  alternative: /root/overture/gfw_all_domain.txt
  matcher: full-map
hostsFile:
  hostsFile: /etc/hosts
  finder: full-map
minimumTTL: 0
domainTTLFile: /root/overture/domain_ttl_sample
cacheSize: 100
cacheRedisUrl:
cacheRedisConnectionPoolSize:
rejectQType:
  - 255

配置文件解释如下:

  1. primaryDNS表示主DNS列表(国内),alternativeDNS表示副DNS列表(国外)
  2. 关键配置在于ipNetworkFiledomainFile,一个确定匹配到国内ip段将直接返回,一个确定匹配到国外域名就直接返回

配置中使用到的国内IP与国外域名来源:

  • 国内IP段:https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt
  • 国外域名集合:https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt

由于china_ip_list.txtgfw_all_domain.txt文件都是需要定期更新,所以写一个脚本来实现

编辑文件:/root/overture/ip_and_domain_update.sh

#/bin/bash
#author:Chancel.Yang
#date:2023/09/21

wget https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt
curl https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt | base64 -d | sort -u | sed '/^$\|@@/d'| sed 's#!.\+##; s#|##g; s#@##g; s#http:\/\/##; s#https:\/\/##;' | sed '/\*/d; /apple\.com/d; /sina\.cn/d; /sina\.com\.cn/d; /baidu\.com/d; /qq\.com/d' | sed '/^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$/d' | grep '^[0-9a-zA-Z\.-]\+$' | grep '\.' | sed 's#^\.\+##' | sort -u > temp_gfwlist.txt
curl https://raw.githubusercontent.com/hq450/fancyss/master/rules/gfwlist.conf | sed 's/ipset=\/\.//g; s/\/gfwlist//g; /^server/d' > temp_koolshare.txt
cat temp_gfwlist.txt temp_koolshare.txt | sort -u > gfw_all_domain.txt
rm -f temp_gfwlist.txt temp_koolshare.txt

执行脚本,然后查看目录下是否包含了china_ip_list.txtgfw_all_domain.txt

cd /root/overture && bash ip_and_domain_update.sh

运行overture

./overture-linux-amd64 -c ./config.yml

确保执行结果中没有error提示,然后用局域网内其他机器测试解析github.com的网址

➜  dig @192.168.10.2 www.github.com

; <<>> DiG 9.18.4 <<>> @192.168.10.2 www.github.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8772
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.github.com.                        IN      A

;; ANSWER SECTION:
www.github.com.         1552    IN      CNAME   github.com.
github.com.             60      IN      A       192.30.255.113

;; Query time: 243 msec
;; SERVER: 192.168.10.2#53(192.168.10.2) (UDP)
;; WHEN: Tue Jul 26 18:46:44 CST 2022
;; MSG SIZE  rcvd: 107

最后使用supervisor将overture设置为daemon程序运行

编辑文件:/etc/supervisor.d/overture.ini

[program:overture]
directory=/root/overture
command=/root/overture/overture-linux-amd64 -c /root/overture/config.yml
autostart=true
autorestart=true
user=root

更新supervisor运行

supervisorctl update

检查运行状态

supervisorctl status

最后修改Alpine网络配置中dns服务地址

编辑文件:/etc/network/interfaces

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
        address 192.168.10.2
        netmask 255.255.255.0
        gateway 192.168.10.1
        nameservers 127.0.0.1

重启网络

service networking restart

将局域网其他设备设置为192.168.10.2,并验证透明网关是否正确分流国内外ip

5. 结束语

本文仅抛砖引玉,若对透明网关有兴趣还需要多多查阅资料

资料参考