diff --git a/Makefile b/Makefile index c0d79ea085..ac3d4a1116 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/OpenRA.FileFormats/Support/Log.cs b/OpenRA.FileFormats/Support/Log.cs index d1be337801..1d5bcd4cf2 100755 --- a/OpenRA.FileFormats/Support/Log.cs +++ b/OpenRA.FileFormats/Support/Log.cs @@ -26,7 +26,7 @@ namespace OpenRA public static class Log { static string LogPathPrefix = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + Path.DirectorySeparatorChar; - static Dictionary channels = new Dictionary(); + public static readonly Dictionary Channels = new Dictionary(); public static string LogPath { @@ -47,7 +47,7 @@ namespace OpenRA public static void AddChannel(string channelName, string baseFilename) { - if (channels.ContainsKey(channelName)) return; + if (Channels.ContainsKey(channelName)) return; foreach (var filename in FilenamesForChannel(channelName, baseFilename)) try @@ -55,7 +55,7 @@ namespace OpenRA var writer = File.CreateText(filename); writer.AutoFlush = true; - channels.Add(channelName, + Channels.Add(channelName, new ChannelInfo() { Filename = filename, @@ -70,7 +70,7 @@ namespace OpenRA public static void Write(string channel, string format, params object[] args) { ChannelInfo info; - if (!channels.TryGetValue(channel, out info)) + if (!Channels.TryGetValue(channel, out info)) throw new Exception("Tried logging to non-existant channel " + channel); info.Writer.WriteLine(format, args); diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 61c7dbb96e..57bc24b722 100755 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -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; @@ -257,6 +263,24 @@ namespace OpenRA Log.AddChannel("perf", "perf.log"); Log.AddChannel("debug", "debug.log"); Log.AddChannel("sync", "syncreport.log"); + Log.AddChannel("server", "server.log"); + + try + { + NatUtility.Logger = Log.Channels["server"].Writer; + NatUtility.Verbose = Settings.Server.VerboseNatDiscovery; + NatUtility.DeviceFound += DeviceFound; + NatUtility.DeviceLost += DeviceLost; + Settings.Server.NatDeviceAvailable = false; + NatUtility.StartDiscovery(); + Log.Write("server", "NAT discovery started."); + } + catch (Exception e) + { + Log.Write("server", "Can't discover UPnP-enabled device: {0}", e); + Settings.Server.NatDeviceAvailable = false; + Settings.Server.AllowUPnP = false; + } FileSystem.Mount("."); // Needed to access shaders Renderer.Initialize( Game.Settings.Graphics.Mode ); @@ -268,6 +292,44 @@ namespace OpenRA Sound.Create(Settings.Sound.Engine); InitializeWithMods(Settings.Game.Mods); + + RunAfterDelay(Settings.Server.NatDiscoveryTimeout, () => + { + NatUtility.StopDiscovery(); + Log.Write("server", "NAT discovery stopped."); + if (natDevice == null) + { + Log.Write("server", "No NAT devices with UPnP enabled found within {0} ms deadline. Disabling automatic port forwarding.".F(Settings.Server.NatDiscoveryTimeout)); + Settings.Server.NatDeviceAvailable = false; + Settings.Server.AllowUPnP = false; + } + }); + } + + public static void DeviceFound(object sender, DeviceEventArgs args) + { + natDevice = args.Device; + + Log.Write("server", "NAT device discovered."); + Log.Write("server", "Type: {0}", natDevice.GetType()); + Log.Write("server", "Your external IP is: {0}", natDevice.GetExternalIP()); + + foreach (var mp in natDevice.GetAllMappings()) + Log.Write("server", "Existing port mapping: protocol={0}, public={1}, private={2}", mp.Protocol, mp.PublicPort, mp.PrivatePort); + + Settings.Server.NatDeviceAvailable = true; + 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()); + + Settings.Server.NatDeviceAvailable = false; + Settings.Server.AllowUPnP = false; } public static void InitializeWithMods(string[] mods) @@ -416,7 +478,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) @@ -433,7 +495,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; } diff --git a/OpenRA.Game/GameRules/Settings.cs b/OpenRA.Game/GameRules/Settings.cs index b0a70f84e0..1cb8eae1d5 100644 --- a/OpenRA.Game/GameRules/Settings.cs +++ b/OpenRA.Game/GameRules/Settings.cs @@ -29,7 +29,10 @@ 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; // let the user disable it + public bool NatDeviceAvailable = false; // internal check if discovery succeeded + public int NatDiscoveryTimeout = 1000; // ms to search for UPnP enabled NATs + public bool VerboseNatDiscovery = false; // print very detailed logs for debugging public bool AllowCheats = false; public string Map = null; public string[] Ban = null; @@ -49,6 +52,9 @@ namespace OpenRA.GameRules AdvertiseOnline = other.AdvertiseOnline; MasterServer = other.MasterServer; AllowUPnP = other.AllowUPnP; + NatDeviceAvailable = other.NatDeviceAvailable; + NatDiscoveryTimeout = other.NatDiscoveryTimeout; + VerboseNatDiscovery = other.VerboseNatDiscovery; AllowCheats = other.AllowCheats; Map = other.Map; Ban = other.Ban; diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 19947ca9e6..9bf40cad2a 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -60,8 +60,7 @@ - - + @@ -70,6 +69,12 @@ False ..\thirdparty\SharpFont.dll + + False + False + mono.nat + ..\thirdparty\Mono.Nat.dll + False ..\thirdparty\Tao\Tao.OpenAl.dll @@ -151,7 +156,6 @@ - diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 43c8477d43..6468f0c11a 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -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(); @@ -74,11 +80,9 @@ namespace OpenRA.Server { foreach (var t in ServerTraits.WithInterface()) t.GameEnded(this); - if (Settings.AllowUPnP) - 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 +95,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(trait) ); @@ -156,7 +126,7 @@ namespace OpenRA.Server var timeout = ServerTraits.WithInterface().Min(t => t.TickTimeout); for( ; ; ) { - var checkRead = new ArrayList(); + var checkRead = new List(); checkRead.Add( listener.Server ); foreach( var c in conns ) checkRead.Add( c.socket ); foreach( var c in preConns ) checkRead.Add( c.socket ); @@ -168,7 +138,7 @@ namespace OpenRA.Server break; } - foreach( Socket s in checkRead ) + foreach( var s in checkRead ) if( s == listener.Server ) AcceptConnection(); else if (preConns.Count > 0) { @@ -187,6 +157,8 @@ namespace OpenRA.Server if (State == ServerState.ShuttingDown) { EndGame(); + if (Settings.AllowUPnP) + RemovePortforward(); break; } } @@ -202,16 +174,33 @@ namespace OpenRA.Server } + void ForwardPort() + { + try + { + var 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) + { + 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); + var 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) + catch (Exception e) { - OpenRA.Log.Write("server", "Can not remove UPnP portforwarding rules: {0}", e); + Log.Write("server", "Can not remove UPnP portforwarding rules: {0}", e); + Settings.AllowUPnP = false; } } diff --git a/OpenRA.Game/Server/UPnP.cs b/OpenRA.Game/Server/UPnP.cs deleted file mode 100644 index a4c7a0951e..0000000000 --- a/OpenRA.Game/Server/UPnP.cs +++ /dev/null @@ -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(""+ - "{0}"+ - "{1}{0}" + - "{2}1" + - "{3}"+ - "0", - 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("" + - "{0}"+ - "{1}", 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, "" + - "", "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 = "" + - "" + - "" + - soap + - "" + - ""; - 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; - } - } - } - } -} diff --git a/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs index e93d00a2f4..c15dac9da7 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs @@ -69,6 +69,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var UPnPCheckbox = panel.Get("UPNP_CHECKBOX"); UPnPCheckbox.IsChecked = () => allowUPnP; UPnPCheckbox.OnClick = () => allowUPnP ^= true; + UPnPCheckbox.IsDisabled = () => !Game.Settings.Server.NatDeviceAvailable; } void CreateAndJoin() diff --git a/mods/cnc/chrome/createserver.yaml b/mods/cnc/chrome/createserver.yaml index 839eea09f4..9f6ce6eaaf 100644 --- a/mods/cnc/chrome/createserver.yaml +++ b/mods/cnc/chrome/createserver.yaml @@ -75,7 +75,7 @@ Container@CREATESERVER_PANEL: Y:180 Width:300 Height:20 - Text:Allow UPnP port forwarding + Text:Automatic port forwarding Label@EXTERNAL_PORT_LABEL: X:15 Y:219 diff --git a/mods/ra/chrome/create-server.yaml b/mods/ra/chrome/create-server.yaml index 3a81044bd2..c8427d0fc5 100644 --- a/mods/ra/chrome/create-server.yaml +++ b/mods/ra/chrome/create-server.yaml @@ -65,7 +65,7 @@ Background@CREATESERVER_BG: Y:165 Width:300 Height:20 - Text:Allow UPnP port forwarding + Text:Automatic port forwarding Button@CREATE_BUTTON: X:130 Y:PARENT_BOTTOM - 45 diff --git a/packaging/package-all.sh b/packaging/package-all.sh index b3c48d64ad..91961f0418 100755 --- a/packaging/package-all.sh +++ b/packaging/package-all.sh @@ -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 diff --git a/packaging/windows/OpenRA.nsi b/packaging/windows/OpenRA.nsi index bc1d82df90..4275cbd933 100644 --- a/packaging/windows/OpenRA.nsi +++ b/packaging/windows/OpenRA.nsi @@ -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" @@ -91,13 +92,13 @@ Section "Client" Client File "${SRCDIR}\OpenRA.ico" File "${SRCDIR}\Tao.*.dll" File "${SRCDIR}\SharpFont.*.dll" - + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$StartMenuFolder" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\OpenRA.lnk" $OUTDIR\OpenRA.Game.exe "" \ "$OUTDIR\OpenRA.Game.exe" "" "" "" "" !insertmacro MUI_STARTMENU_WRITE_END - + SetOutPath "$INSTDIR\cg" File "${SRCDIR}\cg\*.fx" SetOutPath "$INSTDIR\glsl" @@ -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 diff --git a/thirdparty/Mono.Nat.dll b/thirdparty/Mono.Nat.dll new file mode 100644 index 0000000000..a9b77a3a8a Binary files /dev/null and b/thirdparty/Mono.Nat.dll differ