fqrouter不认为有任何一个人可以一直与GFW对抗下去。但是想法却是永生的。fqrouter如果能传递一个想法,相信会和西厢计划鼓励了我写fqrouter一样,薪火相传下去。所以写一个能用的工具是不够的,工具总会有失去维护的那一天。把工具的构成原理,以及为什么设计成这样写下来,也是相同重要的事情。
那么先来了解一下fqrouter的整体构成吧。
整体构成
从最大的方面来说,fqrouter由fq和router两部分组成。第一部分解决可访问性问题,第二部分解决翻墙网络的共享问题。
可访问性部分又由DNS反劫持,代理,穿墙三部分组成。DNS反劫持是针对域名解析不正常问题的。穿墙是针对关键字检查导致TCP连接重置问题的。代理是用来解决IP被封锁问题的。可访问性可以理解为把fqrouter接入到了一个虚拟的自由网络里。
共享由无线中继与Pick & Play两部分组成。无线中继操纵无线芯片的驱动,启动无线网络共享翻墙网络。Pick & Play利用ARP协议,扫描局域网里的设备,并选择性劫持ta们的网络进出流量以经过fqrouter。
列表如下(点击链接进入系列子文章):
可访问性在有ROOT权限下,都是用iptables实现的接入。对于没有ROOT权限的手机,接入使用的是Android的VpnService API。无论用的是iptables还是VPN模式,真正的翻墙穿墙逻辑实现都是同一套的。
可访问性部分中DNS与穿墙都是不需要代理服务器的,实现方式也相对成熟了。但是可惜的是,仅仅依赖DNS与穿墙这两个模块是无法访问所有的网站的。所有IP被封了的网站都必须依赖代理才能访问。而且穿墙还需要ROOT权限,以及NFQUEUE的支持,有不少手机是用不了的。所以fqrouter目前的各种不稳定问题,都是由代理模块工作不正常引起的。代理模块目前支持goagent与http-connect两种代理,分别用来解决HTTP服务器的IP被封问题,以及HTTPS服务器的IP被封问题。所有的服务器均由网络搜集而来。无论是代理的数量,质量,以及冗余程度都不能达到稳定使用的要求。让翻墙网络稳定可靠,就必须扩展代理模块,支持更多的代理协议,并让用户配置自己私有的代理服务器。
fqrouter一直没有上架Play Store,也没有正式发布,都是因为代理这个部分还需要很多工作要做。fqrouter与其他工具的区别在于希望接入方式的多元化。这么设计的主要原因是:
- 一站式解决方案:无论什么代理服务器,都可以利用fqrouter做翻墙网络共享
- 尽可能不依赖代理服务器:DNS与穿墙都不依赖代理服务器
- 尽可能使用免费的资源:免费意味着不稳定,多元化与冗余可以提高整体的稳定性
- P2P:将来fqrouter的手机在无线网络联网的条件下要加入一个P2P网络。代理可能不仅仅是国外的服务器了,还可能是国内的运行了fqrouter的手机。
可以总结为,如果只是为了稳定翻墙,解决个人与team的上网需求,搞一个私有的服务器,跑一个代理服务器客户端就可以了。fqrouter的实现方式绝对overkill了。哪怕可以优化一下访问速度的问题,但是不值得这么大费周章。如果是为了给GFW找点小麻烦,DNS与穿墙模块也就够了,要是校长不管,我们就接着用好了。如果为了给GFW找大麻烦,那必须得P2P。fqrouter是朝着P2P去的,虽然还没有到那个地步。
另外一种说法是fqrouter是一个大杂烩。fine,它确实是一个大杂烩。如果我的目标是卖代理服务器,fqrouter肯定就只支持我自己发明的协议,会简单得多,然后还可以赚点钱。你们需要一个fqrouter,还是yet another vpn 奸商呢?
共享现在有很多种方式。手机所有内置的网络共享方式,都可以使用。只要把网络共享出去了,启动fqrouter,共享出去的网络也自动翻墙了。所以不是一定要用无线中继的,在3G网络下用系统自带的无线共享功能也是一样可以的。利用系统自带的功能,可以实现:
- 3G上行,无线下行
- 3G上行,蓝牙下行
- 3G上行,USB下行
- 无线上行,蓝牙下行
- 无线上行,USB下行
像tpmini大眼睛这样的Android机顶盒,还可以实现:
无线中继是一个非常有益的补充。其设计的使用场景是带着手机去办公室,手机连上了无线网,而且插着充电线。这个时候要打开笔记本开始办公了。然后手机运行fqrouter用无线中继功能共享出一个翻墙的无线网给笔记本使用。这就实现了
但是受限于3G的流量费,USB/蓝牙的使用不方便,以及无线中继的硬件支持范围有限。Pick & Play就变得必要了。Pick & Play支持了
- 无线上行,无线下行(无需硬件支持)
- 无线上行,有线下行(无需硬件支持)
Pick & Play并不创建新的网络,而是附着在已有的局域网上,局域网可以是纯无线的,也可以是无线有线结合的组网方式。其好处是不需要硬件支持,坏处是使用不如无线中继来着自然。
目前共享部分已经相对成熟,需要改进的地方不是特别多了。
[groupid=39]Demo俱乐部[/groupid]
本帖最后由 Test 于 2015-10-24 19:32 编辑
fqrouter为了解决可访问性问题,接入到自由的网络要解决的第一个问题就是DNS。DNS不仅仅是查询一个域名能够拿到IP那么简单,光DNS就可以分解为如下的子问题:
- DNS的配置
- 域名解析的正确性
- 联通电信的线路优化
- Host文件的管理
DNS的配置
如果我们以前使用的是socks5(ssh)代理,根本不会有DNS劫持的问题,因为域名是由远端的sock5代理服务器解析的。如果我们以前使用的是VPN代理,而且DNS服务器是国外的,那么DNS解析也不会是问题,VPN代理会保护我们的DNS解析操作。DNS问题只有在翻墙与访问的设备不在一台机器上时才会是问题。如果是socks5代理,要求用户配置浏览器,让浏览器利用socks5代理服务器解析域名。如果是VPN代理,要求用户配置DNS服务器的IP到国外的服务器。fqrouter做为一个路由器设备,显然不希望用户重新手工配置每一个接入了fqrouter网络的设备。所以,这里解决的主要是一个是否需要手工配置的问题。
fqrouter对于配置问题的解决办法是暴力的。对于手机自身的网络流量,使用一条iptables规则解决问题:
iptables -t nat -I OUTPUT -p udp ! -s 10.1.2.3 --dport 53 -j DNAT --to-destination 10.1.2.3:5353对于加入fqrouter网络的设备产生的DNS查询,使用另外一条iptables规则:
iptables -t nat -I PREROUTING -p udp ! -s 10.1.2.3 --dport 53 -j DNAT --to-destination 10.1.2.3:5353这两台规则劫持了所有目标端口号是DNS端口号(53)的UDP连接。虽然浏览器仍然认为它连接的是系统配置的DNS服务器(比如8.8.8.8),但其实真正连接到的是10.1.2.3:5353这个端口的DNS服务器。
这种iptables的暴力做法显而易见的好处是完全不需要在设备或者浏览器上做任何配置。只要DNS查询经过了fqrouter所在的机器,就会被劫持到fqrouter提供的DNS服务器上。
上面的iptables规则中有一个诡异的地方时排除了所有来源地址是10.1.2.3的连接。这是因为把流量倒给了fqdns之后,fqdns自己还是需要去查询上级DNS的。而这些查询为了避免被这两条iptables规则再次劫持,就必须绑定到10.1.2.3这个源IP做查询,然后在iptables规则里排除掉这些查询。否则就死循环了。这种排除法除了可以用排除源IP的方法,还可以使用–mark或者–owner利用mark或者进程的用户/组的ID做排除。不过在Android手机是不能保证所有的手机都支持–mark和–owner,只有比较新的手机才支持。
域名解析的正确性
直接使用国外的DNS服务器,是不能解析出很多了域名的正确结果的。原因是GFW抢答了错误答案。解决办法是记录一个GFW抢答的错误答案的黑名单,如果发现答案是黑名单中的,则丢弃等待DNS服务器回答的正确答案。有的时候,GFW会对DNS查询丢包,这样无论等待多久也不会收到DNS服务器回答的正确答案了。这个时候需要切换到TCP协议,查询DNS服务器。虽然GFW可以对DNS over TCP做关键字检查,但是目前还没有大规模使用。所以在目前的形势下,用黑名单结合DNS over TCP就可以比较完美的解决域名解析的正确性问题。
这部分的代码实现是由fqdns子项目提供的。前面通过iptables重定向到的DNS服务器,就是fqdns。fqdns自身是一个DNS服务器,所有的DNS查询先由fqdns接收,然后再由fqdns查询上一级的DNS服务器。
使用–strategy pick-right参数启动fqdns就实现了GFW的错误答案黑名单。
使用–fallback-timeout 3参数启动fqdns就实现了超时之后切换到DNS over TCP。
联通电信的线路优化
因为国内有很多网站都是有联通和电信两种线路的。当用户以联通的IP去做域名解析的时候,得到的是联通线路的服务器地址。当用户以电信的IP去做域名解析的时候,得到的就是电信线路的服务器地址。如果联通的用户拿到的是电信的服务器地址,访问速度就会变得慢很多。如果我们用8.8.8.8做域名解析,8.8.8.8再去向权威服务器查询域名,那么8.8.8.8就是权威DNS服务器认为的客户端。如果它认为8.8.8.8是电信线路更优的话,即便是联通的用户,也会被分配到电信的服务器地址。
这个问题没有一个特别好的办法。我们不能使用联通、电信分配给你的DNS服务器。因为用这些国内的DNS服务器,是无论如何无法保证域名解析的正确性的。我们也不能总是依赖8.8.8.8这些国外的服务器,因为它不能保证线路的最优。fqdns的实现办法是内置一个国内比较大的网站的域名列表。对于这些域名的解析,使用114dns来做DNS查询。至于是否选择了告诉的线路,这就要靠114dns来保证了。
使用–enable-china-domain –china-upstream 114.114.114.114参数启动fqdns实现了对国内域名解析的线路优化。
Host文件的管理
Host文件可以对某些域名指定IP。这种方式对于google这样的网站来说特别重要。一方面google的ip众多,只要指定的IP没有被GFW关照,就可以获得非常好的使用体验。另外一方面,Google自身封了很多的代理服务器,因为Google不欢迎别人通过代理服务器来爬它的查询结果。而且Google的很多HTTPS服务处于半残废的状态,GFW会以时间为周期完全丢包,造成其服务不稳定的状况。这种丢包是针对特定IP的,只要选择的Google IP没有被关照,就不会出现这样的不稳定状况。
但是Host文件的缺点是需要手工更新。fqrouter显然不希望每次要更新Google的IP都让用户升级一个新的版本。解决办法就是对于指定的域名,比如说google.com,改成google.com.fqrouter.com来解析。这样fqrouter就可以控制google.com解析的结果了,相当于在云端保存了一份公共的Host文件。
使用–enable-hosted-domain –hosted-at fqrouter.com启动fqdns就可以达到关照特殊域名的目的。哪些域名被特殊处理是内置的,基本上都是google的域名。
fqdns-master.zip
解决可访问性问题除了DNS是远远不够的。为了避免关键字触发连接重置,以及能够访问IP被封的网站,还需要代理服务器。fqrouter使用子项目fqsocks处理代理连接。其解决了如下的问题:
- 代理的配置
- 走什么样的代理
- 直连优化
- 代理服务器失效检测
- 多代理并用
- 代理服务器的地址获取
代理的配置
传统的代理使用方式是设置浏览器,要么是socks要么是http代理。如果是浏览器不支持的代理协议,比如shadowsocks或者ssh,就需要在本地启动一个客户端,然后把私有的代理协议转换成浏览器支持的socks/http代理协议。所以相比之下,VPN就要方便很多,一旦启动浏览器不用设置也走代理了。
fqrouter做为路由器显然不希望接入了fqrouter网络的设备还要一个浏览器一个浏览器的去设置。fqsocks和fqdns一样,使用iptables来劫持所有的TCP连接。为什么只有TCP,而不管UDP?因为GFW也不管UDP。
对于手机自身收发的网络流量,使用OUTPUT:
iptables -t nat -I OUTPUT -p tcp ! -s 10.1.2.3 -j DNAT --to-destination 10.1.2.3:8319对于手机做为路由器的过境流量,使用PREROUTING:
iptables -t nat -I PREROUTING -p tcp ! -s 10.1.2.3 -j DNAT --to-destination 10.1.2.3:8319与fqdns同样的道理,排除了来源ip是10.1.2.3的流量,避免无限循环。10.1.2.3:8319接到了TCP请求之后,实际的情况是浏览器认为是直接与目标服务器建立了连接。而8319知道自己不是目标服务器,拿着了别人的连接就要给别人干活的。所以左右拿着与浏览器的TCP连接,右手再与目标服务器建立TCP连接,再在两个TCP连接之间做数据对拷。
browser <=TCP=> 10.1.2.3:8319 (fqsocks) <=TCP=> http server10.1.2.3:8319会特意以10.1.2.3为源IP去与目标服务器建立连接从而避免死循环。这里比fqdns要麻烦的地方在于,对于fqdns来说,上级的DNS服务器可以是写死的,比如8.8.8.8。也就是无论浏览器是用什么DNS服务器做查询,我不管,我始终使用我认为正确的DNS服务器。但是对于fqsocks来说,就必须在与浏览器建立TCP连接的通知,得知浏览器本来的目标服务器是什么。这里是依赖于Linux/iptables的socket API来得知的:
def _get_original_destination(sock, src_ip, src_port): dst = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) dst_port, dst_ip = struct.unpack("!2xH4s8x", dst) dst_ip = socket.inet_ntoa(dst_ip) return dst_ip, dst_port这种用iptables做TCP连接劫持,然后用socket api查询真正目标地址,从而实现透明代理的做法是从redsocks抄袭来的。redsocks除了支持iptables,还支持好几种其他的redirector,其获取目标地址的代码在这里:https://github.com/darkk/redsocks/blob/master/base.c
是否走代理
fqsocks左手握着与浏览器的TCP连接,它可以决定右手握着一个什么样的连接。而且它还可以去做不同的尝试,选择一个最好的上级代理。有三种办法:
最简单的一个选择办法是基于IP地址。比如使用VPN代理的,应该知道chnroutes或者bestreoutetb。这些基于路由表的解决方案是根据目标IP地址选择是否走VPN。fqsocks虽然不是VPN,但是它也可以知道目标IP地址,做一个类似的选择并不困难。fqsocks目前对于来源是局域网,目标也是局域网的TCP连接采取直连的措施。对于所有目标地址是中国国内IP的TCP连接采取直连的措施。这里所谓的直连并不是真正的直连,因为一旦连接落到了fqsocks手上,必然要经过fqsocks代理的。所谓的直连只是fqsocks不经过代理服务器连接目标服务器,但是浏览器仍然是通过fqsocks连接目标服务器的。
第二个选择办法是基于域名。对于socks代理或者http代理,因为是配置浏览器的,浏览器会直接用代理协议告诉代理服务器它要访问的域名。利用switchysharp之类的浏览器插件,或者一些本地的HTTP代理服务器之类的工具是可以做到基于域名选择代理线路的。但是fqsocks要苦难一些。fqsocks是工作TCP层面的代理服务器,其自身并不限制客户端的连接必须是某种协议,比如都是http的。这就要求fqsocks能够在TCP连接的基础上智能检测TCP的字节流里到底跑的是什么协议,然后把域名从协议里提取出来。fqsocks目前可以嗅探两种协议,第一个中明文的http,第二种是https sni里的域名。根据域名fqsocks可以选择最佳的代理服务器。fqsocks目前内置的策略是,如果域名是twitter的域名,总是走代理,无需先测试是否可以连接。因为twitter的ip是被秒封的。
第三个选择方法是试探。这个比前两种更复杂。其工作过程如下,fqsocks探测到TCP里跑的协议是明文的HTTP,所以可能会触发GFW的URL关键字(请求),和全文关键字(响应)。为了知道是否真的会触发。fqsocks用HTTP协议读取出浏览器发送的完整的HTTP请求。然后fqsocks先不急着响应浏览器,它先拿着这个HTTP请求尝试直接与目标服务器交流,等目标服务器给了响应,而且完整无缺的到了fqrouter手上之后再把响应交回给浏览器。如果HTTP请求触发了URL关键字,或者HTTP响应触发了全文关键字,fqsocks就知道直连是没戏了。反正浏览器也不知道。这个时候切换到代理的线路,再重复一遍以上过程就是了。
直连优化
fqsocks自身是用python实现的。用python代理在两个TCP连接之间进行对拷,肯定要比一个直连的TCP连接要慢。这个主要体现在比较高的CPU占用上。虽然fqsocks使用的是python的异步I/O(gevent),但python毕竟是python。
所以为了能够开着fqrouter挂迅雷下载,我们希望对于可以直连的tcp连接,完全不经过fqsocks。
迅雷 <=TCP=> fqsocks <=TCP=> 国外IP迅雷 <=TCP=> 国内IP要达到这样的目的,就需要在fqsocks的iptables规则之前加上另外一条规则,如果目标地址是国内IP,直接连接。但是问题是,国内的IP不是一个两个啊,用iptables写的话得好多条呢。fqrouter的做法是用nfqueue实现了一个ipset的功能,参见:http://fqrouter.tumblr.com/post/51351094394/android-ipset
代理服务器的失效检测
因为fqsocks是自己用代理的协议连接代理服务器,然后通过代理服务器连接目标服务器的。所以代理服务器是否失效了,通过代理服务器的对与代理协议的不同响应就可以知道。比如说对与goagent
浏览器 <=HTTP=> fqsocks <=GoAgent Protocol=> GoAgent Server <=URL Fetch=> 目标服务器如果fqsocks,连接GoAgent Server得到的响应是503(Over Quota)我们就知道免费的额度今天已经用完了,这个代理就不用再试了。或者
浏览器 <=HTTPS=> fqsocks <=HTTP CONNECT=> Http-Connect代理服务器 <=TCP=> 目标服务器fqsocks在发送了CONNECT命令给代理服务器如果得到的结果是403(Forbidden)的话,就知道这个公开代理已经被加上密码了,不能再用了。
与之前基于探测决定是否走代理同样的原理,即便是发现使用的代理服务器失效了,并不代表浏览器的这个TCP连接就挂了,每个TCP连接有三次选择代理服务器的机会。只要试成功了一次,就可以保住这个TCP连接的正常工作。如果不这么做的话,发先GoAgent超出额度了,出现一个错误页面,然后用户还要再重刷一下。
多代理并用
多代理并用可以有两种策略。一种是对于一个TCP连接,尝试不同的代理,选择最快的那个。另外一种是对于一个TCP连接,只用一个代理。但是不同的TCP连接使用不同的代理。fqsocks的实现方式是每次TCP连接进来,随机从代理池里选择一个使用,如果代理失效了,再选择另外一个。因为一个WEB页面上,除了提供HTML的第一个连接之外,接下来还会开很多加载JS/CSS/图片的TCP连接的。所以实际的效果来说就是,打开一个复杂的页面,可以有好几个代理在同时工作。如果一个代理繁忙,也不会导致整个页面完全打不开。
代理服务器地址的获取
最简单的做法是把代理服务器的地址写在代码里,每次换地址就升级版本。这种做法显然是笨拙的。
第二种做法是搞一个WEB页面,或者HTTP的REST API。问题是在域名被污染,URL关键字被TCP RST的情况下,这样的更新策略是很脆弱的。
fqsocks的做法是利用DNS的TXT记录来分发代理服务器的地址。可以用dig proxy1.fqrouter.com TXT来看效果。
fqsocks-master.zip
穿墙: fqting-master.zip socks代理转VPN
socks代理是指工作在TCP层面的代理,比如socks4/5或者http代理这些。VPN是指工作在IP层面的代理,比如OpenVPN这些。两者之间的区别是,socks代理是转发二进制字节流,而VPN代理是转发IP包。用黑话来说VPN是L3的,socks代理是L4的。那么出于什么动机会有人希望把socks代理转VPN呢?如果只是为了全局走代理的话,用iptables之类的工具就可以实现全局流量都走L4的代理。其目的是为了支持在Android的手机上不用ROOT就可以使用代理。从Android 4.0开始,支持了一个新的API叫VpnService,使用这个API可以让应用程序启动自己的VPN而不需要手机本身获得ROOT权限(添加iptables规则需要ROOT权限)。但是VpnService的实现是基于tun设备的,也就是在ip包层面的,所以这要求代理能够转发ip包而不是字节流。这也就是为什么OpenVPN可以在Android上实现非ROOT支持,而ssh和socks4/5这些代理却必须要求手机必须先ROOT。
问题是什么?
要全面解析这个tricky的实现要解决的问题在哪,我们先来看最简单形式的scoks代理
浏览器(配置了socks代理)==TCP连接(代理协议)==>socks代理服务器==TCP连接(http)==>目标服务器这是最简单的一种联网方式。浏览器配置过之后知道它不能直接与目标服务器建立TCP连接,所以它就用代理的协议(socks4/5或者http代理协议)与代理服务器建立连接,要求代理服务器帮其连接上目标服务器。这个时候浏览器实际上是不与目标服务器建立直接的TCP连接的。代理服务器自身是在两个连接之间互相拷贝数据。
这种联网方式的缺陷是浏览器需要经过特殊配置。有没有办法不去配置每个具体的应用,在系统的层面设置一个socks代理,然后所有的应用程序就自动应用上了呢?办法是有的,它有一个名字叫做透明代理(transparent socks redirector)。它有另外一个几乎等价的名字叫redsocks。
使用了redsocks之后,联网方式就变得更加复杂了
浏览器(不配置socks代理)==iptables/TCP连接(http)==>redsocks==TCP连接(代理协议)==>socks代理服务器==TCP连接(http)==>目标服务器redsocks在其中干了一件什么龌龊的事情呢?它其实是把自己假装成了目标服务器。浏览器根本不知道它连接上的是代理,它还认为自己是直连目标服务器的呢。因为这个TCP连接的劫持过程是有iptables REDIRECT完成的,所以redsocks可以从系统查询到这个TCP连接其实真正的目标服务器地址是什么,然后它再去用配置好的代理服务器,通过代理连接真正的目标服务器,再把数据拷贝回浏览器与redsocks建立的连接。所以redsocks自身也是一个代理,实现方式也是在两个TCP连接之间对拷数据。
那么VPN是怎么工作的呢?
浏览器==IP包==>VPN客户端建立的虚拟网卡==VPN协议==>VPN服务器端建立的虚拟网卡==IP包转发==>VPN服务器的真正网卡==>目标服务器整个VPN都是工作在IP层面的,所以它不再是字节流的拷贝,而是IP包的转发。那么IP包与字节流之间到区别在哪里?区别就是少了一层TCP协议。按照TCP协议,可以把一组IP包还原成一段字节流。这个还原过程是在Linux内核的TCP/IP实现里完成的。所以这就给我们造成了一个如下图的困境
浏览器==IP包==>Android VpnService建立的虚拟网卡==IP包==>我们的代理程序==TCP连接(socks代理协议)==>socks代理服务器==TCP连接(http)==>目标服务器“我们的代理程序”的左手边从Android VpnService拿到的是IP包,而右手边与socks代理服务器建立的是TCP连接,也就是字节流。这就要求代理程序要做一个IP包的TCP协议重组,从而构建出一个字节流来。但是这怎么能够办到呢?
ShadowSocks Android的解决方法
@ofmax写的ShadowSocks的Android版是我所知的第一个实现Android上socks代理不用ROOT的。其实现方式是这样的。
第一个要解决的问题是怎么样从Android VpnService拿到IP包?VpnService自身的实现是创建一个Linux的tun设备。该设备对外提供的API就是一个file descriptor,也就是一个整数,代表操作系统的一个文件句柄。用file descriptor可以进行文件读,每次读出来的内容就是接收一个ip包,也可以进行文件写,每次写入的内容就是发送一个ip包。问题是该file descriptor是VpnService给java进程的。如果java进程启动了一个子进程,那么这个子进程还能用java进程拿到的这个file descriptor进行IP包的发送和接收吗?
简单来说是不能。为什么不能比较复杂。普通的linux父子进程情况下,子进程是可以访问自己创建之前由父进程打开的file descriptor的。但是java的Runtime.exec的实现里禁止了这样的行为。结果就是如果使用java自己的exec api启动子进程的话,子进程是无法读取父进程的file descriptor的。Shadowsocks的实现方式是不使用java自带的函数启动子进程,使用了jni自己编译了一个新的api来绕过java的限制:https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/system.cpp
解决了IP包的收发问题之后。更大的问题是如何把IP包转字节流。其实现方式是使用了tun2socks。tun2socks是一个进程,这个进程可以做到左手连接一个tun设备,右手连接一个sock5代理,然后在两者之间做数据对拷。所以使用了tun2socks之后,shadowsocks自身的客户端提供的就是sock5的代理接口,所以这样就接上了:
浏览器==IP包==>Android VpnService建立的虚拟网卡==IP包==>tun2socks==sock5协议==>ShadowSocks客户端==TCP连接(ShadowSocks协议)==>socks代理服务器==TCP连接(http)==>目标服务器那么tun2socks自己又是怎么实现这个IP包转字节流的过程的呢?秘诀在于tun2socks内置了一个lwip实现的TCP/IP栈,相当于重复了Linux的TCP/IP实现,只不过是在用户态做的而不是在内核做的。但是使用tun2socks也带来了新的问题。
问题之一是tun2socks不支持直接传递tun的file descriptor做参数。为此@ofmax的做法是patch了tun2socks,让它可以直接接受tun-fd做为命令行输入。
问题之二是tun2socks只可以把TCP的流量转给sock5代理,对于UDP的流量需要一个remote udp gateway。@ofmax为此在一个国外的服务器上建立了一个公开的remote udp gateway(u.maxcdn.info),所有的udp流量都会从这台服务器转发。
如果你认为这样做就万事大全了你就错了。还有一个问题是如果VpnService可以劫持所有的本地流量走tun设备,那么凭什么shadowsocks的客户端做为一个本地的应用程序不会再次被VpnService劫持呢?这就要看VpnService是怎么把本地流量倒给tun设备的呢。其实现方式是路由表。如果我们在创建这个tun设备的时候设置的路由是0.0.0.0/0就会导致,无论目标地址是何方,都会经过这个tun设备。所以如果路由表是这么设置的话,shadowsocks的客户端是无法工作的,会进入一个死循环。@ofmax的解决办法是把shadowsocks的服务器地址排除在需要走tun设备的路由表之外,这样就解决了死循环的问题。
所以说,让没有ROOT过的Android设备用上ShadowSocks,是非常不容易的。
fqrouter的解决方法
在@ofmax的实现方式的启发下,fqrouter也做了一个实现,同样是利用VpnService,同样是需要把socks代理转成VPN。所以需要解决的问题也是类似的。再重复一下需要解决的问题。
- file descriptor的父子进程问题
- IP包转字节流问题
- 防止死循环,代理自身的连接不走代理
首先来解决file descriptor的问题。虽然java启动的子进程没法访问父进程的file descriptor,但是linux有通用的进程间共享file descriptor的机制。黑话就是SCM_RIGHTS。其原理是两个进程之间建立一个unxi domain socket,然后用sendmsg这个linux的api把file descriptor从一个进程发给另外一个进程,设置SCM_RIGHTS这个特殊的标记。在这个一收一发之间,内核就悄无声息地帮我完成了进程之间共享file descriptor的Black Magic:参见http://www.lst.de/~okir/blackhats/node121.html
但是问题是无论是python2.7还是java都不直接提供公开的api让你使用这样的black magic。好消息是Android的SDK让你这么用!Android提供了LocalSocket的api,其中有两个函数
- public void setFileDescriptorsForSend (FileDescriptor[] fds)
- public FileDescriptor[] getAncillaryFileDescriptors ()
这两个函数内部就是封装了sendmsg/SCM_RIGHTS这样的magic。对应的python2.7有一个_multiprocessing的内部模块,提供了
- def recvfd(sockfd)
- def sendfd(sockfd, fd)
利用这几个api就可以解决file descriptor在进程之间共享到问题。也就避免了用JNI实现一个system.exec,可以使用普通的java api启动子进程。接下来要解决IP包转字节流的问题。因为tun2socks需要额外运维一个udp gateway,实际上带来了一个共享的中央服务器,给可用性带来了隐患。我希望能够使用fqdns的实现,不依赖代理服务器直接穿过GFW对DNS的劫持。而且tun2socks内置了lwip也是一个很重的解决方案。TCP/IP栈博大精深,咱最好还是依赖内核的TCP/IP栈吧。所以fqrouter选择了不使用tun2socks。为了说明解决方案,让我们假设tun设备的IP是10.25.1.1/24,现在要访问的目标服务器是8.8.8.8:53。发给tun设备的IP包就会是
10.25.1.1:src_port => 8.8.8.8:53
这个src_port是随机的,由内核分配的。fqrouter从tun设备里读出这个IP包之后经过改写,再重新写入tun设备,也就是重发了一个IP包
10.25.1.100:src_port => 10.25.1.1:12345
这个IP包除了src_ip,src_port,dst_ip,dst_port以及一些checksum不一样之外,与之前收到的IP包完全相同。然后我们在10.25.1.1启动一个类似redsocks的透明代理,这样TCP连接就会直接与10.25.1.1:12345这个服务器建立。于是10.25.1.1:12345就会对我们虚构出来的10.25.1.100这个地址进行应答
10.25.1.1:12345 => 10.25.1.100:src_port
因为10.25.1.100是在tun的网段里,所以tun设备又会读出这个ip,于是再次进行改写:
8.8.8.8:53 => 10.25.1.1:src_port
这样从浏览器的角度来讲,它是认为与8.8.8.8:53建立了直接的TCP连接。其实它是通过tun设备与10.25.1.1:12345建立了TCP连接。这样的实现方式就不需要lwip实现一个用户态的TCP/IP协议栈,但是代价就是要对IP包就行两次NAT,而且需要维护一个NAT的对应表。对应关系是10.25.1.1的src_port做为key,value是原始的TCP连接的目标地址。这个NAT对应关系可以用来给透明代理去请求原始服务器的内容,同时可以做为返回IP包的改写依据。
第三个要解决的问题是如何避免死循环。这需要使用VpnService的protect函数。这个函数接收TCP的socket或者UDP的socket或者一个file descriptor。经过protect的socket收发的数据包就不再经过VPN的tun设备了。同样这里也有file descriptor的跨进程问题。如果在子进程里创建socket,然后把socket的fileno(file descriptor)传递给父进程是无效的。正确的做法是子进程向父进程请求创建socket,父进程创建好socket,运行protect,然后用unix domain socket传递给子进程。
至此socks代理就转成了VPN了。完整代码在github上: https://github.com/fqrouter/fqrouter/tree/master/manager。同时利用以上这个实现,fqrouter从1.14.0开始,翻墙部分可以正常工作在没有ROOT的Android手机上了,下载链接:http://fqrouter.tumblr.com/android-latest。
vpn.py
- import gevent.monkey
- gevent.monkey.patch_all(ssl=False, thread=False)
- import logging
- import logging.handlers
- import sys
- import os
- import _multiprocessing
- import socket
- import httplib
- import fqdns
- import fqsocks.fqsocks
- import fqsocks.config_file
- import fqsocks.gateways.proxy_client
- import fqsocks.networking
- import contextlib
- import gevent
- import gevent.socket
- import dpkt
- import config
- import traceback
- import urllib2
- import fqsocks.httpd
- FQROUTER_VERSION = 'UNKNOWN'
- LOGGER = logging.getLogger('fqrouter.%s' % __name__)
- LOG_DIR = '/data/data/fq.router2/log'
- MANAGER_LOG_FILE = os.path.join(LOG_DIR, 'manager.log')
- FQDNS_LOG_FILE = os.path.join(LOG_DIR, 'fqdns.log')
- nat_map = {} # sport => (dst, dport), src always be 10.25.1.1
- default_dns_server = config.get_default_dns_server()
- DNS_HANDLER = fqdns.DnsHandler(
- enable_china_domain=True, enable_hosted_domain=True,
- original_upstream=('udp', default_dns_server, 53) if default_dns_server else None)
- def handle_ping(environ, start_response):
- try:
- LOGGER.info('VPN PONG/%s' % FQROUTER_VERSION)
- except:
- traceback.print_exc()
- os._exit(1)
- start_response(httplib.OK, [('Content-Type', 'text/plain')])
- yield 'VPN PONG/%s' % FQROUTER_VERSION
- fqsocks.httpd.HANDLERS[('GET', 'ping')] = handle_ping
- def handle_exit(environ, start_response):
- gevent.spawn(exit_later)
- start_response(httplib.OK, [('Content-Type', 'text/plain')])
- return ['EXITING']
- fqsocks.httpd.HANDLERS[('POST', 'exit')] = handle_exit
- def redirect_tun_traffic(tun_fd):
- while True:
- try:
- redirect_ip_packet(tun_fd)
- except:
- LOGGER.exception('failed to handle ip packet')
- def redirect_ip_packet(tun_fd):
- gevent.socket.wait_read(tun_fd)
- try:
- ip_packet = dpkt.ip.IP(os.read(tun_fd, 8192))
- except OSError, e:
- LOGGER.error('read packet failed: %s' % e)
- gevent.sleep(3)
- return
- src = socket.inet_ntoa(ip_packet.src)
- dst = socket.inet_ntoa(ip_packet.dst)
- if hasattr(ip_packet, 'udp'):
- l4_packet = ip_packet.udp
- elif hasattr(ip_packet, 'tcp'):
- l4_packet = ip_packet.tcp
- else:
- return
- if src != '10.25.1.1':
- return
- if dst == '10.25.1.100':
- orig_dst_addr = nat_map.get(l4_packet.dport)
- if not orig_dst_addr:
- raise Exception('failed to get original destination')
- orig_dst, orig_dport = orig_dst_addr
- ip_packet.src = socket.inet_aton(orig_dst)
- ip_packet.dst = socket.inet_aton('10.25.1.1')
- ip_packet.sum = 0
- l4_packet.sport = orig_dport
- l4_packet.sum = 0
- else:
- nat_map[l4_packet.sport] = (dst, l4_packet.dport)
- ip_packet.src = socket.inet_aton('10.25.1.100')
- ip_packet.dst = socket.inet_aton('10.25.1.1')
- ip_packet.sum = 0
- l4_packet.dport = 12345
- l4_packet.sum = 0
- gevent.socket.wait_write(tun_fd)
- os.write(tun_fd, str(ip_packet))
- def get_original_destination(sock, src_ip, src_port):
- if src_ip != '10.25.1.100': # fake connection from 10.25.1.100
- raise Exception('unexpected src ip: %s' % src_ip)
- return nat_map.get(src_port)
- fqsocks.networking.SPI['get_original_destination'] = get_original_destination
- def create_tcp_socket(server_ip, server_port, connect_timeout):
- fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- with contextlib.closing(fdsock):
- fdsock.connect('\0fdsock2')
- fdsock.sendall('OPEN TCP,%s,%s,%s\n' % (server_ip, server_port, connect_timeout * 1000))
- gevent.socket.wait_read(fdsock.fileno())
- fd = _multiprocessing.recvfd(fdsock.fileno())
- if fd == 1:
- LOGGER.error('failed to create tcp socket: %s:%s' % (server_ip, server_port))
- raise socket.error('failed to create tcp socket: %s:%s' % (server_ip, server_port))
- sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
- os.close(fd)
- return sock
- fqsocks.networking.SPI['create_tcp_socket'] = create_tcp_socket
- def create_udp_socket():
- fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- with contextlib.closing(fdsock):
- fdsock.connect('\0fdsock2')
- fdsock.sendall('OPEN UDP\n')
- gevent.socket.wait_read(fdsock.fileno())
- fd = _multiprocessing.recvfd(fdsock.fileno())
- if fd == 1:
- LOGGER.error('failed to create udp socket')
- raise socket.error('failed to create udp socket')
- sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_DGRAM)
- os.close(fd)
- return sock
- fqdns.SPI['create_udp_socket'] = create_udp_socket
- fqdns.SPI['create_tcp_socket'] = create_tcp_socket
复制代码
vpn.py- import gevent.monkey
- gevent.monkey.patch_all(ssl=False, thread=False)
- import logging
- import logging.handlers
- import sys
- import os
- import _multiprocessing
- import socket
- import httplib
- import fqdns
- import fqsocks.fqsocks
- import fqsocks.config_file
- import fqsocks.gateways.proxy_client
- import fqsocks.networking
- import contextlib
- import gevent
- import gevent.socket
- import dpkt
- import config
- import traceback
- import urllib2
- import fqsocks.httpd
- FQROUTER_VERSION = 'UNKNOWN'
- LOGGER = logging.getLogger('fqrouter.%s' % __name__)
- LOG_DIR = '/data/data/fq.router2/log'
- MANAGER_LOG_FILE = os.path.join(LOG_DIR, 'manager.log')
- FQDNS_LOG_FILE = os.path.join(LOG_DIR, 'fqdns.log')
- nat_map = {} # sport => (dst, dport), src always be 10.25.1.1
- default_dns_server = config.get_default_dns_server()
- DNS_HANDLER = fqdns.DnsHandler(
- enable_china_domain=True, enable_hosted_domain=True,
- original_upstream=('udp', default_dns_server, 53) if default_dns_server else None)
- def handle_ping(environ, start_response):
- try:
- LOGGER.info('VPN PONG/%s' % FQROUTER_VERSION)
- except:
- traceback.print_exc()
- os._exit(1)
- start_response(httplib.OK, [('Content-Type', 'text/plain')])
- yield 'VPN PONG/%s' % FQROUTER_VERSION
- fqsocks.httpd.HANDLERS[('GET', 'ping')] = handle_ping
- def handle_exit(environ, start_response):
- gevent.spawn(exit_later)
- start_response(httplib.OK, [('Content-Type', 'text/plain')])
- return ['EXITING']
- fqsocks.httpd.HANDLERS[('POST', 'exit')] = handle_exit
- def redirect_tun_traffic(tun_fd):
- while True:
- try:
- redirect_ip_packet(tun_fd)
- except:
- LOGGER.exception('failed to handle ip packet')
- def redirect_ip_packet(tun_fd):
- gevent.socket.wait_read(tun_fd)
- try:
- ip_packet = dpkt.ip.IP(os.read(tun_fd, 8192))
- except OSError, e:
- LOGGER.error('read packet failed: %s' % e)
- gevent.sleep(3)
- return
- src = socket.inet_ntoa(ip_packet.src)
- dst = socket.inet_ntoa(ip_packet.dst)
- if hasattr(ip_packet, 'udp'):
- l4_packet = ip_packet.udp
- elif hasattr(ip_packet, 'tcp'):
- l4_packet = ip_packet.tcp
- else:
- return
- if src != '10.25.1.1':
- return
- if dst == '10.25.1.100':
- orig_dst_addr = nat_map.get(l4_packet.dport)
- if not orig_dst_addr:
- raise Exception('failed to get original destination')
- orig_dst, orig_dport = orig_dst_addr
- ip_packet.src = socket.inet_aton(orig_dst)
- ip_packet.dst = socket.inet_aton('10.25.1.1')
- ip_packet.sum = 0
- l4_packet.sport = orig_dport
- l4_packet.sum = 0
- else:
- nat_map[l4_packet.sport] = (dst, l4_packet.dport)
- ip_packet.src = socket.inet_aton('10.25.1.100')
- ip_packet.dst = socket.inet_aton('10.25.1.1')
- ip_packet.sum = 0
- l4_packet.dport = 12345
- l4_packet.sum = 0
- gevent.socket.wait_write(tun_fd)
- os.write(tun_fd, str(ip_packet))
- def get_original_destination(sock, src_ip, src_port):
- if src_ip != '10.25.1.100': # fake connection from 10.25.1.100
- raise Exception('unexpected src ip: %s' % src_ip)
- return nat_map.get(src_port)
- fqsocks.networking.SPI['get_original_destination'] = get_original_destination
- def create_tcp_socket(server_ip, server_port, connect_timeout):
- fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- with contextlib.closing(fdsock):
- fdsock.connect('\0fdsock2')
- fdsock.sendall('OPEN TCP,%s,%s,%s\n' % (server_ip, server_port, connect_timeout * 1000))
- gevent.socket.wait_read(fdsock.fileno())
- fd = _multiprocessing.recvfd(fdsock.fileno())
- if fd == 1:
- LOGGER.error('failed to create tcp socket: %s:%s' % (server_ip, server_port))
- raise socket.error('failed to create tcp socket: %s:%s' % (server_ip, server_port))
- sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
- os.close(fd)
- return sock
- fqsocks.networking.SPI['create_tcp_socket'] = create_tcp_socket
- def create_udp_socket():
- fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- with contextlib.closing(fdsock):
- fdsock.connect('\0fdsock2')
- fdsock.sendall('OPEN UDP\n')
- gevent.socket.wait_read(fdsock.fileno())
- fd = _multiprocessing.recvfd(fdsock.fileno())
- if fd == 1:
- LOGGER.error('failed to create udp socket')
- raise socket.error('failed to create udp socket')
- sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_DGRAM)
- os.close(fd)
- return sock
- fqdns.SPI['create_udp_socket'] = create_udp_socket
- fqdns.SPI['create_tcp_socket'] = create_tcp_socket
- def setup_logging():
- logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
- handler = logging.handlers.RotatingFileHandler(
- MANAGER_LOG_FILE, maxBytes=1024 * 256, backupCount=0)
- handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
- logging.getLogger('fqrouter').addHandler(handler)
- handler = logging.handlers.RotatingFileHandler(
- FQDNS_LOG_FILE, maxBytes=1024 * 256, backupCount=0)
- handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
- logging.getLogger('fqdns').addHandler(handler)
- def exit_later():
- gevent.sleep(0.5)
- os._exit(1)
- def read_tun_fd_until_ready():
- LOGGER.info('connecting to fdsock')
- while True:
- tun_fd = read_tun_fd()
- if tun_fd:
- return tun_fd
- else:
- LOGGER.info('retry in 3 seconds')
- gevent.sleep(3)
- def read_tun_fd():
- fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- with contextlib.closing(fdsock):
- try:
- fdsock.connect('\0fdsock2')
- fdsock.sendall('TUN\n')
- gevent.socket.wait_read(fdsock.fileno(), timeout=3)
- tun_fd = _multiprocessing.recvfd(fdsock.fileno())
- if tun_fd == 1:
- LOGGER.error('received invalid tun fd')
- return None
- return tun_fd
- except:
- return None
- class VpnUdpHandler(object):
- def __init__(self, dns_handler):
- self.dns_handler = dns_handler
- def __call__(self, sendto, request, address):
- try:
- src_ip, src_port = address
- dst_ip, dst_port = get_original_destination(None, src_ip, src_port)
- if 53 == get_original_destination(None, src_ip, src_port)[1]:
- self.dns_handler(sendto, request, address)
- else:
- sock = fqdns.create_udp_socket()
- try:
- sock.sendto(request, (dst_ip, dst_port))
- response = sock.recv(8192)
- sendto(response, address)
- finally:
- sock.close()
- except:
- LOGGER.exception('failed to handle udp')
- fqsocks.fqsocks.DNS_HANDLER = VpnUdpHandler(DNS_HANDLER)
- if '__main__' == __name__:
- setup_logging()
- LOGGER.info('environment: %s' % os.environ.items())
- LOGGER.info('default dns server: %s' % default_dns_server)
- FQROUTER_VERSION = os.getenv('FQROUTER_VERSION')
- try:
- gevent.monkey.patch_ssl()
- except:
- LOGGER.exception('failed to patch ssl')
- args = [
- '--log-level', 'INFO',
- '--log-file', '/data/data/fq.router2/log/fqsocks.log',
- '--tcp-gateway-listen', '10.25.1.1:12345',
- '--dns-server-listen', '10.25.1.1:12345',
- '--no-http-manager', # already started before
- '--no-tcp-scrambler', # no root permission
- ]
- args = config.configure_fqsocks(args)
- fqsocks.fqsocks.init_config(args)
- fqsocks.config_file.path = '/data/data/fq.router2/etc/fqsocks.json'
- http_manager_port = fqsocks.config_file.read_config()['http_manager']['port']
- try:
- response = urllib2.urlopen('http://127.0.0.1:%s/exit' % http_manager_port, '').read()
- if 'EXITING' == response:
- LOGGER.critical('!!! find previous instance, exiting !!!')
- gevent.sleep(3)
- except:
- LOGGER.exception('failed to exit previous')
- fqsocks.httpd.LISTEN_IP, fqsocks.httpd.LISTEN_PORT = '', http_manager_port
- fqsocks.httpd.server_greenlet = gevent.spawn(fqsocks.httpd.serve_forever)
- try:
- tun_fd = read_tun_fd_until_ready()
- LOGGER.info('tun fd: %s' % tun_fd)
- except:
- LOGGER.exception('failed to get tun fd')
- sys.exit(1)
- greenlet = gevent.spawn(redirect_tun_traffic, tun_fd)
- gevent.spawn(fqsocks.fqsocks.main)
- greenlet.join()
复制代码
无线中继:wifi.py
- import os
- import logging
- import socket
- import re
- import traceback
- import shlex
- import shutil
- from gevent import subprocess
- import gevent
- import iptables
- import shell
- import hostapd_template
- try:
- WIFI_INTERFACE = subprocess.check_output(['getprop', 'wifi.interface']).strip() or 'wlan0'
- except:
- traceback.print_exc()
- WIFI_INTERFACE = 'wlan0'
- RE_CURRENT_FREQUENCY = re.compile(r'Current Frequency:(\d+\.\d+) GHz \(Channel (\d+)\)')
- RE_FREQ = re.compile(r'freq: (\d+)')
- RE_IFCONFIG_IP = re.compile(r'inet addr:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
- RE_MAC_ADDRESS = re.compile(r'[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+')
- RE_DEFAULT_GATEWAY_IFACE = re.compile('default via .+ dev (.+)')
- LOGGER = logging.getLogger('wifi')
- MODALIAS_PATH = '/sys/class/net/%s/device/modalias' % WIFI_INTERFACE
- WPA_SUPPLICANT_CONF_PATH = '/data/misc/wifi/wpa_supplicant.conf'
- P2P_SUPPLICANT_CONF_PATH = '/data/misc/wifi/p2p_supplicant.conf'
- P2P_CLI_PATH = '/data/data/fq.router2/wifi-tools/p2p_cli'
- IW_PATH = '/data/data/fq.router2/wifi-tools/iw'
- HOSTAPD_PATH = '/data/data/fq.router2/wifi-tools/hostapd'
- IWLIST_PATH = '/data/data/fq.router2/wifi-tools/iwlist'
- DNSMASQ_PATH = '/data/data/fq.router2/wifi-tools/dnsmasq'
- KILLALL_PATH = '/data/data/fq.router2/busybox killall'
- IFCONFIG_PATH = '/data/data/fq.router2/busybox ifconfig'
- IP_PATH = '/data/data/fq.router2/busybox ip'
- CP_PATH = '/data/data/fq.router2/busybox cp'
- SH_PATH = '/data/data/fq.router2/busybox sh'
- WHICH_PATH = '/data/data/fq.router2/busybox which'
- FQROUTER_HOSTAPD_CONF_PATH = '/data/data/fq.router2/hostapd.conf'
- CHANNELS = {
- '2412': 1, '2417': 2, '2422': 3, '2427': 4, '2432': 5, '2437': 6, '2442': 7,
- '2447': 8, '2452': 9, '2457': 10, '2462': 11, '2467': 12, '2472': 13, '2484': 14,
- '5180': 36, '5200': 40, '5220': 44, '5240': 48, '5260': 52, '5280': 56, '5300': 60,
- '5320': 64, '5500': 100, '5520': 104, '5540': 108, '5560': 112, '5580': 116,
- '5600': 120, '5620': 124, '5640': 128, '5660': 132, '5680': 136, '5700': 140,
- '5745': 149, '5765': 153, '5785': 157, '5805': 161, '5825': 165
- }
- netd_sequence_number = None # turn off by default
- has_started_before = False
- RULES = [
- (
- &nb
|