底层IP数据包如何组装成应用层SOCKS5代理协议(xFsRedir功能之NAT网关和网关代理)

                                                              by fanxiushu 2021-04-13 转载或引用请注明原始作者。

正如上篇文章所描述的那样,在xFsRedir软件中使用WFP驱动框架实现了虚拟局域网功能
(包括创建虚拟局域网节点和桥接到真实局域网),
还打算利用现成的WFP驱动,再实现点别的什么功能。
因为WFP足够强大,只实现虚拟局域网还没压榨完WFP的价值,于是想到了代理。
而且只需要在我原来的虚拟局域网驱动稍作修改,就可实现代理功能的驱动部分。
当然到了应用层的处理逻辑部分,还得另外开发。
虽然我平时其实也不大使用代理上网。但是看到许多类似软件,各种杂七杂八的。
最多的可能就是做游戏加速器,
不过好像实现局域网的主机代理的软件不是很多。
所谓主机代理,也就是之前在CSDN上的文章描述的那样:
https://blog.csdn.net/fanxiushu/article/details/109098980  (基于WFP等网络驱动实现局域网内所有设备通过代理上网)

简单的说,就是把某A电脑当成网关机器,局域网内其他设备的网关地址设置成A电脑地址,
然后A电脑按照正常处理逻辑就是在IP和TCP/UDP层修改对应的地址和端口,
然后转发局域网内所有机器的数据包到真正的目标地址,这个就是NAT网关功能。
如果A同时作为一个代理机器,则把这些数据包转发到代理服务器,然后代理服务器再真正把数据发送到真正的目标地址。
也就是网关A机器即充当局域网内其他设备的 NAT 网关路由的角色,同时也充当了代理客户端的角色。
这就是主机代理,为了方便我把它说成NAT网关代理。

在上面链接的文章《基于WFP等网络驱动实现局域网内所有设备通过代理上网》描述中如何实现数据包代理转发,
在文章结尾的时候,简单的阐述如何直接把截取到的IP数据包,通过自定义的协议格式封装起来,然后发送到服务端,
服务端自己解封出IP数据包,然后再利用虚拟网卡和NAT路由把IP数据包转发到真正的目标。
也就是整个代理通讯过程,都是走的IP数据包,都是在底层完成的。
这是一种比较高效的做法,因为直接转发IP数据包,不必绕来绕去的。
当然也有个不大好的,就是代理协议格式得自己定义,服务端得自己实现。不像SOCKS5,HTTP代理那样有一大堆服务器软件。
不过如果使用linux系统做服务器,虚拟网卡和NAT路由都是系统自带现成的,也不用花精力再去开发。
所以整体也不用花太大精力在代理服务器上的开发。
而且正是因为通讯协议格式是自定义的,因此我们可以尽量采用各种混淆算法,避免防火墙通过协议格式来拦截通讯。

而本文描述的,就是如何把截取到IP数据包,组装成SOCKS5协议,然后发送到通用的SOCKS5代理服务器上去。
这篇文章选择SOCKS5协议,方便大家容易理解些,
另外是SOCKS5协议格式TCP部分够简单,底层的IP数据包的TCP部分比较容易转换成SOCKS5协议。
但实际上SOCKS5处理UDP代理非常费劲,而且可能不同的SOCKS5代理服务器不一定支持或者支持得让人费解,
这可能是早期定义SOCKS5协议的那帮人脑回路清奇,就跟早期定义的 FTP 协议一样。
只是用的时间太长,已经定型了,无法再改变。
(因此在xFsRedir集成的代理功能,很可能会使用自定义的协议传输,这个到时再定,
不过可以确定的是,我并不想在xfs_rdsvr服务端集成虚拟网卡和NAT路由功能,
因为并不求高效,只求xfs_rdsvr服务端在各个平台能通用和尽量简单,
也就是也会像这篇文章描述的那样,把IP数据包转成应用层通讯协议。)

IP数据包组包和解析成应用层数据,不是个简单的过程,正如在
https://blog.csdn.net/fanxiushu/article/details/87958656 描述的那样
想在NAT外网一侧把IP数据包转成应用层的socket套接字,就等于实现的是个TCP协议栈。

在windows系统中,在本机实现某个进程或者某些进程的SOCKS5代理
(哪怕这些程序本身不支持SOCKS5代理),方式是挺多的。
以前的文章也简单提到过,比如应用层使用LSP啊,或者直接hook某个进程的send,recv等函数。
或者使用WFP驱动的 Connect Redirect (连接重定向)功能把连接转发到127.0.0.1的某个端口,
然后本地实现一个程序侦听这个端口,实现到SOCKS5服务器的代理连接。等等。。。
但是本文描述的功能不单要实现本机代理,还包括把本机作为NAT网关。
这个时候,以上的方法就不再适用了,只能是处理IP数据包,而又要把数据通过SOCKS5代理协议发出去,
就只能把IP数据包转换成应用层的SOCKS5协议了。

我们先来看看SOCKS5协议格式(,以TCP代理部分为准,且以版本5为准,不包括版本4)
其实是挺简单,为了尽量简单,下面不包括密码验证部分。
首先我们在应用层创建一个socket套接字之后,调用connect函数成功连接到SOCKS5代理服务端,
客户端就开始向SOCKS5服务端发送请求协议版本以及认证方式,
1,具体就是发送3个字节的认证:
      0x05 0x01 0x00 意思是SOCKS版本是 5, 请求一种认证方式,认证方式是无密码的认证。     
2,SOCKS5服务端回复2个字节的认证结果:
     0x05 0x00 第一个字节表示版本5, 第2个字节如果是0 表示认证成功,可以继续
3,客户端发送需要连接的真正目标地址和端口信息,以IPV4地址为例,一共是10个字节的数据:
    0x05 0x01 0x00 0x01 | 0x0a 0x0b 0x0c 0x0d | 0x0A 0x0B 
     第一个字节是版本号,第2个字节是表示CONNECT连接,第3个是保留设置0,第四个是后面地址类型,1表示IPV4地址,
     接下来4个字节表示IPv4的地址,是网络序,最后两个字节是端口,也是网络序。
     意思就是告诉SOCKS5服务端,我想连接真正的服务端地址是 0x0a 0x0b 9x0c 0x0d ,连接的真正端口是 0x0A 0x0B
4,  SOCKS5服务器也回应10个字节应答数据:
     0x05 0x00 0x00 0x01 | 0x00 0x00 0x00 0x00  | 0x00 0x00
     第一个字节代表版本号,第2个字节如果是0 表示成功,其他值表示失败,第3个字节保留设置0,第4个字节表示地址类型,1表示IPv4地址
     接下来4个字节表示代理服务器的地址,最后两个字节表示代理服务器端口,不过这两个是可选的,
     本身客户端connect的时候就已经知道代理服务端的地址和端口了,所以这两个就是多余的。

以上就是普通的 请求-应答流程,一共两个来回。
客户端一共向服务端发送 3 + 10  = 13个字节的数据, 服务端回复 2 + 10 = 12个字节的数据。
这些信息,都对在下面讲述的 IP数据包 组装成SOCKS5协议有用。

接下来再来看看 TCP三次握手的过程:
1, 客户端在应用层调用connect发起连接请求的时候,
       操作系统底层首先会发送一个只包含TCP头以及一些附加信息的IP数据包给服务端,
       这个IP数据包就是 在TCP头部只设置SYN(不会设置ACK标志)标志的数据包,简称SYN包。
       同时SYN包还会包括一个系统随机生成的seq序列号,这个SYN数据包 简单记作:
       SYN = X,(X数系统随机生成的序列号)
2,服务端接收了请求,同样也回复一个只包含TCP头的IP数据包给客户端,
      这个IP数据包TCP头部设置了 SYN标志和ACK标志,我们称作 SYN+ACK 数据包。
      同时SYN+ACK数据包还有一个服务端系统随机生成的seq序列号, 同时会把请求ack序列号增加1,具体就是:
      SYN=Y,ACK=X+1 (Y是服务端系统随机生成的序列号,ACK的数值是客户端序列号+1)
3,客户端接收到了这个syn+ack包,回复ack确认数据包,这个确认数据包的 ack号=Y+1
      这个时候connect成功返回,TCP连接建立,我们之后就可以使用send,recv等函数发送和接收数据了。

有了以上两个方面的知识,我们就可以开始组装底层的IP数据包了(处理IP数据包的TCP部分),
首先我们肯定得跟踪每条完整的TCP连接,如何跟踪TCP连呢?
根据上面的TCP三次握手过程,我们应该很容易找到TCP的线头,那就是SYN数据包,
找到SYN数据包的时候,记录下目标地址,目标端口,源地址,源端口,协议(当然是TCP),也就是我们通常说的五元租。
然后以后的所有IP数据包中,都根据这个五元组确定是否属于某条TCP连接上的数据传输。这个就是TCP跟踪。
不过这里代理的情况有点特殊,因为IP数据包都是统一朝SOCKS5服务端发送,也就是目标地址和目标端口都是固定的,
我们得根据三元组来再加上SOCKS服务端地址来确定每条TCP连接,这个是否能确定呢?
答案是绝大概率可以的,因为我们都是connect发起方,不是服务端accept接收方。
每次connect的时候,系统都会分配不同的端口,不会有端口重复。即使重复也是非常小概率事件,
或者人为的故意在connect之前调用bind绑定。

当我们追踪到一条完整的TCP连接,但是这条TCP连接是朝真正的目标地址发起的,
该如何修改这条TCP连接,让它按照我们的要求,走SOCKS5代理服务器呢?
那就得修改对应的IP数据包,发出去的IP数据包的目标地址和端口改成SOCKS5服务端的地址和端口,
从SOCKS5接收到的IP数据包再把源地址和端口改回原来发出去的真正目标地址和端口。
光这样改还不行,因为SOCKS5是应用层协议,SOCKS5本身有协议头,
因此我们还得修改IP数据包内容,具体就是在上面描述的那样,在客户端发送13个字节的数据,
同时接收的时候还得首先处理SOCKS5服务端发来的12个字节的回复,
完成这个步骤之后,剩下的数据才能原封不动的只管投递即可。

IP数据包的修改注意事项,我在很早的文章有描述:
https://blog.csdn.net/fanxiushu/article/details/8624364 (网络数据拦截之:修改TCP包内容时注意的问题)
其中最麻烦的是seq和ack序列号的追踪和修改。

好在这里的SOCKS5协议足够简单,否则改起来还真不容易。可是即便这样,修改IP数据包也并不容易。
如何在TCP连接中首先插入这13个字节的SOCKS5协议头呢?

我们在发送SYN数据包的时候,记录下真正的目标地址,然后修改目标地址到SOCKS5服务器地址,
这里有个关键问题:关于这个seq序列号的问题:
因为我们可以预见,我们需要一共额外的发送13个字节给服务端,
如果这个SYN数据包不修改这个seq值,那在以后每个数据包都得调整seq和ack值,这样做就比较麻烦。
所以可以考虑在SYN包的时候,把seq序列号前退13个字节, 具体就是  seq = seq - 13 ,

然后在接收到SOCKS5服务端回复的SYN+ACK数据包的时候,开始执行发送SOCKS5协议头计划。
这个接收到的SYN+ACK数据包暂时不朝我们的系统投递,
我们在这里组装一个ACK回复包 + 3个字节的SOCKS5认证的数据,朝SOCKS5服务端发送,
这样SOCKS5接收到数据包之后,就认为连接已经建立了,并且还同时接收到了3个字节的认证数据。
组装这个数据包的时候,也有个关键点: 那就是SYN+ACK回复包中的服务端的seq序列号问题。
因为我们可以预见,我们需要一共额外的从服务端接收12个字节的SOCKS5协议的应答头,
同样的如果这个SYN+ACK数据包不修改这个seq值,那在以后每个数据包都得调整seq和ack值,这样做就比较麻烦。
所以可以考虑在回复ACK包的时候,把seq序列号前退12个字节, 具体就是  seq = seq - 12 ,

接着SOCKS5服务端确认认证方式,发送2个字节的确认数据包过来,
我们接着自己处理这个数据包,
然后再组装10个字节的真正目标地址的SOCKS5请求协议朝SOCKS5服务端发送,
注意调整seq和ack的值。
再然后就是接收SOCKS5服务端发来的10个字节的最终SOCKS5确认信息,
这样SOCKS5协议头的交互就完成了,我们与SOCKS5完成了真正的连接。

但是回到我们本身系统来。
自从我们拦截到本地系统发来SYN数据包之后,就一直在自己跟SOCKS5服务端交互,从来没回复过数据给系统。
因此在完成SOCKS5协议头认证之后,我们得开始回复SYN+ACK数据包给系统,
可以使用之前保存的SOCKS5回复的SYN+ACK数据包给本地系统,记得修改源地址。
因为之前seq和ack都采用了回退算法,这里的以及后面的数据包的 seq和ack都不需要修改了。

再然后系统接受到 SYN+ACK,它认为到目标地址连接成功了(其实是我们做了手脚,真正连接到SOCKS5代理服务器上去了)
然后系统就会开始回复ACK数据包,开始收发数据。在以后的数据包中,我们只需修改对应的地址和端口,然后忠实的投递即可。

以上讲述得比较简单,实际操作的时候,其实需要考虑许多问题,
比如需要考虑丢包重传问题,虽然我们自己交互的数据包就4个,不多,但是也无法保证不会丢包,因此必须处理这种情况。
比如需要仔细计算seq和ACK序列号问题。比如认证失败的时候如何断开两端的问题, 等等。。。

从理论上来说,我们可以在SYN+ACK的时候,直接回复 13个字节给 SOCKS5服务端,这样可以减少处理步骤。
因为TCP本身就是流式连接,这样也不算错。
但是考虑到我们通常的编程习惯和处理数据的习惯,还是分开来好些。

以上算法,倒是是我联想到了另外一种情况:
https://blog.csdn.net/fanxiushu/article/details/87958656  (NDIS协议驱动开发(另类的NAT路由程序开发))

当时就是想把NAT路由程序的外网一侧转成 应用层的 socket 套接字,
当时确实实现了这么一个程序,而且也确实成功把外网一侧的IP数据包转成了socket套接字连接真正的目标,
但是当时使用的是简单的组包TCP的算法,比起系统自己的组包效果差远了,所以在网络不太好的情况下,容易出问题。

而借这次把IP数据包通过修改包内容,把IP数据包转成SOCKS5协议的做法,
完全可以把IP数据包添加某个代理协议头,然后开启本地某个程序,接收这些特定代理协议的TCP连接,
不就堂堂正正的把IP数据包转成socket连接了吗? 而且TCP组包依然使用系统自己的组包算法。
因为我们自己组装TCP包太复杂太麻烦。肯定不如经过几十年发展得稳定的TCP组包算法。

写这篇文章的时候,正在开发 xFsRedir 的代理功能,并且实现了上面所述的把IP数据包转成SOCKS5代理协议的TCP部分,
不过在仔细研究SOCKS5的UDP代理之后,可能会放弃SOCKS5,转而使用自己定义的协议来处理。
xFsRedir是需要实现具备NAT功能的代理,也就是能代理内网的其他机器的通讯,因此也只能采用修改IP数据包的算法,
当然同时也会一起处理本机代理,也是通过修改IP数据包。

如果有兴趣,可关注GITHUB上的xFsRedir
https://github.com/fanxiushu/xFsRedir

 

你可能感兴趣的:(网络驱动,游戏加速器)