山寨:不是最好的,是最适合我们的!欢迎体验山寨 中文版MSDN

Blog @ Blog

当华美的叶片落尽,生命的脉络才历历可见。 -- 聂鲁达

常用链接

统计

积分与排名

BBS

Blog

Web

最新评论

P2P 之 UDP穿透NAT的原理与实现(附源代码)

原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt


论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:
    NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。
    最开始NAT是运行在路由器上的一个功能模块。
   
    最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
    因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
    关于基本的NAT可以参看RFC 1631
   
    另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:
                                Server S1                        
                         18.181.0.31:1235                         
                                      |
          ^  Session 1 (A-S1)      ^      |  
           |  18.181.0.31:1235     |       |  
          v 155.99.25.11:62000  v      |   
                                      |
                                     NAT
                                 155.99.25.11
                                      |
          ^  Session 1 (A-S1)  ^      | 
          |  18.181.0.31:1235   |      | 
          v   10.0.0.1:1234      v      | 
                                      |
                                   Client A
                                10.0.0.1:1234
    有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
    首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。
    一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

    呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
    看看下面的情况:
    Server S1                                     Server S2
 18.181.0.31:1235                              138.76.29.7:1235
        |                                             |
        |                                             |
        +----------------------+----------------------+
                               |
   ^  Session 1 (A-S1)     ^      |      ^  Session 2 (A-S2)      ^
   |  18.181.0.31:1235      |      |       |  138.76.29.7:1235      |
   v 155.99.25.11:62000 v      |       v 155.99.25.11:62000  v
                               |
                            Cone NAT
                          155.99.25.11
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |       |      |  138.76.29.7:1235   |
   v   10.0.0.1:1234      v      |      v   10.0.0.1:1234       v
                                    |
                            Client A
                         10.0.0.1:1234
    接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
    这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)
  
    好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。
    但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。
    那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。
    现在我们来看看一个P2P软件的流程,以下图为例:

                       Server S (219.237.60.1)
                          |
                          |
   +----------------------+----------------------+
   |                                                           |
 NAT A (外网IP:202.187.45.3)           NAT B (外网IP:187.34.1.56)
   |   (内网IP:192.168.0.1)                      | (内网IP:192.168.0.1)
   |                                                           |
Client A  (192.168.0.20:4000)             Client B (192.168.0.10:40000)

    首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
    此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。
    总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
   
    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。
   
    下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。
    程序现在支持三个命令:send , getu , exit
   
    send格式:send username message
    功能:发送信息给username
   
    getu格式:getu
    功能:获得当前服务器用户列表
   
    exit格式:exit
    功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)
       
    代码很短,相信很容易懂,如果有什么问题,可以给我发邮件zhouhuis22@sina.com  或者在CSDN上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。
   
    最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助

P2PServer.c

/* P2P 程序服务端
 * 
 * 文件名:P2PServer.c
 *
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 
*/

#pragma comment(lib, 
"ws2_32.lib")

#include 
"windows.h"
#include 
"..\proto.h"
#include 
"..\Exception.h"

UserList ClientList;

void InitWinSock()
{
 WSADATA wsaData;

 
if (WSAStartup(MAKEWORD(22), &wsaData) != 0)
 
{
  printf(
"Windows sockets 2.2 startup");
  
throw Exception("");
 }

 
else{
  printf(
"Using %s (Status: %s)\n",
   wsaData.szDescription, wsaData.szSystemStatus);
  printf(
"with API versions %d.%d to %d.%d\n\n",
   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
  
 }

}


SOCKET mksock(
int type)
{
 SOCKET sock 
= socket(AF_INET, type, 0);
 
if (sock < 0)
 
{
        printf(
"create socket error");
  
throw Exception("");
 }

 
return sock;
}


stUserListNode GetUser(
char *username)
{
 
for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator
!=ClientList.end();
       
++UserIterator)
 
{
  
if( strcmp( ((*UserIterator)->userName), username) == 0 )
   
return *(*UserIterator);
 }

 
throw Exception("not find this user");
}


int main(int argc, char* argv[])
{
 
try{
  InitWinSock();
  
  SOCKET PrimaryUDP;
  PrimaryUDP 
= mksock(SOCK_DGRAM);

  sockaddr_in local;
  local.sin_family
=AF_INET;
  local.sin_port
= htons(SERVER_PORT); 
  local.sin_addr.s_addr 
= htonl(INADDR_ANY);
  
int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));
  
if(nResult==SOCKET_ERROR)
   
throw Exception("bind error");

  sockaddr_in sender;
  stMessage recvbuf;
  memset(
&recvbuf,0,sizeof(stMessage));

  
// 开始主循环.
  
// 主循环负责下面几件事情:
  
// 一:读取客户端登陆和登出消息,记录客户列表
  
// 二:转发客户p2p请求
  for(;;)
  
{
   
int dwSender = sizeof(sender);
   
int ret = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);
   
if(ret <= 0)
   
{
    printf(
"recv error");
    
continue;
   }

   
else
   
{
    
int messageType = recvbuf.iMessageType;
    
switch(messageType){
    
case LOGIN:
     
{
      
//  将这个用户的信息记录到用户列表中
      printf("has a user login : %s\n", recvbuf.message.loginmember.userName);
      stUserListNode 
*currentuser = new stUserListNode();
      strcpy(currentuser
->userName, recvbuf.message.loginmember.userName);
      currentuser
->ip = ntohl(sender.sin_addr.S_un.S_addr);
      currentuser
->port = ntohs(sender.sin_port);
      
      ClientList.push_back(currentuser);

      
// 发送已经登陆的客户信息
      int nodecount = (int)ClientList.size();
      sendto(PrimaryUDP, (
const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));
      
for(UserList::iterator UserIterator=ClientList.begin();
        UserIterator
!=ClientList.end();
        
++UserIterator)
      
{
       sendto(PrimaryUDP, (
const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); 
      }


      
break;
     }

    
case LOGOUT:
     
{
      
// 将此客户信息删除
      printf("has a user logout : %s\n", recvbuf.message.logoutmember.userName);
      UserList::iterator removeiterator 
= NULL;
      
for(UserList::iterator UserIterator=ClientList.begin();
       UserIterator
!=ClientList.end();
       
++UserIterator)
      
{
       
if( strcmp( ((*UserIterator)->userName), recvbuf.message.logoutmember.userName) == 0 )
       
{
        removeiterator 
= UserIterator;
        
break;
       }

      }

      
if(removeiterator != NULL)
       ClientList.remove(
*removeiterator);
      
break;
     }

    
case P2PTRANS:
     
{
      
// 某个客户希望服务端向另外一个客户发送一个打洞消息
      printf("%s wants to p2p %s\n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);
      stUserListNode node 
= GetUser(recvbuf.message.translatemessage.userName);
      sockaddr_in remote;
      remote.sin_family
=AF_INET;
      remote.sin_port
= htons(node.port); 
      remote.sin_addr.s_addr 
= htonl(node.ip);

      in_addr tmp;
      tmp.S_un.S_addr 
= htonl(node.ip);
      printf(
"the address is %s,and port is %d\n",inet_ntoa(tmp), node.port);

      stP2PMessage transMessage;
      transMessage.iMessageType 
= P2PSOMEONEWANTTOCALLYOU;
      transMessage.iStringLen 
= ntohl(sender.sin_addr.S_un.S_addr);
      transMessage.Port 
= ntohs(sender.sin_port);
                        
      sendto(PrimaryUDP,(
const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));

      
break;
     }

    
    
case GETALLUSER:
     
{
      
int command = GETALLUSER;
      sendto(PrimaryUDP, (
const char*)&command, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

      
int nodecount = (int)ClientList.size();
      sendto(PrimaryUDP, (
const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

      
for(UserList::iterator UserIterator=ClientList.begin();
        UserIterator
!=ClientList.end();
        
++UserIterator)
      
{
       sendto(PrimaryUDP, (
const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); 
      }

      
break;
     }

    }

   }

  }


 }

 
catch(Exception &e)
 
{
  printf(e.GetMessage());
  
return 1;
 }


 
return 0;
}



/* P2P 程序客户端
 * 
 * 文件名:P2PClient.c
 *
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 
*/


#pragma comment(lib,
"ws2_32.lib")

#include 
"windows.h"
#include 
"..\proto.h"
#include 
"..\Exception.h"
#include 
<iostream>
using namespace std;

UserList ClientList;

 

#define COMMANDMAXC 256
#define MAXRETRY    5

SOCKET PrimaryUDP;
char UserName[10];
char ServerIP[20];

bool RecvedACK;

void InitWinSock()
{
 WSADATA wsaData;

 
if (WSAStartup(MAKEWORD(22), &wsaData) != 0)
 
{
  printf(
"Windows sockets 2.2 startup");
  
throw Exception("");
 }

 
else{
  printf(
"Using %s (Status: %s)\n",
   wsaData.szDescription, wsaData.szSystemStatus);
  printf(
"with API versions %d.%d to %d.%d\n\n",
   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
 }

}


SOCKET mksock(
int type)
{
 SOCKET sock 
= socket(AF_INET, type, 0);
 
if (sock < 0)
 
{
        printf(
"create socket error");
  
throw Exception("");
 }

 
return sock;
}


stUserListNode GetUser(
char *username)
{
 
for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator
!=ClientList.end();
       
++UserIterator)
 
{
  
if( strcmp( ((*UserIterator)->userName), username) == 0 )
   
return *(*UserIterator);
 }

 
throw Exception("not find this user");
}


void BindSock(SOCKET sock)
{
 sockaddr_in sin;
 sin.sin_addr.S_un.S_addr 
= INADDR_ANY;
 sin.sin_family 
= AF_INET;
 sin.sin_port 
= 0;
 
 
if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)
  
throw Exception("bind error");
}


void ConnectToServer(SOCKET sock,char *username, char *serverip)
{
 sockaddr_in remote;
 remote.sin_addr.S_un.S_addr 
= inet_addr(serverip);
 remote.sin_family 
= AF_INET;
 remote.sin_port 
= htons(SERVER_PORT);
 
 stMessage sendbuf;
 sendbuf.iMessageType 
= LOGIN;
 strncpy(sendbuf.message.loginmember.userName, username, 
10);

 sendto(sock, (
const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote,sizeof(remote));

 
int usercount;
 
int fromlen = sizeof(remote);
 
int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
 
if(iread<=0)
 
{
  
throw Exception("Login error\n");
 }


 
// 登录到服务端后,接收服务端发来的已经登录的用户的信息
 cout<<"Have "<<usercount<<" users logined server:"<<endl;
 
for(int i = 0;i<usercount;i++)
 
{
  stUserListNode 
*node = new stUserListNode;
  recvfrom(sock, (
char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
  ClientList.push_back(node);
  cout
<<"Username:"<<node->userName<<endl;
  in_addr tmp;
  tmp.S_un.S_addr 
= htonl(node->ip);
  cout
<<"UserIP:"<<inet_ntoa(tmp)<<endl;
  cout
<<"UserPort:"<<node->port<<endl;
  cout
<<""<<endl;
 }

}


void OutputUsage()
{
 cout
<<"You can input you command:\n"
  
<<"Command Type:\"send\",\"exit\",\"getu\"\n"
  
<<"Example : send Username Message\n"
  
<<"          exit\n"
  
<<"          getu\n"
  
<<endl;
}


/* 这是主要的函数:发送一个消息给某个用户(C)
 *流程:直接向某个用户的外网IP发送消息,如果此前没有联系过
 *      那么此消息将无法发送,发送端等待超时。
 *      超时后,发送端将发送一个请求信息到服务端,
 *      要求服务端发送给客户C一个请求,请求C给本机发送打洞消息
 *      以上流程将重复MAXRETRY次
 
*/

bool SendMessageTo(char *UserName, char *Message)
{
 
char realmessage[256];
 unsigned 
int UserIP;
 unsigned 
short UserPort;
 
bool FindUser = false;
 
for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator
!=ClientList.end();
      
++UserIterator)
 
{
  
if( strcmp( ((*UserIterator)->userName), UserName) == 0 )
  
{
   UserIP 
= (*UserIterator)->ip;
   UserPort 
= (*UserIterator)->port;
   FindUser 
= true;
  }

 }


 
if(!FindUser)
  
return false;

 strcpy(realmessage, Message);
 
for(int i=0;i<MAXRETRY;i++)
 
{
  RecvedACK 
= false;

  sockaddr_in remote;
  remote.sin_addr.S_un.S_addr 
= htonl(UserIP);
  remote.sin_family 
= AF_INET;
  remote.sin_port 
= htons(UserPort);
  stP2PMessage MessageHead;
  MessageHead.iMessageType 
= P2PMESSAGE;
  MessageHead.iStringLen 
= (int)strlen(realmessage)+1;
  
int isend = sendto(PrimaryUDP, (const char *)&MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));
  isend 
= sendto(PrimaryUDP, (const char *)&realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));
  
  
// 等待接收线程将此标记修改
  for(int j=0;j<10;j++)
  
{
   
if(RecvedACK)
    
return true;
   
else
    Sleep(
300);
  }


  
// 没有接收到目标主机的回应,认为目标主机的端口映射没有
  
// 打开,那么发送请求信息给服务器,要服务器告诉目标主机
  
// 打开映射端口(UDP打洞)
  sockaddr_in server;
  server.sin_addr.S_un.S_addr 
= inet_addr(ServerIP);
  server.sin_family 
= AF_INET;
  server.sin_port 
= htons(SERVER_PORT);
 
  stMessage transMessage;
  transMessage.iMessageType 
= P2PTRANS;
  strcpy(transMessage.message.translatemessage.userName, UserName);

  sendto(PrimaryUDP, (
const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));
  Sleep(
100);// 等待对方先发送信息。
 }

 
return false;
}



// 解析命令,暂时只有exit和send命令
// 新增getu命令,获取当前服务器的所有用户
void ParseCommand(char * CommandLine)
{
 
if(strlen(CommandLine)<4)
  
return;
 
char Command[10];
 strncpy(Command, CommandLine, 
4);
 Command[
4]='\0';

 
if(strcmp(Command,"exit")==0)
 
{
  stMessage sendbuf;
  sendbuf.iMessageType 
= LOGOUT;
  strncpy(sendbuf.message.logoutmember.userName, UserName, 
10);
  sockaddr_in server;
  server.sin_addr.S_un.S_addr 
= inet_addr(ServerIP);
  server.sin_family 
= AF_INET;
  server.sin_port 
= htons(SERVER_PORT);

  sendto(PrimaryUDP,(
const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&server, sizeof(server));
  shutdown(PrimaryUDP, 
2);
  closesocket(PrimaryUDP);
  exit(
0);
 }

 
else if(strcmp(Command,"send")==0)
 
{
  
char sendname[20];
  
char message[COMMANDMAXC];
  
int i;
  
for(i=5;;i++)
  
{
   
if(CommandLine[i]!=' ')
    sendname[i
-5]=CommandLine[i];
   
else
   
{
    sendname[i
-5]='\0';
    
break;
   }

  }

  strcpy(message, 
&(CommandLine[i+1]));
  
if(SendMessageTo(sendname, message))
   printf(
"Send OK!\n");
  
else 
   printf(
"Send Failure!\n");
 }

 
else if(strcmp(Command,"getu")==0)
 
{
  
int command = GETALLUSER;
  sockaddr_in server;
  server.sin_addr.S_un.S_addr 
= inet_addr(ServerIP);
  server.sin_family 
= AF_INET;
  server.sin_port 
= htons(SERVER_PORT);

  sendto(PrimaryUDP,(
const char*)&command, sizeof(command), 0, (const sockaddr *)&server, sizeof(server));
 }

}


// 接受消息线程
DWORD WINAPI RecvThreadProc(LPVOID lpParameter)
{
 sockaddr_in remote;
 
int sinlen = sizeof(remote);
 stP2PMessage recvbuf;
 
for(;;)
 
{
  
int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf), 0, (sockaddr *)&remote, &sinlen);
  
if(iread<=0)
  
{
   printf(
"recv error\n");
   
continue;
  }

  
switch(recvbuf.iMessageType)
  
{
  
case P2PMESSAGE:
   
{
    
// 接收到P2P的消息
    char *comemessage= new char[recvbuf.iStringLen];
    
int iread1 = recvfrom(PrimaryUDP, comemessage, 2560, (sockaddr *)&remote, &sinlen);
    comemessage[iread1
-1= '\0';
    
if(iread1<=0)
     
throw Exception("Recv Message Error\n");
    
else
    
{
     printf(
"Recv a Message:%s\n",comemessage);
     
     stP2PMessage sendbuf;
     sendbuf.iMessageType 
= P2PMESSAGEACK;
     sendto(PrimaryUDP, (
const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));
    }


    delete []comemessage;
    
break;

   }

  
case P2PSOMEONEWANTTOCALLYOU:
   
{
    
// 接收到打洞命令,向指定的IP地址打洞
    printf("Recv p2someonewanttocallyou data\n");
    sockaddr_in remote;
    remote.sin_addr.S_un.S_addr 
= htonl(recvbuf.iStringLen);
    remote.sin_family 
= AF_INET;
    remote.sin_port 
= htons(recvbuf.Port);

    
// UDP hole punching
    stP2PMessage message;
    message.iMessageType 
= P2PTRASH;
    sendto(PrimaryUDP, (
const char *)&message, sizeof(message), 0, (const sockaddr*)&remote, sizeof(remote));
                
    
break;
   }

  
case P2PMESSAGEACK:
   
{
    
// 发送消息的应答
    RecvedACK = true;
    
break;
   }

  
case P2PTRASH:
   
{
    
// 对方发送的打洞消息,忽略掉。
    
//do nothing 
    printf("Recv p2ptrash data\n");
    
break;
   }

  
case GETALLUSER:
   
{
    
int usercount;
    
int fromlen = sizeof(remote);
    
int iread = recvfrom(PrimaryUDP, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
    
if(iread<=0)
    
{
     
throw Exception("Login error\n");
    }

    
    ClientList.clear();

    cout
<<"Have "<<usercount<<" users logined server:"<<endl;
    
for(int i = 0;i<usercount;i++)
    
{
     stUserListNode 
*node = new stUserListNode;
     recvfrom(PrimaryUDP, (
char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
     ClientList.push_back(node);
     cout
<<"Username:"<<node->userName<<endl;
     in_addr tmp;
     tmp.S_un.S_addr 
= htonl(node->ip);
     cout
<<"UserIP:"<<inet_ntoa(tmp)<<endl;
     cout
<<"UserPort:"<<node->port<<endl;
     cout
<<""<<endl;
    }

    
break;
   }

  }

 }

}



int main(int argc, char* argv[])
{
 
try
 
{
  InitWinSock();
  
  PrimaryUDP 
= mksock(SOCK_DGRAM);
  BindSock(PrimaryUDP);

  cout
<<"Please input server ip:";
  cin
>>ServerIP;

  cout
<<"Please input your name:";
  cin
>>UserName;

  ConnectToServer(PrimaryUDP, UserName, ServerIP);

  HANDLE threadhandle 
= CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);
  CloseHandle(threadhandle);
  OutputUsage();

  
for(;;)
  
{
   
char Command[COMMANDMAXC];
   gets(Command);
   ParseCommand(Command);
  }

 }

 
catch(Exception &e)
 
{
  printf(e.GetMessage());
  
return 1;
 }

 
return 0;
}



/* 异常类
 *
 * 文件名:Exception.h
 *
 * 日期:2004.5.5
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 
*/


#ifndef __HZH_Exception__
#define __HZH_Exception__

#define EXCEPTION_MESSAGE_MAXLEN 256
#include 
"string.h"

class Exception
{
private:
 
char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];
public:
 Exception(
char *msg)
 
{
  strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);
 }


 
char *GetMessage()
 
{
  
return m_ExceptionMessage;
 }

}
;

#endif


/* P2P 程序传输协议
 * 
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 
*/


#pragma once
#include 
<list>

// 定义iMessageType的值
#define LOGIN 1
#define LOGOUT 2
#define P2PTRANS 3
#define GETALLUSER  4

// 服务器端口
#define SERVER_PORT 2280

// Client登录时向服务器发送的消息
struct stLoginMessage
{
 
char userName[10];
 
char password[10];
}
;

// Client注销时发送的消息
struct stLogoutMessage
{
 
char userName[10];
}
;

// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息
struct stP2PTranslate
{
 
char userName[10];
}
;

// Client向服务器发送的消息格式
struct stMessage
{
 
int iMessageType;
 union _message
 
{
  stLoginMessage loginmember;
  stLogoutMessage logoutmember;
  stP2PTranslate translatemessage;
 }
message;
}
;

// 客户节点信息
struct stUserListNode
{
 
char userName[10];
 unsigned 
int ip;
 unsigned 
short port;
}
;

// Server向Client发送的消息
struct stServerToClient
{
 
int iMessageType;
 union _message
 
{
  stUserListNode user;
 }
message;

}
;

//======================================
// 下面的协议用于客户端之间的通信
//======================================
#define P2PMESSAGE 100               // 发送消息
#define P2PMESSAGEACK 101            // 收到消息的应答
#define P2PSOMEONEWANTTOCALLYOU 102  // 服务器向客户端发送的消息
                                     
// 希望此客户端发送一个UDP打洞包
#define P2PTRASH        103          // 客户端发送的打洞包,接收端应该忽略此消息

// 客户端之间发送消息格式
struct stP2PMessage
{
 
int iMessageType;
 
int iStringLen;         // or IP address
 unsigned short Port; 
}
;

using namespace std;
typedef list
<stUserListNode *> UserList;




posted on 2008-01-24 23:25 isabc 阅读(2706) 评论(2)  编辑 收藏 引用

评论

# re: P2P 之 UDP穿透NAT的原理与实现(附源代码) 2008-01-25 10:52 isabc

Network Working Group K. Egevang
Request for Comments: 1631 Cray Communications
Category: Informational P. Francis
NTT
May 1994

The IP Network Address Translator (NAT)

Status of this Memo

This memo provides information for the Internet community. This memo
does not specify an Internet standard of any kind. Distribution of
this memo is unlimited.

Abstract

The two most compelling problems facing the IP Internet are IP
address depletion and scaling in routing. Long-term and short-term
solutions to these problems are being developed. The short-term
solution is CIDR (Classless InterDomain Routing). The long-term
solutions consist of various proposals for new internet protocols
with larger addresses.

It is possible that CIDR will not be adequate to maintain the IP
Internet until the long-term solutions are in place. This memo
proposes another short-term solution, address reuse, that complements
CIDR or even makes it unnecessary. The address reuse solution is to
place Network Address Translators (NAT) at the borders of stub
domains. Each NAT box has a table consisting of pairs of local IP
addresses and globally unique addresses. The IP addresses inside the
stub domain are not globally unique. They are reused in other
domains, thus solving the address depletion problem. The globally
unique IP addresses are assigned according to current CIDR address
allocation schemes. CIDR solves the scaling problem. The main
advantage of NAT is that it can be installed without changes to
routers or hosts. This memo presents a preliminary design for NAT,
and discusses its pros and cons.

Acknowledgments

This memo is based on a paper by Paul Francis (formerly Tsuchiya) and
Tony Eng, published in Computer Communication Review, January 1993.
Paul had the concept of address reuse from Van Jacobson.

Kjeld Borch Egevang edited the paper to produce this memo and
introduced adjustment of sequence-numbers for FTP. Thanks to Jacob
Michael Christensen for his comments on the idea and text (we thought

for a long time, we were the only ones who had had the idea).

1. Introduction

The two most compelling problems facing the IP Internet are IP
address depletion and scaling in routing. Long-term and short-term
solutions to these problems are being developed. The short-term
solution is CIDR (Classless InterDomain Routing) [2]. The long-term
solutions consist of various proposals for new internet protocols
with larger addresses.

Until the long-term solutions are ready an easy way to hold down the
demand for IP addresses is through address reuse. This solution takes
advantage of the fact that a very small percentage of hosts in a stub
domain are communicating outside of the domain at any given time. (A
stub domain is a domain, such as a corporate network, that only
handles traffic originated or destined to hosts in the domain).
Indeed, many (if not most) hosts never communicate outside of their
stub domain. Because of this, only a subset of the IP addresses
inside a stub domain, need be translated into IP addresses that are
globally unique when outside communications is required.

This solution has the disadvantage of taking away the end-to-end
significance of an IP address, and making up for it with increased
state in the network. There are various work-arounds that minimize
the potential pitfalls of this. Indeed, connection-oriented protocols
are essentially doing address reuse at every hop.

The huge advantage of this approach is that it can be installed
incrementally, without changes to either hosts or routers. (A few
unusual applications may require changes). As such, this solution can
be implemented and experimented with quickly. If nothing else, this
solution can serve to provide temporarily relief while other, more
complex and far-reaching solutions are worked out.

2. Overview of NAT

The design presented in this memo is called NAT, for Network Address
Translator. NAT is a router function that can be configured as shown
in figure 1. Only the stub border router requires modifications.

NAT's basic operation is as follows. The addresses inside a stub
domain can be reused by any other stub domain. For instance, a single
Class A address could be used by many stub domains. At each exit
point between a stub domain and backbone, NAT is installed. If there
is more than one exit point it is of great importance that each NAT
has the same translation table.

\ | / . /
+---------------+ WAN . +-----------------+/
|Regional Router|----------------------|Stub Router w/NAT|---
+---------------+ . +-----------------+\
. | \
. | LAN
. ---------------
Stub border

Figure 1: NAT Configuration

For instance, in the example of figure 2, both stubs A and B
internally use class A address 10.0.0.0. Stub A's NAT is assigned the
class C address 198.76.29.0, and Stub B's NAT is assigned the class C
address 198.76.28.0. The class C addresses are globally unique no
other NAT boxes can use them.

\ | /
+---------------+
|Regional Router|
+---------------+
WAN | | WAN
| |
Stub A .............|.... ....|............ Stub B
| |
{s=198.76.29.7,^ | | v{s=198.76.29.7,
d=198.76.28.4}^ | | v d=198.76.28.4}
+-----------------+ +-----------------+
|Stub Router w/NAT| |Stub Router w/NAT|
+-----------------+ +-----------------+
| |
| LAN LAN |
------------- -------------
| |
{s=10.33.96.5, ^ | | v{s=198.76.29.7,
d=198.76.28.4}^ +--+ +--+ v d=10.81.13.22}
|--| |--|
/____\ /____\
10.33.96.5 10.81.13.22

Figure 2: Basic NAT Operation

When stub A host 10.33.96.5 wishes to send a packet to stub B host
10.81.13.22, it uses the globally unique address 198.76.28.4 as
destination, and sends the packet to it's primary router. The stub
router has a static route for net 198.76.0.0 so the packet is
forwarded to the WAN-link. However, NAT translates the source address
10.33.96.5 of the IP header with the globally unique 198.76.29.7

before the package is forwarded. Likewise, IP packets on the return
path go through similar address translations.

Notice that this requires no changes to hosts or routers. For
instance, as far as the stub A host is concerned, 198.76.28.4 is the
address used by the host in stub B. The address translations are
completely transparent.

Of course, this is just a simple example. There are numerous issues
to be explored. In the next section, we discuss various aspects of
NAT.

3. Various Aspects of NAT

3.1 Address Spaces

Partitioning of Reusable and Non-reusable Addresses

For NAT to operate properly, it is necessary to partition the IP
address space into two parts - the reusable addresses used internal
to stub domains, and the globally unique addresses. We call the
reusable address local addresses, and the globally unique addresses
global addresses. Any given address must either be a local address or
a global address. There is no overlap.

The problem with overlap is the following. Say a host in stub A
wished to send packets to a host in stub B, but the local addresses
of stub B overlapped the local addressees of stub A. In this case,
the routers in stub A would not be able to distinguish the global
address of stub B from its own local addresses.

Initial Assignment of Local and Global Addresses

A single class A address should be allocated for local networks. (See
RFC1597 [3].) This address could then be used for internets with no
connection to the Internet. NAT then provides an easy way to change
an experimental network to a "real" network by translating the
experimental addresses to globally unique Internet addresses.

Existing stubs which have unique addresses assigned internally, but
are running out of them, can change addresses subnet by subnet to
local addresses. The freed adresses can then be used by NAT for
external communications.

3.2 Routing Across NAT

The router running NAT should never advertise the local networks to
the backbone. Only the networks with global addresses may be known
outside the stub. However, global information that NAT receives from
the stub border router can be advertised in the stub the usual way.

Private Networks that Span Backbones

In many cases, a private network (such as a corporate network) will
be spread over different locations and will use a public backbone for
communications between those locations. In this case, it is not
desirable to do address translation, both because large numbers of
hosts may want to communicate across the backbone, thus requiring
large address tables, and because there will be more applications
that depend on configured addresses, as opposed to going to a name
server. We call such a private network a backbone-partitioned stub.

Backbone-partitioned stubs should behave as though they were a non-
partitioned stub. That is, the routers in all partitions should
maintain routes to the local address spaces of all partitions. Of
course, the (public) backbones do not maintain routes to any local
addresses. Therefore, the border routers must tunnel through the
backbones using encapsulation. To do this, each NAT box will set
aside one global address for tunneling. When a NAT box x in stub
partition X wishes to deliver a packet to stub partition Y, it will
encapsulate the packet in an IP header with destination address set
to the global address of NAT box y that has been reserved for
encapsulation. When NAT box y receives a packet with that destination
address, it decapsulates the IP header and routes the packet
internally.

3.3 Header Manipulations

In addition to modifying the IP address, NAT must modify the IP
checksum and the TCP checksum. Remember, TCP's checksum also covers a
pseudo header which contains the source and destination address. NAT
must also look out for ICMP and FTP and modify the places where the
IP address appears. There are undoubtedly other places, where
modifications must be done. Hopefully, most such applications will be
discovered during experimentation with NAT.

The checksum modifications to IP and TCP are simple and efficient.
Since both use a one's complement sum, it is sufficient to calculate
the arithmetic difference between the before-translation and after-
translation addresses and add this to the checksum. The only tricky
part is determining whether the addition resulted in a wrap-around
(in either the positive or negative direction) of the checksum. If

so, 1 must be added or subtracted to satisfy the one's complement
arithmetic. Sample code (in C) for this is as follows:

void checksumadjust(unsigned char *chksum, unsigned char *optr,
int olen, unsigned char *nptr, int nlen)
/* assuming: unsigned char is 8 bits, long is 32 bits.
- chksum points to the chksum in the packet
- optr points to the old data in the packet
- nptr points to the new data in the packet
*/
{
long x, old, new;
x=chksum[0]*256+chksum[1];
x=~x;
while (olen) {
if (olen==1) {
old=optr[0]*256+optr[1];
x-=old & 0xff00;
if (x<=0) { x--; x&=0xffff; }
break;
}
else {
old=optr[0]*256+optr[1]; optr+=2;
x-=old & 0xffff;
if (x<=0) { x--; x&=0xffff; }
olen-=2;
}
}
while (nlen) {
if (nlen==1) {
new=nptr[0]*256+nptr[1];
x+=new & 0xff00;
if (x & 0x10000) { x++; x&=0xffff; }
break;
}
else {
new=nptr[0]*256+nptr[1]; nptr+=2;
x+=new & 0xffff;
if (x & 0x10000) { x++; x&=0xffff; }
nlen-=2;
}
}
x=~x;
chksum[0]=x/256; chksum[1]=x & 0xff;
}

The arguments to the File Transfer Protocol (FTP) PORT command
include an IP address (in ASCII!). If the IP address in the PORT
command is local to the stub domain, then NAT must substitute this.
Because the address is encoded in ASCII, this may result in a change
in the size of the packet (for instance 10.18.177.42 is 12 ASCII
characters, while 193.45.228.137 is 14 ASCII characters). If the new
size is the same as the previous, only the TCP checksum needs
adjustment (again). If the new size is less than the previous, ASCII
zeroes may be inserted, but this is not guaranteed to work. If the
new size is larger than the previous, TCP sequence numbers must be
changed too.

A special table is used to correct the TCP sequence and acknowledge
numbers with source port FTP or destination port FTP. The table
entries should have source, destination, source port, destination
port, initial sequence number, delta for sequence numbers and a
timestamp. New entries are created only when FTP PORT commands are
seen. The initial sequence numbers are used to find out if the
sequence number of a packet is before or after the last FTP PORT
command (delta may be increased for every FTP PORT command). Sequence
numbers are incremented and acknowledge numbers are decremented. If
the FIN bit is set in one of the packets, the associated entry may be
deleted soon after (1 minute should be safe). Entries that have not
been used for e.g. 24 hours should be safe to delete too.

The sequence number adjustment must be coded carefully, not to harm
performance for TCP in general. Of course, if the FTP session is
encrypted, the PORT command will fail.

If an ICMP message is passed through NAT, it may require two address
modifications and three checksum modifications. This is because most
ICMP messages contain part of the original IP packet in the body.
Therefore, for NAT to be completely transparent to the host, the IP
address of the IP header embedded in the data part of the ICMP packet
must be modified, the checksum field of the same IP header must
correspondingly be modified, and the ICMP header checksum must be
modified to reflect the changes to the IP header and checksum in the
ICMP body. Furthermore, the normal IP header must also be modified as
already described.

It is not entirely clear if the IP header information in the ICMP
part of the body really need to be modified. This depends on whether
or not any host code actually looks at this IP header information.
Indeed, it may be useful to provide the exact header seen by the
router or host that issued the ICMP message to aid in debugging. In
any event, no modifications are needed for the Echo and Timestamp
messages, and NAT should never need to handle a Redirect message.

SNMP messages could be modified, but it is even more dubious than for
ICMP messages that it will be necessary.

Applications with IP-address Content

Any application that carries (and uses) the IP address inside the
application will not work through NAT unless NAT knows of such
instances and does the appropriate translation. It is not possible or
even necessarily desirable for NAT to know of all such applications.
And, if encryption is used then it is impossible for NAT to make the
translation.

It may be possible for such systems to avoid using NAT, if the hosts
in which they run are assigned global addresses. Whether or not this
can work depends on the capability of the intra-domain routing
algorithm and the internal topology. This is because the global
address must be advertised in the intra-domain routing algorithm.
With a low-feature routing algorithm like RIP, the host may require
its own class C address space, that must not only be advertised
internally but externally as well (thus hurting global scaling). With
a high-feature routing algorithm like OSPF, the host address can be
passed around individually, and can come from the NAT table.

Privacy, Security, and Debugging Considerations

Unfortunately, NAT reduces the number of options for providing
security. With NAT, nothing that carries an IP address or information
derived from an IP address (such as the TCP-header checksum) can be
encrypted. While most application-level encryption should be ok, this
prevents encryption of the TCP header.

On the other hand, NAT itself can be seen as providing a kind of
privacy mechanism. This comes from the fact that machines on the
backbone cannot monitor which hosts are sending and receiving traffic
(assuming of course that the application data is encrypted).

The same characteristic that enhances privacy potentially makes
debugging problems (including security violations) more difficult. If
a host is abusing the Internet is some way (such as trying to attack
another machine or even sending large amounts of junk mail or
something) it is more difficult to pinpoint the source of the trouble
because the IP address of the host is hidden.

4. Conclusions

NAT may be a good short term solution to the address depletion and
scaling problems. This is because it requires very few changes and
can be installed incrementally. NAT has several negative
characteristics that make it inappropriate as a long term solution,
and may make it inappropriate even as a short term solution. Only
implementation and experimentation will determine its
appropriateness.

The negative characteristics are:

1. It requires a sparse end-to-end traffic matrix. Otherwise, the NAT
tables will be large, thus giving lower performance. While the
expectation is that end-to-end traffic matrices are indeed sparse,
experience with NAT will determine whether or not they are. In any
event, future applications may require a rich traffic matrix (for
instance, distributed resource discovery), thus making long-term use
of NAT unattractive.

2. It increases the probability of mis-addressing.

3. It breaks certain applications (or at least makes them more difficult
to run).

4. It hides the identity of hosts. While this has the benefit of
privacy, it is generally a negative effect.

5. Problems with SNMP, DNS, ... you name it.

Current Implementations

Paul and Tony implemented an experimental prototype of NAT on public
domain KA9Q TCP/IP software [1]. This implementation manipulates
addresses and IP checksums.

Kjeld implemented NAT in a Cray Communications IP-router. The
implementation was tested with Telnet and FTP. This implementation
manipulates addresses, IP checksums, TCP sequence/acknowledge numbers
and FTP PORT commands.

The prototypes has demonstrated that IP addresses can be translated
transparently to hosts within the limitations described in this
paper.

REFERENCES

[1] Karn, P., "KA9Q", anonymous FTP from ucsd.edu
(hamradio/packet/ka9q/docs).

[2] Fuller, V., Li, T., and J. Yu, "Classless Inter-Domain Routing
(CIDR) an Address Assignment and Aggregation Strategy", RFC1519,
BARRNet, cisco, Merit, OARnet, September 1993.

[3] Rekhter, Y., Moskowitz, B., Karrenberg, D., and G. de Groot,
"Address Allocation for Private Internets", RFC1597, T.J. Watson
Research Center, IBM Corp., Chrysler Corp., RIPE NCC, March 1994.

Security Considerations

Security issues are not discussed in this memo.

Authors' Addresses

Kjeld Borch Egevang
Cray Communications
Smedeholm 12-14
DK-2730 Herlev
Denmark

Phone: +45 44 53 01 00
EMail: kbe@craycom.dk

Paul Francis
NTT Software Lab
3-9-11 Midori-cho Musashino-shi
Tokyo 180 Japan

Phone: +81-422-59-3843
Fax +81-422-59-3765  回复  更多评论   

# re: P2P 之 UDP穿透NAT的原理与实现(附源代码)[未登录] 2008-01-25 18:09 汪江涛

很好很强大,但不知道实用价值如何,万一client用的是Symmetric NAT怎么办?拒绝为它服务?有点太狠了吧?再说UDP大互联网应用中意义也不太吧,还是内网用的多,但内网也不用搞什么NAT了  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


广告信息(免费广告联系)

中文版MSDN:
欢迎体验