Jenkins Software

NAT Traversal Architecture

How to use combine UPNP, NAT type detection, NAT punchthrough, and Router2 so P2P connections complete quickly and efficiently

RakNet utilizes 4 separate systems, each of which deal partly with not being able to connect to other systems. These systems are:

  1. NAT type detection - Find out if we have a router, and how restrictive that router is
  2. UPNP - Tell the router to open a specified port
  3. NAT punchthrough - Connect past routers by sending simultaneously between two systems
  4. Router2 (Optional) - Use other player's bandwidth for routers that cannot connect
  5. UDPProxyClient (Optional) - Route failed connections through your own servers

What follows is the best way to combine these systems to quickly connect players in a peer to peer network.

Requirements:

  1. You have a remote server running the NATCompleteServer (or use the default found at the sample \Samples\NATCompletePeer )
  2. On your game client, you have attached the plugins NatTypeDetectionClient, NATPunchthroughClient, and optionally Router2 and/or UDProxyClient
  3. On your game client, you have linked in and built miniupnp, located at DependentExtensions\miniupnpc-1.5

To build MiniUPNP

  1. Include DependentExtensions\miniupnpc-1.5 in the include paths
  2. Define STATICLIB in the preprocessor if necessary (See DependentExtensions\miniupnpc-1.5\declspec.h)
  3. Link ws2_32.lib and IPHlpApi.Lib

Step 1: Connect to the server

Connect to your server running NATCompleteServer using the usual method RakPeerInterface::Connect().

Step 2: Detect router type

Call NatTypeDetectionClient::DetectNATType(). You should get back a packet ID_NAT_TYPE_DETECTION_RESULT indicating the NAT type. For example:

if (packet->data[0]==ID_NAT_TYPE_DETECTION_RESULT) {
RakNet::NATTypeDetectionResult detectionResult = (RakNet::NATTypeDetectionResult) packet->data[1]; }

If detectionResult is NATTypeDetectionResult::NAT_TYPE_NONE then this system does not have a router. You can connect to every system, and every system can connect to you.

You should tell the server this system is directly connectable, so that incoming systems do not waste time trying to do NAT punch to this system. See Appendix A, passing NAT_TYPE_NONE. Connect to every existing user in the game session.

Step 3: Use UPNP to open the router

Assuming our router in step 2 was not NATTypeDetectionResult::NAT_TYPE_NONE, we are going to try to use UPNP to open the router.

#include "miniupnpc.h"
#include "upnpcommands.h"
#include "upnperrors.h"

bool OpenUPNP(RakPeerInterface *rakPeer, SystemAddress serverAddress)
{	
	struct UPNPDev * devlist = 0;
	devlist = upnpDiscover(2000, 0, 0, 0);
	if (devlist)
	{
		char lanaddr[64];	/* my ip address on the LAN */
		struct UPNPUrls urls;
		struct IGDdatas data;
		if (UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr))==1)
		{
			DataStructures::List< RakNetSmartPtr< RakNetSocket> > sockets;
			rakPeer->GetSockets(sockets);

			char iport[32];
			Itoa(sockets[0]->boundAddress.GetPort(),iport,10);
			char eport[32];
			Itoa(rakPeer->GetExternalID(serverAddress).GetPort(),eport,10);

			int r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, eport, iport, lanaddr, 0, "UDP", 0);
			if(r!=UPNPCOMMAND_SUCCESS)
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}
	else
	{
		return false;
	}

	return true;
}
If OpenUPNP returned true, you are done. You can connect to every system, and every system can connect to you. Remote systems should connect to your external port as seen by the server.

You should tell the server this system is directly connectable, so that incoming systems do not waste time trying to do NAT punch to this system. See Appendix A, passing NAT_TYPE_SUPPORTS_UPNP. Connect to every existing user in the game session.

Step 4: Run NATPunchthroughClient

  1. Download the list of remote players in the game session from the server, including their connectivity status.
  2. If the remote player's connectivity status is NAT_TYPE_SUPPORTS_UPNP or NAT_TYPE_NONE, you can connect to them directly. Store in memory this player as punchthrough succeeded, we will deal with this player in step 6.
  3. If the remote player's connectivity status is NAT_TYPE_SYMMETRIC and our own nat type from step 2 is also NAT_TYPE_SYMMETRIC, NATPunchthroughClient will fail for this player. Store in memory player as punchthrough failed, we will deal with this player in step 5.
  4. Otherwise, call NatPunchthroughClient::OpenNAT for that remote player and mark that player as processing.

For each player that we called OpenNAT for, we should get one of the following response codes:

  • ID_NAT_TARGET_NOT_CONNECTED - Remove this user from the list of remote players in the game session
  • ID_NAT_TARGET_UNRESPONSIVE - Remove this user from the list of remote players in the game session
  • ID_NAT_CONNECTION_TO_TARGET_LOST - Remote this user from the list of remote players in the game session
  • ID_NAT_ALREADY_IN_PROGRESS - Ignore
  • ID_NAT_PUNCHTHROUGH_FAILED - Store in memory this player as punchthrough failed, we will deal with this player in step 5.
  • ID_NAT_PUNCHTHROUGH_SUCCEEDED - Store in memory this player as punchthrough succeeded, we will deal with this player in step 6.

Step 5: Use Router2 or UDPProxyClient (optional)

For players that failed NATPunchthrough, you can route their connections through players that did not fail, using the Router2 plugin. You can also use the UDPProxyClient while you are running your own UDPProxyServer to forward those connections through a server.

Router2 will return ID_ROUTER_2_FORWARDING_NO_PATH if forwarding is not possible and ID_ROUTER_2_FORWARDING_ESTABLISHED on success.

UDPPRoxyClient will return ID_UDP_PROXY_GENERAL. Byte 1 indicates the return value. Success is returned on ID_UDP_PROXY_FORWARDING_SUCCEEDED. The remote system will get ID_UDP_PROXY_FORWARDING_NOTIFICATION on success. Anything else means failure.

If these solutions fail, or you do not use them, then it is not possible to complete a peer to peer gaming session. Leave the game session on the server. You should show a dialog box to the user that they need to manually open ports on their router before they can play. Or you can just try a different session.

Step 6: Connect to all other players in the session that we did not already connect to.

Step 6 assumes all users that failed connectivity were already connected to in step 5. If not, leave the game session on the server. You should show a dialog box to the user that they need to manually open ports on their router before they can play.

For players previously marked with NAT_TYPE_NONE, NAT_TYPE_SUPPORTS_UPNP, or that passed NAT punchthrough, you should now connect to these users. You can assume the connections will complete.

Appendix A: Inform server of peer's connectivity status

The server should track which peers are directly connectable, and if not, which type of router they have. This way incoming peers do not waste time performing NAT punchthrough to other peers that do not need it. You can program this manually, however the CloudServer plugin can handle this too. Here is an example of how to post to the server our connectivity state:

void PostConnectivityState(RakNet::NATTypeDetectionResult result, RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
{
RakNet::CloudKey cloudKey("NATConnectivityState",0);
RakNet::BitStream bs;
bs.WriteCasted<unsigned char>(result); // This could be anything such as player list, game name, etc.
cloudClient->Post(&cloudKey, bs.GetData(), bs.GetNumberOfBytesUsed(), serverGuid);
}

See the sample \Samples\NATCompletePeer

Simpler Solution

Just UPNP and NatPunchthroughClient

This simpler solution will work in nearly all cases, and is easier to code. The drawback is connecting to a game session may take longer, and if a player fails there are no fallbacks.

  1. Run step 1 as above
  2. Call the OpenUPNP() function in step 3. You don't need to upload any status to the server. If the function fails, just ignore it.
  3. Call NatPunchthroughClient::OpenNATGroup() on whichever player is the session/room host. You will get ID_NAT_GROUP_PUNCH_SUCCEEDED, or a failure code. If a failure code, then you can't connect to that room and should tell the user to open the game port on their router.

See the sample \Samples\NATSimplePeer

See Also

Index
Cloud Computing
MiniUPnP
NAT punchthrough
NAT type detection
Router2