use Mono.Nat for UPnP port forwarding

- might support more devices
- supports internal and external port mapping
- discover the device only once (at startup)
This commit is contained in:
Matthias Mailänder
2013-04-05 14:18:15 +02:00
parent 8ab817f466
commit 989c23e632
9 changed files with 95 additions and 213 deletions

View File

@@ -1,7 +1,7 @@
CSC = gmcs
CSFLAGS = -nologo -warn:4 -debug:full -optimize- -codepage:utf8 -unsafe -warnaserror
DEFINE = DEBUG;TRACE
COMMON_LIBS = System.dll System.Core.dll System.Drawing.dll System.Xml.dll thirdparty/ICSharpCode.SharpZipLib.dll thirdparty/FuzzyLogicLibrary.dll
COMMON_LIBS = System.dll System.Core.dll System.Drawing.dll System.Xml.dll thirdparty/ICSharpCode.SharpZipLib.dll thirdparty/FuzzyLogicLibrary.dll thirdparty/Mono.Nat.dll
PHONY = core tools package all mods clean distclean dependencies
.SUFFIXES:
@@ -258,6 +258,7 @@ install: all
@$(INSTALL_PROGRAM) thirdparty/FuzzyLogicLibrary.dll $(INSTALL_DIR)
@$(INSTALL_PROGRAM) thirdparty/SharpFont.dll $(INSTALL_DIR)
@cp thirdparty/SharpFont.dll.config $(INSTALL_DIR)
@$(INSTALL_PROGRAM) thirdparty/Mono.Nat.dll $(INSTALL_DIR)
@echo "#!/bin/sh" > openra
@echo 'BINDIR=$$(dirname $$(readlink -f $$0))' >> openra

View File

@@ -21,6 +21,10 @@ using OpenRA.Network;
using OpenRA.Support;
using OpenRA.Widgets;
using Mono.Nat;
using Mono.Nat.Pmp;
using Mono.Nat.Upnp;
using XRandom = OpenRA.Thirdparty.Random;
namespace OpenRA
@@ -34,6 +38,8 @@ namespace OpenRA
public static ModData modData;
static WorldRenderer worldRenderer;
public static INatDevice natDevice;
public static Viewport viewport;
public static Settings Settings;
@@ -256,6 +262,18 @@ namespace OpenRA
Log.AddChannel("perf", "perf.log");
Log.AddChannel("debug", "debug.log");
Log.AddChannel("sync", "syncreport.log");
Log.AddChannel("server", "server.log");
try {
NatUtility.DeviceFound += DeviceFound;
NatUtility.DeviceLost += DeviceLost;
NatUtility.StartDiscovery();
OpenRA.Log.Write("server", "NAT discovery started.");
} catch (Exception e) {
OpenRA.Log.Write("server", "Can't discover UPnP-enabled device: {0}", e);
Settings.Server.AllowUPnP = false;
}
FileSystem.Mount("."); // Needed to access shaders
Renderer.Initialize( Game.Settings.Graphics.Mode );
@@ -269,6 +287,31 @@ namespace OpenRA
InitializeWithMods(Settings.Game.Mods);
}
public static void DeviceFound (object sender, DeviceEventArgs args)
{
natDevice = args.Device;
Log.Write ("server", "NAT device discovered.");
Log.Write ("server", "Type: {0}", natDevice.GetType ().Name);
Log.Write ("server", "Your external IP is: {0}", natDevice.GetExternalIP ());
foreach (Mapping mp in natDevice.GetAllMappings()) {
Log.Write ("server", "Existing port mapping: protocol={0}, public={1}, private={2}", mp.Protocol, mp.PublicPort, mp.PrivatePort);
}
Settings.Server.AllowUPnP = true;
}
public static void DeviceLost (object sender, DeviceEventArgs args)
{
natDevice = args.Device;
Log.Write("server", "NAT device Lost");
Log.Write("server", "Type: {0}", natDevice.GetType().Name);
Settings.Server.AllowUPnP = false;
}
public static void InitializeWithMods(string[] mods)
{
// Clear static state if we have switched mods
@@ -415,7 +458,7 @@ namespace OpenRA
public static void CreateServer(ServerSettings settings)
{
server = new Server.Server(new IPEndPoint(IPAddress.Any, settings.ListenPort),
Game.Settings.Game.Mods, settings, modData);
Game.Settings.Game.Mods, settings, modData, natDevice);
}
public static int CreateLocalServer(string map)
@@ -432,7 +475,7 @@ namespace OpenRA
settings.AllowUPnP = false;
server = new Server.Server(new IPEndPoint(IPAddress.Loopback, 0),
Game.Settings.Game.Mods, settings, modData);
Game.Settings.Game.Mods, settings, modData, natDevice);
return server.Port;
}

View File

@@ -29,7 +29,7 @@ namespace OpenRA.GameRules
public int ExternalPort = 1234;
public bool AdvertiseOnline = true;
public string MasterServer = "http://master.open-ra.org/";
public bool AllowUPnP = false;
public bool AllowUPnP = true;
public bool AllowCheats = false;
public string Map = null;
public string[] Ban = null;

View File

@@ -60,8 +60,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core">
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
@@ -69,6 +68,12 @@
<Reference Include="SharpFont">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\thirdparty\SharpFont.dll</HintPath>
<Reference Include="Mono.Nat, Version=1.1.0.0, Culture=neutral">
<SpecificVersion>False</SpecificVersion>
<Private>False</Private>
<Package>mono.nat</Package>
<HintPath>..\thirdparty\Mono.Nat.dll</HintPath>
</Reference>
</Reference>
<Reference Include="Tao.OpenAl, Version=1.1.0.1, Culture=neutral, PublicKeyToken=a7579dda88828311">
<SpecificVersion>False</SpecificVersion>
@@ -151,7 +156,6 @@
<Compile Include="Server\Server.cs" />
<Compile Include="Server\ServerOrder.cs" />
<Compile Include="Server\TraitInterfaces.cs" />
<Compile Include="Server\UPnP.cs" />
<Compile Include="Sound.cs" />
<Compile Include="Support\Arguments.cs" />
<Compile Include="Support\PerfHistory.cs" />

View File

@@ -16,14 +16,18 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using UPnP;
using System.Threading;
using OpenRA.FileFormats;
using OpenRA.GameRules;
using OpenRA.Network;
using XTimer = System.Timers.Timer;
using Mono.Nat;
using Mono.Nat.Pmp;
using Mono.Nat.Upnp;
namespace OpenRA.Server
{
public enum ServerState : int
@@ -50,6 +54,8 @@ namespace OpenRA.Server
public readonly IPAddress Ip;
public readonly int Port;
public INatDevice NatDevice;
int randomSeed;
public readonly Thirdparty.Random Random = new Thirdparty.Random();
@@ -78,7 +84,7 @@ namespace OpenRA.Server
RemovePortforward();
}
public Server(IPEndPoint endpoint, string[] mods, ServerSettings settings, ModData modData)
public Server(IPEndPoint endpoint, string[] mods, ServerSettings settings, ModData modData, INatDevice natDevice)
{
Log.AddChannel("server", "server.log");
@@ -91,46 +97,12 @@ namespace OpenRA.Server
Settings = settings;
ModData = modData;
NatDevice = natDevice;
randomSeed = (int)DateTime.Now.ToBinary();
if (Settings.AllowUPnP)
{
try
{
if (UPnP.NAT.Discover())
{
Log.Write("server", "UPnP-enabled router discovered.");
Log.Write("server", "Your IP is: {0}", UPnP.NAT.GetExternalIP() );
}
else
{
Log.Write("server", "No UPnP-enabled router detected.");
Settings.AllowUPnP = false;
}
}
catch (Exception e)
{
OpenRA.Log.Write("server", "Can't discover UPnP-enabled routers: {0}", e);
Settings.AllowUPnP = false;
}
}
if (Settings.AllowUPnP)
{
try
{
if (UPnP.NAT.ForwardPort(Port, ProtocolType.Tcp, "OpenRA"))
Log.Write("server", "Port {0} (TCP) has been forwarded.", Port);
else
Settings.AllowUPnP = false;
}
catch (Exception e)
{
OpenRA.Log.Write("server", "Can not forward ports via UPnP: {0}", e);
Settings.AllowUPnP = false;
}
}
ForwardPort();
foreach (var trait in modData.Manifest.ServerTraits)
ServerTraits.Add( modData.ObjectCreator.CreateObject<ServerTrait>(trait) );
@@ -194,6 +166,9 @@ namespace OpenRA.Server
foreach (var t in ServerTraits.WithInterface<INotifyServerShutdown>())
t.ServerShutdown(this);
if (Settings.AllowUPnP)
RemovePortforward();
preConns.Clear();
conns.Clear();
try { listener.Stop(); }
@@ -202,16 +177,33 @@ namespace OpenRA.Server
}
void ForwardPort()
{
try
{
Mapping mapping = new Mapping (Protocol.Tcp, Settings.ExternalPort, Settings.ListenPort);
NatDevice.CreatePortMap(mapping);
Log.Write("server", "Create port mapping: protocol={0}, public={1}, private={2}", mapping.Protocol, mapping.PublicPort, mapping.PrivatePort);
}
catch (Exception e)
{
OpenRA.Log.Write("server", "Can not forward ports via UPnP: {0}", e);
Settings.AllowUPnP = false;
}
}
void RemovePortforward()
{
try
{
if (UPnP.NAT.DeleteForwardingRule(Port, ProtocolType.Tcp))
Log.Write("server", "Port {0} (TCP) forwarding rules has been removed.", Port);
Mapping mapping = new Mapping (Protocol.Tcp, Settings.ExternalPort, Settings.ListenPort);
NatDevice.DeletePortMap (mapping);
Log.Write("server", "Remove port mapping: protocol={0}, public={1}, private={2}", mapping.Protocol, mapping.PublicPort, mapping.PrivatePort);
}
catch (Exception e)
{
OpenRA.Log.Write("server", "Can not remove UPnP portforwarding rules: {0}", e);
Settings.AllowUPnP = false;
}
}

View File

@@ -1,163 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2008-2009 http://www.codeproject.com/Members/Harold-Aptroot
* Source: http://www.codeproject.com/Articles/27992/NAT-Traversal-with-UPnP-in-C
* This file is licensed under A Public Domain dedication.
* For more information, see http://creativecommons.org/licenses/publicdomain/
*/
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Xml;
using System.IO;
namespace UPnP
{
public class NAT
{
static string _serviceUrl;
public static bool Discover()
{
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
s.ReceiveTimeout = 3000; //3 seconds
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);
IPEndPoint ipe = new IPEndPoint(IPAddress.Broadcast, 1900);
byte[] buffer = new byte[0x1000];
try
{
s.SendTo(data, ipe);
int length = 0;
do
{
length = s.Receive(buffer);
string resp = Encoding.ASCII.GetString(buffer, 0, length).ToLower();
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)))
{
s.Close();
return true;
}
}
} while (length > 0);
s.Close();
return false;
}
catch
{
s.Close();
return false;
}
}
private static String GetServiceUrl(string resp)
{
XmlDocument desc = new XmlDocument();
HttpWebRequest r = (HttpWebRequest)WebRequest.Create(resp);
r.KeepAlive = false;
using (WebResponse wres = r.GetResponse())
{
using (Stream ress = wres.GetResponseStream())
{
desc.Load(ress);
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;
Uri respUri = new Uri(resp);
Uri combinedUri = new Uri(respUri, node.Value);
return combinedUri.AbsoluteUri;
}
}
}
public static bool ForwardPort(int port, ProtocolType protocol, string description)
{
if (string.IsNullOrEmpty(_serviceUrl))
throw new Exception("No UPnP service available or Discover() has not been called");
string body = String.Format("<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"+
"<NewRemoteHost></NewRemoteHost><NewExternalPort>{0}</NewExternalPort>"+
"<NewProtocol>{1}</NewProtocol><NewInternalPort>{0}</NewInternalPort>" +
"<NewInternalClient>{2}</NewInternalClient><NewEnabled>1</NewEnabled>" +
"<NewPortMappingDescription>{3}</NewPortMappingDescription>"+
"<NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping>",
port, protocol.ToString().ToUpper(),Dns.GetHostAddresses(Dns.GetHostName())[0], description);
if (SOAPRequest(_serviceUrl, body, "AddPortMapping") != null)
return true;
else
return false;
}
public static bool DeleteForwardingRule(int port, ProtocolType protocol)
{
if (string.IsNullOrEmpty(_serviceUrl))
throw new Exception("No UPnP service available or Discover() has not been called");
string body = String.Format("<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">" +
"<NewRemoteHost></NewRemoteHost><NewExternalPort>{0}</NewExternalPort>"+
"<NewProtocol>{1}</NewProtocol></u:DeletePortMapping>", port, protocol.ToString().ToUpper() );
if (SOAPRequest(_serviceUrl, body, "DeletePortMapping") != null)
return true;
else
return false;
}
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 body = "<?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>";
HttpWebRequest r = (HttpWebRequest)WebRequest.Create(url);
r.KeepAlive = false;
r.Method = "POST";
byte[] b = Encoding.UTF8.GetBytes(body);
r.Headers.Add("SOAPACTION", "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + function + "\"");
r.ContentType = "text/xml; charset=\"utf-8\"";
r.ContentLength = b.Length;
Stream newStream = r.GetRequestStream();
newStream.Write(b, 0, b.Length);
XmlDocument resp = new XmlDocument();
using (WebResponse wres = r.GetResponse())
{
using (Stream ress = wres.GetResponseStream())
{
resp.Load(ress);
return resp;
}
}
}
}
}

View File

@@ -50,6 +50,9 @@ cp thirdparty/FuzzyLogicLibrary.dll packaging/built
# SharpFont for FreeType support
cp thirdparty/SharpFont* packaging/built
# Mono.NAT for UPnP support
cp thirdparty/Mono.Nat.dll packaging/built
# Copy game icon for windows package
cp OpenRA.Game/OpenRA.ico packaging/built

View File

@@ -84,6 +84,7 @@ Section "Client" Client
File "${SRCDIR}\OpenRA.Renderer.Null.dll"
File "${SRCDIR}\ICSharpCode.SharpZipLib.dll"
File "${SRCDIR}\FuzzyLogicLibrary.dll"
File "${SRCDIR}\Mono.Nat.dll"
File "${SRCDIR}\COPYING"
File "${SRCDIR}\HACKING"
File "${SRCDIR}\INSTALL"
@@ -263,6 +264,7 @@ Function ${UN}Clean
Delete $INSTDIR\OpenRA.Renderer.SdlCommon.dll
Delete $INSTDIR\ICSharpCode.SharpZipLib.dll
Delete $INSTDIR\FuzzyLogicLibrary.dll
Delete $INSTDIR\Mono.Nat.dll
Delete $INSTDIR\Tao.*.dll
Delete $INSTDIR\SharpFont.*.dll
Delete $INSTDIR\COPYING

BIN
thirdparty/Mono.Nat.dll vendored Normal file

Binary file not shown.