Principle and implementation of UDP penetration NAT in P2P
Disclaimer: Original from P2P China Net, C # realization is what I do, for reference
Keywords: P2P, UDP, NAT, .NET
On the forum, there is often discussions on P2P principles, but discussing, rarely having substantial things (source code). Oh, here I use the source code you implement to illustrate the principle of UDP crossing NAT.
First introduce some basic concepts:
NAT (Network Address Translators), Network Address Conversion: Network Address Transformation is increasingly lacking in the IP address, the main purpose is to be able to be reused. NAT is divided into two categories, basic NAT and Napt (Network Address / Port Translators).
The first NAT is a functional module running on the router.
The first proposed is the basic NAT, which produces only the following fact: only a few nodes in a private network (domain) need to connect to the external network (Oh, this is proposed in the mid-1990s) . Then there is only a few nodes in this subnet that require a world's unique IP address, and the IP address of other nodes should be reused.
Therefore, the basic NAT implementation is simple, and a reserved IP subnet section is used in the subnet, which is invisible to these IPs. Only a few IP addresses in the subnet can correspond to the real world unique IP address. If these nodes need to access the external network, the basic NAT is responsible for transforming the subnet IP of this node into a world unique IP and then sending out. (Basic NAT will change the original IP address in the IP package, but will not change the port in the IP package)
About basic NAT can see RFC 1631
Another NAT is called NAPT, which we can also see from the name, NAPT will change the IP address of the IP datagram passed through this NAT device, but also change the TCP / UDP port of the IP datagram. Basic NAT's equipment may have no more (huh, I have not seen it), Napt is the protagonist of our truly discussed. Look at the picture below:
There is a private network 10. *. *. *, Client A is one of the computers. The external network IP of this network (a NAT device) is 155.99.25.11 (there should be an intranet IP address, such as 10.0.0.10). If a process in Client A (this process creates a UDP socket, this socket binding 1234 port) wants to access the 1235 port of the external network host 18.181.0.31, then what happens when the data package is passed by NAT?
First, NAT will change the original IP address of this packet, change to 155.99.25.11. Then NAT will create a session for this transmission (Session is an abstract concept, if it is TCP, maybe the session starts by a SYN package, ending with a FIN package. And the first one of this IP UDP starts, end, huh, maybe it's a few minutes, maybe it's a few hours, this depends on the specific implementation) and assigns a port to this session, such as 62000, then change the source port of this packet 62000. So the data (10.0.0.1: 1234->18.181.0.0.31:1235) packaged to the Internet (155.99.25.11:62000-18.181.0.31:1235).
Once NAT creates a session, NAT will remember that the 62000 port corresponds to the 1234 port of 10.0.0.1, and the data sent from 18.181.0.31 to 62000 port will be forwarded to 10.0.0.1. (Note: This is a data that is sent to 62000 port 18.181.0.31 will be forwarded, and the data sent to this port will be abandoned by NAT) so that Client A is established with Server S1 to connect. Oh, the basics above may know many people, then the following is a key part.
Take a look at the situation below:
Connected to the above example, if the original Socket (the UDP socket that binds 1234 port) is then sent to another Server S2, the UDP package will be through NAT?
There are two cases that may occur at this time, one is NAT to create a session again, and allocate a port number for this session (such as: 62001). The other is NAT to create a session again, but will not be allocated a port number, but the port number 62000 allocated. The previous NAT is called Symmetric Nat, and the latter is called CONE NAT. We expect our NAT to be the second, huh, if your NAT is just the first, then there will be a lot of P2P software failure. (Can you celebrate, now the vast majority of NAT belongs to the latter, namely CONE NAT)
Ok, we see that the computer in the subnet is easy to connect to the outside (NAT is equivalent to transparent, the computer in the subnet does not need to know NAT).
But if the external computer wants to access the computer in the subnet (this is also required for P2P).
So if we want to send a data to the intranet from the outside to the intra computer? First, we must play a "hole" on the NAT of the intranet (that is, we said in the NAT to establish a session). This hole cannot be hit by the outside, can only be hit by the host in the intranet. And this hole is a direction, such as a UDP package from an internal host (such as: 192.168.0.10) to the outside, such as: 219.237.60.1), then play on the NAT device of this intranet "Cave" in a direction of 219.237.60.1 (this is called UDP HOLE PUNCHING) After 219.237.60.1, it can be contacted by 192.168.0.10 through this hole. (However, other IPs cannot take advantage of this hole).
Oh, now the round to our topic P2P. With the theory of the above, the host communication of the two intangnet is the last step: That is the problem of eggs or egg growth chops, no connection requests on both sides, no one knows the public network address , How can we play this hole? We need a middleman to contact these two intranet hosts.
Now let's take a look at a process of P2P software, the following figure is:
First, the Client A logs in the server, NAT A is assigned a port 60000 for this session, then the address of the Client A received by Server S is 202.187.45.3:60000, which is the outer network address of Client a. Similarly, Client B logs in Server S, NAT B is 40000 assigned to this session, then the address received by Server S is 187.34.1.56:40000. At this point, Client A and Client B can communicate with Server S. If Client A wants to send information directly to Client B, then he can get B's public network address 187.34.1.56:40000 from Server S, is it CLIENT A to send information to this address CLIENT B? Can you receive it? The answer is not, because if this is sent, NAT B will discard this information (because such information is not please, most NAT will perform discarding action). Now we need to play a hole in Nat B to 202.187.45.3 (ie the outer network address of Client a), then the Client A is sent to the information of 187.34.1.56:40000, and Client B can be received. Who is from anyone command, huh, Of course, Server S.
Summarize this process: If Client A wants to send information to the Client B, the Client A sends a command to Server S, request the Server S command Client B to hoist in the direction of the client. Oh, is it very winding, but it doesn't matter, I want to think very clearly. Let's also have source code. (Hou teacher said: There is no secret 8 in front of the source code), then Client A can pass Client B The external network address is communicated with the Client B.
Note: The above process is only suitable for CONE NAT. If it is Symmetric Nat, then when Client B has a hole in the CLIENT A hole has been redistributed, Client B will not know this port (if the port of Symmetric Nat is sequentially assigned Then we may guess this port number, but because there are too many factors that may lead to failure, we do not recommend this way to guess ports).
Below is a source code for analog P2P chat, the process is simple, P2PServer runs on a computer with public IP, and P2PClient is running after two different NATs (note that if the two clients run after a NAT This program is likely to be normal, depending on whether your NAT supports loopback translation, see http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt, of course, this problem It can be solved by both sides to try to connect the other party's intranet IP, but this code is just to verify the principle, there is no processing of these issues), and the login computer can get the username of the first login computer, and the computer is logged over by Send UserName Message. Format to send a message. If the send is successful, you have achieved successful connection directly to the other party.
The program now supports three commands: Send, Getu, EXIT
Send format: send username message
Function: Send information to username
Getu format: Getu function: Get list of current server users
EXIT format: exit
Function: Logout and server connection (server does not automatically monitor whether customers hang)
Source code
Note: The original code is written with C , here only C # code
1. WellkNown Public Library
Namespace p2p.wellknown
{
Using system;
Using system.io;
Using system.runtime.serialization.formatters.binary;
///
/// p2pconsts's summary description.
/// summary>
Public Class P2pConsts
{
///
// server listening port number
/// summary>
Public const INT SRV_PORT = 2280;
}
///
/// User's summary description.
/// summary>
[Serializable]
Public Class User
{
Protected string username;
Protected IpendPoint Netpoint;
Public user (String UserName, IpendPoint Netpoint)
{
THIS.USERNAME = UserName;
THIS.NETPOINT = NetPoint;
}
Public String Username
{
Get {returna usrname;}
}
Public IpendPoint NetPoint
{
Get {return netpoint;}
Set {NetPoint = Value;}
}
}
///
Summary of /// UserCollection.
/// summary>
[Serializable]
Public Class UserCollection: CollectionBase
{
Public Void Add (user user)
{
InnerList.Add (user);
}
Public Void Remove (User User)
{
InnerList.Remove (user);
}
Public User this [int index]
{
Get {return (user) innerlist [index];
}
Public user find (String Username)
{
Foreach (User User In this)
{
IF (String.Compare (username, user.username, true) == 0)
{
Return User;
}
}
Return NULL;
}
}
///
/// FormatterHelper serialization, anti-serialization message help class
/// summary>
Public Class FormatterHelper
{
Public static Byte [] Serialize (Object Obj)
{
BinaryFormatter binaryf = new binaryformatter ();
MemoryStream MS = New MemoryStream (1024 * 10);
Binaryf.Serialize (MS, OBJ);
Ms.seek (0, seekorigin.begin); Byte [] buffer = new byte [(int) ms.length];
MS.Read (buffer, 0, buffer.length);
Ms.close ();
Return buffer;
}
Public Static Object DeserialIze (byte [] buffer
{
BinaryFormatter binaryf = new binaryformatter ();
MemoryStream MS = New MemoryStream (Buffer, 0, Buffer.Length, False);
Object obj = binaryf.deserialize (ms);
Ms.close ();
Return Obj;
}
}
///
/// Message Base Class
/// summary>
[System.serializable]
Public Abstract Class MessageBase
{
}
// Message from Client To Server
Namespace C2S
{
///
/// The client sends a message base class to the server
/// summary>
Public Abstract Class CsMessage: MessageBase
{
PRIVATE STRING UserName;
Protected Csmessage (String Anusername)
{
Username = anusername;
}
Public String Username
{
Get {returna usrname;}
}
}
///
/// user login message
/// summary>
Public Class LoginMessage: Csmessage
{
PRIVATE STRING Password;
Public loginMessage (String Username, String Password): Base (username)
{
this.password = password;
}
Public String Password
{
Get {return password;}
}
}
///
/// User logout message
/// summary>
Public Class LogoutMessage: Csmessage
{
Public logoutMessage (String Username): Base (username)
{}
}
///
/// request a user list message
/// summary>
Public Class Getusersmessage: CsMessage
{
Public getUsersSage (String Username): Base (username)
{}
}
///
/// Request Purch Hole Message
/// summary>
Public Class TranslateMessage: Csmessage
{
Protected string Tousername;
Public TranslateMessage (String Username): Base (username)
{
THIS.TOUSERNAME = Tousername;
}
Public string tousername {
Get {return this.tousername;
}
}
}
// Message from Server to The Client
Namespace S2C
{
///
// server Send to the client message base class
/// summary>
Public Abstract Class Scmessage: MessageBase
{}
///
// / Request a user list answer message
/// summary>
Public Class GetUsersResponseMessage: Scmessage
{
Private userCollection UserList;
Public GetUsersResponseMessage (UserCollection Uses)
{
THIS. IUSERLIST = Users;
}
Public UserCollection UserList
{
Get {return userlist;}
}
}
///
/// Forward requests Purch Hole message
/// summary>
Public Class SomeOneCallyoumessage: Scmessage
{
Protected system.net.IpendPoint RemotePoint;
Public SomeneCallyouMessage (System.Net.Ipendpoint Point)
{
THIS.RemotePoint = POINT;
}
Public system.net.IpendPoint RemotePoint
{
Get {return Remotepoint;
}
}
}
// Message from Peer to the peer
Namespace P2P
{
///
/// point-to-point message base class
/// summary>
Public Abstract Class PpMessage: MessageBase
{}
///
// / Test message
/// summary>
Public Class Workmessage: PpMessage
{
PRIVATE STRING MESSAGE;
Public WorkMessage (String MSG)
{
Message = msg;
}
Public String Message
{
Get {returnome;}
}
}
///
/// Test a response message
/// summary>
Public Class AckMessage: PpMessage
{
}
///
/// P2P Purch Hole Message
/// summary>
Public Class Trashment: ppMessage
{}
}
}
2. P2pserver
Namespace p2p.p2pserver
{
Using system;
Using system.net;
Using system.net.sockets;
Using system.threading;
Using p2p.wellknown;
///
/// AppClass's summary description.
/// summary>
Public Class AppClass
{
Public static void main () {
Server Server = new server ();
Try
{
Server.Start ();
Console.readline ();
Server.stop ();
}
Catch
{
}
}
}
///
/// Server's summary description.
/// summary>
Public Class Server
{
Private udpclient server;
Private userCollection UserList;
Private thread serverthread;
Private IpendPoint RemotePoint;
Public server ()
{
UserList = new usercollection ();
RemotePoint = New IpendPoint (iPaddress.Any, 0);
Serverthread = New Thread (New ThreadStart (RUN);
}
Public void start ()
{
Try
{
Server = new udpclient (p2pconsts.srv_port);
ServerthRead.start ();
Console.WriteLine ("P2P Server Started, Waiting Client Connect ...");
}
Catch (Exception Exp)
{
Console.writeline ("Start P2P Server Error:" Exp.Message);
Throw Exp;
}
}
Public void stop ()
{
Console.writeLine ("P2P Server Stopping ...");
Try
{
Serverthread.abort ();
Server.close ();
Console.writeline ("STOP OK.");
}
Catch (Exception Exp)
{
Console.writeline ("STOP Error:" Exp.Message);
Throw Exp;
}
}
Private vid run ()
{
BYTE [] Buffer = NULL;
While (True)
{
Byte [] msgbuffer = server.receive (RemotePoint);
Try
{
Object msgobj = formatterHelper.deSerialize (msgbuffer);
TYPE MSGTYPE = msgobj.gettype ();
IF (MsgType == TypeOf (p2p.wellknown.c2s.loginmessage))
{
// Convert the acceptable message
P2p.wellknown.c2s.loginmessage lginmsg = (p2p.wellknown.c2s.loginMessage) msgobj;
Console.writeline ("HAS An User Login: {0}", LGINMSG.USERNAME);
// Add user to list
IpendPoint UseRendPoint = New IpendPoint (RemotePoint.Address, RemotePoint.port);
User USER = New User (LGINMSG.USERNAME, UserendPoint);
UserList.Add (user);
// Send a reply message p2p.wellknown.s2c.getusersResponseMessage usersmsg = new p2p.wellknown.s2c.getusersResponseMessage (userlist);
Buffer = formatterHelper.Serialize (userSMSG);
Server.send (Buffer, Buffer.length, RemotePoint);
}
Else IF (MsgType == TypeOf (p2p.wellknown.c2s.logoutMessage))
{
// Convert the acceptable message
P2p.wellknown.c2s.logoutMessage Lgoutmsg = (p2p.wellknown.c2s.logoutMessage) msgobj;
Console.writeline ("HAS An User Logout: {0}", LGOUTMSG.USERNAME);
// Remove users from the list
User LGOUTUSER = UserList.Find (LGOUTMSG.USERNAME);
IF (LGOUTUSER! = NULL)
{
UserList.Remove (LGOUTUSER);
}
}
Else IF (MsgType == TypeOf (p2p.wellknown.c2s.translateMessage))
{
// Convert the acceptable message
P2p.wellknown.c2s.translateMessage Transmsg = (p2p.wellknown.c2s.translateMessage) msgobj;
Console.writeline ("{0} (1) Wants to P2P {2}, RemotePoint.Address.tostring (), Transmsg.username, Transmansg.Tousername;
/ / Get target users
User Touser = UserList.Find (Transmansg.Tousername);
/ / Forward Purch Hole request message
IF (Touser == Null)
{
Console.writeline ("Remote Host {0} Cannot Be Found At Index Server", Transmansg.Tousername;
}
Else
{
P2p.wellknown.s2c.somePoNecallyouMessage Transmsg2 = new p2p.wellknown.s2c.someOneCallyouMessage (RemotePoint);
Buffer = formatterHelper.Serialize (Transmsg);
Server.send (buffer, buffer.length, barship.netpoint);
}
}
Else IF (MsgType == TypeOf (p2p.wellknown.c2s.getusersmessage))
{
// Send current user information to all login customers
P2p.wellknown.s2c.getusersResponseMessage SRVRESMSG = New P2p.wellknown.s2c.getusersResponseMessage (UserList);
Buffer = formatterHelper.Serialize (SRVRESMSG);
Foreach (User UserList)
{
Server.send (Buffer, Buffer.Length, User.netPoint);
}
}
Thread.sleep (500);
Catch {}
}
}
}
}
3. P2pclient
Namespace p2p.p2pclient
{
Using system;
Using system.net;
Using system.net.sockets;
Using system.threading;
Using p2p.wellknown;
///
/// AppClass's summary description.
/// summary>
Public Class AppClass
{
Public static void main ()
{
Client Client = New Client ("202.96.134.103");
Client.connectToServer ("MyName", "Mypassword");
Client.start ();
Console.writeline ("Test Arguments");
While (True)
{
String str = console.readline ();
Client.paserCommand (STR);
}
}
}
///
/// client's summary description.
/// summary>
Public Class Client: IDisposable
{
Private const Int maxRetry = 10;
PRIVATE UDPCLIENT Client;
Private IpendPoint HostPoint;
Private IpendPoint RemotePoint;
Private userCollection UserList;
PRIVATE STRING MyName;
Private bool receivedack;
PRIVATE Thread ListentHread;
Public Client (String Serverip)
{
Receivedack = false;
RemotePoint = New IpendPoint (iPaddress.Any, 0);
HostPoint = New IpendPoint (iPaddress.Parse (Serverip), P2PConsts.srv_port);
Client = new udpclient ();
UserList = new usercollection ();
Listenthread = New Thread (New ThreadStart (RUN);
}
Public void start ()
{
IF (this.listenthread.threadstate == threadState.unstarted)
{
THISTENTHREAD.START ();
Console.writeline ("You CAN Input You Command: / N");
Console.WriteLine ("Command Type: /" Send / ", /" EXIT / ", /" GetU / ");
Console.writeline ("Example: Send UserName Message);
Console.writeline ("exit");
Console.writeLine ("Getu");
}
}
Public void connectiontose (string username, string password) {
MyName = username;
// Send a login message to the server
P2p.wellknown.c2s.loginmessage lginmsg = new p2p.wellknown.c2s.loginMessage (username, password);
Byte [] buffer = formatterHelper.Serialize (LGINMSG);
Client.send (Buffer, Buffer.Length, HostPoint);
/ / Accept server login answering message
Buffer = Client.Receive (RemotePoint);
P2p.wellknown.s2c.getusersResPonseMessage Srvresmsg = (p2p.wellkno.s2c.getusersResponseMessage) FormatterHelper.DeselIze (buffer);
/ / Update the user list
UserList.clear ();
Foreach (user user in srvresmsg.userlist)
{
UserList.Add (user);
}
This.DisplayUsers (userlist);
}
///
/// This is the main function: send a message to a user (c)
/// Process: Send a message directly to the external network IP of a user, if it did not contact
/// Then this message will not be sent, the sender waits for timeout.
/// After the timeout, the sender will send a request information to the server, requiring the server to send
/// give the customer C a request, request c to send a hole message to this machine
/// * The above process will repeat maxRetry times
/// summary>
/// Erupi User Name param>
/// Message to be sent param>
///
Private Bool SendMessageTo (String Tousername, String Message)
{
User Touser = UserList.Find (TouserName);
IF (Touser == Null)
{
Return False;
}
For (INT i = 0; i { P2p.wellknown.p2p.workMessage Workmsg = new p2p.wellknown.p2p.workmessage (message); Byte [] buffer = formatterHelper.Serialize (WORKMSG); Client.send (buffer, buffer.length, touser.netpoint); // Waiting for the receiving thread to modify the tag For (int J = 0; j <10; j ) { IF (this.ReceiveDack) { this.ReceiveDack = false; Return True; } Else { Thread.sleep (300); } } // No response to the target host, I think the port mapping of the target host is not // Open, then send request information to the server, to tell the target host // Open the mapping port (UDP hole) P2p.wellknown.c2s.translateMessage Transmsg = new p2p.wellknown.c2s.translateMessage (MyName, Tousername); Buffer = formatterHelper.Serialize (Transmsg); Client.send (Buffer, Buffer.Length, HostPoint); // Wait for the other party to send information first Thread.sleep (100); } Return False; } Public void PaserCommand (String cmdstring) { cmdstring = cmdstring.trim (); String [] args = cmdstring.split (new char [] {''}); IF (args.length> 0) { IF (String.Compare (Args [0], "EXIT", true) == 0) { P2p.wellknown.c2s.logoutMessage Lgoutmsg = new p2p.wellknown.c2s.logoutMessage (MyName); Byte [] buffer = formatterHelper.Serialize (LGOUTMSG); Client.send (Buffer, Buffer.Length, HostPoint); // Do Clear Something Here Dispose (); System.Environment.exit (0); } Else IF (String.Compare (Args [0], "Send", true) == 0) { IF (Args.Length> 2) { String Tousername = args [1]; String message = ""; For (int i = 2; i { IF (Args [I] == "") Message = "" Else Message = Args [i]; } IF (this.sendMessageto (Tousername, Message)) { Console.writeline ("Send OK!"); } Else Console.writeline ("Send Failed!"); } } Else IF (String.Compare (Args [0], "Getu", true) == 0) { P2p.wellknown.c2s.getusersMessage getUsermsg = new p2p.wellknown.c2s.getusersMessage (MyName); Byte [] buffer = formatterHelper.serialize (getusermsg); Client.send (Buffer, Buffer.Length, HostPoint); } Else { Console.Writeline ("Unknown Command {0}", cmdstring); } } } Private Void DisplayUsers (UserCollection Users) { Foreach (User Users) { Console.writeline ("UserName: {0}, IP: {1}, port: {2}", user.username, user.netpoint.address.tostring (), user.netpoint.port); } Private vid run () { BYTE [] BUFFER While (True) { Buffer = Client.Receive (RemotePoint); Object msgobj = formatterHelper.deSerialize (buffer); TYPE MSGTYPE = msgobj.gettype (); IF (MsgType == TypeOf (p2p.wellknown.s2c.getusersResponseMessage)) { // Convert message P2p.wellknown.s2c.getusersResponseMessage UserSMSG = (p2p.wellknown.s2c.getusersResponseMessage) msgobj; / / Update the user list UserList.clear (); Foreach (user user in usersmsg.userlist) { UserList.Add (user); } This.DisplayUsers (userlist); } Else IF (MsgType == TypeOf (p2p.wellknown.s2c.somenecally,)) { // Convert message P2p.wellknown.s2c.someneCallyouMessage Purchreqmsg = (p2p.wellknown.s2c.somenectallyoumessage) msgobj; // Send a hole message to the remote host P2p.wellknown.p2p.trashmessage trashmsg = new p2p.wellknown.p2p.trashmessage (); Buffer = formatterHelper.Serialize (trashmsg); Client.send (Buffer, Buffer.Length, Purchreqmsg.RemotePoint); } Else IF (MsgType == TypeOf (p2p.wellknown.p2p.workmessage)) { // Convert message P2p.wellknown.p2p.workMessage Workmsg = (p2p.wellknown.p2p.workmessage) msgobj; Console.writeline ("Receive a Message: {0}", Workmsg.Message; // Send a reply message P2p.wellknown.p2p.ackmessage Ackmsg = new p2p.wellknown.p2p.ackmessage (); Buffer = formatterHelper.Serialize (ACKMSG); Client.send (Buffer, Buffer.length, RemotePoint); } Else IF (MsgType == TypeOf (p2p.wellknown.p2p.ackmessage)) { THIS.ReceiveDack = True; } Else IF (MsgType == TypeOf (p2p.wellknown.p2p.trashmessage)) { Console.WriteLine ("Recieve a Trash Message); } Thread.sleep (100); } #Region IDisposable member Public void dispose () { Try { THISTENTHREAD.ABORT (); This.Client.close (); } Catch {} } #ndregion } }