Socks代理的研究和实践报告

这篇文章讨论的是如何用twisted写一个socks v5的代理程序。项目的启发来自著名的shadowsocks,出于对再造轮子的极度偏爱,以及练习twisted的目的。我用twisted实现了一个类似的玩意。不过就现在的成果来看,要能够稳定运行还是一条挺长的路……先放项目地址:MoeSocket。这份代码是能够运行的,具体的,在浏览器中使用socks v5代理,然后端口使用local中监听的那个端口就好。我要说的是,这份代码目前运行相当不稳定,在本机测试丢包也很严重,由于不管是套接字编程还是twisted我都是初心者—-事实上,这是我第一个套接字级程序—所以暂时还无法找到问题。后面会逐步进行一些修改,或者用defer之类的重写,总之在能正常稳定运行之前暂时不会坑掉……

由于笔者开始并不知道写socks v5程序需要研读rfc1982,所以参考了大量的来自于blog这里的代码。请务必注意。

先说说为什么是socks v5协议的代理。一个很重要的原因是,http代理现在已经相当靠不住了,而使用sock级的代理,可以自己造协议,封锁起来很困难—-当然并不是不可能,一些比如流量检测之类的方法还是可以封锁的,但是依然可以使用流量分散的方法去应对。总之,socks代理具有更大的灵活性。

然后如果希望写一个socks v5的代理的话,这一份文档请务必仔细研读,那就是著名(?)的rfc1982。这份文档详细讲述了整个请求代理到建立socks代理连接的过程中,每一步客户端和服务器的应答内容。在笔者没看这份文档之前,折腾了好多天,完全看不懂为什么别人要这么做,而对照这文档,一切都是清清楚楚的。当然您不想看也没关系,这篇文章会提到多数要点。

先说说代理的思路,很简单。首先一个条件是,您需要有一台非国内的服务器,用这个干净的服务器作为自己的代理。当然,gae的服务器理论上也可以,但是个人并不推荐。本机上,把浏览器的请求,加密之后,发送到服务器上的代理程序进行解密,再把解密出的正确请求发送给远端的目标主机,取得的数据再通过服务器加密发送到本机解密再发送给浏览器就好。基本过程还是容易理解的。

注意到一个事实,事实上,套接字流经过了两次代理,而每一个代理都既是服务器又是客户端。

基本流程是这样。下面是结合文档的详细步骤和实现代码。

为了方便叙述,我们先实现一个local的代理,成果的标志是,通过这个本地代理,我们能正常使用浏览器通过代理进行浏览网页的操作。

当然,我们先在浏览器上配置,以使用代理,假设使用端口为7777。由于我这里使用的chromium,所以我也只说chormium系的方法。其实和shadowsocks一样,下载switchysharp插件,新建一个情景模式,使用手动配置,在SOCKS代理处,地址填写127.0.0.1,端口为7777。点保存的话,就配置好了,选择刚才的那个就好。

然后是一个问题,浏览器是如何通过socks v5和代理进行通信的。这需要翻rfc1982。

在这个过程中,浏览器是客户端,本地代理是服务器。参考文档,我们知道,客户端连接到服务器之后,会发送请求来协商版本和认证方法。具体的,是这样:

VERNMETHODSMETHODS
111 to 255

上面那个表格很清楚地显示了请求包的内容—-第一个字节是version值,当然对socks v5来说,这个值就是05,如果不是,大可以中断连接。第二个字节是methods的数目,也就是后面有几个字节,第三个值规定了方法,比如,00代表匿名连接,02就是要求用户名以及密码进行验证这样。基本上,我们采用00,因为最方便。然后代码实现的话:

def listen(self,data):
    print data
    ver,nmethods = struct.unpack('!BB',data[:2])
    if not ver == 5:
        print 'Please use Socket V5?'
        self.tansport.loseConection()
        sys.exit(1)

    if nmethods<1:
        print "What's this!?"
        self.tansport.loseConnection()
        sys.exit(1)

    methods = data[2:2+nmethods]
    for method in methods:
        ## no auth,no need account and pass
        if ord(method) == 0:
            ## 
            resp = struct.pack('!BB',5,0)
            self.transport.write(resp)
            self.state = 'wait_connect'
            return # continue?
        elif ord(meth) == 2:
            resp = struct.pack('!BB',5,2)
            self.transport.write(resp)
            self.state = 'wait_auth_connect'
            return
        elif ord(meth)==255:
            self.transport.loseConnection()
            return
        else:
            ## maybe use 01,02,03,80....
            ## but it is not necessary
            ## so cut off the connection
            self.transport.loseConnection()

上面的代码实现了对基本请求的解包和处理,这里提一下,数据流其实是字节流。当然,事实上,我们只写了wait_connect这个函数。正如上面提到了,我们实际上匿名连接就好。

服务器收到了匿名连接的请求,会给出05 00做出回应。当然,如果是受到的非匿名连接的请求,就会麻烦一些,服务器应该给出05 02,然后客户端会进入验证过程。但是这个过程相对会复杂一些,所以这里不讨论。

当我们的服务器给出了05 00的回复之后,我们让它进入等待连接状态,接受下一次请求。

这一次的请求的数据比较麻烦,具体的可以看这个表。

VERCMDRSVATYPDST.ADDRDST.PORT
11X'00'1Variable2

这里面的一些值。VER依然是05以代表使用socks v5协议。CMD的值可以取 01,02,03。其中01指代connect,02指代bind,03指代使用udp。就这个程序而言,我们只使用01。

这里顺便说一下,socks v4和socks 05的区别主要有两个。第一是是否支持udp,第二是是否支持ipv6。

RSV是指reserve—也就是保留。这个字段无论如何都是00。

然后ATYP指示使用的地址类型。具体的,01指代目标地址是ipv4版本的ip地址,03指代目标地址是一个域名。04指代目标地址是ipv6协议的ip地址。

如果是01的话,理所当然随后的四个字节是ip地址,04的话就是十六个。唯一不同的是使用域名的时候,第一个字节是域名的长度,随后才是真正的域名,请务必注意这点。

最后这部分长度获取完了,接下来两个字节是端口号。不用多说了吧?

实现的话,我挑出来了解析请求这部分,就是下面这样的。

ver,cmd,rsv,atyp = struct.unpack('!BBBB',data[:4])
data = data[4:]
if cmd == 1:
    if atyp == 1: #IP V4
        b1,b2,b3,b4 = struct.unpack('!BBBB',data[:4])
        host = '%i.%i.%i.%i' % (b1,b2,b3,b4)
        data = data[4:]
    elif atyp == 3: # domain name
        # the first octet is the number of lenth
        lenth = struct.unpack('!B',data[0])
        lenth = lenth[0]
        host = data[1:1+lenth]
        data = data[1+lenth:]
    elif atyp == 4:#ip v6
        pass
    else:
        self.transport.loseConnection()
        return
    port = struct.unpack('!H',data[:2])
    port = port[0]
    data = data[2:]
    print host,port
    return self.connect(host,port)

解析出来了这部分,正如上面代码最后所显示的,就是真正开始连接远端主机了。

事实上,在请求远端主机的时候,我们的代理程序不再是服务器,而是一个客户端。所以,我们还需要一组协议,remoteProtocol协议去充当一个客户端。

实现的话,factory=XXXXXXX() reactor.connectTCP(xxx,xxx,xxx 这样……

如果我们的代理连上了远端主机,那么,给客户端(浏览器)发送一个05 00 标志着一切准备结束,以后就是直传数据了。

直传数据这部分,基本上,就是取得两部分的tansport,然后任意一方有data到了,那么就通过对方的transport发送过去。实现可以看源代码,不太容易描述。

总之最主要的就是上面的请求的回应的部分。基本上,严格按照上面的过程的话,一个本地的代理就写好了。这个代理,可以把你的80端口的数据,全部转移到指定的端口……聊胜于无的一个小玩意。

但是这是成功的一大部分。想想,现在这个我们是放在本机的,如果把这个放在服务器上,会怎么样?

当然我们还需要一个代理,这个代理起的作用,只是简单地把两端(比如说,服务器上的代理程序和浏览器)的数据连起来,类似于管道这样的玩意。这也是local.py的实现思路。简单的说,服务器在初始化阶段就连接上目的主机,把浏览器的请求原封不动地发给目的主机(服务器上的代理),然后把目的主机的回复原封不动给浏览器。目前的问题是,这个过程丢包很严重……如果有大神指点,不胜感激。

如果未来希望能FQ,可以很容易地扩展local.py,让它能解码编码就好了。然后编解码协议,自己随便换着玩吧。

基本上,我想说的就是这些,谢谢您能看完这篇文章(鞠躬)。

明天就回学校了,有点忧郁呢。

(在墙角画圈圈的AS……)

以上。