diff --git a/AUTHORS b/AUTHORS index 7220de3846..b63b43ca0d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -173,9 +173,8 @@ under the MIT license. Using FuzzyLogicLibrary (fuzzynet) by Dmitry Kaluzhny and released under the GNU GPL terms. -Using Open.Nat by Lucas Ontivero, based on the work -of Alan McGovern and Ben Motmans and distributed -under the MIT license. +Using Mono.Nat by Alan McGovern, Ben Motmans, +Nicholas Terry distributed under the MIT license. Using ICSharpCode.SharpZipLib initially by Mike Krueger and distributed under the GNU GPL terms. diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 4e53ea124f..6a7fd585eb 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -56,7 +56,6 @@ namespace OpenRA public static string EngineVersion { get; private set; } public static LocalPlayerProfile LocalPlayerProfile; - static Task discoverNat; static bool takeScreenshot = false; static Benchmark benchmark = null; @@ -360,8 +359,7 @@ namespace OpenRA } } - if (Settings.Server.DiscoverNatDevices) - discoverNat = UPnP.DiscoverNatDevices(Settings.Server.NatDiscoveryTimeout); + Nat.Initialize(); var modSearchArg = args.GetValue("Engine.ModSearchPaths", null); var modSearchPaths = modSearchArg != null ? @@ -472,16 +470,6 @@ namespace OpenRA JoinLocal(); - try - { - discoverNat?.Wait(); - } - catch (Exception e) - { - Console.WriteLine("NAT discovery failed: {0}", e.Message); - Log.Write("nat", e.ToString()); - } - ChromeMetrics.TryGet("ChatMessageColor", out chatMessageColor); ChromeMetrics.TryGet("SystemMessageColor", out systemMessageColor); if (!ChromeMetrics.TryGet("SystemMessageLabel", out systemMessageLabel)) diff --git a/OpenRA.Game/Network/Nat.cs b/OpenRA.Game/Network/Nat.cs new file mode 100644 index 0000000000..bbb950d8ae --- /dev/null +++ b/OpenRA.Game/Network/Nat.cs @@ -0,0 +1,101 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Threading; +using Mono.Nat; + +namespace OpenRA.Network +{ + public enum NatStatus { Enabled, Disabled, NotSupported } + + public class Nat + { + public static NatStatus Status => NatUtility.IsSearching ? natDevice != null ? NatStatus.Enabled : NatStatus.NotSupported : NatStatus.Disabled; + + static Mapping mapping; + static INatDevice natDevice; + static bool initialized; + + public static void Initialize() + { + if (initialized) + return; + + if (Game.Settings.Server.DiscoverNatDevices) + { + NatUtility.DeviceFound += DeviceFound; + NatUtility.StartDiscovery(); + } + + initialized = true; + } + + static readonly SemaphoreSlim Locker = new SemaphoreSlim(1, 1); + + static async void DeviceFound(object sender, DeviceEventArgs args) + { + await Locker.WaitAsync(); + try + { + // Only interact with one at a time. Some support both UPnP and NAT-PMP. + natDevice = args.Device; + + Log.Write("nat", "Device found: {0}", natDevice.DeviceEndpoint); + Log.Write("nat", "Type: {0}", natDevice.NatProtocol); + } + finally + { + Locker.Release(); + } + } + + public static bool TryForwardPort(int listen, int external) + { + if (natDevice == null) + return false; + + var lifetime = Game.Settings.Server.NatPortMappingLifetime; + mapping = new Mapping(Protocol.Tcp, listen, external, lifetime, "OpenRA"); + try + { + natDevice.CreatePortMap(mapping); + } + catch (Exception e) + { + Console.WriteLine("Port forwarding failed: {0}", e.Message); + Log.Write("nat", e.StackTrace); + return false; + } + + return true; + } + + public static bool TryRemovePortForward() + { + if (natDevice == null) + return false; + + try + { + natDevice.DeletePortMap(mapping); + } + catch (Exception e) + { + Console.WriteLine("Port removal failed: {0}", e.Message); + Log.Write("nat", e.StackTrace); + return false; + } + + return true; + } + } +} diff --git a/OpenRA.Game/Network/UPnP.cs b/OpenRA.Game/Network/UPnP.cs deleted file mode 100644 index 15fd29b696..0000000000 --- a/OpenRA.Game/Network/UPnP.cs +++ /dev/null @@ -1,84 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using System; -using System.Diagnostics; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -using Open.Nat; - -namespace OpenRA.Network -{ - public enum UPnPStatus { Enabled, Disabled, NotSupported } - - public class UPnP - { - static NatDevice natDevice; - static Mapping mapping; - static bool initialized; - - public static IPAddress ExternalIP { get; private set; } - public static UPnPStatus Status => - initialized ? natDevice != null ? - UPnPStatus.Enabled : UPnPStatus.NotSupported : UPnPStatus.Disabled; - - public static async Task DiscoverNatDevices(int timeout) - { - initialized = true; - - NatDiscoverer.TraceSource.Switch.Level = SourceLevels.Verbose; - var logChannel = Log.Channel("nat"); - NatDiscoverer.TraceSource.Listeners.Add(new TextWriterTraceListener(logChannel.Writer)); - - var natDiscoverer = new NatDiscoverer(); - var token = new CancellationTokenSource(timeout); - natDevice = await natDiscoverer.DiscoverDeviceAsync(PortMapper.Upnp, token); - try - { - ExternalIP = await natDevice.GetExternalIPAsync(); - } - catch (Exception e) - { - Console.WriteLine("Getting the external IP from NAT device failed: {0}", e.Message); - Log.Write("nat", e.StackTrace); - } - } - - public static async Task ForwardPort(int listen, int external) - { - mapping = new Mapping(Protocol.Tcp, listen, external, "OpenRA"); - try - { - await natDevice.CreatePortMapAsync(mapping); - } - catch (Exception e) - { - Console.WriteLine("Port forwarding failed: {0}", e.Message); - Log.Write("nat", e.StackTrace); - } - } - - public static async Task RemovePortForward() - { - try - { - await natDevice.DeletePortMapAsync(mapping); - } - catch (Exception e) - { - Console.WriteLine("Port removal failed: {0}", e.Message); - Log.Write("nat", e.StackTrace); - } - } - } -} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index a10e4a94e8..3ff8696d7e 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index b790e39a97..964f39b92e 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -221,8 +221,8 @@ namespace OpenRA.Server if (type != ServerType.Local && settings.EnableGeoIP) GeoIP.Initialize(); - if (type != ServerType.Local && UPnP.Status == UPnPStatus.Enabled) - UPnP.ForwardPort(Settings.ListenPort, Settings.ListenPort).Wait(); + if (type != ServerType.Local) + Nat.TryForwardPort(Settings.ListenPort, Settings.ListenPort); foreach (var trait in modData.Manifest.ServerTraits) serverTraits.Add(modData.ObjectCreator.CreateObject(trait)); @@ -310,8 +310,8 @@ namespace OpenRA.Server if (State == ServerState.ShuttingDown) { EndGame(); - if (type != ServerType.Local && UPnP.Status == UPnPStatus.Enabled) - UPnP.RemovePortForward().Wait(); + if (type != ServerType.Local) + Nat.TryRemovePortForward(); break; } } diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index 47d2dad544..3a8a12a357 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -49,11 +49,11 @@ namespace OpenRA [Desc("Locks the game with a password.")] public string Password = ""; - [Desc("Allow users to enable NAT discovery for external IP detection and automatic port forwarding.")] + [Desc("Allow users to search UPnP/NAT-PMP enabled devices for automatic port forwarding.")] public bool DiscoverNatDevices = false; - [Desc("Time in milliseconds to search for UPnP enabled NAT devices.")] - public int NatDiscoveryTimeout = 5000; + [Desc("Time in seconds for UPnP/NAT-PMP mappings to last.")] + public int NatPortMappingLifetime = 36000; [Desc("Starts the game with a default map. Input as hash that can be obtained by the utility.")] public string Map = null; diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs index 806ed8e111..f085afc051 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs @@ -116,20 +116,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (noticesNoUPnP != null) { noticesNoUPnP.IsVisible = () => advertiseOnline && - (UPnP.Status == UPnPStatus.NotSupported || UPnP.Status == UPnPStatus.Disabled); + (Nat.Status == NatStatus.NotSupported || Nat.Status == NatStatus.Disabled); var settingsA = noticesNoUPnP.GetOrNull("SETTINGS_A"); if (settingsA != null) - settingsA.IsVisible = () => UPnP.Status == UPnPStatus.Disabled; + settingsA.IsVisible = () => Nat.Status == NatStatus.Disabled; var settingsB = noticesNoUPnP.GetOrNull("SETTINGS_B"); if (settingsB != null) - settingsB.IsVisible = () => UPnP.Status == UPnPStatus.Disabled; + settingsB.IsVisible = () => Nat.Status == NatStatus.Disabled; } var noticesUPnP = panel.GetOrNull("NOTICES_UPNP"); if (noticesUPnP != null) - noticesUPnP.IsVisible = () => advertiseOnline && UPnP.Status == UPnPStatus.Enabled; + noticesUPnP.IsVisible = () => advertiseOnline && Nat.Status == NatStatus.Enabled; var noticesLAN = panel.GetOrNull("NOTICES_LAN"); if (noticesLAN != null) @@ -145,16 +145,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (advertiseOnline) { - noticesLabelA.Text = "Internet Server (UPnP "; + noticesLabelA.Text = "Internet Server (UPnP/NAT-PMP "; var aWidth = Game.Renderer.Fonts[noticesLabelA.Font].Measure(noticesLabelA.Text).X; noticesLabelA.Bounds.Width = aWidth; - var status = UPnP.Status; - noticesLabelB.Text = status == UPnPStatus.Enabled ? "Enabled" : - status == UPnPStatus.NotSupported ? "Not Supported" : "Disabled"; + noticesLabelB.Text = Nat.Status == NatStatus.Enabled ? "Enabled" : + Nat.Status == NatStatus.NotSupported ? "Not Supported" : "Disabled"; - noticesLabelB.TextColor = status == UPnPStatus.Enabled ? ChromeMetrics.Get("NoticeSuccessColor") : - status == UPnPStatus.NotSupported ? ChromeMetrics.Get("NoticeErrorColor") : + noticesLabelB.TextColor = Nat.Status == NatStatus.Enabled ? ChromeMetrics.Get("NoticeSuccessColor") : + Nat.Status == NatStatus.NotSupported ? ChromeMetrics.Get("NoticeErrorColor") : ChromeMetrics.Get("NoticeInfoColor"); var bWidth = Game.Renderer.Fonts[noticesLabelB.Font].Measure(noticesLabelB.Text).X; diff --git a/OpenRA.Server/Program.cs b/OpenRA.Server/Program.cs index 054b5e6cfa..c19b759dc3 100644 --- a/OpenRA.Server/Program.cs +++ b/OpenRA.Server/Program.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; +using OpenRA.Network; using OpenRA.Support; namespace OpenRA.Server @@ -56,6 +57,8 @@ namespace OpenRA.Server Game.InitializeSettings(arguments); var settings = Game.Settings.Server; + Nat.Initialize(); + var envModSearchPaths = Environment.GetEnvironmentVariable("MOD_SEARCH_PATHS"); var modSearchPaths = !string.IsNullOrWhiteSpace(envModSearchPaths) ? FieldLoader.GetValue("MOD_SEARCH_PATHS", envModSearchPaths) : diff --git a/mods/cnc/chrome/multiplayer-createserver.yaml b/mods/cnc/chrome/multiplayer-createserver.yaml index caed021f28..ef386ef249 100644 --- a/mods/cnc/chrome/multiplayer-createserver.yaml +++ b/mods/cnc/chrome/multiplayer-createserver.yaml @@ -162,7 +162,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - You can enable UPnP (if supported by your router) in the + Text: - You can enable UPnP/NAT-PMP (if supported by your router) Label@SETTINGS_B: X: 7 Y: 60 @@ -170,7 +170,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: Advanced tab of the OpenRA settings menu. + Text: in the Advanced tab of the settings menu. Container@NOTICES_UPNP: X: 20 Y: 145 @@ -196,14 +196,14 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - OpenRA will use UPnP to automaticaly configure port forwarding. + Text: - Game will automatically configure port forwarding. Label@SETTINGS_A: Y: 36 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can disable UPnP in the OpenRA settings menu. + Text: - You can disable UPnP/NAT-PMP in the settings menu. Background@MAP_BG: X: PARENT_RIGHT - 189 Y: 15 diff --git a/mods/cnc/chrome/settings-advanced.yaml b/mods/cnc/chrome/settings-advanced.yaml index ba13f0a3b3..92cc974cd8 100644 --- a/mods/cnc/chrome/settings-advanced.yaml +++ b/mods/cnc/chrome/settings-advanced.yaml @@ -15,7 +15,7 @@ Container@ADVANCED_PANEL: Width: 200 Height: 20 Font: Regular - Text: Enable Network Discovery (UPnP) + Text: Enable UPnP/NAT-PMP Discovery Checkbox@PERFTEXT_CHECKBOX: X: 15 Y: 73 diff --git a/mods/common/chrome/multiplayer-createserver.yaml b/mods/common/chrome/multiplayer-createserver.yaml index 6d514da36d..b270cfb58c 100644 --- a/mods/common/chrome/multiplayer-createserver.yaml +++ b/mods/common/chrome/multiplayer-createserver.yaml @@ -157,7 +157,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - You can enable UPnP (if supported by your router) in the + Text: - You can enable UPnP/NAT-PMP (if supported by your router) Label@SETTINGS_B: X: 7 Y: 60 @@ -165,7 +165,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: Advanced tab of the OpenRA settings menu. + Text: in the Advanced tab of the settings menu. Container@NOTICES_UPNP: X: 25 Y: 176 @@ -191,14 +191,14 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - OpenRA will use UPnP to automaticaly configure port forwarding. + Text: - Game will automatically configure port forwarding. Label@SETTINGS_A: Y: 36 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can disable UPnP in the OpenRA settings menu. + Text: - You can disable UPnP/NAT-PMP in the settings menu. Background@MAP_BG: X: PARENT_RIGHT - 194 Y: 45 diff --git a/mods/common/chrome/settings-advanced.yaml b/mods/common/chrome/settings-advanced.yaml index b179ec6dfc..5e5c6d9a05 100644 --- a/mods/common/chrome/settings-advanced.yaml +++ b/mods/common/chrome/settings-advanced.yaml @@ -9,7 +9,7 @@ Container@ADVANCED_PANEL: Width: 200 Height: 20 Font: Regular - Text: Enable Network Discovery (UPnP) + Text: Enable UPnP/NAT-PMP Discovery Checkbox@PERFTEXT_CHECKBOX: X: 15 Y: 73