存档

文章标签 ‘协议分析’

抓取飞信协议数据包的bash脚本

十二月 11th, {2010 9 条评论 8,880 人阅读过  

昨晚想写个抓飞信协议包的脚本,结果刚写了一点就去看电影了,今天把它给完善了一下贴出来,也顺便可以帮助大家更好地理解飞信协议,也为了更多的人能够加入到openfetion的开发中来。

其实不是我不懂“工欲善其事必先利其器”这个道理,只是之前在写openfetion的时候我只能跑到windows下去抓包,windows里面的什么vbs之类的脚本我也不会写,就用最笨的办法用wireshark把数据包抓出来然后复制到txt中,现在在linux下可以抓openfetion的数据包,它的包格式与官方飞信相同(这句是废话,要不然也不可能实现互联互通)。

简单地说一下这个脚本,其实不用说它也很简单,就这么几行,飞信在登录的时候需要向一台sipc服务器注册,随后主要的数据包都是与这台服务器之前交换的,而这台服务器我想肯定也是分布式架构了,不同的飞信号会对应于自己的sipc服务器,而服务器的地址是通过配置文件传递给客户端的,客户端在注册之前先向nav.fetion.com.cn这个地址POST一个http请求,随后服务器会响应一个大的xml,之前版本的openfeion并没有对这些数据做本地缓存,所以在每次登录的时候都需要获取这个配置文件,所以经常会有用户反应卡在“正在下载配置文件”这一步进行不下去了,现在的方法是在本地把需要用到的字段做了缓存,再次登录的时候用缓存版本号查询服务器,如果版本号是最新则会返回一个很小的xml结构,速度当然也会比完全下载快得多。

接着说这个脚本,请求到配置文件之后便会得到飞信号对应的sipc服务器地址,之后就用tcpdump来抓取通往这个地址的包和来自这个地址的包,然后就简单做了一下解析。

#!/bin/sh
#written by @levin108
#This script is used to capture the packets of fetion client,and help to anlysis fetion protocol.
#You need to have openfetion installed in your computer,or maybe libfetion,I don't know,whatever...
 
if [ $UID -ne 0 ]; then
	echo "perminission denied,you must be root to run this script"
	exit 1
fi
 
if [ $# == 0 ]; then
	echo -n "please input the mobile number:"
	read mobileno
else
	mobileno=$1
fi
 
if [ ${#mobileno} -ne 11 ];then
	echo "wrong mobile number"
	exit 1
fi
 
protocol_version="4.0.2510"
config_uri="http://nav.fetion.com.cn/nav/getsystemconfig.aspx"
config_body="<config><user mobile-no=\"$mobileno\"/> \
			<client type=\"PC\" version=\"$protocol_version\" \
			platform=\"W5.1\"/><servers version=\"0\"/> \
			<parameters version=\"0\"/><hints version=\"0\"/></config>"
 
echo "Getting fetion sipc servr address..."
 
config_xml=`curl -d "$config_body" -A "IIC2.0/PC $protocol_version" $config_uri 2> /dev/null`
proxy_endpoint=`echo $config_xml | sed 's/.*<sipc-proxy>\([^<]*\).*/\1/'`
proxy_ip=`echo $proxy_endpoint | sed 's/:[0-9]*$//'`
proxy_port=`echo $proxy_endpoint | sed 's/.*://'`
 
echo "Sipc server address : $proxy_ip:$proxy_port"
echo "Start capturing,now start your fetion client and login."
echo
 
tcpdump -n -A -l -t -s 0 tcp and host $proxy_ip \
	and port $proxy_port or host nav.fetion.com.cn 2>/dev/null | awk '
/^IP/{
	if($NF == 0) {
		is_data_seg = 0;
	} else {
		printf("\n%s ----> %s\n", $2, $4)
		printf("-----------------------------------\n\n")
		# whether it is a data segment
		is_data_seg = 1;
		# eth/ip/tcp header length
		bytes_before_protocol = 52;
	}
}
 
!/^IP /{
	if(is_data_seg) {
		if(length($0) < bytes_before_protocol) {
			bytes_before_protocol -= length($0)
			next;
		}
		if(length($0) >= bytes_before_protocol && 
			bytes_before_protocol != 0) {
			$0 = substr($0, bytes_before_protocol);
			bytes_before_protocol = 0;
		}
		print $0
	}
}
'

分类: Linux 标签: , , ,

近期飞信开发手记(群相关介绍)

八月 14th, {2010 22 条评论 11,514 人阅读过  

实验室项目中期检查结束,两天一夜的超负载工作换来了额外两天的休息时间,时间不敢随意浪费,飞信好久没有更新了,软件总不可能没有bug,用户的需求也不可能有止境,还是得赶紧把这么长时间以来网友提出的问题和要求给解决一下,用了近三天的时间完成了飞信i18n和飞信群功能,宅在宿舍里面写代码时间长了果然会觉得无聊,闲话少说,切入正题,写一下飞信群的开发过程。

另外,现在还不打算发布新版本,还想再测试两天看看,这里就截两个图先展示一下吧

image image

其实飞信群这个功能本来也没几个人在用,分析了一下协议,没有什么特别复杂的地方,很多东西都是套路,首先是在登录的时候获取一系统群相关的信息,如群列表,群成员列表,群详细信息,群个人信息,推荐群,群话题等等,一些没用的信息像推荐群,群话题这些在windows下都不会有人去点的信息就直接给忽略了,登录的过程中发送一系统的获取信息的信令,然后再统一根据 callid从服务器返回的信息中提取各自相关的信息,获取群列表信令如下:

S fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 3
Q: 1 S
N: PGGetGroupList
L: 27

<args><group-list /></args>

获取群详细信息信令如下:

S fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 5
Q: 1 S
N: PGGetGroupInfo
L: 150

<args><groups attributes=”all”><group uri=”sip:PG9777218@fetion.com.cn;p=12205″ /><group uri=”sip:PG31809932@fetion.com.cn;p=12207″ /></groups></args>

获取群成员列表信令如下:

S fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 12
Q: 1 S
N: PGGetGroupMembers
L: 229

<args><groups attributes=”member-uri;member-nickname;member-iicnickname;member-identity;member-t6svcid”><group uri=”sip:PG9777218@fetion.com.cn;p=12205″ member-list-major-version=”" member-list-minor-version=”" /></groups></args>

这些信息发送至服务器这后,服务器会将相关的请求信息推送过来,程序编写时在发送每一条信令都记住其相关的callid,再根据返回信息中的callid来判断其是什么信息,然后再将其解析。

这些信息获取之后就需要订阅每个群的Presence信息,信令如下:

SUB fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 2
Q: 2 SUB
N: PGPresence
L: 215

<args><subscription><groups><group uri=”sip:PG9777218@fetion.com.cn;p=12205″ /></groups><presence><basic attributes=”all” /><member attributes=”all” /><management attributes=”all” /></presence></subscription></args>

之后服务器会将群成员的Presence信息推送过来,信令格式大致如下:

BN 916098834 SIP-C/4.0
N: PresenceV4
I: 1
L: 159
Q: 55 BN
<events><event type=”PresenceChanged”><contacts><c id=”464933706″><pr di=”PCCL030524118392″ b=”400″ d=”" dt=”PC” dc=”137″></pr></c></contacts></event></events>

至此,群相关的信息都已经获取到了,已经可以在界面上展示飞信群了,但要发送群信息还不够,必须还要发起群会话邀请,也就是向sip服务器发送Invite信令,格式如下:

I fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 10
Q: 1 I
T: sip:PG31809932@fetion.com.cn;p=12207
K: text/html-fragment
K: multiparty
K: nudge
K: share-background
K: fetion-show
L: 21

s=session m=message

服务器返回100 trying,之后返回200 OK,收到OK之后再向服务器发送Ack信令:

A fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 9
Q: 1 A
T: sip:PG9777218@fetion.com.cn;p=12205

这样会话邀请就已经完成了,可以发送群信息了。但在这之后我发送群信息的时候却出了一点小问题,每次发送到服务器的Message信息格式完全正确,但服务器返回的是请求失败的信令,想了好久才想明白,其实发起会话邀请这一步并不是做给服务器看的,而是要通过它来注册一个callid,Invite信令的callid必须与Message信令的callid一致,消息才能发送成功,这表示发送的群消息是属于这个会话的,而标识正是在发起邀请时所用的callid,群消息发送的信令如下:

M fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 9
Q: 2 M
T: sip:PG31809932@fetion.com.cn;p=12207
C: text/html-fragment
K: SaveHistory
L: 60

<Font Face=” Color=’-16777216′ Size=’10′>hello</Font>

然后就是群短信:

M fetion.com.cn SIP-C/4.0
F: ×××××××××
I: 25
Q: 1 M
T: sip:PG31809932@fetion.com.cn;p=12207
N: PGSendCatSMS
L: 12

hello fetion

群短信这里的callid就无所谓啦,可以让它使用递增的全局变量,当然也只有群的超级管理员才可以发送群短信,连管理员也不能发,所以我觉得这个功能限制也太多啦。

其实关于飞信群做的事情也就这些,像其它的一些邀请好友加入群,群管理之类的功能我都没做,本来就没有几个人在用这个功能,而且linux下用的就更少了,而且我也确实拿不出再多的时间去整了,就先把基本功能都弄好了让过来提示需求的网友先凑合着吧,如果将来飞信群用的人多了我再想办法后时间来完善它。

另外,我弄这个群比飞信的官方软件里面的群帅的地方就是群成员可以显示头像,当然啦,这些也都是浮云。

DNS解析协议的C语言简单实现

五月 11th, {2010 9 条评论 23,444 人阅读过  

仔细看了看DNS协议的相关东西,其实实际编程的时候根本用不到DNS细节的东西,要获取域名的时候经终端下用host或者nslookup指令就可以,在c里面使用gethostbyname或者getaddrinfo都能很轻松得将dns域名解析为ip地址,写这个纯粹出于个人兴趣,或者说是闲得吧。

在进行域名解析的时候,解析程序向域名服务器发起请求,域名服务器也就是在操作系统网络配置的时候写进去的那个DNS服务器地址,或者也有可能是由ISP提供的自动获取的,原理都一样,域名服务器收到请求后进行处理,首先在本地缓存中查找对应的域名,找到后将IP地址直接返回,找不到就向其它的授权服务器请求数据,又可以分为著名的递归查询和非递归查询。

递归查询就是说自始至终都由一台域名服务器进行查询,它在自己这里找不到的时候会向其它的域名服务器请求并且获取数据,然后返回给请求方。

非递归查询是指域名服务器收到请求后,如果自己有这个域名的信息就返回,如果没有就返回其它域名服务器的指针,请求方再根据这些域名服务器再发起查询。

按自己的理解瞎扯了一通,也不知道准不准确,关于DNS的相关资料网上有的是,中文的都大批大批的。

DNS服务器的原理其实没什么好说的,每天都在跟DNS打交道,但DNS的协议在实现上还是稍微有点意思的,本来想写个程序来测试一个我所了解的DNS协议,后来在写的时候还真发现一个小问题,DNS域名有时候会是一个主域名的别名,比如www.baidu.com,它就是www.a.shifen.com这个域名的别名,在DNS请求发送过去之后,response里面会有一个类型为CNAME的Answers项,里面包含了主域名的相关信息(其实也就是主域名的名称和TTL),在这个应答消息里面可能会出现多个域名消息,比如每个Answers的第一个字段就是一个域名,当然为了减少数据包的容量,DNS系统对域名进行了压缩,同一个域名只会出现一次,其它的时候再出现的话就会用一个DNS指针表示。
比如域名:www.baidu.com在数据包中的表示是 03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00

粗体的是长度,将域名中的点去掉,用长度来分隔域名,以0结束。DNS允许的长度为0-63个字节,所以一个8位的长度最高两位都为0。

而如果此处域名重复出现,信令中便会用DNS指针代替长度,指针为两个字节,16位的最位都为1,剩下的14位表示在在整个数据包中的偏移量,当程序读取到c00c的时候很容易判断它是一个指针而不是一个长度字段,于是根据c00c指向的领移量,即从数据包开始后的第12个字节,跳转过去读取出域名信息。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
 
#define DNS_SVR "211.68.71.4"
 
#define DNS_HOST  0x01
#define DNS_CNAME 0x05
 
int socketfd;
struct sockaddr_in dest;
 
static void 
send_dns_request(const char *dns_name);
 
static void
parse_dns_response();
 
/**
 * Generate DNS question chunk
 */
static void 
generate_question(const char *dns_name
		, unsigned char *buf , int *len);
 
/**
 * Check whether the current byte is 
 * a dns pointer or a length
 */
static int
is_pointer(int in);
 
/**
 * Parse data chunk into dns name
 * @param chunk The complete response chunk
 * @param ptr The pointer points to data
 * @param out This will be filled with dns name
 * @param len This will be filled with the length of dns name
 */
static void
parse_dns_name(unsigned char *chunk , unsigned char *ptr
		, char *out , int *len);
 
int main(int argc , char *argv[]){
 
	if(argc != 2){
		printf("Usage : %s <domain name>\n" , argv[0]);
		exit(-1);
	}
	socketfd = socket(AF_INET , SOCK_DGRAM , 0);
	if(socketfd < 0){
		perror("create socket failed");
		exit(-1);
	}
	bzero(&dest , sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(53);
	dest.sin_addr.s_addr = inet_addr(DNS_SVR);
 
	send_dns_request(argv[1]);
 
	parse_dns_response();
 
	return 0;
}
 
static void parse_dns_response(){
 
	unsigned char buf[1024];
	unsigned char *ptr = buf;
	struct sockaddr_in addr;
	char *src_ip;
	int n , i , flag , querys , answers;
	int type , ttl , datalen , len;
	char cname[128] , aname[128] , ip[20] , *cname_ptr;
	unsigned char netip[4];
	size_t addr_len = sizeof(struct sockaddr_in);
 
	n = recvfrom(socketfd , buf , sizeof(buf) , 0
		, (struct sockaddr*)&addr , &addr_len);
	ptr += 4; /* move ptr to Questions */
	querys = ntohs(*((unsigned short*)ptr));
	ptr += 2; /* move ptr to Answer RRs */
	answers = ntohs(*((unsigned short*)ptr));
	ptr += 6; /* move ptr to Querys */
	/* move over Querys */
	for(i= 0 ; i < querys ; i ++){
		for(;;){
			flag = (int)ptr[0];
			ptr += (flag + 1);
			if(flag == 0)
				break;
		}
		ptr += 4;
	}
	printf("-------------------------------\n");
	/* now ptr points to Answers */
	for(i = 0 ; i < answers ; i ++){
		bzero(aname , sizeof(aname));
		len = 0;
		parse_dns_name(buf , ptr , aname , &len);
		ptr += 2; /* move ptr to Type*/
		type = htons(*((unsigned short*)ptr));
		ptr += 4; /* move ptr to Time to live */
		ttl = htonl(*((unsigned int*)ptr));
		ptr += 4; /* move ptr to Data lenth */
		datalen = ntohs(*((unsigned short*)ptr));
		ptr += 2; /* move ptr to Data*/
		if(type == DNS_CNAME){
			bzero(cname , sizeof(cname));
			len = 0;
			parse_dns_name(buf , ptr , cname , &len);
			printf("%s is an alias for %s\n" , aname , cname);
			ptr += datalen;
		}
		if(type == DNS_HOST){
			bzero(ip , sizeof(ip));
			if(datalen == 4){
				memcpy(netip , ptr , datalen);
				inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));
				printf("%s has address %s\n" , aname , ip);
				printf("\tTime to live: %d minutes , %d seconds\n"
						, ttl / 60 , ttl % 60);
			}
			ptr += datalen;
		}
 
	}
	ptr += 2;
}
 
static void
parse_dns_name(unsigned char *chunk
		, unsigned char *ptr , char *out , int *len){
	int n , alen , flag;
	char *pos = out + (*len);
 
	for(;;){
		flag = (int)ptr[0];
		if(flag == 0)
			break;
		if(is_pointer(flag)){
			n = (int)ptr[1];
			ptr = chunk + n;
			parse_dns_name(chunk , ptr , out , len);
			break;
		}else{
			ptr ++;
			memcpy(pos , ptr , flag);	
			pos += flag;
			ptr += flag;
			*len += flag;
			if((int)ptr[0] != 0){
				memcpy(pos , "." , 1);
				pos += 1;
				(*len) += 1;
			}
		}
	}
 
}
 
static int is_pointer(int in){
	return ((in & 0xc0) == 0xc0);
}
 
static void send_dns_request(const char *dns_name){
 
	unsigned char request[256];
	unsigned char *ptr = request;
	unsigned char question[128];
	int question_len;
 
 
	generate_question(dns_name , question , &question_len);
 
	*((unsigned short*)ptr) = htons(0xff00);
	ptr += 2;
	*((unsigned short*)ptr) = htons(0x0100);
	ptr += 2;
	*((unsigned short*)ptr) = htons(1);
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	memcpy(ptr , question , question_len);
	ptr += question_len;
 
	sendto(socketfd , request , question_len + 12 , 0
	   , (struct sockaddr*)&dest , sizeof(struct sockaddr));
}
 
static void
generate_question(const char *dns_name , unsigned char *buf , int *len){
	char *pos;
	unsigned char *ptr;
	int n;
 
	*len = 0;
	ptr = buf;	
	pos = (char*)dns_name; 
	for(;;){
		n = strlen(pos) - (strstr(pos , ".") ? strlen(strstr(pos , ".")) : 0);
		*ptr ++ = (unsigned char)n;
		memcpy(ptr , pos , n);
		*len += n + 1;
		ptr += n;
		if(!strstr(pos , ".")){
			*ptr = (unsigned char)0;
			ptr ++;
			*len += 1;
			break;
		}
		pos += n + 1;
	}
	*((unsigned short*)ptr) = htons(1);
	*len += 2;
	ptr += 2;
	*((unsigned short*)ptr) = htons(1);
	*len += 2;
}

分类: Protocol 标签: , ,

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 标签: , ,

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 标签: , ,

飞信2010身份验证过程及算法详述

三月 14th, {2010 23 条评论 65,906 人阅读过  

本人还是在校学生,由于滤涉世未深,对互联网还心存敬畏,没能像当初分析2006飞信协议的大哥那样洒脱,出于各种顾虑,当初在写飞信登录协议相关的文章的时候,都没详细地阐述登录过程,那篇文章当初也只是做为自己业余爱好的一个总结,却没想会受到大家如此的关注,很多人留言或发邮件问我具体的身份验证过程及算法,我也无暇一一详细解答,后来想了想,互联网应该是一个开放的环境,大家对飞信的关注其实也是为了给更多人提供方便或者学习交流,我想我也无须吝啬,就在这里把详细地身份验证过程阐述一下,仅用于学习交流,希望大家把它用在正途。

另外,我本人也在用业余时间写了一个Linux环境下的飞信,期待大家的加入,有兴趣的可以在这里留言。

具体身份验证流程我在下面这篇文章里面说得比较详细了,有需要的话可以先阅读一下这篇文章:

http://basiccoder.com/fetion2010-auth-algorithm.html

在里面有两个重要的过程:

1.密码的处理方式。

密码并不是以明文的方式进行加密的,而是用SHA1做了散列之后才进行加密的,散列的过程与2008略有不同,文字表达能力有限,就贴几个C函数吧:

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
char* hash_password_v1(const unsigned char* b0 , int b0len
                                   , const unsigned char* password , int psdlen) 
{
	unsigned char* dst = (unsigned char*)malloc(b0len + psdlen + 1);
	unsigned char tmp[20];
	char* res;
	memset(tmp , 0 , sizeof(tmp));
	memset(dst , 0 , b0len + psdlen + 1);
	memcpy(dst , b0 , b0len);
	memcpy(dst + b0len , password , psdlen);
	SHA_CTX ctx;
	SHA1_Init(&ctx);
	SHA1_Update(&ctx , dst , b0len + psdlen );
	SHA1_Final(tmp , &ctx);
	free(dst);
	res = hextostr(tmp , 20);
	return res;
}
char* hash_password_v2(const char* userid , const char* passwordhex) 
{
	int id = atoi(userid);
	char* res;
	unsigned char* bid = (unsigned char*)(&id);
	unsigned char ubid[4];
	int bpsd_len;
	unsigned char* bpsd = strtohex(passwordhex , &bpsd_len);
	memcpy(ubid , bid , 4);
	res = hash_password_v1(ubid , sizeof(id) , bpsd , bpsd_len);
	free(bpsd);
	return res;
}
char* hash_password_v4(const char* userid , const char* password)
{
	const char* domain = "fetion.com.cn:";
	char *res , *dst;
	unsigned char* udomain = (unsigned char*)malloc(strlen(domain));
	unsigned char* upassword = (unsigned char*)malloc(strlen(password));
	memset(udomain , 0 , strlen(domain));
	memcpy(udomain , (unsigned char*)domain , strlen(domain));
	memset(upassword , 0 , strlen(password));
	memcpy(upassword , (unsigned char*)password , strlen(password));
	res = hash_password_v1(udomain , strlen(domain) , upassword , strlen(password));
	free(udomain);
	free(upassword);
	if(userid == NULL || strlen(userid) == 0)
	{
		return res;
	}
	dst = hash_password_v2(userid , res);
	free(res);
	return dst;
}

第三个函数是用来对密码进行散列处理的函数,输入是userid和密码,这个函数返回一个40字节的16进制字符串。

另外还有两个函数是在unsigned char串和char串之间相互转换:

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
unsigned char* strtohex(const char* in , int* len) 
{
	unsigned char* out = (unsigned char*)malloc(strlen(in)/2 );
	int i = 0 , j = 0 , k = 0 ,length = 0;
	char tmp[3] = { 0 };
	memset(out , 0 , strlen(in) / 2);
	while(i < (int)strlen(in))
	{
		tmp[k++] = in[i++];
		tmp[k] = '\0';
		if(k == 2)
		{
			out[j++] = (unsigned char)strtol(tmp , (char**)NULL , 16);
			k = 0;
			length ++;
		}
	}
	if(len != NULL )
		*len = length;
	return out;
}
char* hextostr(const unsigned char* in , int len) 
{
	char* res = (char*)malloc(len * 2 + 1);
	int i = 0;
	memset(res , 0 , len * 2 + 1);
	while(i < len)
	{
		sprintf(res + i * 2 , "%02x" , in[i]);
		i ++;
	};
	i = 0;
	while(i < (int)strlen(res))
	{
		res[i] = toupper(res[i]);
		i ++;
	};
	return res;
}

然后是计算response过程:

1
2
3
/*这当然是不符合语法的,这样写大家应该就能明白*/
unsigned char* response_tmp = (unsigned char*)nonce
          + strtohex(hashed_password) + strtohex(aedkey);

最后把response_tmp通过服务器发过来的公钥进行RSA加密后得到response,传回服务器就可以通过验证了。

具体的信令格式都是明文,大家自己抓包看就好了,我在这里也就不多说了。

分类: Protocol 标签: , ,