存档

2010年4月 的存档

State of Peer-to-Peer Communication across NAT(rfc5128中文)

四月 29th, {2010 1 条评论 10,622 人阅读过  

翻译了一下RFC5128,并为我拙劣的英语水准和汉语言表达技巧感到汗颜,如果有人不幸搜到这篇文章,英语不是太差的话就不要看了。
英文原版地址:http://tools.ietf.org/html/rfc5128

1.Introduction and Scope

当前的Internet已经普遍布署了网络地址转换器(NATs).存在各种各样的NAT设备和各种各样使用NAT设备布署的网络拓扑。由NAT设备创建的非对称的地址和连接形式,给一些p2p应用程序和协议带来了它们特有的问题,像电话会议,多媒体播放器,网络游戏,这些问题甚至有可能持续到IPv6世界,在向IPv6过渡的时期,有些NAT可能需要使只支持IPv4的节点跟只支持IPv6的节点进行通信,尽管这种使用NAT的合适的协议或方法尚未解决。即使是将来的纯IPv6世界可能仍然包括防火墙,它仍然拥有和NAT类似的过滤功能,但没有地址转换功能,这种过滤行为也影响着P2P应用程序的功能。出于这个原因,这篇文章中讨论的NAT转换所使用的技术同样也适用于和带有和NAT转换类似的过滤功能的某些防火墙。

当前部署的NAT设备主要围绕client/server这种模式设计的,在这种模式下匿名,私有网络中的匿名客户端设备使用固定的IP地址和DNS域名访问处于公有网络的服务器。中途遇到的NAT设备为客户端主机动态地分配地址。这种匿名性和NAT设备后面的主机的不可访问性对于像web浏览器这样的应用程序来说并不是问题,web浏览器只需要创建outgoint连接。这种匿名性和不可访问性有时候对于隐私的保护是有利的。

在p2p的模式中,Internet主机通常情况下被称为是“客户端”,这种客户端不仅能够向其它的peer节点发起会话,还能接受来自其它节点发起的会话。发起者和响应者可能处于不同的NAT设备的后面,它们可能都没有永久的IP地址或者其它形式的公有网络属性。举个例子来说,一个普通的网络游戏的结构中,所有参与游戏的应用程序主机和一台公网可寻址的公共服务器交互来进行注册,并且发现其它的peer主机。在与这台公共服务器进行通信之后,主机彼此之间建立直接的连接以在游戏期间提供快速高效的数据更新。类似的,一个文件共享程序可能跟一台公共服务器进行交互进行资源发现和搜索,但却在peer主机之间建立直接连接进行数据传输。NAT设备给p2p的连接带来了麻烦,因为NAT设备后面的主机在Internet上没有固定的公网可见端口,其它的peer也就无法对起发起Incoming TCP或UDP连接。RFC 3234[NAT-APPL]简单地强调了这个问题。

在这篇文章中,我们总结了当前熟知的一些方法,使应用程序工作在NAT设备环境中而不需要直接改变NAT设备。

2. Terminology and Conventions Used

2.1 端点(Endpoint)

一个Endpint是指在一台终端主机上的和会话相关的元组。对于每一种IP协议,endpoint的表示方式也可能不一样。比如,UDP或者TCP会话的endpoint是以(IP地址,UDP/TCP端口号)这种元组的形式表示的。

2.2 端点映射(Endpoint Mapping)
当私有网络中的主机通过NAT设备向公有网络发起一个outgoing的会话,NAT设备分配一个公有的endpoint来转换私有endpoint,于是随后来自外部主机的响应包就可以被NAT接收,转换,并且转发到私有endpoint去。NAT设备将私有endpoint转换成公有endpoint,或者反过来的这种分配就称为是端点映射。NAT使用端点映射在会话的过程中执行转换。

2.3 端点独立映射(Endpoint-Independent Mapping)

“端点独立映射”在 [BEH-UDP]中像下面这样定义:

对于从一个相同的内部IP地址和端口(X:x)发往任何外部IP地址和端口的随后的数据包,NAT会重用端口映射。

2.4 端点依赖性映射(Endpoint-Dependent Mapping)

“端点依赖性映射”是指“地址依赖性映射”和“地址和端口依赖性映射”的组合,在[BEH-UDP]中定义。

地址依赖性映射: (Address-Dependent Mapping)

对于接下来要从同一个内部IP地址和端口(X:x)发往同一个外部地址,不管外部端口号是多少,NAT都会重用端口映射。

地址和端口依赖性映射:(Address and Port-Dependent Mapping)

对于接下来要从同一个同部IP地址和端口(X:x)发往同一个外部地址和端口号的数据包,当映射依然处于活动状态的时候,NAT设备会重用端口映射。

2.5 端点独立的过滤(Endpoint-Independent Filtering)

“端点独立的过滤”在[BEH-UDP]中像下面这样定义:

不管外部IP地址和端口号(Z:z)是多少,NAT都只过滤不发往内部IP地址和端口号(X:x)的数据包。NAT会转发所有发往X:x的数据包。换句话说,从NAT内侧发信外部IP地址的数据包足以让任何数据包发回到内部endpoint。

使用“端点独立映射”和“端点独立过滤”的NAT设备可以接受任何位于公有网络的外部endpoint上的已经映射的端口的incoming流量。

2.6 端点依赖性过滤(Endpoint-Dependent Filtering)

“端点依赖性过滤”是指“地址依赖性过滤”和“地址和端口依赖性过滤”的组合,在[BEH-UDP]中定义

地址依赖性过滤(Address-Dependent Filtering)

NAT过滤不发往内部地址X:x的数据包。并且,如果X:x之前没有发送任何数据包给Y(和Y使用的端口无关),那么NAT设备就会过滤从Y:y发往X:x的数据包。换句话说,要从一个特定的外部endpoint接收数据包,内部endpoint必须先向特定的外部endpoint发送过数据包。

地址和端口依赖性过滤(Address and Port-Dependent Filtering)

NAT过滤不发往内部地址X:x的数据包。并且,如果X:x之前没有发送任何数据包给Y:y,那么NAT设备就会过滤从Y:y发往X:x的数据包。换句话说,要从一个特定的外部endpoint接收数据包,内部endpoint必须先向特定的外部endpoint发送过数据包。

(译者注:区别就仅仅是和外部端点的端口有没有关系)

使用“端点依赖性过滤”的NAT设备只会接受有限的一组公网外部端点的发往一个已经映射的公有端口的incoming流量。(这句话我认为我翻译的很失败,附上原文)

A NAT device employing “Endpoint-Dependent Filtering” will accept
incoming traffic to a mapped public port from only a restricted set
of external endpoints on the public network.

2.7 P2P程序(P2P Application)

P2P程序是指使用同一个endpoint向peer主机发起outgoing会话,并且接收来自peer主机的incoming会话的应用程序。P2P程序可以使用多endpoint进行p2p通信。

2.8 NAT友好的P2P应用程序(NAT-Friendly P2P Application)

NAT友好的P2P程序是指,即使peer节点处于由一个或者多个NAT连接不同的IP地址域中,也能够有效运行的p2p程序。

一种P2P程序建立peer会话并保持NAT友好的通用方法是使用一台公网可寻址的公共服务器用做注册和peer发现。

2.9 端点独立映射NAT(EIM-NAT)

EIM-NAT是使用端点独立映射的NAT设备。EMI-NAT可以有任何过滤行为。BEHAVE-comiliant NAT设备设备就是EIM-NAT设备的很好的例子。使用地址依赖映射的NAT设备就是非EIM-NAT设备的例子。

2.10 发夹(Hairpinning)

发夹在[BEH-UDP]中像下面这样定义:

如果两台主机(称为X1和X2)在同一个NAT设备之后,并且交换流量,NAT可以在它外面为X2分配地址,称为X2′x2′,如果X1向X2′x2′发送流量,它就会到达NAT,然后NAT再将流量从X1传递到X1,这被称为是发夹。

并不是目前所有的NAT设备都支持发夹。

3.P2P程序穿越NAT设备所用到的技术。

这一小节从软件设备者或协议设计者的角度上,详细地回顾了在现有的NAT设备基础上实现p2p通信的一些熟知的技术。

3.1中继

在存在NAT设备的环境中实现p2p通信,最可靠但效率最低的方法是让p2p通过中继,使用看上去像client/server的方式进行通信。考虑图表1中的场景。两个客户端主机,A和B,都向一台公共服务器ServerS发起了TCP或UDP连接,这台公共服务器有一个公网可寻址的IP地址,用来进行注册,发现和中继。NAT设备后面的主机向这台服务器注册。Peer可以发现NAT设备后面的主机,并且使用这台服务器中继端对端的消息。客户端驻留在各自的私有网络里面,它们各自的NAT设备阻止它们直接同对方发起连接。

Registry, Discovery
Combined with Relay
Server S
192.0.2.128:20001
|
+—————————-+—————————-+
| ^ Registry/              ^   ^ Registry/              ^ |
| | Relay-Req Session(A-S) |   | Relay-Req Session(B-S) | |
| | 192.0.2.128:20001      |   |  192.0.2.128:20001     | |
| | 192.0.2.1:62000        |   |  192.0.2.254:31000     | |
|                                                         |
+————–+                                 +————–+
| 192.0.2.1    |                                 | 192.0.2.254  |
|              |                                 |              |
|    NAT A     |                                 |    NAT B     |
+————–+                                 +————–+
|                                                         |
| ^ Registry/              ^   ^ Registry/              ^ |
| | Relay-Req Session(A-S) |   | Relay-Req Session(B-S) | |
| |  192.0.2.128:20001     |   |  192.0.2.128:20001     | |
| |     10.0.0.1:1234      |   |     10.1.1.3:1234      | |
|                                                         |
Client A                                                 Client B
10.0.0.1:1234                                        10.1.1.3:1234

Figure 1: Use of a Relay Server to communicate with peers

两个客户端可以简单地通过Server S在他们之间传递消息,而不是试图进行直接连接。举个例子,要发送一条消息给Client B,Client A只需要在同Server S建立了cs连接之后将消息发给Server S即可,Server S可以通过它和Client B之间已经建立的cs连接将消息转发给Client B。

这种方法的优点在于,只要两个Client都能够连接到Server,这种方法就一直有效。中间的NAT设备也不需要是EIM-NAT.这种方法的明显缺点在于,它消息了Server的处理器能力和网络带宽,同时,即使Server拥有足够的I/O带宽并且处于正确良好的拓扑结构中,client之间的通信延迟也会有所增长。The TURN protocol
[TURN] defines a method of implementing application agnostic,
session-oriented, packet relay in a relatively secure fashion.

3.2 连接逆转(Connection Reversal)

接下来要讨论的进行直接通信的连接逆转技术只有在一个peer处在NAT设备的后面,而另一个peer不处在NAT设备后面时才有效。举个例子,考虑图表2中的场景。client A处在NAT设备的后面,而client B拥有一个公网可寻址的IP地址。公共server S有一个公网可寻址的IP地址,它用做注册和发现。处在NAT后面的设备通过server注册他们的endpoint,peer主机通过这台服务器发现处在NAT后面的主机的endpoint。

Registry and Discovery
Registry and Discovery
Server S
192.0.2.128:20001
|
+—————————-+—————————-+
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| | 192.0.2.128:20001     |     |  192.0.2.128:20001    | |
| | 192.0.2.1:62000       |     |  192.0.2.254:1234     | |
|                                                         |
| ^ P2P Session (A-B)     ^     |  P2P Session (B-A)    | |
| | 192.0.2.254:1234      |     |  192.0.2.1:62000      | |
| | 192.0.2.1:62000       |     v  192.0.2.254:1234     v |
|                                                         |
+————–+                                            |
| 192.0.2.1    |                                            |
|              |                                            |
|    NAT A     |                                            |
+————–+                                            |
|                                                         |
| ^ Registry Session(A-S) ^                               |
| |  192.0.2.128:20001    |                               |
| |     10.0.0.1:1234     |                               |
|                                                         |
| ^ P2P Session (A-B)     ^                               |
| |  192.0.2.254:1234     |                               |
| |     10.0.0.1:1234     |                               |
|                                                         |
Private Client A                                 Public Client B
10.0.0.1:1234                                    192.0.2.254:1234

Figure 2: Connection reversal using Rendezvous server

client A拥有私有地址10.0.0.1,程序使用TCP端口1234. 这台client已经使用公有地址192.0.2.128,端口20001同server S建立了连接。NAT A在它的公有地址192.0.2.1上为client A分配了TCP端口62000作为client A同server S会话的临时公有端点地址。因此,server S认为client A的IP地址为192.0.2.1,使用端口62000。client B有它自己的固定IP地址192.0.2.254,client B上的程序正在监听端口1234以接收TCP连接。

现在假设client B希望同client A直接建立通信会话。B可能首先尝试连接A自己认为拥有的地址,也就是10.0.0.1:1234,或者连接由server S观察到的A的地址,也就是192.0.2.1:62000,在任何情况下连接都会失败。
在第一种情况下,发告往IP地址10.0.0.1的流量会直接被网络丢弃,因为10.0.0.1不是公网可路由的IP地址。在第二种情况下,从B发来的TCP SYN请求包将会到达NAT A并发送到端口62000,但NAT A会拒绝连接请求,因为NAT A只允许outgoint连接。

尝试和A直接建立连接并失败后,B可以使用server S传递一个请求给client A立一个到client B的“逆转”连接。client A在通过S收到这个中继请求后,向client B的公网地址和端口创建一个TCP连接,NAT A允许这个连接通过,因为这个连接是在防火墙的内部发起的,而client B也可以接收到这个连接,因为它不处在NAT设备的后面。

当前各种p2p程序都实现了这种技术。当然,它的最大的限制在于,它仅仅工作在只有一个通信的peer处在NAT设备后面。如果NAT设备是EIM-NAT,public client就可以联系外部的server S来确定可以接收从client A发起连接并允许这些连接的特定端点。如果NAT设备不是EIM-NAT,public client就不能确定哪些端点可以接收由client A发起的连接。在两个端点都处于NAT后面的情况越来越普遍的情况下,这种连接逆转的技术就会失败。连接逆转不是这种p2p连接问题的通用解决办法。如果前向连接和逆转连接都不能建立,程序只能退回去使用其它的机制,比如中继。

3.3 UDP 打孔技术

UDP打孔技术依赖于此EIM-NAT的一些属性,从而允许设计恰当的p2p程序在NAT设备上“打孔”来建立彼此之间的直接连接,即使两个客户端同时处于NAT设备之后也可以。当一台主机不是处在EIM-NAT之后时,peering主机不能获知要给哪个已经映射的endpoint发起连接。进一步说,处在非EIM-NAT设备后面的主机上的程序不能够使用已经建立的endpoint映射和外部目的地址进行通信,打孔技术将会失败。

我们将会考虑两个特定的场景,看看如何设计程序来优雅地处理这两种情况。第一个场景代表最常见的情况,处在两个不同的NAT设备后面的两个client希望直接建立p2p通信。第二种情况下,两个客户端事实上处在同一个NAT后面,但它们自己并不知道。

3.3.1 处在不同的NAT设备之后的Peers

看一下图表3中的场景。client A和client B都拥有私有地址,并且处在不同的NAT设备后面。公共服务器S拥有公网可路由的IP地址,用做注册,发现和有限制的中继。NAT后面的主机在这个Server上注册他们的公共endpoint。peer主机使用这台服务器来发现NAT后面主机的公共endpoint。不像3.1中描述的那样,peer主机只使用server传递连接初始控制信息,而是发送end-to-end信息。

运行在client A和client B上的p2p程序使用UDP端口1234.公共server使用UDP端口20001。A和B各自向server S创建了通信会话,从而分别使NAT A为clinet A和 serverA的会话分配了它的公共UDP端口62000,NAT B为clinet B和server S的会话分配了它的公共端口31000。

Registry and Discovery Combined
with Limited Relay
Server S
192.0.2.128:20001
|
+—————————-+—————————-+
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| | 192.0.2.128:20001     |     |  192.0.2.128:20001    | |
| | 192.0.2.1:62000       |     |  192.0.2.254:31000    | |
|                                                         |
| ^ P2P Session (A-B)     ^     ^  P2P Session (B-A)    ^ |
| | 192.0.2.254:31000     |     |  192.0.2.1:62000      | |
| | 192.0.2.1:62000       |     |  192.0.2.254:31000    | |
|                                                         |
+————–+                                 +————–+
| 192.0.2.1    |                                 | 192.0.2.254  |
|              |                                 |              |
| EIM-NAT A    |                                 | EIM-NAT B    |
+————–+                                 +————–+
|                                                         |
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| |  192.0.2.128:20001    |     |  192.0.2.128:20001    | |
| |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
|                                                         |
| ^ P2P Session (A-B)     ^     ^  P2P Session (B-A)    ^ |
| |  192.0.2.254:31000    |     |  192.0.2.1:62000      | |
| |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
|                                                         |
Client A                                                 Client B
10.0.0.1:1234                                        10.1.1.3:1234

Figure 3: UDP Hole Punching to set up direct connectivity

现在假设client A想和client B直接建立UDP通信会话。如果A简单地发送一个UDP消息到B的公共endpoint 192.0.2.254:31000,B通常会丢弃这些incoming消息(除非它使用了端点独立(Endpoint-Indepentent)的过滤),因为这条消息的源地址和端口号不和S的匹配,而outgoing会话是和S建立的。同样,B简单地发送一个UDP消息到A的公共endpoint,那么NAT A也通常会丢弃这些消息。

假设A开始发送UDP消息到B的公共endpoint,同时通过server S向B传递请求,让B也发送UDP消息到A的公共endpoint。A直接发往B的公共endpoint(192.0.2.254:31000)的outgoint消息将会导致EIM-NAT A在A的私有endpoint和B的公共endpoint之间打开一个新的通信会话。同时,B发往A的公共endpoint(192.0.2.1:62000)的消息会导致EMI-NAT B在B的私有endpoint和B的公共endpoint之音打开一个新的通信会话。一旦在每个方向上都打开了UDP会话之后,client A和client B就可以直接和彼此进行通信,而不会进一步带来server S的负担。帮助NAT后面的peer节点传递连接初始化请求的server S最后被称为peer主机的“中介”节点。

UDP打孔技术有很多有用的属性。一旦两个处在NAT设备之后的peer主机建立了直接的p2p-udp连接,连接的每一部分都可以反过来充当“中介”来帮助其它的部分和其它的peer建立p2p连接,减少了初始中介服务器S的负载。既然上面的过程在即使任一个或者两个client恰好都不处于NAT设备之后的情况下,一样可以很好创建p2p的通信连接,那么程序也就不需要检测它前面的NAT设备的类型。UDP打孔技术甚至在多个NAT的情况下也可以自动地工作,这种情况下一个或者两个客户端在到达公有Internet网络时经过了两级或者更多的地址转换。

3.3.2 处于同一个NAT设备后面的peers

现在考虑这样一个场景:两个客户恰巧处于同一个EMI-NAT后面,因此也处于同一个私有地址空间中,像图表4描述的那样。公共服务器S拥有公网可路由的IP地址用于注册,发现和有限制地中继。处在NAT后面的主机向服务器注册。peer主机通过服务器发现NAT后面的主机,并且通过服务器传递消息。不像3.1节描述的那样,peer主机只传递控制信息,而是传递所有的点对点的消息。

client A已经和server S建立了UDP会话,EIM-NAT也已经给client A分配了公共端口号62000。同样client B也同server S建立了UDP会话,EIM-NAT也为它分配了公共端口号62001。

Registry and Discovery Combined
with Limited Relay
Server S
192.0.2.128:20001
|
^ Registry Session(A-S) ^  | ^ Registry Session(B-S) ^
| 192.0.2.128:20001     |  | |  192.0.2.128:20001    |
| 192.0.2.1:62000       |  | |  192.0.2.1:62001      |
|
+————–+
| 192.0.2.1    |
|              |
|   EIM-NAT    |
+————–+
|
+—————————–+—————————-+
| ^ Registry Session(A-S) ^      ^ Registry Session(B-S) ^ |
| |  192.0.2.128:20001    |      |  192.0.2.128:20001    | |
| |     10.0.0.1:1234     |      |     10.1.1.3:1234     | |
|                                                          |
| ^ P2P Session-try1(A-B) ^      ^ P2P Session-try1(B-A) ^ |
| | 192.0.2.1:62001       |      |  192.0.2.1:62000      | |
| |     10.0.0.1:1234     |      |     10.1.1.3:1234     | |
|                                                          |
| ^ P2P Session-try2(A-B) ^      ^ P2P Session-try2(B-A) ^ |
| |     10.1.1.3:1234     |      |     10.0.0.1:1234     | |
| |     10.0.0.1:1234     |      |     10.1.1.3:1234     | |
|                                                          |
Client A                                                   Client B
10.0.0.1:1234                                         10.1.1.3:1234

Figure 4: Use of local and public endpoints to communicate with peers

假设A和B使用上面所描述的UDP打孔技术通过server S做为中介建立一个通信信道,然后A和B将会获取对方由server S所观察到的公共endpoint。只要NAT设备允许内网主机同其它的内网主机建立转换的UDP会话,而不仅仅是和外部主机建立,两台client就能够彼此之间进行通信。这种情况被称为“Hairpinning”,因为数据包从私有网络到达NAT之后被转换然后又环回到私有网络,而不是转发到公有网络。

举个例子,考虑上面的p2p session-try1。当A发送一个UDP包到B的公有endpoint时,这个包最初拥有源endpoint(10.0.0.1:1234)和目的endpoint(192.0.2.1:2001)。NAT收到这个包然后把它的源endpoint转换成192.0.2.1:62000,目的endpoint转换成10.1.1..3:1234,然后转发给B.

即便NAT设备支持hairpinning,在这种情况下转换和转发的步骤明显是没有必要的,并且增加了A和B之间会话的延迟,同时也增加了NAT的负载。解决的办法是直接转发。下面将会介绍:

当A和B最初通过公共服务器S交换地址信息的时候,他们包含他们自己所看到的IP地址和端口号,以及服务器S所看到的他们的公网IP地址和端口号。客户端同步地向它们知道的对方的任一个地址发送数据包,并且使用第一个连接成功的第址。如果两个客户端处理同一个NAT之后,那么直接发送到他们私有endpoint的数据包很可能先到达,从而实现了不需要NAT参与的直接通信信道。如果两个客户端处在不同的NAT后面,那么直接发往它们私有endpoint的数据包将不会到达,但可客户端将会按我们所期望的使用他们各自的公有endpoint建立连接。既然在不同的NAT的情况下,A发往B的私有endpoint的消息很有可能到达A的私有网络上的不相关的节点,反过来也也一样,那么这些对数据包在某种方式上经过认证是很重要的。

The [ICE] protocol employs this technique effectively, in that
multiple candidate endpoints (both private and public) are
communicated between peering end hosts during an offer/answer
exchange.  Endpoints that offer the most efficient end-to-end
connection(s) are selected eventually for end-to-end data transfer.

3.3.3 被多个NAT分隔的peers

在某些包含NAT设备的拓扑中,在对拓扑结构没有确定的了解的情况下,很难在两个客户端之间建立“最佳的”p2p路由。举个例子,考虑一下图表5中的场景:

Registry and Discovery Combined
with Limited Relay
Server S
192.0.2.128:20001
|
^ Registry Session(A-S) ^ | ^ Registry Session(B-S) ^
| 192.0.2.128:20001     | | | 192.0.2.128:20001     |
| 192.0.2.1:62000       | | | 192.0.2.1:62001       |
|
+————–+
| 192.0.2.1    |
|              |
|  EIM-NAT X   |
| (Supporting  |
| Hairpinning) |
+————–+
|
+—————————-+—————————-+
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| |  192.0.2.128:20001    |     |  192.0.2.128:20001    | |
| |  192.168.1.1:30000    |     |  192.168.1.2:31000    | |
|                                                         |
| ^ P2P Session (A-B)     ^     ^ P2P Session (B-A)     ^ |
| |  192.0.2.1:62001      |     |  192.0.2.1:62000      | |
| |  192.168.1.1:30000    |     |  192.168.1.2:31000    | |
|                                                         |
+————–+                                  +————–+
| 192.168.1.1  |                                  | 192.168.1.2  |
|              |                                  |              |
| EIM-NAT A    |                                  | EIM-NAT B    |
+————–+                                  +————–+
|                                                        |
| ^ Registry Session(A-S) ^    ^ Registry Session(B-S) ^ |
| |  192.0.2.128:20001    |    |  192.0.2.128:20001    | |
| |     10.0.0.1:1234     |    |     10.1.1.3:1234     | |
|                                                        |
| ^ P2P Session (A-B)     ^    ^  P2P Session (B-A)    ^ |
| |  192.0.2.1:62001      |    |  192.0.2.1:62000      | |
| |     10.0.0.1:1234     |    |     10.1.1.3:1234     | |
|                                                        |
Client A                                                  Client B
10.0.0.1:1234                                        10.1.1.3:1234

Figure 5: Use of Hairpinning in setting up direct communication

假设NAT X是由大型的ISP部署的一个EMI-NAT,用来将大量的用户复用到少数的公有IP地址上,NAT A和NAT B是两个ISP的用户独立部署的小型的用户NAT网关,用来将它们的私有家庭网络复用到他们分别由ISP提供的IP地址上。NAT A和NAT B所使用的”公有”IP地址对于ISP的地址域来说是私有的,而相应的,客户端A和客户端B的地址对于NAT A和NAT B来说也是私有的。像上一节一样,服务器S也是用来注册,发现和有限制的中继的。peer主机使用服务器传递连接初始化控制信息,而不是所有的点到点信息。

现在假设client A和client B要建立直接的p2p-udp连接。最佳方法是client A发送信息到client B在NAT B处的公有地址,这个地址在ISP的地址池中是192.168.1.2:31000,client B发送信息到client A在NAT A处的公有地址,也就是192.168.1.2:31000。不幸的是,A和B没有办法获取这些地址,因为server S只能观察到客户端的公有地址,192.0.2.1:62000和192.0.2.1:62001。即使A和B通过某种方式获取了这些地址,也不能保证这些地址一定可用,因为这些在ISP的私有地址池中分配的地址很有可能和客户端的私有地址池中分配的不相关的地址冲突。客户端因此别无选择只能使用它们由S观察到的全局公有endpoint来进行p2p通信,并且依赖NAT X来提供hairpinning。

3.4 TCP打孔技术

在这一节里,我们将会讨论“TCP打孔”技术,用来在一对都处在NAT设备后面的节点之间建立直接的TCP连接。和UDP打洞打孔技术一样,TCP打孔技术也依赖于EIM-NAT的一些属性,允许设计恰当的p2p程序通过在NAT设备上打孔来建立彼此之间的直接连接,即使通信的主机都处在NAT设备之后也一样奏效。这种技术有时也被叫做“TCP同时打开”。

很多TCP会话都以一个endpoint发送SYN数据包,接收方回复一个SYN-ACK数据包开始。然而两个endpoint同时打开TCP会话也是可以的,即同时发给对方一个SYN数据包,然后每一方收到后回复一个ACK数据包。这个过程被称为“TCP同时打开”。然而在很多系统上包括一些NAT设备上,“TCP同时打开”技术并不能正确的执行。如果一个NAT设备从私有网络外部收到一个试图创建incoming TCP连接的TCP SYN数据包,NAT设备通常丢弃这个SYN数据包,或者回复一个TCP RST数据包,以此来拒绝连接。在SYN超时或者连接重置的情况下,程序endpoint就会继续重新发送SYN包,直到对端peer和它做同样的事情。

我们考虑NAT设备支持“TCP同时打开”会话的情况。如果NAT设备认为一个SYN数据包的源endpoint和目的endpoint是和一个活动的TCP连接相关的,那么这个SYN数据包到达NAT设备的时候,NAT设备就会允许这个包通过。特别的,如果一个NAT设备已经看到并且使用相同的地址和端口号转换了一个outgoint连接,它就会认为这个会话是活动的,并且允许incoming SYN包通过。如果client A和client B各自向发起的outgoing TCP连接中另一方超时了,那么每个客户端的SYN包在到达对方的NAT设备之前都通过了自己本地的NAT设备,那么一个TCP连接就会建立起来。

出于下面的原因,这种技术可能并不会一直有效。如果任何一个SYN包到达远程NAT太快(在这个peer节点有机会发送SYN包之前),那么远程NAT设备可能会丢弃这个SYN包或者发送一个RST包来拒绝这个SYN包。这会导致本地NAT设备立即结束这个新的NAT会话,或者开始会话结束的超时等待,在超时结束的时候结束这个会话。即使peer节点继续同步地发起SYN重传尝试,如果一个NAT会话处于一种(end-of-session timeout)会话结束超时状态,一些远程的NAT主机可能允许incoming SYN通过,这就有可能会阻止TCP连接的建立。

事实上,大多数的NAT设备(超过50%)都支持端点独立(Endpoint-Indenptent)映射,并不会发送ICMP错误或者RST包来响应未经同意的incoming SYN包。结果是,对于大多数的TCP连接尝试,TCP同时打开技术在穿越NAT设备的时候并不会生效。

3.5 UDP端口预测

在一些支持端点独立映射的NAT存在的情况下,仍然有一种UDP打孔技术的变种可以用来创建p2p-UDP连接。这种方法有时候被称为”N+1″技术[BIDIR],Takeda[SYM-STUN]详细讨论了这种方法。这种方法分析NAT的行为并尝试预测在将来的会话中它将给分配的公网端口号。分配的公有端口号一般是可预测的,因为大多数的NAT映射端口的分配是按顺序来的。

考虑图表6中的避场景。两个客户端A和B,每一个都处于一个单独的NAT后面,并且同公共服务器S建立了单独的UDP连接。公共服务器有公网可寻址的IP地址,用来注册和发现。NAT后面的主机在服务器上注册他们的endpoint。peer主机通过这台服务器发现NAT后面主机的endpoint。

Registry and Discovery
Server S
192.0.2.128:20001
|
|
+—————————-+—————————-+
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| | 192.0.2.128:20001     |     |  192.0.2.128:20001    | |
| | 192.0.2.1:62000       |     |  192.0.2.254:31000    | |
|                                                         |
| ^ P2P Session (A-B)     ^     ^  P2P Session (B-A)    ^ |
| | 192.0.2.254:31001     |     |  192.0.2.1:62001      | |
| | 192.0.2.1:62001       |     |  192.0.2.254:31001    | |
|                                                         |
+———————+                       +——————–+
| 192.0.2.1           |                       |        192.0.2.254 |
|                     |                       |                    |
|    NAT A            |                       |        NAT B       |
| (Endpoint-Dependent |                       | (Endpoint-Dependent|
|  Mapping)           |                       |  Mapping)          |
+———————+                       +——————–+
|                                                         |
| ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
| |  192.0.2.128:20001    |     |  192.0.2.128:20001    | |
| |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
|                                                         |
| ^ P2P Session (A-B)     ^     ^ P2P Session (B-A)     ^ |
| |  192.0.2.254:31001    |     |  192.0.2.1:62001      | |
| |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
|                                                         |
Client A                                                 Client B
10.0.0.1:1234                                        10.1.1.3:1234

Figure 6: UDP Port Prediction to set up direct connectivity

NAT A为A和S的通信会话分配了它的UDP端口62000,NAT B为B和S之间的会话分配了它的端口31000。通过和服务器S通信,A和B获取了对方由服务器S观察到的公有endpoint。client A现在向地址192.0.2.254的端口31001发送UDP消息(注意端口的增长),client B同时开始向地址192.0.2.1的端口62001发送UDP消息。如果NAT A和NAT B分新的会话连续地分端口,并且如果A-S和B-S的会话建立之后并没有经过太长时间,那么A和B之间的就会建立起来双向的可用通信信道。A发给B的消息将会导致NAT A打开一个新的会话,NAT A将会按我的期望地分配端口号62001,因为62001是它刚才分配给A和S的端口的下一个端口。同样,B发送给A的消息也会导致NAT B打开一个新的会话,NAT B为这个新的会话会配了端口号31001。如果两个客户端都正确地猜到了它们各自对应的NAT分配给新会话的端口号,那么一个双向的UDP通信信道就可以建立起来了。

很明显,很多因素会导致这种策略失败。如果任何一个NAT预测到的端口恰好已经被分配给了一个不相关的会话,那么NAT就会略过这个端口号,那么连接尝试就会失败。如果任何一个NAT有时候或者一直都不是按顺序地分配端口号,那么这种策略也会失败。如果NAT A后面的另一个客户端(不是A)在A同服务器S建立连接之后,并且还发送第一条消息给B之前,打开一个新的outgoing UDP会话到任何外部目的地,那么这个不相关的客户端就会不经意地“偷走”这个我们需要的端口号。因此这种方法在任何一个相关的NAT过载的情况下都可能不会生效。

既然实现了这种策略的程序即使有一个NAT使用了端点独立的映射时仍然需要能够运行,程序就需要预先知道每一个端点所涉及的NAT都是什么类型,并且相应地修改它们的行为,这就提高了算法的复杂度,同时也增大了网络的弱性。最后,端口预测方法在每一个客户端都处在两级或者更多级的NAT之后很少能够工作。

3.6 TCP端口预测

有一种“TCP打孔技术”的变种可以创建穿越依赖性映射NAT的p2p-TCP直接会话。不幸的是,这种方法相比如刚才介绍的UDP端口映射方法相比更加脆弱,对时间更加敏感。首先,预测到的一个NAT可能分配的端口可能是错的。其实,如果任何一个客户端的SYN包到达对方的NAT设备太快,那么远程NAT设备就可能丢掉SYN包或者发送一个RST包,导致本地的NAT设备转而关闭新的会话,接着徒劳地使用相同的端口继续进行失败的SYN重传。

4. Recent Work on NAT Traversal

5. Summary of Observations

5.1  TCP/UDP打孔(TCP/UDP Hole Punching)

TCP/UDP打孔看上去是现存的在两个都处于NAT后面的节点之间建立基于TCP/UDP的p2p通信的最有效方法。在很多种现存的NAT上都广泛使用了这项技术。然而程序在直接通信不能建立的情况下需要随时准备好回归到最简单的中继方式进行通信。

使用TCP/UDP打孔技术有一点需要注册,它只对转换NAT是EIM-NAT的情况生效。当参与路由的NAT设备不是EIM-NAT的时候,程序就不能够重用已经建立的端点映射和不同的外部目的地进行通信,这种技术就会失败。然而现在在Internet中部署的NAT大多数都是EIM-NAT,那就保证了TCP/UDP打孔技术广泛适用。不管怎样,仍然有大量的部署的NAT使用端点依赖性映射,但却不支持TCP/UDP打孔。

5.2 使用端点依赖性映射的NAT(NATs Employing Endpoint-Dependent Mapping)

端点依赖性映射的NAT对于像web浏览器这种cs程序来说并不是问题,它只需要创建outgoing连接。然而在近期像IM和VOIP这样的p2p程序现在广泛应用。使用端点依赖性映射的NAT对p2p程序的一些技术并不适用,像TCP/UDP打孔技术在穿越这些NAT设备的时候就会失败。

5.3 peer发现

应用程序peer可以出现在同一个NAT域内,也可以处于不同的NAT域内。为了让所有的peer发现程序的endpoint,一个程序可以选择把它自己的私有endpoint和公有endpoint到一台公共服务器上注册。

5.4 发夹(Hairpinning)

支持发夹技术有很大的好处,它可以允许EIM-NAT后面的主机和同一个NAT设备之后的其它主机通过它们的公有的,可能被转换的endpoints进行通信。支持发夹技术在多级NAT的场景中第一次部署了大容量的NAT的情况是非常有用的。像3.3.3节描述的那样。处在相同第一级NAT但不同第二级NAT后面的主机无法使用TCP/UDP打孔技术和彼此进行通信。

分类: Protocol 标签: , ,

Peer-to-Peer Architecture

四月 28th, {2010 没有评论 6,843 人阅读过  

阅读了rfc5694,从里面挑了一些概念点的东西翻译了一下,这篇rfc是对p2p体系统结构的一个大体的介绍,没有涉及到具体实现的技术细节。

P2P的定义

We consider a system to be P2P if the elements that form the system
share their resources in order to provide the service the system has
been designed to provide. The elements in the system both provide
services to other elements and request services from other elements.

如果组成系统的元素通过共享他们的资源来提供系统最初设计时所要提供的功能,系统中的元素可以向其它元素提供服务,也可以向其它元素请求服务,我们把这样的系统称为P2P系统。

定义为P2P的系统也有可能有一些例外,有一些系统有一个集中式的注册服务器,这样的系统也可以被称为是P2P服务器。然而,一些系统把节点划分为peer和client,peer可以向其它节点提供服务,也可以向其它节点请求服务,而client只是向其它节点请求服务,而不具备提供服务的功能,如果一个系统把大多数的节点的行为都是client,那么这个系统严格地说不能称为P2P系统。

P2P系统也应该具有其它的一些属性,peers之间能够不能过任何中介进行直接通信,P2P系统应该可以是自组织的,而且拥有非集中式的控制。

DNS

DNS是一个分层的分布式系统,有时候被分类为分层的cs系统,有时候被分类为P2P系统。通过上面的定义来看,其实是DNS并不能算是P2P系统,因为DNS解析器只充当服务请求者的角色,而未充当服务提供者的角色。只有系统中的节点同时充当服务请求者和服务提供者时,系统才能被称为是P2P系统。

BitTorrent

BitTorrent是一种发布文件的协议。一个特定文件的发布所包含的所有的节点组合称为一个群,文件被划分成很多块,一个节点要下载这个文件的话会从群中的其它节点中把这个文件的所有块都下载下来。端点在下载文件块的同时也上传他们已经下载到的文件块,一个同时下载文件块和上传文件块的节点称为吸血虫,当一个节点下载完所有的文件块之后它就不再下载而只提供上传功能,这时候它被称为种子。BitTorrent系统是一种P2P系统,因为它在向其它节点请求服务的时候也向其它节点提供服务。

P2P系统的功能:

P2P系统包含很多功能。下面的功能是独立于P2P系统提供的服务的。他们管理种子如何和系统进行交互。

1.注册功能:节点在加入到P2P系统之前必须获取有效证书。注册功能控制节点的认证和授权。

2.Peer发现功能:为了加入一个p2p系统,一个节点需要和系统中的一个或者多个其它的peer建立连接,peer发现功能允许节点发现系统中的peer以便和他们进行交互。

上面的这两个功能在一些p2p系统中使用集中式的方式提供(比如集中式的注册服务器)。

下面的这些功能依赖于p2p系统所提供的服务。也就是说,不是所有的p2p系统都要实现所有的功能。比如说,一个只用来存储数据的p2p系统可能不需要实现计算功能。再举个例子,一个只用来计算的p2p系统可能不需要实现数据存储功能。同样的,一些功能在某些p2p系统中采用集中式的方式实现。

1.数据检索功能:它处理系统中存储数据的检索。

2.数据存储功能:它处理向系统中存储数据和从系统中取出数据。

3.计算功能:它处理系统所执行的计算,这些计算可能是数据处理或者是实时的媒体处理。

4.消息传输功能:它处理peer之间的消息交换。peer之音可以通过集中式的服务器交换协议信息,也可以在它们之间直接传递协议消息,也可以通过提供应用层路由(师兄常说的overlay)的种子来实现,这些都依赖于功能的实现方式。

P2P系统的分类方法:

p2p系统最有用的分类方法是根据数据检索的方式。也就是,数据检索功能是如何实现的。一个p2p的索引可以是集中式的,本地的,或者分布式的。一个集中式的索引中,一如中央服务器保存着所有peer中数据的引用。本地索引中,每一个种子只保存它自己的数据的索引。分布式的索引,保存着很多节点中数据的引用。Napster,早期的Gnutella和基于分布式散列表(DHT)的系统就分别是集中式,本地式,和分布式的例子。

p2p系统的分类方法很多,不一一列举。

分类: Protocol 标签:

对于飞信文件p2p文件传输的疑惑

四月 27th, {2010 1 条评论 9,412 人阅读过  

近期在写飞信文件传输部分的代码,又分析了一下飞信文件传输的协议。

飞信的文件传输应该有三种模式,p2p,relay和block这三种方式。block我没弄明白是怎么回事,其它两种是比较熟悉的,p2p现在应该算是一种比较成熟的技术了,年轻的时候不知道NAT是怎么回事,以为p2p简单的只需要一台主机充当server的角色,另一台主机充当client的角色,然后client向服务器发送连接(TCP)或者直接发送数据(UDP)就可以了,可当后来知道了在这个主流的IPv4世界里面,NAT的存在给p2p程序带来了很大的麻烦,两台主机之间建立连接并不像想象中地那么简单,数据流必须要能成功地穿越NAT设备,当然也有可能存在疑问,为什么连接一台web服务器的时候不需要考虑NAT的存在呢?因为web服务器一般是位于公网的,它具有一个公网的地址,任何一种NAT都可以允许它隐藏下的内网设备主动访问公网,并且会允许回传的数据流,而p2p程序面临的问题就是两台主机可能同时处于私有网络(无论是同一个私有网络还是不同的私有网络),它们直接建立连接的时候,NAT设备就很有可能会丢掉他们的数据包,导致连接失败,这个时候p2p穿越成为一种必不可少的手段。

在进行p2p穿越之前确定主机与NAT设备的相对位置是一种很好的策略,可以根据它们的相对位置来决定p2p-NAT穿越的具体策略,确定p2p和NAT设备的相对位置实际上是一种很简单的操作,IETF为我们提供了一处轻量级的协议STUN(前两篇文章做了比较详细地说明),之间对STUN这个协议的具体细节并没有了解太多,昨天读了一下rfc,然后写程序测试了一下,后来苦于在国内找不到免费的STUN服务器程序只简单了完成了Binding Request的发送和Binding Response的最简单接收,各个属性的实现原理都无法通过程序来一一检测,后来就突然想到了飞信,飞信的p2p无疑也要进行上面的一系统操作来使两台主机之间建立起p2p连接,那飞信的STUN服务器???猜测始终不如亲眼证实,打开飞信的配置文件(就是那个xml),果然在里面找到了stun-server-urls这一项,它用stun-server用的是stunserver.org,这是一个国外的免费服务器,于是便解决了我一直以来对于飞信文件输过程的一个疑惑,当我在教育网私有网络内向公网发起文件传输请求时,不管我请求的方式是不是p2p,最终进行传输所用的始终都是中继的方式,HTTP中继是一种效率多低的传输方式啊,不考虑HTTP服务器的带宽和负载,单单是上行传输的带宽限制就可以把文件传办理的速率拉低。所以我不明白的是为什么飞信不自己开发一个stun服务器,而是要用stunserver.org这个国外的免费服务器,这样对于教育网的用户来说是很无奈的。

也可能我对飞信了解还不够,但它确实用了stunserver.org,这是我不能理解的。

分类: My Life 标签: , , ,

STUN协议的C程序

四月 26th, {2010 2 条评论 13,786 人阅读过  

写了一个小程序测试STUN协议,只可惜国内没有可用的STUN Server,UDP协议又不能穿透HTTP代理,所以我在教育网内没法测试更多功能了,只能写这么多,发出去的包wireshark显示为STUN,但由于测试用Server是国外的服务器,不加代理没法访问,所以就没办法了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#define STUN_SERVER_IP "132.177.123.13"
#define STUN_SERVER_PORT 3478
 
int socketfd;
 
struct stun_header{
	unsigned short type;
	unsigned short length;
	unsigned int transId1;
	unsigned int transId2;
	unsigned int transId3;
	unsigned int transId4;
};
 
struct mapped_address{
	unsigned padding : 8;
	unsigned family : 8;
	unsigned short port;
	unsigned int address;
};
 
int 
init_socket(){
	socketfd = socket(AF_INET , SOCK_DGRAM , 0);
}
 
int 
stun_rand(){
	srand(time(NULL));
	return rand();
}
 
struct stun_header* 
build_stun_header(unsigned short type){
 
	struct stun_header *header ;
 
	header = (struct stun_header*)malloc(sizeof(struct stun_header));
	header->type = htons(type);
	header->length = 0;
	header->transId1 = stun_rand();
	header->transId2 = stun_rand();
	header->transId3 = stun_rand();
	header->transId4 = stun_rand();
 
	return header;
}
 
int 
udp_send(void *data , int len
		, const char *ip , int port){
 
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(ip);
	addr.sin_port = htons(port);
 
	return sendto(socketfd , data , len , 0
			, (struct sockaddr*)&addr , sizeof(struct sockaddr));
 
}
 
int 
main(int argc , char *argv[]){
 
	init_socket();
	struct stun_header recv_header;
	struct sockaddr_in addr;
	size_t length;
 
	struct stun_header* header
		= build_stun_header(0x0001);
 
	udp_send(header , sizeof(struct stun_header)
			, STUN_SERVER_IP , STUN_SERVER_PORT);
 
	length = sizeof(struct sockaddr);
	recvfrom(socketfd , (void*)&recv_header , sizeof(recv_header) , 0
			, (struct sockaddr*)&addr , &length);
 
	return 0;
}

分类: C/C++ 标签: , , ,

STUN协议简介(部分翻译自rfc3489)

四月 26th, {2010 1 条评论 10,642 人阅读过  

STUN( Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators (NATs))是一种轻量级的协议,它允许应用程序发现它们和公网之间是否存在NATs和防火墙,并确定NATs和防火墙的类型。它也可以让应用程序确定NAT分配给它们的公网IP地址和端口号。STUN是一种client-server的协议,也是一种request-response的协议,STUN的默认端口是3478。

NAT的类型:

Full Cone NAT:

所有从同一个内网IP和端口号发送过来的请求都会被映射成同一个外网IP地址和端口号。而且任何一个外网主机都可以通过这个映射的外网IP和端口号向这台内网主机发关怉。

Restricted Cone:

它也是所有从同一个内网IP和端口号发送过来的请求都会被映射成同一个外网IP和端口号。和Full-Cone NAT不同的是,一个拥有IP地址X的外网主机如果想要给内网主机发送数据包,必须是这台内网主机之前给IP地址X发送过数据包才可以。

Port Restricted Cone:

它和Restricted Cone很相似,只不过它包括端口号,也就是,一台IP地址X和端口P的外网主机想给内网主机发送数据包,必须是这台内网主机之前给这个IP地址X和端口P发送过数据包才可以。

Symmetric:

对称NAT就是,所有从同一个内网IP和端口发送到一个特定的目的IP和端口的请求,都会被映射到同一个IP和端口。如果同一台主机用相同的源地址和端口号发送数据包,但是发往不同的目的地,NAT将会使用不同的映射。进一步说,只有当外网主机收到内网主机发送来的数据包之后才能向内网主机往回发送数据包。

STUN Binding Request使用UDP协议发送到STUN服务器,当Binding Request消息到达服务器的时候它可能经过了一个或者多个NAT。结果是STUN服务器收到的request消息的源IP地址被映射成最靠近STUN服务器的NAT的IP地址,STUN服务器把这个源IP地址和端口号复制到一个Bind Response消息中,通过发送回拥有这个IP地址和端口号的客户端,对于上面提到的所有类型的NAT,这个消息都会到达客户端。

当STUN客户端收到STUN Binding Response消息之后,它会将自己发送Request时bind的本地IP地址和端口号同Response消息中的IP地址端口号进行比较,如果不匹配,就表示客户端正处于一个或者多个NAT的前面。在Full-Cone NAT的情况下,在STUN Response消息中的IP地址和端口是属于公网的,公网上的任何主机都可以使用这个IP地址和端口号向这个应用程序发送数据包,应用程序只需要在刚才发送STUN Binding Request的IP地址和端口上监听即可。

当然,主机可能并不在一个full-core NAT的前面,实际上,它并不知道自己在一个什么类型的NAT的前面。为了确定NAT的类型,客户端使用附加的STUN Binding Request.具体过程是很灵活的,但一般都会像下面这样工作。客户端再发送一个STUN Binding Request,这次发往另一个IP地址,但是使用的是跟上一次同一个源IP地址和源端口号,如果返回的数据包里面的IP地址和端口号和第一次返回的数据包中的不同,客户端就会知道它是在一个对称NAT的前面。客户端为了确定自己是否在一个完全锥形NAT的前面,客户端可以发送一个带有标志的STUN Binding Request,这个标志告诉STUN Server另一个IP地址和端口发送Response,这个IP地址和端口要和刚才收到Request的IP地址和端口不同。换句话说,如果客户端使用x:y的IP地址:端口对向A:B的IP地址:端口对发送Binding Request,STUN Server会使用源IP地址和源端口号为C:D的地址对向X:Y发送Response.如果客户端收到了这个Response,它就知道它是在一个Full-Cone NAT前面。

STUN协议允许客户端请求服务器从收到Request的IP地址往回发Binding Response,但是要使用不同的端口号。这可以用来检查客户端是在Port Restricted Cone NAT的前面还是在Restricted Cone NAT的前面。

STUN 消息是使用大端字节流编码的TLV(type-length-value).所有的STUN消息都以一个STUN头开始,紧跟着STUN的载核数据(Payload)。Payload是一系列的STUN属性集合,它们取决于STUN消息的类型。STUN消息的type可以是Binding Request,Binding Response,Binding ERROR Response, Shared Secret Request,Shared Secrect Response 或 Shared Secret Error Response.

Transaction ID的作用是将请求(Request)和响应(Response)联系起来。长度字段代表STUN Payload数据的整个长度。Shared Secret Requests一直都是承载于TCP之上发送的(实际上,是使用了承载于TCP这上的TLS发送的)。

STUN协议也定义了很多的STUN属性。第一个是MAPPED-ADDRESS属性,它是一个IP地址和端口对,Binding Response里面一直都有它,它代表了服务器在Binding Request中看到的源IP地址和源端口号。还有一个RESPONSE-ADDRESS属性,包含一个IP地址和端口。RESPONSE-ADDRESS可以被放到Binding Request中,它告诉服务器Binding Request将会被发送到哪里。它是可选的,当不填写的时候,Binding Request会被卧发送到Binding Request的源IP地址和源端口号。

第三个属性是CHANGE-REQUEST 属性,它包含了两个flag,这两个flag控制用来发送Response的IP地址和端口号。这两个标志被称为“change IP”和“change port”标志,CHANGE-REQUEST标志只允许在Binding Request中出现,在确定客户端是在Restricted Cone NAT之前还是Port Restricted Cone NAT之前的时候,这两个标志是很有用的。它们指示Server从不同的源地址和源端口发送Binding Response。在Binding Request中CHANGE-REQUEST属性是可选的。

第四个属性是CHANGED-ADDRESS属性,它出现在Binding Response中。如果客户端请求使用”change IP”和”change port”行为,它会通知客户端将会使用的源IP地址和源端口号。

第五个属性是SOURCE-ADDRESS属性,它只出现在Binding Response中,它指示发送Response的源IP地址和源端口号,它在检测两个NAT配置的时候是很有用的。

第五个是USERNAME,它只在Shared Secret Response中出现,忽略了…

第六个属性是ERROR-CODE属性,它出现在Binding Error Response和Shared Secret Error Response中,它指出发生的错误。

忽略了三四个属性…..

消息头:
所有的STUN消息都包含20个字节的消息头:
0                                    1                                  2                                     3
0  1  2  3  4  5  6 7 8  9  0  1  2  3  4  5 6 7 8  9 0  1  2 3 4 5 6 7  8  9  0  1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          STUN Message Type                   |        Message Length                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Transaction ID
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Message Type可以取下面这些值:

0×0001 : Binding Request
0×0101 : Binding Response
0×0111 : Binding Error Response
0×0002 : Shared Secret Request
0×0102 : Shared Secret Response
0×0112 : Shared Secret Error Response

Message Length是载核数据的字节长度,不包含消息头的长度。

Transaction ID是一个128位的标识符,可以随机生成。

消息属性

STUN消息头后面跟着0个或多个属性,所有的属性都是TLV形式的,包含16位的类型,16位的长度,和变长的值。

0                                 1                                  2                                      3
0  1 2  3 4  5 6 7 8  9  0  1 2  3  4  5 6  7 8  9 0  1 2  3  4  5 6  7 8  9 0  1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Type                     |                           Length                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Value ….
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

下面是类型的定义:

0×0001: MAPPED-ADDRESS
0×0002: RESPONSE-ADDRESS
0×0003: CHANGE-REQUEST
0×0004: SOURCE-ADDRESS
0×0005: CHANGED-ADDRESS
0×0006: USERNAME
0×0007: PASSWORD
0×0008: MESSAGE-INTEGRITY
0×0009: ERROR-CODE
0x000a: UNKNOWN-ATTRIBUTES
0x000b: REFLECTED-FROM

MAPPED-ADDRESS

这个属性表示映射的IP地址和端口。

0 1 2 3
0 1  2 3  4  5  6 7  8  9  0 1  2 3  4 5  6 7  8  9  0 1  2 3  4 5  6 7  8 9  0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   x x x x x x x x  |        Family        |                     Port                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                 Address                                                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Family一般都是0×01,表示IPV4

RESPONSE-ADDRESS

这个属性的数据包形式跟MAPPED-ADDRESS相同。

其它的就不列出来了,具体去看rfc就好了

http://www.faqs.org/rfcs/rfc3489.html

分类: Protocol 标签: , ,

HTTP代理原理及Base64编码

四月 14th, {2010 2 条评论 14,075 人阅读过  

有网友留言希望我在飞信的新版本中加上代理功能,这两天折腾了一下,把HTTP代理给加上了,SOCKS代理太麻烦了,暂时应该不会考虑加它了,而且用的人也不多,加不加意义不大了。

写一下HTTP代理的原理,其实原理很简单,就是通过HTTP协议与代理服务器建立连接,协议信令中包含要连接到的远程主机的IP和端口号,如果有需要身份验证的话还需要加上授权信息,服务器收到信令后首先进行身份验证,通过后便与远程主机建立连接,连接成功之后会返回给客户端200,表示验证通过,就这么简单,下面是具体的信令格式:

CONNECT 59.64.128.198:21 HTTP/1.1
Host: 59.64.128.198:21
Proxy-Authorization: Basic bGV2I1TU5OTIz
User-Agent: OpenFetion

其中Proxy-Authorization是身份验证信息,Basic后面的字符串是用户名和密码组合后进行base64编码的结果,也就是对username:password进行base64编码。

其实编码对安全性没什么意义,base64严格意义上都已经不能算是加密了,现在信息安全这么受重视的年代,不需要密钥的加密算法还是叫编码更贴切一些,抓到这种包之后瞬间就可以得到用户名和密码。

HTTP/1.0 200 Connection established

OK,客户端收到收面的信令后表示成功建立连接,接下来要发送给远程主机的数据就可以发送给代理服务器了,代理服务器建立连接后会在根据IP地址和端口号对应的连接放入缓存,收到信令后再根据IP地址和端口号从缓存中找到对应的连接,将数据通过该连接转发出去。

下面说一下base64这种编码方式,它还是很常用的,优点是使用起来简单,缺点是编码效率低劲(每3个字节会编码成4个字节),安全性差(这个刚才已经说过了)。

以前用.NET和java的时候形形色色的编码加密散列都见过也差不多都用过,不过高级语言会提供相应的类,一两句话就可以实现加密解密,当然在C里面也可以用OpenSSL来实现,同样简单,飞信里面也用到了base64编码,就是在2010版本里面要求输入图片验证码的时候用到的,服务器端会将生成的图片验证码图片的二进制数据流进行base64编码,以明文的方式放到xml中发送给客户端,客户端对其进行解码,然后就可以得到验证码图片(还是觉得这个过程有点多此一举)。

今天看了一下base64的编码规则,把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式,这样就形成了4个8位的字节,然后再将每个8位字节转换成10进制,再从编码表中找到对应的字符就可以了,编码的时候3个字节一组进行编码,当不足3个字节的时候,进行完上面的操作后,将剩余的位用0补齐(注意:并不是补到32位),于是2个字节编成3个字节,不足四个字节的用’='补齐,哈,感觉自己写得乱七八糟的,不过这种东西在网上一搜一大片,也没有必要细说了,另外附上刚才写的一个编码的小程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
char* base64_encode(const char *src);
 
int main(int argc , char *argv[])
{
	const char in[] = "levin";
	char *res = base64_encode(in);
	printf("%s\n" , res);
	free(res);
	return 0;
}
char* base64_encode(const char *src){
 
	char* dest;
	char in[4];
	long buf = 0 , tmp = 0;
	int i = 0 , j = 0 , count = 0;
	char table[] = {
	    'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G'
	  , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N'
	  , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U'
	  , 'V' , 'W' , 'X' , 'Y' , 'Z' , 'a' , 'b'
	  , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i'
	  , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p'
	  , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w'
	  , 'x' , 'y' , 'z' , '0' , '1' , '2' , '3'
	  , '4' , '5' , '6' , '7' , '8' , '9' , '+'
	  , '/' , '='
	};
 
	count = strlen(src) / 3 + (strlen(src) % 3 == 0 ? 0 : 1);
	dest = (char*)malloc(count * 4 + 1);
 
	for( ; j < count ; j++){
 
		bzero(in , sizeof(in));
		strncpy(in , src + j * 3 , 3 );
 
		buf = 0 , buf = 0 , i = 0; 
 
		for(; i < strlen(in) ; i++){
			tmp = (long)in[i];
			tmp <<= (16 - i * 8);
			buf |= tmp;
		}
 
		for(i = 0; i < 4 ; i ++){
			if(strlen(in) + 1 > i){
				tmp = buf >> (18 - i * 6);
				tmp &= 0x3F;
				dest[j * 4 + i] = table[tmp];
			}else{
				dest[j * 4 + i] = '=';
			}
		}
	}
	return dest;
}

上面的程序会输出bGV2aW4=,因为最后一组不足3字节,用=补了一个字节。

程序很简单,写它纯粹是为了强化一下对base64编码规则的记忆,解码的就不写了。

我程序里面是直接用OpenSSL来干的,少量数据进行编码的时候一个函数就够了,很方便:

1
EVP_EncodeBlock(out , (unsigned char*)in , strlen(in));

分类: C/C++ 标签: , , ,

UDP并发服务器的设计

四月 7th, {2010 没有评论 10,666 人阅读过  

任务目的:设计一个代理服务器,客户端与该服务器使用承载在UDP之上的SIP信令进行交互,SIP消息体中包裹着FTP信令,由代理服务器代为进行FTP业务连接,将返回数据同样再用SIP信令封装返回组客户端。

客户端发送INVITE信令到服务器,服务器收到INVITE后新建FTP连接,并将FTP 连接socket和SIP dialogId做为一个结构体推入队列,相当于会话ID与FTP连接绑定,来自该会话的信令经过解析后都会通过对应的socket发送到FTP服务器,取回数据后再通过该dialogId回传给客户端。
暂时想到的解决方案:

方法一是参考TFTP服务器的实现,TFTP服务器收到文件下载信令后,会fork一个子进程(或者创建一个thread,结节就先不管了),子进程来负责从磁盘上读出数据,并新创建一个socket绑定到一个ephemeral端口,通过这个端口将数据发送回客户端,由UDP无需创建conneciton就可以直接接收数据,所以收到的文件数据除了传输层的端源端口号不一样外,对客户端来说别无二致。

每收到一条FTP信令新建一个线程或者fork一个子进程来对信令进行处理,这里的FTP信令是指用承载在UDP之上的由SIP封装过的FTP信令,处理完后成在线程或者子进程内部将封装后的数据回传给客户端经。

优点是实现简单。

缺点是该过程不同于TFTP服务器,业务过程中会有信令频繁进行交互,势必会造成线程的大量创建,无形中浪费了巨大的服务器资源,得不偿失。

另一个方法是在服务器中创建一条事件队列,定义一个结构体:

1
2
3
4
struct resource{
	int dialogId;
	int socketId;
};

创建一条resource链表(散列表最好,考虑实现简单暂时先用链表),服务器主线程进行信令监听,每收到一条信令随即放入事件队列中,继续进行信令监听,主线程所做的工作只是将收到的信令推入队列,这样不会造成主线程的阻塞,另外新建一条处理线程,该线程循环地读取队列中的数据,若为空则不做处理,若不为空则处理队头数据,从resource链表中取出对应的ftp socket,通过此socket从FTP服务器取回数据,封装后通过dialogId发送回客户端。

优点是一方面防止服务器监听线程阻塞,另一方面又避免了方法一中的线程大量创建所造成的资源消耗。

缺点是在子线程内部可能会造成阻塞,当收到大量信令,处理线程来不及处理时,客户端便会一直等待服务器返回数据,造成阻塞。

如何在UDP代理服务器中寻找一中折衷的方法是一个难题。通过UDP代理进行连接TCP连接着实是一个问题!!!理想的办法是在首次收到FTP连接请求时创建一个TCP请求,以后的信令解析后扔给这个线程处理,问题是想直接扔给线程是不可能的,给线程传递数据只能通过创建线程时传递线程执行函数的入口参数。

因此只有通过共享数据的方式来达到目的。

解决方法可以参照上面的全局消息队列的方法,将方法二改进定下,把一条处理线程改为多条处理线程,每个线程都与一个FTP连接一一对应,第一次收到FTP连接相关的信令后创建FTP线程,每收到一条连接的信令便创建一个FTP线程,这些线程都时刻检查队列,如果发现其中有交给自己的信令,便取出来处理,
处理完成后将返回数据封装后直接通过队列消息中的dialogId发送回客户端(由于客户端和代理服务器的协议是UDP,所以不需要建立连接)。其实Linux内核中进程调度也是用了类似的方法,只不过我这个简单的多了。

需要注意的一个问题是线程同步问题 ,Linux内核中进程对共享资源的并发访问需要加锁,这里应该也不例外,主线程对消息队列中有写的权限,FTP子线程对消息队列有读写权限,这样就必然要做好线程同步工作,加锁的过程以后再考虑,今天还有别的事。

分类: Protocol 标签: , ,

SVN使用小结(在google code上管理代码)

四月 6th, {2010 3 条评论 11,587 人阅读过  

前几天发布了openfetion的新协议版本,在Ubuntu中文论坛上发了个贴子宣传了一下,Ubuntu在中国果然不愧是拥有最状大的用户群,以前在其它的开源社区发的时候点击率都很低(当然也可能是大家觉得这个很没意思),不过这次Ubuntu论坛里面的网友们让我很感动,大家回馈了丰富的修改意思,也提交了各种现存的bug,还有的网友帮我打了deb包和rpm包,甚是感动,在这里先谢一下,由于bug太多,一个多星期的时候更新了两个版本,本来这个东西就是做出来玩玩的,看来还是有很多人喜欢的,所以我也决定用继续用我的休息时间把它做下去,之后考虑加上文件传输和非移动号码登陆这些有用的功能。

OK,扯远了,这篇文章是要总结一下svn的用法的,很多人发邮件问我关于飞信相关协议的问题,我也都给一一回复了,可也着实花费了不少的时间,后来就写了几篇相关的文章,仍然有人对一些细节不解,没办法,后来我也管不了那么多了,就把飞信给完全开源了,希望我的行为能对祖国的开源事业做一点小小的贡献吧。

突然觉得sourceforge和google code的svn代码管理很有用,以前在服务器上搭过cvs,后来嫌麻烦就一直没用,看了看svn的一些介绍,发现用起来着实简单方便了不少,于是就想用一用了,本来想在实验室服务器上搭一个的,后来想既然开源了就放到网上跟大家共享吧,gg去香港后在实验室上就很不稳定了,偶尔上得去偶尔上不去,于是想放到sf上去,结果sf的svn服务器教育网不加代理访问不了,抓包看了下svn的协议是TCP承载的TLS,只有http的代理,也不知道行不行,懒得研究就用gg了。

又扯了一大堆费话,总结一下svn的基本命令吧。

svn checkout :简写是svn co
example:

$svn checkout https://ofetion.googlecode.com/svn/trunk/ ofetion –username levin108

这个没什么好说的,跟cvs的checkout一个意思,从版本库中取出一个项目的拷贝,输入后会提示输入验证码,验证通过后会提示是否以保存在本地,保存的话以后就不用输入了。

svn commit :简写是svn ci
example:

$svn commit -m “modified some bugs”

将本地项目拷由的修改提交到版本库藏, 不加-m会的话提示一些乱七八糟的东西,没细看。

svn copy :简写svn cp

复制本地项目副本到版本库

svn move (mv), svn delete (rm,del),svn mkdir

故名思义吧,跟copy差不多,懒得说细地写了。这些操作所做的修改都会保存到本地缓存中,执行svn ci后才会同步到版本库。

svn import
example:

$svn import -m “initial import” https://ofetion.googlecode.com/svn/trunk/ ofetion –username levin108

递归地把一个目录下的文件提交到版本库,这个目录提交上去后它里面并没有版本库信息,我觉得应该在执行完这条命令后就应该在所有的目录中创建.svn文件夹,以保存版本库信息的。

所以就只能再把版本库中的项目checkout下来,这样的话各目录中就有了版本控制信息了,就可以进行版本控制操作了。

svn blame
example:

$svn blame Makefile.am

2 levin108 SUBDIRS = src include skin resource
2 levin108
2 levin108 EXTRA_DIST = LICENSE LICENSE.OpenSSL
2 levin108
2 levin108 install: install-recursive
2 levin108 @echo “———————————————————-”;
2 levin108 @echo “| |”;
2 levin108 @echo “| OpenFetion 1.2 by lwp(levin108@gmail.com) |”;
2 levin108 @echo “| |”;
2 levin108 @echo “| OpenFetion a fetion client for linux based on GTK+2.0,|”;
2 levin108 @echo “| using Fetion Protocol Version 4. It supports most |”;
2 levin108 @echo “| useful functions of Mobile Fetion,more important. |”;
2 levin108 @echo “| It\`s small and fast,and is better in look. |”;
2 levin108 @echo “| More information at http://basiccoder.com/openfetion |”;
2 levin108 @echo “———————————————————-”;

显示指定文件(本地的或者远程版本库中的)作者和修订版本信息。 当然看到这个命令的输出的时候才发现原来版本控制软件真的很强大。

svn log
example

$ svn log
————————————————————————
r11 | levin108 | 2010-04-06 19:31:11 +0800 | 1 行
4.5
————————————————————————
r10 | levin108 | 2010-04-06 18:49:41 +0800 | 1 行
test
————————————————————————
r9 | levin108 | 2010-04-06 18:49:18 +0800 | 1 行

显示提交日志信息。

svn status

打印工作拷贝文件和目录的状态,如A,D,U…详细如下:

A 添加
D 删除
U 更新
C 冲突
G 合并

svn list
example:

$ svn list https://ofetion.googlecode.com/svn/trunk/ –username levin108
AUTHORS
COPYING
ChangeLog
INSTALL
LICENSE

列出版本库目录的条目

svn update
example:

$svn update -r7

用版本号为7的版本库项目更新你的本地项目副本,不加-r就默认用最新的版本更新,这个命令很重要。

常用的命令差不多就这些了,其它不常用的我也记不住了,写一下以后自己忘了可以过来查

分类: Linux 标签: ,

写了一个统计代码的小脚本

四月 1st, {2010 1 条评论 6,305 人阅读过  

没怎么写过脚本,想统计一下自己的代码,于是就简单写了个。

玩linux这么久了写过最多的脚本居然还是js,哈,以后得加强之方便的练习了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
 
number=0
count()
{
	for file in *
	do
		if [ -d $file ]; then
			cd $file
			count
			cd ..
		elif echo $file | grep "\.[ch]$" > /dev/null; then
			pot=$(wc -l $file | cut -d" " -f1)
			number=`expr $number + $pot`
			echo $pot $file
		fi
	done
}
count
echo line total:$number

补充一下删除目录下所有隐藏的.swp文件(vi给生成的,vi不小心关闭的时候就会残留下来,烦死了)

1
find . -name "*.swp" -exec rm {} \;

分类: Linux 标签: