如何让NAT支持PPTP协议

                                                                                              范秀树 2015-02-27 转载或引用请注明原作者。

NAT,早前的几篇文章中说过这么一个内容,是大量存在于中国网络中的隐身英雄,没有它,大部分人都无法在网络世界畅游。

因为我一直使用自己开发的NAT网关程序来上网,
最近使用VPN拨号,发现PPTP拨号无法拨号成功,一开始还以为是GFW封网。
后来研究PPTP协议,才知道PPTP协议需要NAT经过特殊处理才能支持。
PPTP协议分为两个部分:一部分是控制协议,走的是TCP,服务端的端口固定为是1723, 
NAT支持TCP和UDP,这个毫不怀疑,所有PPTP的控制协议能被NAT支持。
可是糟糕的是PPTP的另外一部分协议: 数据协议,它被封装到GRE协议中,GRE跟TCP和UDP并行在同一层。
因为 我的NAT只支持TCP和UDP的穿透,无法支持PPTP拨号。
因此必须修改NAT程序,让他支持 PPTP的 GRE协议。
GRE是通用路由协议的统称,它被封装到IP网际层中进行传输,他没有TCP和UDP那样有端口的概念,
跟TCP,UDP并行,对应的IP协议protocol类型是 47 。
而且根据GRE封装不同的协议,它有不同的格式定义,因此这里只讨论GRE封装 PPTP数据的内容。
封装PPTP的GRE协议头的c语言定义如下:
struct pptp_gre
{
       unsigned short  FlagsAndVersion;  ////标志和版本,具体含义可查阅 GRE的RFC文档
       unsigned short Protocol;     ///承载的数据协议,PPTP总是设置为 0x880B,即承载的数据是PPP协议。
       unsigned short DataLength; /// 数据长度,不包括GRE头部大小
       unsigned short  CallID;  ///  会话双方中的 对方的唯一ID,正是因为这个的存在,NAT才可能支持PPTP,下面会重点讲解
       unsigned int      SeqNo;  /// 数据序列号,类似TCP的SeqNo概念
       unsigned int      AckNo;   ///数据应答号,类似TCP的AckNo概念
};

NAT是根据端口来支持内网机器访问外网的,TCP和UDP都有端口的概念,因此NAT能支持TCP和UDP。
可是GRE没有端口,幸好 PPTP的GRE封包,提供了CallID的概念,才让NAT支持PPTP提供了可能。
CallID是PPTP在控制协议通信双方进行协商时候中分配的,
用来表示自己在回话中唯一标志,是 16位大小即双字节大小的一个数字。
PPTP在数据协议通讯中,都会在GRE头填写对方的CallID,
就是如果PPTP客户端要发送GRE数据包给PPTP服务端话,客户端就会填写PPTP服务端的CallID。反之则填写客户端CallID。

首先看看CallID是如何产生的:
当使用PPTP拨号时候,客户端使用TCP连接到PPTP服务端1723端口,
发送 Start-Control-Connection-Request 数据包,服务端回答Start-Control-Connection-Reply数据包,
接着客户端发送 Outgoing-Call-Request 数据包,在这个数据包里,客户端会分配一个自己的CallID,表示在GRE通讯中,
我使用这个CallID,然后 服务端回答 Outgoing-Call-Reply数据包,同时在此包分配服务端的CallID,表示服务端使用这个CallID通讯。
接着客户端发送Set-Link-Info 数据包协商一些信息。
在保持通话过程中,双方会时不时发送 Echo-Request 和回答 Echo-Reply数据包来保持通讯不被中断。
通讯过程中如果PPTP服务端头错误发生,会发送WAN-Error-Notify给各个客户端。
要断开连接,客户端发送Call-Clear-Request 表示断开,服务端回答Call-Disconnect-Notify。
要详细了解 PPTP的控制协议,请查阅 RFC 2637 。
我们这里只关心 CallID,以及如何利用它达到NAT穿透。

我们先假设不对CallID做任何修改,只让我们的NAT网关程序提供对GRE的IP修改支持,
也就是 内网的GRE数据包到达NAT,仅仅修改内网IP地址为外网地址,然后发送出去。

如果内网同一时间里,只有一台机器使用PPTP协议通讯,这没有任何问题。
比如内网只有A机器使用PPTP协议,A机器首先使用TCP协议发送PPTP控制协议到PPTP服务端,互相交换信息和CallID,
PPTP控制协议成功之后, A机器发送GRE数据包,由于GRE数据包只修改A机器的IP为外网IP,
同时记录下A机器的IP,等PPTP服务器传回GRE数据包到 NAT,NAT接着修改外网IP为A机器的IP,
然后传递给 A机器,这样 GRE数据包就能畅通的穿过 NAT网关程序。

但是如果内网里有多台机器同时使用PPTP呢?
这个时候,PPTP服务端发回来的GRE数据包,就只有外网IP可以识别,可是这个GRE数据包究竟该传给内网哪台机器呢?
显然这种情况下只修改IP是不行了, 因此必须利用CallID达到多机器同时使用PPTP的目的,
而CallID刚好是 2字节大小,完全可以把他们当成假想的TCP或UDP的源端口和目标端口来处理。

CallID有PPTP服务端的CallID和客户端的CallID,但是我们仔细再想想,有没有只修改一个CallID就能达到目的呢,
答案是只需修改客户端的CallID即可。为何这么说呢?
多台内网的机器通过PPTP控制协议,发送Outgoing-Call-Request  数据包报告自己的CallID,
经过NAT网关,内网机器IP全部被修改成同一个外网IP, 因此PPTP服务端只看到一个IP发送了多条 PPTP 控制信息过来,
因此他会根据同一个IP的不同CallID, 给自己分配不同的服务器CallID,如果没这样做,那肯定是PPTP服务器程序的BUG。

可是我们还能不能再懒惰点,连客户端的CallID也不修改呢?
不能再懒惰了,因为内网的多个机器的PPTP协议都是各自为战,自己构造自己的CallID,各个机器之间不会协商。
难免会出现重复的CallID值,而且这种重复概率是相当大,几乎都会重复。

经过上面的简单讨论,
我们可以找到解决PPTP穿透NAT的办法了。
 
首先在 NAT网关程序,监控远端的端口是 1723 的TCP数据包,如果是,说明是PPTP的控制协议,然后分析这个端口下的每个数据包。
如果遇到符合条件的 Outgoing-Call-Request  数据包, 找到是哪个内网机器发送的,内网IP标记为 orginIP,
找到他的原始CallID,标记为 orginCallID,这个是内网机器给自己分配的CallID。
然后在NAT网关程序里分配一个唯一的CallID,我们简单标记为 externelCallID,
然后把externelCallID 与 orginCallID和 orginIP关联保存起来,一般保存为MAP结构。
修改 这个orginCallId为 externelCallID,重新计算TCP和IP的校验码,最后按照NAT处理TCP协议方式正常处理这个数据包。
这还不算完,整个PPTP 的 TCP连接,都必须修改客户端CallID为我们在NAT网关程序里新分配的CallID,直到它断开为止。
除开 Outgoing-Call-Request  数据包,其他PPTP控制协议的数据包,只需要从MAP结构里查找对应的CallID。
需要修改的PPTP控制协议数据包包括 :
 Outgoing-Call-Request 
Outgoing-Call-Reply
 Call-Clear-Request
WAN-Error-Notify
具体请详细查阅 PPTP的RFC文档。
看起来不算多,整个过程只需修改客户端CallID,PPTP服务端的CallID保持不变。这样 PPTP控制协议搞定了。

接着修改GRE数据包的CallID,从PPTP客户端出去的GRE数据包里是服务端的CallID,因此不需要修改CallID。
然后就是从PPTP服务端传回来的GRE数据包,这个时候 客户端CallID就是我们在网关程序里自己构造的 ExternelCallID,
我们通过 在MAP结构里根据这个externelCallID找到对应的内网 orginCallID 和内网机器的IP,
就知道这个GRE数据包发给哪台内网机器了,修改之后传给内网这台机器。 
至此, PPTP被NAT搞定。

附:
GFW会封锁 PPTP协议。
一般防火墙两种工作方式:一种是串接阻断,一种是旁路搞破坏。
串接阻断是最直接的,而且也是无能为力的,除非你不通过他的防火墙,另外拉一条物理线路。
但是串接阻断也有坏处,如果是大型骨干网,承载能力需要非常大的设备才行,如果防火墙出现故障,会影响整个骨干网。
GFW很多时候是采用旁路阻断的方式。这种破坏方式这对TCP连接是非常有效的,
如果他不想让你的TCP连接,随时给你发送一个RST数据包,TCP就得断开。
而且这种方式只是把防火墙并行的挂载到骨干网络上,即使防火墙出现故障也不会影响骨干网。

如果GFW封锁PPTP只是简单的对PPTP控制协议发送RST包来阻断。
这就可以找到一个办法来防止这种阻断,我们在客户端机器开发一个网络过滤驱动,对发来的RST数据包一律屏蔽即可。
很可惜任何防火墙都不是那种只单一使用一种工作方式,他们往往采用多种方式混合来达到防护目的。
 比如一开始它使用旁路方式来分析数据包,如果发现某个IP流量异常或者是应该封锁的,他会采用串接阻断方式直接封锁IP或端口。
 

你可能感兴趣的:(C++,驱动开发)