Click here to Skip to main content
15,891,895 members
Articles / Programming Languages / C#

NAT Traversal with UPnP in C#

Rate me:
Please Sign up or sign in to vote.
4.89/5 (44 votes)
14 Jan 2009Public Domain5 min read 525.3K   10.2K   145   221
NAT traversal with UPnP in C#, without any libraries.

Introduction

In writing network code, NAT can be a real problem. For most applications, requiring the user to add port forwarding rules to their router is not a good idea, or even acceptable, because often users are not allowed to change the settings of their router or do not know how to do so.

Fortunately, there are ways to automate the process of adding port forwarding rules. UPnP is one of the ways, to my knowledge the most commonly used way.

Another major problem with networking programs is that they appear to have no reliable way of finding the external IP address of the computer it is running on. UPnP partly solves this, but it is, of course, not completely reliable (not all routers support it, and routers that do may have UPnP turned off for security reasons).

If you have ever tried to use existing UPnP libraries, you may have found that most of them simply do not work, or do not cover the NAT aspects of it. This prompted me to write my own, very small, UPnP library that only covers the NAT aspects, and not even fully at that - only UPnP device discovery, basic port forwarding, and retrieving the external IP address. However, for many networking programs, this is all that is needed.

Using the code

In the source code, you will find a public class NAT, containing the following public methods:

  • static bool Discover()
  • static IPAddress GetExternalIP()
  • static void ForwardPort(int port, ProtocolType protocol, string description)
  • static void DeleteForwardingRule(int port, ProtocolType protocol)

Please note that these methods only do limited error checking, and will fail if there is no UPnP device or if the first UPnP device is too picky about the command syntax. Before GetExternalIP or ForwardPort will work, Discover has to be called. Discover can take up to 3 seconds by default (it will time out after that - you can change the time out value), possibly minutes if you set the time out high, so for any GUI application, it is advisable to place the call on a separate thread.

To forward a port the simplest way, code like this will suffice:

C#
UPnP.NAT.Discover();
UPnP.NAT.ForwardPort(12345, ProtocolType.Tcp, "MyApp (TCP)");

In practice, it would be best to check for exceptions - and the value returned by Discover.

UPnP + NAT

For the people who want to do it themselves, or who are interested, here is what I found out about UPnP in combination with NAT routers.

The first thing to do is find the UPnP device you are interested in. Apparently, the best way to do that is by using SSDP.

Finding the UPnP device using SSDP comes down to broadcasting a single packet over UDP and examining the replies. The packet looks like this (or at least, when it looks like this, it'll work):

"M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST:upnp:rootdevice\r\n" +
"MAN:\"ssdp:discover\"\r\n" +
"MX:3\r\n\r\n";

in ASCII. While the Host is 239.255.255.250, the packet still has to be broadcasted to the standard IPAddress.Broadcast address.

Examining the replies is pretty straightforward. You should only get one, but if you get more anyway, the right one is identifiable. A standard reply from a NAT device looks like this:

HTTP/1.1 200 OK
ST:upnp:rootdevice
USN:uuid:00-0C-F6-12-95-D3-FE7BA8C00::upnp:rootdevice
Location:http://192.168.123.254:80/desc.xml
Cache-Control:max-age=1800
Server:IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
Ext:

The ST should be "upnp:rootdevice"; if it's not, it isn't the right device, and it shouldn't have replied in the first place, but one never knows what happens in a network.

Anyhow, we're interested in the location, since at that location there will be an XML file containing information about the device - we will need this to find out how to send commands to the device.

Sending commands to a UPnP device is made possible through something they called a "service". The XML file describing the device is quite long, so suffice it to know that the XPath to the service we're interested in is:

"//tns:service[tns:serviceType=\"urn:schemas-upnp-
     org:service:WANIPConnection:1\"]/tns:controlURL/text()"

Since it uses namespaces, you will have to use an XmlNamespaceManager, but that's a whole different subject.

The result of this query should look somewhat like "/serv3.xml", once again an XML file. Note that this is a relative location, so you will need to append it to the base-URL, which is "http://192.168.123.254:80" in this example.

This file has a double purpose: first, its contents tell you something about what kinds of commands it accepts and how it will reply to them, and second, you will be POST-ing SOAP to it. That's correct, XML again.

Before this, I had never used a HTTP-POST on an XML file, and it greatly surprised me, but apparently, this is the way it should work.

Sending commands then is just using SOAP in the right way. However, this is not very straightforward - if you are using a WebRequest (as am I), then you will not only have to set the Method to "POST", but also add a header, and change the content-type:

C#
Headers.Add("SOAPACTION", 
  "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + 
  function + "\"");
ContentType = "text/xml; charset=\"utf-8\"";

The variable function here must be the same as the function you are sending in the contents.

That is pretty much all there is to it. With this information, you should be able to implement your own UPnP NAT traversal methods.

Points of interest

While writing the code for this tiny library, I was absolutely stunned by the complete absence of clear information on the subject. Most sites deal only with UPnP for media and such, or explain the lack of security of UPnP NAT-traversal, without going very far into details on how to actually do it. When there is such information, it is generally written in such a way that at least I do not understand half of it.

In the end, I used WireShark to examine µTorrent's UPnP traffic, and reverse-engineered the process. I hope that this article changes matters for some people so that they will not have to reverse-engineer anything to get UPnP NAT-traversal working.

History

  • July 18, 2008: Wrote the first version of the UPnP NAT library.
  • July 22, 2008: Wrote the first version of this article.
  • July 22, 2008 (later that day): Updated the source and the article to work on some stricter routers and in cases where the port is not 80 (with thanks to ajiau).
  • July 23, 2008: Updated source to work with some additional routers (thanks to Chris Harper)
  • January 14, 2009: Updated source and article to (hopefully) work with yet some more additional routers, with thanks to everyone who posted feedback in the past half year.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication



Comments and Discussions

 
GeneralRe: Mono.Nat Pin
harold aptroot9-Sep-08 22:40
harold aptroot9-Sep-08 22:40 
GeneralToo hardcoded to work universally Pin
ajiau22-Jul-08 2:26
ajiau22-Jul-08 2:26 
GeneralRe: Too hardcoded to work universally Pin
harold aptroot22-Jul-08 3:01
harold aptroot22-Jul-08 3:01 
GeneralRe: Too hardcoded to work universally [modified] Pin
ajiau22-Jul-08 5:05
ajiau22-Jul-08 5:05 
GeneralRe: Too hardcoded to work universally Pin
harold aptroot22-Jul-08 5:49
harold aptroot22-Jul-08 5:49 
GeneralRe: Too hardcoded to work universally Pin
ajiau22-Jul-08 20:56
ajiau22-Jul-08 20:56 
GeneralRe: Too hardcoded to work universally Pin
harold aptroot23-Jul-08 4:35
harold aptroot23-Jul-08 4:35 
GeneralRe: Too hardcoded to work universally Pin
ajiau26-Jul-08 5:25
ajiau26-Jul-08 5:25 
I got a little carried away, and refactored the whole thing. Changes include:

1) Add, Delete, Query all mappings.
2) Supports multiple routers on one subnet.
3) Supports router discovery without SSDP.
4) Supports multiple network card setups.
5) Includes improved SOAP compliance, and a nicer abstraction of all soap actions.
6) Added a lot of comments to the code.
7) Broke all Async code (sorry, but you should make the async entrypoints jsut wrap the regular ones.)

**Note to all who try this, please let me know whether it works for you!**

// SNIP //
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Xml;
using System.IO;

namespace UPnP
{
//public delegate void Method();

public class NAT
{

//public static void BeginDiscover(Method callback)
//{
// Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

// s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);

// String req =
// "M-SEARCH * HTTP/1.1\r\n" +
// "HOST: 239.255.255.250:1900\r\n" +
// "ST:upnp:rootdevice\r\n" +
// "MAN:\"ssdp:discover\"\r\n" +
// "MX:3\r\n\r\n";

// byte[] data = Encoding.ASCII.GetBytes(req);

// s.BeginSendTo(data, 0, data.Length, SocketFlags.None, new IPEndPoint(IPAddress.Broadcast, 1900),
// delegate(IAsyncResult ar)
// {
// s.EndSendTo(ar);
// byte[] buffer = new byte[0x1000];
// s.BeginReceive(buffer, 0, 0x1000, SocketFlags.None, delegate(IAsyncResult rar)
// {
// int l = s.EndReceive(rar);
// String resp = Encoding.ASCII.GetString(buffer, 0, l);
// if (resp.ToLower().Contains("upnp:rootdevice"))
// {
// resp = resp.Substring(resp.ToLower().IndexOf("location:") + "Location:".Length);
// resp = resp.Substring(0, resp.IndexOf("\r")).Trim();
// if (resp.ToLower().Contains(".xml"))
// {
// descUrl = resp;
// BeginGetServiceUrl(callback);
// }
// }

// }, null);
// }, null);
//}

/// <summary>
/// Below are several possible Service Description URL Suffixes
/// Some are found through personal testing, other are found
/// from Google searchs for potential SSDP query results.
/// </summary>
static String[] ServiceSuffixes = new String[] {
"/desc.xml", // DLink DWL....
"/description.xml",
"/InternetGatewayDevice.xml",
"/upnp/service/des_ppp.xml",
"/wanipconn-361.xml",
":5678/igd.xml",
":5678/rootDesc.xml",
":49000/igddesc.xml",
":49152/gateway.xml", // DG834Gv3
":49153/gateway.xml", // DG834Gv1
":54877/",
":49152/description.xml",
":1064/",
":1900/igd.xml",
""};

/// <summary>
/// Here are some possible SSDP Broadcast addresses to try.
/// The standard requires only the first, but we're trying to
/// handle devices which may not necessarily obey the standard too.
/// </summary>
static String[] SSDPAddresses = new String[] {
"239.255.255.250"/*,
"255.255.255.255"*/
};

/// <summary>
/// Here we store the results for SSDP and Default Gateway Probing.
/// </summary>
static List<String> PotentialHosts = new List<String>();

/// <summary>
/// Here we store the confirmed UPnP service control URLs
/// </summary>
static List<String> ServiceURLs = new List<String>();

static IPAddress MyAddress = null;

/// <summary>
/// Begin the discovery process.
/// </summary>
/// <returns>The number of NAT devices discovered</returns>
public static int Discover()
{
PotentialHosts.Clear();
ServiceURLs.Clear();

DefaultGateways_Probe();
SSPD_Probe();

GetServiceUrls();

return ServiceURLs.Count;
}

/// <summary>
/// Use SSDP to discover NAT routers in the local network.
/// </summary>
private static void SSPD_Probe()
{
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
s.ReceiveTimeout = 100;

foreach (String sSSDPAddress in SSDPAddresses)
{
String req = String.Format(
"M-SEARCH * HTTP/1.1\r\n" +
"HOST: {0}:1900\r\n" +
"ST:upnp:rootdevice\r\n" +
"MAN:\"ssdp:discover\"\r\n" +
"MX:3\r\n\r\n",
sSSDPAddress);

byte[] data = Encoding.ASCII.GetBytes(req);

byte[] buffer = new byte[0x1000];

for (int i = 0; i < 2; i++)
{
try
{
s.SendTo(data, new IPEndPoint(IPAddress.Broadcast, 1900));

while (true)
{
int l = s.Receive(buffer);

String resp = Encoding.ASCII.GetString(buffer, 0, l);

if (resp.ToLower().Contains("upnp:rootdevice"))
{
resp = resp.Substring(resp.ToLower().IndexOf("location:") + "Location:".Length);
resp = resp.Substring(0, resp.IndexOf("\r")).Trim();

if (!PotentialHosts.Contains(resp))
{
PotentialHosts.Add(resp);
}
}
}
}
catch
{
}
}
}
}

/// <summary>
/// Iterate through all local network adaptors. Find out the
/// default gateway, and store possible addresses on the default
/// gateway for further probing later.
/// </summary>
private static void DefaultGateways_Probe()
{
IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();

foreach (NetworkInterface networkCard in NetworkInterface.GetAllNetworkInterfaces())
{
foreach (GatewayIPAddressInformation gatewayAddr in networkCard.GetIPProperties().GatewayAddresses)
{
// Store the first IP Address of the first Network interface which
// has a valid default gateway. (Where there are multiple network
// interfaces on a single PC, this one is most likely the one we want.
if (MyAddress == null)
{
MyAddress = networkCard.GetIPProperties().UnicastAddresses[0].Address;
}

// Transform the discovered gateway to a set of possible
// urls, based on possible Service URLS.
foreach (String sSuffix in ServiceSuffixes)
{
String sHost = String.Format(@"http://{0}{1}", gatewayAddr.Address.ToString(), sSuffix);
if (!PotentialHosts.Contains(sHost))
{
PotentialHosts.Add(sHost);
}
}
}
}
}

//static void BeginGetServiceUrl(Method callback)
//{
// WebRequest req = WebRequest.Create(descUrl);
// req.BeginGetResponse(delegate(IAsyncResult ar)
// {
// GetServiceUrl();
// if (callback != null) callback();
// }, null);
//}

/// <summary>
/// Quickly scan through all potential UPnP addresses, discarding
/// anything that looks wrong, and retreiving service URLS for
/// those that work.
/// </summary>
static void GetServiceUrls()
{
foreach (String descUrl in PotentialHosts.ToArray())
{
try
{
WebRequest web = WebRequest.Create(descUrl);
web.Timeout = 50;
XmlDocument desc = new XmlDocument();
desc.Load(web.GetResponse().GetResponseStream());

XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable);
nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");

XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:WANIPConnection:1\"]/tns:controlURL/text()", nsMgr);

if (node != null)
{
String sRelativeURL = node.Value;
int n = descUrl.IndexOf("://");

if (n > -1)
{
n = descUrl.IndexOf('/', n + 3);

if (n > -1)
{
ServiceURLs.Add(descUrl.Substring(0, n) + sRelativeURL);
continue;
}
}
}
}
catch (WebException)
{ }

PotentialHosts.Remove(descUrl);
}
}

public static IPAddress GetExternalIP(int iServiceId)
{
IPAddress addr = null;

String sServiceURL = ServiceURLs[iServiceId];

SoapBuilder soap = new SoapBuilder(sServiceURL, "GetExternalIPAddress");

if (soap.TryPostMessage())
{
try
{
String IP = soap.GetValue("NewExternalIPAddress");
addr = IPAddress.Parse(IP);
}
catch { }
}

return addr;
}

public static Boolean AddPortMapping(int iServiceId, int Port, ProtocolType Protocol, String Description)
{
Boolean bRet = false;

String serviceUrl = ServiceURLs[iServiceId];

SoapBuilder soap = new SoapBuilder(serviceUrl, "AddPortMapping");

soap.AddParams("NewRemoteHost", "");
soap.AddParams("NewExternalPort", Port);
soap.AddParams("NewProtocol", Protocol);
soap.AddParams("NewInternalPort", Port);
soap.AddParams("NewInternalClient", MyAddress);
soap.AddParams("NewEnabled", 1);
soap.AddParams("NewPortMappingDescription", Description);
soap.AddParams("NewLeaseDuration", 0);

if (soap.TryPostMessage())
{
bRet = true;
}

return bRet;
}

public static Boolean GetGenericPortMapping(int iServiceId, int index, out int Port, out ProtocolType Protocol, out IPAddress Client, out Boolean Enabled, out String Description)
{
Boolean bRet = false;

Port = 0;
Protocol = ProtocolType.Unknown;
Client = IPAddress.None;
Enabled = false;
Description = String.Empty;

String sServiceURL = ServiceURLs[iServiceId];

SoapBuilder soap = new SoapBuilder(sServiceURL, "GetGenericPortMappingEntry");

soap.AddParams("NewPortMappingIndex", index);

if (soap.TryPostMessage())
{
try
{
String sPort = soap.GetValue("NewInternalPort");
String sProtocol = soap.GetValue("NewProtocol");
String sClient = soap.GetValue("NewInternalClient");
String sEnabled = soap.GetValue("NewEnabled");
String sDescription = soap.GetValue("NewPortMappingDescription");

Port = int.Parse(sPort);
Protocol = sProtocol.ToLower().Equals("udp") ? ProtocolType.Udp : ProtocolType.Tcp;
Client = IPAddress.Parse(sClient);
Enabled = int.Parse(sEnabled) > 0;
Description = sDescription;

bRet = true;
}
catch { }
}

return bRet;
}

public static Boolean DeletePortMapping(int iServiceId, int Port, ProtocolType Protocol)
{
Boolean bRet = false;

String sServiceURL = ServiceURLs[iServiceId];

SoapBuilder soap = new SoapBuilder(sServiceURL, "DeletePortMapping");

soap.AddParams("NewRemoteHost");
soap.AddParams("NewExternalPort", Port);
soap.AddParams("NewProtocol", Protocol);

if (soap.TryPostMessage())
{
bRet = true;
}

return bRet;
}
}

/// <summary>
/// Abstraction for managing simple SOAP interactions.
/// </summary>
class SoapBuilder
{
readonly String URL;
readonly String Action;

XmlDocument Response = null;
XmlNamespaceManager nsMgr = null;

/// <summary>
/// Construct a new SOAP message
/// </summary>
/// <param name="url">UPnP Control URL</param>
/// <param name="action">SOAP Action</param>
public SoapBuilder(String url, String action)
{
URL = url;
Action = action;
}

List<String> Params = new List<string>();
List<String> Types = new List<string>();
List<object> Values = new List<object>();

public void AddParams(String sParam)
{
AddParams(sParam, String.Empty);
}

public void AddParams(String sParam, object oValue)
{
Params.Add(sParam);

String sType;

if (oValue is int)
{
sType = "ui2";
}
else if (oValue is string)
{
sType = "string";
}
else if (oValue is ProtocolType)
{
sType = "string";
oValue = oValue.ToString().ToUpper();
}
else if (oValue is IPAddress)
{
sType = "string";
}
else
{
sType = "string";
}
Types.Add(sType);

Values.Add(oValue);
}

/// <summary>
/// Build up a SOAP message with the information we now have, send it off, and interpret the response.
/// </summary>
/// <returns>Returns TRUE if no error occurred. (Will return TRUE even if you are requesting an invalid deletion.</returns>
public Boolean TryPostMessage()
{
Boolean bRet = false;

StringBuilder sbRequest = new StringBuilder();

sbRequest.AppendLine("<?xml version=\"1.0\"?>");
sbRequest.Append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">");
sbRequest.Append("<SOAP-ENV:Body>");
sbRequest.AppendFormat("<m:{0} xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">", Action);
for (int i = 0; i < Params.Count; i++)
{
sbRequest.AppendFormat("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", Params[i], Types[i], Values[i]);
}
sbRequest.AppendFormat("</m:{0}>", Action);
sbRequest.Append("</SOAP-ENV:Body>");
sbRequest.Append("</SOAP-ENV:Envelope>");

Byte[] soapRequest = Encoding.UTF8.GetBytes(sbRequest.ToString());

WebRequest webReq = HttpWebRequest.Create(URL);
webReq.Timeout = 500;
webReq.Method = "POST";
webReq.Headers.Add("SOAPACTION", "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + Action + "\"");
webReq.ContentType = "text/xml; charset=\"utf-8\"";
webReq.ContentLength = soapRequest.Length;

try
{
webReq.GetRequestStream().Write(soapRequest, 0, soapRequest.Length);

Response = new XmlDocument();

Response.Load(webReq.GetResponse().GetResponseStream());

nsMgr = new XmlNamespaceManager(Response.NameTable);
nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");

bRet = true;
}
catch { }

return bRet;
}

public String GetValue(String Param)
{
return Response.SelectSingleNode(String.Format("//{0}/text()", Param), nsMgr).Value;
}
}
}
// SNIP //
GeneralRe: Too hardcoded to work universally Pin
harold aptroot26-Jul-08 14:37
harold aptroot26-Jul-08 14:37 
GeneralRe: Too hardcoded to work universally Pin
sdssd13-Aug-08 6:45
sdssd13-Aug-08 6:45 
GeneralRe: Too hardcoded to work universally Pin
sdssd13-Aug-08 6:45
sdssd13-Aug-08 6:45 
GeneralRe: Too hardcoded to work universally Pin
Mutant_Fruit9-Sep-08 21:13
Mutant_Fruit9-Sep-08 21:13 
GeneralRe: Too hardcoded to work universally Pin
harold aptroot9-Sep-08 22:41
harold aptroot9-Sep-08 22:41 
GeneralRe: Too hardcoded to work universally Pin
Mutant_Fruit10-Sep-08 1:41
Mutant_Fruit10-Sep-08 1:41 
GeneralRe: Too hardcoded to work universally Pin
harold aptroot10-Sep-08 3:21
harold aptroot10-Sep-08 3:21 
GeneralRe: Too hardcoded to work universally Pin
Mutant_Fruit10-Sep-08 5:29
Mutant_Fruit10-Sep-08 5:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.