Pages

Sunday, 24 September 2017

socks代理转化为VPN的工具-fqrouter

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
from  http://fqrouter.tumblr.com/post/51474945203/socks%E4%BB%A3%E7%90%86%E8%BD%ACvpn

No comments:

Post a Comment