I’m writing this post for two reasons:
- It is a pretty cool thing to do and very useful
- The amount of useful documentation and links you can find is very small
It took me about 12 hours of futsing with this and Googling things to get this correct. Most of my information actually came from bug reports of various open source projects that use UPnP for peer-to-peer connections.
I haven’t put much error checking in here, I want to keep the code short and as easy to understand as possible. I’ll leave that one up to the users to figure out.
To use this code, you must first call NAT.Discover(). This makes sure you have a UPnP device available. After that you can go ahead and Add and Delete ports as you wish. Heres the code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Xml;
using System.IO;
namespace enChatClient
{
public class NAT
{
static TimeSpan _timeout = new TimeSpan(0, 0, 0, 3);
public static TimeSpan TimeOut
{
get { return _timeout; }
set { _timeout = value; }
}
static string _descUrl, _serviceUrl, _eventUrl;
public static bool Discover()
{
System.Net.NetworkInformation.NetworkInterface nic = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()[0];
System.Net.NetworkInformation.GatewayIPAddressInformation gwInfo = nic.GetIPProperties().GatewayAddresses[0];
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
string req = "M-SEARCH * HTTP/1.1rn" +
"HOST: " + gwInfo.Address.ToString() + ":1900rn" +
"ST:upnp:rootdevicern" +
"MAN:"ssdp:discover"rn" +
"MX:3rnrn";
Socket client = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
IPEndPoint endPoint = new
IPEndPoint(IPAddress.Parse(gwInfo.Address.ToString()), 1900);
client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReceiveTimeout, 5000);
byte[] q = Encoding.ASCII.GetBytes(req);
client.SendTo(q, q.Length, SocketFlags.None, endPoint);
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint senderEP = (EndPoint)sender;
byte[] data = new byte[1024];
int recv = client.ReceiveFrom(data, ref senderEP);
string queryResponse = "";
queryResponse = Encoding.ASCII.GetString(data);
DateTime start = DateTime.Now;
string resp = queryResponse;
if (resp.Contains("upnp:rootdevice"))
{
resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9);
resp = resp.Substring(0, resp.IndexOf("r")).Trim();
if (!string.IsNullOrEmpty(_serviceUrl = GetServiceUrl(resp)))
{
_descUrl = resp;
return true;
}
}
return false;
}
private static string GetServiceUrl(string resp)
{
XmlDocument desc = new XmlDocument();
try
{
desc.Load(WebRequest.Create(resp).GetResponse().GetResponseStream());
}
catch (Exception)
{
return null;
}
XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable);
nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");
XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr);
if (!typen.Value.Contains("InternetGatewayDevice"))
return null;
XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType="urn:schemas-upnp-org:service:WANIPConnection:1"]/tns:controlURL/text()", nsMgr);
if (node == null)
return null;
XmlNode eventnode = desc.SelectSingleNode("//tns:service[tns:serviceType="urn:schemas-upnp-org:service:WANIPConnection:1"]/tns:eventSubURL/text()", nsMgr);
_eventUrl = CombineUrls(resp, eventnode.Value);
return CombineUrls(resp, node.Value);
}
private static string CombineUrls(string resp, string p)
{
int n = resp.IndexOf("://");
n = resp.IndexOf('/', n + 3);
return resp.Substring(0, n) + p;
}
public static void ForwardPort(int port, ProtocolType protocol, string description)
{
if (string.IsNullOrEmpty(_serviceUrl))
throw new Exception("No UPnP service available or Discover() has not been called");
IPHostEntry ipEntry = Dns.GetHostByName(Dns.GetHostName());
IPAddress addr = ipEntry.AddressList[0];
XmlDocument xdoc = SOAPRequest(_serviceUrl,
"<m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1"><NewRemoteHost xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string"></NewRemoteHost><NewExternalPort xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui2">" +
port.ToString() + "</NewExternalPort><NewProtocol xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">" +
protocol.ToString().ToUpper() + "</NewProtocol><NewInternalPort xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui2">" +
port.ToString() + "</NewInternalPort><NewInternalClient xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">" +
addr + "</NewInternalClient><NewEnabled xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="boolean">1</NewEnabled><NewPortMappingDescription xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">" +
description + "</NewPortMappingDescription><NewLeaseDuration xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui4">0</NewLeaseDuration></m:AddPortMapping>",
"AddPortMapping");
}
public static void DeleteForwardingRule(int port, ProtocolType protocol)
{
if (string.IsNullOrEmpty(_serviceUrl))
throw new Exception("No UPnP service available or Discover() has not been called");
XmlDocument xdoc = SOAPRequest(_serviceUrl,
"<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">" +
"<NewRemoteHost></NewRemoteHost>" +
"<NewExternalPort>" + port.ToString() + "</NewExternalPort>" +
"<NewProtocol>" + protocol.ToString().ToUpper() + "</NewProtocol>" +
"</u:DeletePortMapping>", "DeletePortMapping");
}
public static IPAddress GetExternalIP()
{
if (string.IsNullOrEmpty(_serviceUrl))
throw new Exception("No UPnP service available or Discover() has not been called");
XmlDocument xdoc = SOAPRequest(_serviceUrl, "<u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">" +
"</u:GetExternalIPAddress>", "GetExternalIPAddress");
XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable);
nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");
string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value;
return IPAddress.Parse(IP);
}
private static XmlDocument SOAPRequest(string url, string soap, string function)
{
string req = "<?xml version="1.0"?>" +
"<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">" +
"<s:Body>" +
soap +
"</s:Body>" +
"</s:Envelope>";
WebRequest r = HttpWebRequest.Create(url);
r.Timeout = 10000;
r.Method = "POST";
byte[] b = Encoding.UTF8.GetBytes(req);
r.Headers.Add("SOAPACTION", ""urn:schemas-upnp-org:service:WANIPConnection:1#" + function + """);
r.ContentType = "text/xml; charset="utf-8"";
r.ContentLength = b.Length;
r.GetRequestStream().Write(b, 0, b.Length);
XmlDocument resp = new XmlDocument();
WebResponse wres = r.GetResponse();
Stream ress = wres.GetResponseStream();
resp.Load(ress);
return resp;
}
}
}
These SOAP requests use two UPnP “commands”:
- AddPortMapping
- DeletePortMapping
AddPortMapping needs the following parameters:
- NewRemoteHost – Used to make it so that only one remote host can connect. Generally not used and OK to leave blank.
- NewExternalPort – The port that the incoming outside connection should be connecting on.
- NewProtocol – This is the protocol that will be running on the port. should be either “TCP” or “UDP”.
- NewInternalPort – The port on the inside of the network that the connection will be forwarded to.
- NewInternalClient – The internal IP address the connection should be forwarded to.
- NewEnabled – Whether the connection should be enabled or not. You want to set this to 1 so it actually works.
- NewPortMappingDescription – Just a short description of what the port is actually being used for. Generally the program name.
- NewLeaseDuration – Just set this to 0.
If the AddPortMapping doesn’t work for some reason you will get a server 500 error. Otherwise you will get an xml response
DeletePortMapping needs the following parameters:
- NewRemoteHost – Same thing as AddPortMapping. Not really used.
- NewExternalPort – The external port that the forwarding you’re deleting is running on.
- NewProtocol – The protocol it is running. “TCP” or “UDP”
If there is no such port mapping in the table, you will get a server 500 error, so be careful.
You can also check to see if a mapping exists by using GetSpecificPortMapping entry which takes NewRemoteHost, NewExternalPort, and NewPortMappingProtocol, and it returns a bunch of data on the connection. However the downside is that if it doesn’t exist, you get a server 500 error, and these can be trick to handle if you’re using a WebRequest, as it just throws it as a WebException.
The other way to do it is to GetGenericPortMappingEntry which returns the table of them and you can search it for the one you want.
NOTE: AddPortMapping will overwrite the previous mapping on the same port and protocol.
You must log in to post a comment.