网络编程中,对于数据传输实时性要求较高的场合,大家都会选择UDP来作为数据传输协议,在TCP/IP协议族中UDP协议较TCP协议需要的网络系统资源更少。然而在企业应用中,由于网络安全原因等会导致除了特定端口以外的IP数据无法通过专用的路由或网关。为了支持这类应用,制定了专门的支持Socks连接的socks4/socsk5协议。Socks协议允许实现此类功能的代理软件可以允许防火墙(本文以下内容中防火墙与代理的称谓可以等同视之)以内的客户通过防火墙实现对外部的访问,甚至可以允许等待外部的连接。对于防火墙内部的软件客户端,仅同防火墙协商,同防火墙的特定端口取得联系,然后交换数据,而防火墙外部的程序也直同防火墙进行数据交换,外部看不到防火墙的内部网情况,这样起到了防火墙的监护功能,也满足了大多数通过非常用(如http ftp等)端口交换数据的应用程序需求。防火墙内部的应用程序如何通过防火墙将UDP数据传输到防火墙外部,并且接受外部的UDP数据报文,这就是所谓穿透Socks代理的UDP编程。
RFC1928描述了Socks协议的细节,告诉我们客户程序如何同Socks代理协商,取得透过该协议对外传输的途径。英文的URL为:http://www.ietf.org/rfc/rfc1928.txt,中文的翻译参考不是很贴切(但译者还是值得尊敬的),但对于E文不大好的可以将就一下:http://www.china-pub.com/computers/emook/0541/info.htm。建议先了解以上链接内容后在阅读下文。
一般的代理软件都实现了两个版本的Socks协议—Socks4以及Socks5,其中Socks5协议支持UDP报文的传输以及多种验证方法,该协议还考虑IP发展需要,支持Ipv6。
TCP透过代理支持两种方法:Bind以及Connection。Connection是指作为客户端,主动连接代理外部的服务程序,在这种方式中代理将替代客户程序发起真正的对外部服务程序的连接,并来回传输在此连接中需要交流的数据;Bind方式则用在那些需要客户机接受到服务器连接的协议中,例如FTP协议之类的除了需要建立一个客户--服务器的连接报告状态外,还需要建立一个服务器—客户的连接来传输实际的数据(当然要注意这里的FTP协议通过Socks协议连接远程主机,并非通过FTP代理协议)。UDP报文传输则意味着代理充当UDP数据传输的中间人,将防火墙内的主机对外的数据传递出去,将需要引入到防火墙内的UDP数据报文转给防火墙特定的主机。关于TCP穿透的讨论和例子很多(给出一个实现的例子:http://www.codeproject.com/internet/casyncproxysocket.asp ),就不多讲了,在此着重讨论如何实现UDP数据的穿透Socks5代理。
为了测试方便我简单写了一个服务进程在代理外IP为192。168。0。250上监听UDP8100端口,接收到一个UDP的数据报后,返回服务器上的当前时间给发送UDP报文的客户端。代理采用Wingate,他在192。168。0。1上运行,Socks的标准端口1080来运行服务监听程序。我的机器为192。168。0。10,你可以看到,我不能够直接联系运行时间服务的机器,我会向代理提出我的要求,由代理进程负责UDP数据报文的转发。代理软件选择Wingate,并且为了简单起见,采用不需要验证客户的验证方法。好了,交代了背景后,下面我们就开始穿越代理的旅程吧。
无论是TCP还是UDP通过代理,首先要同代理取得联系。为了能够确保在第一阶段顺利确保数据传输,协议规定客户端采用TCP方式连接联系代理服务器。
一旦客户同代理的1080端口连接上,客户首先要发送一个版本标识/方法选择的TCP报文给代理服务器,具体格式为:
版本号(1字节) | 可供选择的认证方法(1字节) | 方法序列(1-255个字节长度)
如果是socks4协议,版本号就是0x04,但是这里是支持UDP的Socks5,所以是字节0x05。此说明对于后面的报文格式解释的版本部分也都适用。
Socks协议定义了0-255种通过代理的认证方法:
0x00 无验证需求
0x01 通用安全服务应用程序接口(GSSAPI)
0x02 用户名/密码(USERNAME/PASSWORD)
0x03 至 X'7F' IANA 分配(IANA ASSIGNED)
0x80 至 X'FE' 私人方法保留(RESERVED FOR PRIVATE METHODS)
0xFF 无可接受方法(NO ACCEPTABLE METHODS)
显然,无论是发起Socks请求的客户端还是负责转发Socks数据的代理都不可能完全实现所有的(起码目前还没有)方法,所以客户端需要把自己能够支持的方法列出来供代理服务器选择。如果支持无验证,那么此报文的字节序列就为:0x05 0x01 0x00,其中的0x01表示客户端只支持一种验证,0x00表示能够支持的方法是编号为0x00的(无验证)的方法。如果客户端还支持用户名/密码的验证方式,那么报文就应当是:0x05 0x02 0x00 0x02。
代理接收到客户的请求,会根据自身系统的实现返回告诉客户验证采用哪一种方法,返回的保文格式为:
版本号 | 服务器选定的方法
如果服务器仅支持无验证的验证方法,它返回字节序列:0x05 0x00。客户端同代理的数据报文的来回应答就是Socks协议的验证方法选择阶段。
接下来就是根据选择的方法来,验证客户身份了。虽然我们这里不需要验证,但是还是简单讲一下0x02的用户名/口令的验证客户端发送报文格式:
0x01 | 用户名长度(1字节)| 用户名(长度根据用户名长度域指定) | 口令长度(1字节) | 口令(长度由口令长度域指定)
不清楚为什么报文的首字节是0x01(按照惯例应当是0x05)。整个报文长度根据用户名和口令的实际长度决定。用户名和口令都不需要以’\0’结束。服务器会根据提供的信息进行验证,返回如下的报文字节序列映像为:
0x01 | 验证结果标志
验证结果标志可以为:0x00 验证通过,其余均表示有故障,不可以继续下一步的协议步骤。
在通过了验证步骤之后,接下来就是确定UDP传输的端口了。这里面需要确定两个重要的端口:1、客户端发送UDP数据的本机端口,一方面可以为发送数据指定端口,另一方面告诉代理,如果有数据返回,就传递给该端口,构成一个UDP传输回路。2、代理想在哪个端口接收客户发送的UDP数据报,作为对外UDP Socket的申请方,双方协商确定一个端口后,可以持续通过此端口向外部主机发送数据,也可以通过此端口由代理接收外部主机发回的UDP数据,再通过此端口发给UDP发送请求客户端。客户端会按照以下格式发送TCP数据字节序列:
协议版本 | Socks命令 |保留字节| 地址类型 | 特定地址 | 特定端口
Socks命令有3种:CONNECT (编号0x01) BIND (0x02) UDP(编号0x03)
保留字节长度1,为0x00
地址类型有3种:
0X01该地址是IPv4地址,长4个8bit字节。
0X03该地址包含一个完全的域名。第一个8bit字节包含了后面名称的8bit的数目,没有中止的’\0’。
0X04 该地址是IPv6地址,长16个8bit字节。
特定地址一般对于多IP的主机有意义,如果不是或者不关心哪一个IP发起UDP数据传输,就可以填0。0。0。0,地址类型选择0x01。比较重要的就是UDP传输将要从哪一个UDP端口发起。一般为了避免因为硬性指定一个端口导致引起冲突,会首先生成一个UDP套接字,用生成的套接字既定端口来作为自己传输UDP的端口,并通过此步骤告知代理服务器。譬如临时生成一个UDP套接字,UDP选择端口2233作为传输UDP数据的本地端口,那么此报文就为:0x05 0x03 0x00 0x00 0x00 0x00 0x00 0x08 0xb9 其中0x08 0xb9换算成10进制就是2233。
代理服务器会根据自己的端口占用情况,给出一个有关代理服务器的端口的回复字节序列,告诉客户可以将UDP数据发送到此地址和端口中去,以实现UDP穿透代理。返回的字节序列为:
版本 | 代理的应答 |保留1字节| 地址类型 | 代理服务器地址 | 绑定的代理端口
代理的应答可以为值:
0X00 成功协商
0X01 常见的Socks故障
0x02 不允许连接
0X03 网络不可到达
0X04 主机不可到达
0X05 连接被重置
0X06 TTL 失效
0X07 命令不支持
0X08 地址类型不支持
0X09 一直到0xff都保留
代理的地址指客户端需要发给那一个IP,绑定的端口指代理将在哪一个端口上为客户接收数据并转发出去。地址类型、地址参照上面的解释。
通过以上的TCP协商几个步骤后,现在客户端明确了自己将需要发送的UDP数据发给代理服务器的某个IP的某个端口了。代理服务器也知道是哪一个IP发送数据报给自己,如果接收到由于转发此UDP数据报而从远端目标主机传回的数据报,他需要根据协议将收到的数据报返回给客户的特定端口。此特定端口就是此步骤中字节序列中绑定的代理端口
在传输UDP数据时,由于通过代理,所以需要按照一定的格式进行包装,在需要传送的数据之前添加一个报头,具体为:
保留2字节的0 | 是否数据报分段重组标志 | 地址类型 | 将要发到代理外的目标地址 | 远端目标主机的端口 |需要通过代理传送出去的数据
是否数据报分段重组标志为0表示该数据报文是独立的不需要重新组合,其他的表示特定的序列号,以利于UDP报文整合。
这里的地址是最终接收此UDP数据的代理外的服务器地址,我们这个例子中就是192。168。0。250。端口就是8100。根据地址类型不同,具体的需要传送的数据起始位置也不同。如果是Ipv4,那么数据从整个UDP报文的10字节处开始,如果是指定了域名,那么就是从262处开始,Ipv6地址类型就从20处开始为数据字段。这些需要我们在实际传输数据时注意。假如要传送10字节的数据9到96.96.96.96的1024端口,那么传送的数据字节序列大致为:
00 00 00 01 60 60 60 60 04 00 09 09 09 …….09
保留 是否分段重组 Ipv4 96.96.96.96目标主机IP 端口1024 从此处开始为数据
而这之后,如果远端的目标主机有数据返回,代理服务器会在将数据传回给UDP 客户端时将数据也做类似上面的封装,即添加一个报头。客户需要接收这个报头,实际上也明确通知UDP 客户端,这个数据报是哪一个服务器发回的。
下面就来看一看给出的代码:见附件工程。我将支持Socks5的UDP写成一个Java类,供大家参考。相关解释见注释部分。
通过上面分析我们可以大体上总结到透过Socks5进行UDP编程需要注意的几点:
1、 Socks5编程的身份验证
由于防火墙作用几乎是隔绝内外的非正常连接,而Socket可以通过任何端口连接到外部,所以作为对Socket4的改进,Socket5增加了对socket协议访问的验证功能。这些验证功能没有规定一定采用什么方法,一般看防火墙自身支持以及客户端能够支持什么方法,这意味着作为客户端必须将自己支持的方法在协商阶段之初就告诉代理服务器,而代理服务器自己根据已经实现支持哪种验证方法而选择特定的方法回复客户端。意味着针对不同的代理服务器以及不同的客户端,很可能对于验证方法支持上有区别,需要视具体的应用环境而定。这些增加了Socket5客户端以及Proxy server软件的编写难度,但是增强了安全性。
2、 TCP保持重要性
要发送穿透代理服务器的UDP数据报,其实首先需要建立客户端到代理服务器的TCP连接,通过一系列的交互,获得代理服务器的许可才能够发送出去(同时代理服务器业记录下连接的在Socks5服务的客户IP和端口),也确保从远端发回的数据能够通过代理服务启发回给某个UDP客户端(因为它登记了一个关于Socket UDP的通路映射)。所以为了发送UDP数据,必须建立和保持这个TCP数据。RFC1928也提到,不能取得代理服务器的通道后就关闭TCP连接,否则代理服务器以为UDP Socket通过代理的请求已经结束,不需要继续保留UDP的对外Socket映射记录,从而导致每发送一次UDP就要重新建立TCP连接协商UDP映射,增加不必要的麻烦。所以,我们需要保持UDP客户端到到代理服务器的TCP连接持久,不必显式关闭它。
3、 UDP本地端口选定
UDP大多数是同具体端口相关的,所以一定要在同代理服务器协商UDP映射时告知客户端UDP的端口。一来将UDP同某个端口绑定,使得代理服务器接收UDP数据并转发,二来也告诉了代理服务器将来在某个端口发出去后得到的反馈数据也按照线路返回给客户的此端口。这一点很重要,笔者在此处犯了错误导致浪费了很多时间。
4、 TCP/UDP连接二重性
可以看到Socks5 的使用会占用至少一个TCP连接,这样导致代理服务器的负担很重。所以在具体的应用时,需要考虑关于代理服务器的存在的负载问题。
以上即关于UDP穿透Socks5代理的一点心得,希望能够得到大家的指正。
TNT