Skip to main content
overcache

那些从墙上学会的知识

不好好学习计算机网络,就会被国家教育。

拓扑图

topological

Shadowsocks怎么穿墙的

端口转发

首先,把SS部署在路由器上,默认情况下是监听1080端口的,所有经过这个端口的数据包都由SS转发到远程服务器上。OpenWrt上的Shadowsocks-libev自带有3个程序,分别是ss-local, ss-redir 和ss-tunnel。这里由ss-redir转发数据到服务器。 在未做配置的情况下,所有的数据都是直接有路由器直接发出去的。怎么才能让数据包多走一步,先发送到1080端口让ss-redir进行再加工呢?这个可以用linux内核内的netfilter框架对进入系统的数据包进行筛选然后端口转发。netfilter在进行端口转发的时候,还会自动维护一些必要的信息,以便做一个回程的逆处理:将服务器返回的数据转回原始的端口。netfilter的配置工具是iptables, 鸟哥那有一些很好的资料,也可看看这篇blog

数据传送

ss-redir从1080端口获取原始数据包之后, 根据选择的加密方式对原始数据包进行加工,目的是让墙无法轻易探知数据包的内容从而逃过被宰杀的命运。VPS上运行的ss-server接受到数据后,根据约定的加密方式还原数据,并发到它的真实目的地。 返程也是类似的过程。

验证

场景

假设GFW的黑名单中有一条IP地址: 206.125.164.82, 为了表述方便,姑且给它个代号叫Evil-IP。所有经过GFW的数据包,如果目的地址是这Evil-IP,就会被无情丢弃。

我们通过telnet发起TCP请求,生成发往Evil-IP的数据包:
telnet 206.125.164.82 80
如果链接成功了,请假装没有看到。毕竟我们假设它被墙了嘛。

添加规则

首先将路由器防火墙的当前规则保存:
iptables-save > iptables.rules

如果因为规则混乱无法上网了可以通过
iptables-restore < iptables.rules
直接恢复而无需重启路由器。

添加自定义的规则,将我们的数据包转发到ss-redir监听的端口上:
iptables -t nat -A PREROUTING -d 206.125.164.82 -p tcp --dport 80 -j REDIRECT --to-port 1080
-t nat表示这条规则添加到nat表,因为端口转发只能在nat表上,所以这里是不能用默认的filter表的。-A PREROUTING表明将这条规则添加到PREROUTING这条规则链的最后。-j REDIRECT是说符合条件的数据包都jump到REDIRECT这个目标,后面的扩展参数--to-port指明了跳转到的端口号。

iptables -t nat --list或者iptables -t nat -L命令看看输出,看到Chain PREROUTING上已经显示这条规则了,添加成功。

iptables的各个表的处理顺序,可以看这张图iptables-flow

可以看到,Client发往Evil-IP的数据被防火墙处理的规则路径被红色箭头标示.
而Router自身发往Evil-IP的数据,依次应用的Chain用蓝色标示。

因此我们将规则添加到PREROUTING链只能转发Client的数据到1080,如果你还要在Router上做羞羞的事,还要再添加一条规则: iptables -t nat -A OUTPUT -d 206.125.164.82 -p tcp --dport 80 -j REDIRECT --to-port 1080

观察到Client的数据和Router的数据最后都经过nat表的POSTROUTING链才发出,那么将转发规则添加到这个链上不就万事大吉了嘛?我们试试:
iptables -t nat -A POSTROUTING -d 206.125.164.82 -p tcp -j REDIRECT --to-port 1080
呵呵,添加不成功,提示“ip_tables: REDIRECT target: used from hooks POSTROUTING, but only usable from PREROUTING/OUTPUT”, 至于为什么REDIRECT不能应用在这个链上,直觉上应该是这个链只能用来做SNAT,更严谨的原因我并没有去探究,如果有人知道我顺便知会我一声。

观察

接下来就要观察数据包是否被正确转发了。

一般来说,抓取数据包的活交给tcpdump这种神器最合适不过了,但是我监听1080端口几千年,都没观察到有数据流量,结果自然是大为沮丧. 后来Google后才发现是掉进了端口转发的坑,端口转发是不能被tcpdump捕获到的(原因). 而且由于路由器上的ss-redir的Verbose mode太安静了,无法观察到什么有用的信息。

我想到的有两个方法来验证数据是否被正确转发(如果有更好的方法请告诉我):

  • 通过服务器端的ss-server观察

    分别在VPS和路由器上运行ss-server和ss-redir,注意添加-v参数可以看到更多的运行消息.
    VPS:/usr/local/bin/ss-server -c /etc/shadowsocks/ching.json -u -v
    router: /usr/bin/ss-redir -c /etc/ching.json -b 0.0.0.0 -v
    在Client或者Router上向Evil-IP发送数据:
    telnet 206.125.164.82 80
    按下会车后应该就可以看到VPS运行的ss-server的工作状态发生了改变,接受了一个连接并向Evil-IP发送了数据。

  • nc命令

    如果只想验证iptables进行端口转发的效果,而不理会Shadowsocks的话。nc命令是一个好工具。关掉Router上运行的ss-redir和uhttpd进程,以释放1080端口和80端口。openwrt自带的nc不能监听端口(原因), 需要安装netcat来补充这个功能:
    opkg install netcat
    然后在路由器上运行两个nc实例,一个监听80端口:
    nc -l -p 80
    另外一个监听1080:
    nc -l -p 1080
    最后我们从Client对Evil-IP的80端口发送数据
    nc 206.125.164.82 80
    如果是监听1080的实例收到了数据,说明端口转发成功, 事实证明确实转发成功了.

转发大量的Evil-IP

如果只有少数几个Evil-IP的话,一个个用iptables添加转发规则也没什么问题,但是如果数量一多,几百几千条的IP,就算添加到规则里不是什么大事,但是每个数据包进出都要做那么多的检查和匹配,时间复杂度是O(n),效率还能进一步提高,利用IPset可以将时间复杂度变成O(1)

  • IP sets

    IP sets是内核里的一个框架,一个IP set可以存储IP、网络、MAC地址等等,查找集合中某元素的的速度极快, 利用ipset工具管理。ipset的的使用方法也很直白,直接验证利用IP sets的结果吧。

  • 验证结果

    建立一个存储IP的集合:SHADOWSOCKS:
    ipset -N SHADOWSOCKS hash:ip
    向集合中添加Evil-IPs:
    ipset add SHADOWSOCKS 206.125.164.82
    ipset add SHADOWSOCKS 74.125.200.113
    查看SHADOWSOCKS集合的元素:
    ipset list SHADOWSOCKS
    还原初始iptables:
    iptables-restore < iptables.rules
    添加针对IP sets进行匹配的规则:
    iptables -t nat -A OUTPUT -p tcp -m set --match-set SHADOWSOCKS dst -j REDIRECT --to-port 1080
    iptables -t nat -A PREROUTING -p tcp -m set --match-set SHADOWSOCKS dst -j REDIRECT --to-port 1080
    最后按上一节的方法进行验证, 可以观察到已经成功通过ss-redir访问这两个Evil-IP。

如何得到Evil-IP

浏览网络过程中,比较用域名而言,利用IP上网还是太原始了,而且域名是固定的,而域名对应的IP地址却是可以动态变化,如果能得到动态变化的Evil-IPs那就好了。由于iptables工作在网络协议第三层和第二层,识别IP识别MAC,但是对高层的域名是无察觉的,所以无法用iptables来匹配Evil-Domain,只能先找到Evil-Domain对应的Evil-IP,将这些IP加入IP set,最后才让iptables上场工作。

dnsmasq-full

dnsmasq是轻量的DHCP和DNS缓存服务,它有个--ipset选项,可以将指定域名查询到的IP地址加入指定的ip set。但是OpenWrt自带的版本不支持ipset选项,需要安装dnsmasq-full:
opkg remove dnsmasq
opkg install dnsmasq
/etc/init.d/dnsmasq restart
验证版本,ipset字样表明支持ipset:
dnsmasq -v

Dnsmasq version 2.73 Copyright (c) 2000-2015 Simon Kelley
Compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth DNSSEC loop-detect inotify

以下第一条参数为Evil-Domain指定DNS服务器,这里要注意即使是利用dnsmasq本身发起查询,也需要明确说明,不然是无法解析Evil-Domain的。第二条参数将解析结果加入之前建立的SHADOWSOCKS ip set
echo “server=/echowhale.com/127.0.0.1#53” >> /etc/dnsmasq.conf
echo “ipset=/h2byte.com/SHADOWSOCKS” >> /etc/dnsmasq.conf
发起DNS查询后,观察SHADOWSOCKS set的变化,可以看到集合里已经多了一条记录,那就是查询到的echowhale.com的IP:
nslookup echowhale.com
ipset list SHADOWSOCKS

直到这里,我们都是假设DNS一直能返回正确的地址。但是事实往往不尽如人意,由于DNS投毒的存在,在访问Evil-Domain时,我们会被虚假的DNS响应带到坑里去。
当你觉得自己学得够多了的时候,墙总会跳出来给你一巴掌,呵呵哒。

解决DNS投毒

要想不被毒粥毒死,有两个思路:

一是从毒锅里盛粥,把有毒的米挑出来扔掉

如果毒米的特征很明显,用这种方法也未尝不可。以前伪DNS响应总是返回固定的若干个假IP,利用iptables匹配53端口的udp数据,再用iptable的u32模块或者string模块找到数据包携带的IP地址,跟我们收集到的伪IP集合对比,发现是有毒的就扔掉就好了。
可惜墙为了督促我们学习,返回的IP地址已经是随机的了。从毒粥里盛粥喝,既危险又不优雅,不过看看先驱们怎么做的也是一种致敬。

二是从安全的锅里盛粥,放心吃

国外的月亮圆,国外的粥也更好吃。DNS投毒是我们大宋特有的独门暗器,国外的DNS才是干净无毒的。只要能从国外的锅里盛,就能放心吃。怎么盛粥学问太多了,但大致可以分为两大类:一是用Shadowsocks进行udp DNS查询,由于Shadowsocks对DNS请求进行了封装,墙无法知道查询内容所以数据包被放行;二是用TCP进行DNS查询,目前墙还未对TCP查询进行干扰。
每个类别都有若干种方法,但是只要弄清楚原理,哪里方法都能用得得心应手,以下是几个方法的验证:

> 用ss-redir转发UDP DNS查询

按照SS的 WiKi ,转发UDP该用ss-tunnel来做,但是通过/etc/bin/ss-redir -h查看ss-redir的帮助,可以看到一个-u选项来让SS转发udp数据,姑且先用ss-redir试试。
让ss-redir支持udp转发:
/usr/bin/ss-redir -c /etc/ching.json -b 0.0.0.0 -u -v
把/etc/dnsmasq.conf里的server=/echowhale.com/127.0.0.1#53改成server=/echwhale.com/127.0.0.1#1080,意思是告诉dnsmasq让Evil-Domain用本机的1080端口查询
重启dnsmasq, 当我们用nslookup echowhale.com发起DNS查询时,ss-redir提示收到了udp数据:

>2015-12-24 04:21:55 INFO: [udp] server receive a packet  
>2015-12-24 04:21:55 INFO: [udp] cache miss: 127.0.0.1:1080 <-> 127.0.0.1:17706  
>2015-12-24 04:56:28 INFO: [udp] server receive a packet  
>2015-12-24 04:56:28 INFO: [udp] cache miss: 127.0.0.1:1080 <-> 127.0.0.1:14898  

服务器的ss-server也收到了UDP数据:  
>2015-12-23 23:56:15 INFO: [udp] server receive a packet  
>2015-12-23 23:56:15 INFO: [udp] cache miss: 127.0.0.1:1080 <-> 60.223.238.62:15850  
>2015-12-23 23:56:20 INFO: [udp] server receive a packet  
>2015-12-23 23:56:20 INFO: [udp] cache miss: 127.0.0.1:1080 <-> 60.223.238.62:5488  


从ss-server的输出可以看到:VPS的1080端口在尝试响应DNS查询,并没有像预料的那样把DNS查询包继续往外发送。说明直接配置dnsmasq的方法好像不太对。捋一捋:
当我们在路由器上发起DNS查询echowhale.com的时候,由于dnsmasq定义的DNS服务器是127.0.0.1#1080,所以upd包的目标地址是127.0.0.1#1080,接着这个包到了1080端口被ss-redir封装发到ss-server,ss-server得到的udp包的目标地址仍然是127.0.0.1#1080。这就解释了为啥VPS的1080在和Router通信。VPS的1080端口并没有DNS查询的服务,鸡同鸭讲,再怎么通信Router也无法从VPS那里得到echowhale.com的IP地址,最后就超时结束了通信。
捋清楚了还得解决问题,基于这个状况,应该有3个解决方法吧:

  • 在VPS的1080端口绑定一个DNS服务。

    yum install dnsmasq #安装
    echo “port=1080” >> /etc/dnsmasq #配置监听的端口
    service dnsmasq start #启动dnsmasq
    dig @127.0.0.1 -p 1080 google.com #验证dnsmasq已正确工作
    最后从Router发起DNS查询Evil-Domain,发现已经能得到正确结果了。

  • 在VPS把127.0.0.1#1080重定向到8.8.8.8#53

    CentOS7 默认的防火墙改成了FirewallD,不再是我刚学的iptables了。就不做这个验证了,应该是可以实现的。如果想试试,把防火墙换回iptables的话可以参考这篇 教程

  • 在Router上用8.8.8.8#53查询Evil-Domain,然后用iptables把发往8.8.8.8的包重定向到Router:1080。

    先配置dnsmasq用4个8查询Evil-Domain: 把127.0.0.1#1080改为8.8.8.8#53
    然后再用iptables把发往8.8.8.8的数据包转发给ss-redir:
    iptables -t nat -A OUTPUT -p udp --dport 53 -d 8.8.8.8 -j REDIRECT --to-port 1080
    然而通过观察,虽然ss-server收到了udp数据,但是udp的目标地址仍然是127.0.0.1#1080。这个方法并不奏效。
    添加一条规则,试试如果使用TCP查询DNS,ss-server能不能进行正常的DNS解析:
    iptables -t nat -A OUTPUT -p tcp --dport 53 -d 8.8.8.8 -j REDIRECT --to-port 1080
    再次通过nslookup发起查询,结果观察到ss-server收到数据后成功向8.8.8.8发送请求,并将响应发回了ss-redir。观察到的ss-server的屏显如下:

    2015-12-24 02:21:54 INFO: accept a connection
    2015-12-24 02:21:54 INFO: connect to: 8.8.8.8:53

    为什么ss-server没能按照预期将udp的查询正确解包呢?或许是ss-redir封包的时候就没能把真实的目标地址包进去吧?这个要弄清楚感觉要好好看网络协议了,暂且挖个坑吧,有人知道也请留言告诉我。

> 用ss-tunnel转发UDP DNS查询

ss-tunnel可以转发udp,而且几乎不需要做什么配置,要方便多了。但是ss-redir对了解原理更有帮助,所以先说了更麻烦的ss-redir。运行ss-tunnel
/usr/bin/ss-tunnel -c /etc/ching.json -b 0.0.0.0 -l 5353 -L 8.8.8.8:53 -u -v
找到dnsmasq.conf里的127.0.0.1#1080,把端口号改成5353

这个方法没什么好说的,懒得折腾其它方法的话用ss-tunnel就好了。
我在长城宽带这个渣网络下,udp丢包异常地高,浏览器经常卡在"Resoving host..."的状态,得不到解析结果网页都打不开。
dns-fail 这种情况下要做一点优化,可以看文章结尾处我的设置。

> iptables + tproxy转发UDP DNS查询

这个方法我没看明白,所以就没打算试了。不懂原理出了问题简直就像傻逼似的, 根本就没法解决。 Shadowsocks-libev官网

> 用unbound做TCP DNS查询

根据网上一些博客的记录,unbound的延时好像很长。

> 用pdnsd做TCP DNS查询

优点是可以设置超长的DNS TTL。 pdnsd官网
教程
教程

目前我暂时使用的方案是让dnsmasq把Evil-Domain发到ss-redir,让ss转发UDP DNS查询,暂时没有引用ss-tunnel、unbound或者pdnsd处理DNS投毒。引进越多的程序,出了问题Debug就越麻烦,最简单的就是最本质的。

如果想优化以下网络访问速度,还要了解TTL。默认情况下,dnsmasq的ttl是继承自上游DNS服务器的。
dig300
通过dig可以看到google.com的ttl是300,也就是5分钟之内如果重复查询google.com的IP地址,dnsmasq不会向上游服务器递交查询,而是直接从缓存拿老的值给你。 最新的dnsmasq v2.73版本添加了一个--min-cache-ttl选项, 可以自定义ttl的值,根据作者的说明,最长也仅能设置为一个小时。有的网站几十年不换IP,而有的网站IP的调整很频繁,TTL设置多长也是仁者见仁智者见智的问题。如果你不想花时间在域名查询上,出了问题宁愿重启dnsmasq,那么TTL可以设置长一点。我暂时先设置为最长的一个小时(min-cache-ttl=3600).
dig3600 进一步优化的话,以下这些选项值得写进dnsmasq.conf:

  • cache-size=65536 #设置缓存大小

  • log-queries #记录DNS查询事件,在openwrt上可以通过logread查看日志,便于观察工作状态

  • neg-ttl=3600 #如果上流DNS服务器没有返回TTL,则用这个代替

  • log-async #如果写日志时被阻塞,开启这个选项后可以保证dnsmasq的其它功能正常工作

  • server=114.114.114.114 #添加114.114.114.114作为备选上流DNS服务器

  • server=119.29.29.29 #添加DNSpod作为备选服务器

  • all-servers #向所有上流服务器发送查询,先返回的结果被采用。

要是还是觉得DNS查询慢,就换pdnsd替代dnsmasq,那玩意据说ttl设为一周都行。要不折腾着自己编译dnsmasq,把一个小时的ttl限制给去掉。
min-cache-ttl是在2.73版本加入的选项,去作者官网可以浏览代码仓库,在2.73的仓库搜索ttl,找到添加min-cache-ttl的那次提交,就可以看到提交者修改了哪些代码了,3600限制应该是在src/config.h里:

define TTL_FLOOR_LIMIT 3600 /* don't allow --min-cache-ttl to raise TTL above this under any circumstances */

我做了个补丁,可以到github参考编译过程或者直接下载编译好的ipk文件。最后我把ttl改成了一天:
dig1day