当NAT遇见PPTP

Sina WeiboBaiduLinkedInQQGoogle+RedditEvernote分享




我建了一个PPTP的帐号,想访问一下公司的内网资源,结果发现,没法传输数据,这是怎么回事?于是在Google里面输入rfc pptp,直接就点进去了第一条带有RFC 2637的链接,开始了今天的穿越。。。。。。

问题出在哪里

首先稍微介绍一下,PPTP有两个流,一个是控制流(RFC2637定义),另外一个数据流(GRE,RFC2784)。和一般的ALG不同的是(比如FTP),NAT遇到PPTP的时候,不是端口或者IP惹的祸,而是PPTP里面邪恶的callID(抛一个球给各位,为何PPTP把call ID整到PPTP控制流里面?)。在PPTP协议里面,有一个call ID的概念,客户端向服务器发起连接,会告诉服务端我的call ID是多少,服务端也会回应客户端它的call ID是多少,并且更重要的一点是,这个call ID在以后的某些(注意不是全部哦)控制传输中需要用到,在所有的数据传输中(GRE)需要用到。而接下来我们从两个方面一起来看一下问题是如何产生,以及如何去解决:

  • 从内网向外网发起PPTP连接请求
  • 从外网向内网发起PPTP连接请求(俗称静态映射)

走,咱们去看看外面的世界

(注:下面的结构是1)提一下PPTP协议;2)提一下GRE协议;3)如何搞定GRE数据流;4)如何搞定PPTP控制流。)

PPTP服务器是监听在TCP/1723端口。客户端发送的第一个携带call ID的数据包是被称为Outgoing-Call-Request的数据包,一个示例如下图所示(忽略了前面的以太网头,IP头,以及TCP头):

image

可以看到我们这边的示例客户端的call ID是16384(这个call ID是给自己分配的),那我就以Client-Call-ID表示吧。

服务端收到这个数据包之后,然后会回应一个称为Outgoing-Call-Reply的数据包,示例如下:

image

这个说明服务端自己的call ID是107(这个服务端是给自己分配的,我就以Server-Call-ID表示), 同时也把Client-Call-ID返回给客户端。咦,这样看来好像没有什么问题嘛,反正这个call ID改不改,对于这一条连接都没有影响,NAT也可以正常运作。但是,各位看官,PPTP还需要用到一个协议,那就是GRE(RFC 2784)。而GRE是一个和TCP以及UDP处在同一个水平线的协议。但是和TCP/UDP之流不同的是,GRE里面不携带端口信息,而它利用的就是call ID来作multiplex以及demultiplex的。我们看一下客户端发给服务端的一个GRE数据包(提示一点,GRE头后面的是PPP协议封装,不过这里不需要关心):

image

里面有call ID,这个call ID不是说自己的,而是说对方的(对,就是PPTP里面的那个Server-Call-ID),也就是服务端的,也就是我这个GRE数据要发给服务端的107“端口”。

而服务端也采用同样的策略,只不过call ID表明的是客户端的,一个示例如下:

image

(注:上面的都是一些协议的原理,如果你读了RFC,未必不懂这些。插一个情景,来自一个电影:C开一辆车在公路上,前面有一个人D骑一辆自行车。C在追D的,C紧张的说:“你。。。你骑自行车未必比我开车快!”)

问题是这些个数据包也要经过我们可怜的NAT啊,GRE会问NAT:“我这个call ID,管还是不管?”,NAT答曰:”不管。”话虽如此,但是谈何容易,因为GRE里面没有端口,如果NAT不伪造一个端口的话,下面的情形就不堪设想了:

  • 一个主机A的IP地址是192.168.1.2,向1.1.1.1(声明:这里仅仅是假设,没有对其进行真正的试验,1.1.1.1请不要见怪)发起PPTP连接请求,Client-Call-ID是16384,Server-Call-ID是107;
  • 还有一个主机B在内网,IP地址是192.168.1.3,也向1.1.1.1发起PPTP连接请求,Client-Call-ID2是16385, Server-Call-ID2是108。

NAT如果仅仅是转换IP地址(假设NAT的公网IP是116.238.184.159),那么数据到能够到达1.1.1.1,可是GRE数据包回到116.238.184.159的时候,NAT这个时候是该把它发给192.168.1.2呢,还是192.168.1.3呢?在这种情况下,NAT必须要找一个“端口”,来决定这个数据包是发给谁,而“端口”需要满足以下特性:

  • 在一台主机上,这个是唯一的;
  • 必须每个数据包中都存在。

而扫描整个GRE数据包,只有call ID具有此特性。所以这个关系是这样的,由于GRE数据包里面没有像TCP/UDP的端口信息,所以需要这样一个信息,而call ID的存在恰恰满足了这样一个条件。但是call ID和TCP/UDP端口有不一样的地方:

  • TCP/UDP的端口在传输层中的位置是一样的,相对偏移量是0,而call ID在传输层里面的偏移量是6;
  • TCP/UDP的端口在每个数据包里面都会携带源端口和目的端口,但是call ID仅仅携带一个,并且是指对方的。

意识到这些不同点非常重要,这会影响到NAT对待TCP/UDP和GRE要完全不同,而也正是PPTP ALG的原因之一!

嘿嘿,是否对为何需要针对GRE作特殊处理的原因了吧?好,那我们看如果保证GRE数据包能够顺利通过NAT。

我们知道,有一个NAT会话的概念(不要嫌我罗嗦。。。),里面有以下几个关键的元素:

  • 内网地址(innerip)
  • 内网端口(innerport)
  • NAT公网地址(outerip)
  • NAT公网端口(outerport)
  • 目的地址(destip)
  • 目的端口(destport)
  • 协议(Protocol)

而从内网的数据包,至少需要innerip以及innerport才能NAT出去(请读者思考protocol在什么情况下也需要作为一个依据;destip在symmetric NAT的情况下也需要考虑,不过这里我们就不关心了),而数据包返回的时候需要outerip, outerport。他们两者虽然查找的依据不一样,但是查找到的会话对于一条连接必须是同一个,这是保证通信畅通的前提。为了要使GRE顺利通过NAT,我们以主机A为例就把其分解为从里面到外面,从外面到里面两个方向。

  • 从里面到外面。这个时候,call ID是Server-Call-ID(107)。innerip是192.168.1.2,可以把Server-Call-ID作为innerport,outerip是116.238.184.159,outerport假设是1234(NAT分配的呢)。OK这个数据包可以发送给服务端;
  • 从外面到里面。这个时候,call ID是Client-Call-ID(16384),数据包的目的是116.238.184.159,那么NAT如何将其发送给192.168.1.2?大家可以看到这个时候16384帮不上任何的忙,因为outerport是1234啊。哦,等等,如果再创建一条NAT会话,把16384作为outerport,innerip作为192.168.1.2,innerport随便了(反正不用),这样不就可以发送给192.168.1.2呢?

所以,为了能够使GRE数据顺利来回,需要创建两条GRE的NAT会话,如下:

维护从里面到外面的数据传输(GRE-SESSION-1):

image

维护从外面到里面的数据传输(GRE-SESSION-2):

image

各位看到问题没有,outerport是16384,如果内网还有一台PC也是使用16384的call ID,那么这样就不出现两个outerport为16384的NAT会话呢?呵呵,这个后果很严重。所以outerport需要有NAT自己来分配一个唯一的(比如1244),这样就算内网还有一台PC使用16384,NAT也可以识别。不过既然outerport不是16384了,也就意味着一个非常重要的话题,就是NAT必须要让服务端知道Client-Call-ID是1244,而不是16384,这是PPTP ALG的另外的原因。这个带来的影响:

  • 在PPTP所有的控制包里面,如果牵扯了Client-Call-ID,都需要修改为1244,如果不修改,服务端就会认为Client-Call-ID是16384了;
  • 从服务端过来的GRE数据包中的call ID从1244还原为16384;
  • 从外面到里面的NAT会话(GRE-SESSION-2)需要存储16384,咦,上面不是说Innerport没有用到吗?我们就可以把1244存储在innerport这个位置,不过仅仅是用来作为call ID从1244还原为16384的依据。

更新后的GRE-SESSION-2应该是这个样子的:

image

这样子的话,GRE就是通行无阻了,从里面到外面匹配GRE-SESSION-1,从外面到里面匹配GRE-SESSION-2。接下来我们看看这两条会话是在什么时候生成的以及控制流中需要做的工作。

前面说到,Client-Call-ID在第一个Outgoing-Call-Request里面首次亮相,那么就可以先生成GRE-SESSION-2(获取outerport,即1244),然后根据GRE-SESSION-2中的outerport再把这个数据包中的call ID改成1244,之后就放了它。

收到Outgoing-Call-Request之后,服务端会返回Outgoing-Call-Reply,此时就可以:

  • 生成GRE-SESSION-1;
  • 把里面的Client-Call-ID从1244还原成16384。

在其后的控制连接中,再次强调,NAT你得记住有一点,只要是牵扯到Client-Call-ID,你就要动手脚。如果是从客户端到服务端,那么得将其修改成1244;如果是从服务端到客户端,那么你就得把它还原成16384。而GRE数据流,就可以让它自己匹配GRE-SESSION-1和GRE-SESSION-2了。

哦,回城了

如果是从外面主动发起请求,希望读者能够自己分析一下。给一点点提示:

  • Outgoing-Call-Request里面的Call ID(Client-Call-ID)就不用修改了,因为这个是外面的;
  • 而Outgoing-Call-Reply里面的Server-Call-ID需要修改,因为这个是属于里面的。

结语

造成PPTP需要做ALG的原因,是GRE数据里面没有携带端口,而又只能通过call ID来模拟“端口”作NAT,而由于需要模拟‘端口”,进而需要修改PPTP控制流中的相关call ID。

还有一个特点是,call ID的修改是单方面,而决定其是否要修改的原因就是这个call ID是由内网的机器产生还是由外网的机器产生。

由于call ID是固定2个字节,这个修改在PPTP中只需要更新TCP和IP校验和,不需要处理seq/ack问题;在GRE中只需要关心IP校验和(由于修改了IP地址),因为GRE自己压根就没有校验和。

(7个打分, 平均:5.00 / 5)

雁过留声

“当NAT遇见PPTP”有20个回复

  1. 理客 于 2010-03-13 2:29 下午

    那所有使用GRE的协议的应用都有这个问题,是否可以只要一次解决了GRE的NAT ALG,就可以解决所有基于GRE的应用了呢?

  2. wenlujon 于 2010-03-13 4:51 下午

    其实GRE协议仅仅提供了一个框架,里面的一些域是留给具体实现的时候确定的,就比如GRE标准协议里面没有规定要使用call ID,只不过是将其定义为Key,并且是可选的。而PPTP在实现的时候利用起来了。
    还有一点,PPTP在实现GRE协议的时候还作了一些修改,增加了Acknowledgment Number。

    最后,GRE里面的校验和是可选的,只不过PPTP没有使用。

    所以其他使用到GRE的应用,理论上是不能生效的。
    不过还有多少应用使用了GRE?

  3. HJ 于 2010-03-13 8:39 下午

    NAT这个技术基本上是个麻烦制造者。IPv6快点普及吧。

  4. 理客 于 2010-03-13 11:33 下午

    再向完全IPV6过度的过程中,NAT会越来越重要。
    完全IPV6后,如果一个单位在访问internet的时候想隐藏自己的内部IP,除了NAT,还有什么方法?

  5. HJ 于 2010-03-15 8:22 下午

    为什么你会想要隐藏IP?
    如果是为了安全的话,NAT并不能提供多少附加的安全性,而其代价是一大堆的应用出问题,为了解决这些问题,又得部署各种ALG技术,增加了不必要的复杂性,我个人认为是得不偿失。

  6. HJ 于 2010-03-15 8:26 下午

    另:据我所知,似乎目前IETF中定义各种IPv6 NAT技术都是为了和v4互通,虽然也有NAT66的draft,但是似乎并没有得到多少支持,那个draft已经过期了。

  7. zeroflag 于 2010-03-15 9:06 下午

    HJ
    其实要处理ALG不仅仅是NAT要处理,防火墙也要处理。NAT遇到的问题,状态检测机制的防火墙都会遇到,解决的方法也是基本类似的。并不是不要NAT了,就可以不要ALG。除非将网络上所有的L4~7层的功能都不要了。
    当然,IPV6内嵌IPSEC以后,也基本上不要所有L4~7的东西了。但是这个方向是正确的吗?网络是否就是要工作在三层,而对L4~7层东西完全不加考虑?!

    个人认为IPV6的这个方向是错误的。

  8. HJ 于 2010-03-15 9:20 下午

    我的意思并不是说不用NAT,这些问题就没有了,而是在地址足够的情况下,NAT增加了很多的额外复杂性,但又没带来什么好处。

    当然我也不认为目前的IPsec能够解决所有的问题,全网部署IPsec的代价太大,不现实。网络安全是一个复杂的问题,没有哪个技术能够单独搞定。
    IPv6和IPv4相比,其实只解决了一个地址的问题,其他并没什么本质的变化。运营商其实根本不愿意升级到v6,只不过他们没办法。。。

  9. 理客 于 2010-03-15 10:28 下午

    to HJ: 一个单位上internet,隐藏真实的IP是否是安全的一个重要需求?为什么?如果是,有什么解决方案呢?

  10. HJ 于 2010-03-15 10:51 下午

    我不认为隐藏真实的IP能够增加多少安全性,个人认为现在企业主要担心的黑客攻击有两种:
    1.窃取信息:这种攻击更多是通过email,网页挂马的方式(就像最近google所受到的攻击)进行的,和你用什么样的IP没什么关系。
    2.DoS攻击:DoS攻击一般都是面向外网的服务器,对于这种情况,你是不会用NAT的。如果是面向内网的服务器,你只要在出口路由器上做相应ACL过滤掉外来的流量就可以了,也很简单。

  11. kevin 于 2010-04-29 12:19 上午

    话不可这样讲,就目前的应用规模。如果所有私网地址不加限制就泛滥到公网,公网路由器哪里受得了。

  12. HJ 于 2010-04-29 9:09 下午

    用私网地址的根本原因是因为没有足够的公有地址,如果地址足够,路由根本不是问题,有的是办法。

    这也是虽然IPv6的地址空间比IPv4大了N倍,如今的路由器依然可以处理的原因。

  13. fpeking 于 2010-04-30 5:26 上午

    HJ关于安全的评论太浅了,哈哈
    网络安全发展了这么多年了,早不是那么简单了。诸如DoS/DDoS,针对转发平面的,控制平面的, IDS/IPS,都不是ACL一句话那么简单了。

  14. HJ 于 2010-04-30 8:54 上午

    我说的意思是如果有针对内网的服务器DOS攻击,既然是内网服务器,就是说其只为内网服务,这样的话,只要在路由器上设置好ACL,过滤掉从外网来的发向内网服务器的流量就可以了,根本不需要什么IDS/IPS。

    除非DOS攻击是来自内网(内网的机器被人种了马),那这样的话,和你用什么样的IP就没关系了。

  15. fpeking 于 2010-05-02 6:23 上午

    理解。不过发自内网的攻击往往不是DoS类型的,都进入内网了,还用作DoS的话就实在太可惜了。哈哈

  16. wildlee 于 2010-09-18 7:39 下午

    有点搞不明白,服务器方的id是107,如果出去的时候修改成1234.服务器怎么会认这个数据包呢?是不是在控制包中把服务器通告的id也修改后在告诉客户端,修改成1234,然后在数据包出去的时候,把1234转换成107

  17. wildlee 于 2010-09-29 11:22 下午

    wenlujon 怎么不上线,对于此文我有一些看法,希望你与我联系下,有几处疑问请教谢谢了qq:448997701

  18. 犇犇 于 2011-10-18 10:42 上午

    写的很好的

  19. netsnake 于 2011-11-04 2:34 上午

    wildlee的疑问是有道理的,这个文写的有点问题啊,至少没说清楚。两个方向的报文可以这样处理:对于client发出去的报文,CGN仅仅需要把源地址替换掉(如替换成A),servercallid不用改,然后发给server;收到server返回的并且是含有clientcallid’的报文后,返回给client(CGN需要在pptp通过控制信息建立隧道的阶段就要分配好clientcallid’,并建立clientcallid’与client真实分配的clientcallid的映射关系)。为实现pptp的alg,CGN的session表应该是类似address Dependent的mapping mode,而无需详细记录servercallid。当然,既然是定制的alg,还可以有其他的实现方法。

  20. wenlujon 于 2012-06-20 3:35 上午

    好久没来了,现在已不搞网络了。
    wildlee,正如netsnake所说,107不要修改成(文章确实没写清楚)1234。