diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61bdb1605..3586876e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: Game: [Ares,TS,YR] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 @@ -27,7 +27,7 @@ jobs: run: ./BuildScripts/Build-${{matrix.Game}}.ps1 shell: pwsh - - uses: actions/upload-artifact@v3.1.1 + - uses: actions/upload-artifact@v3.1.2 name: Upload Artifacts with: name: artifacts-${{matrix.Game}} diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 236043a77..f17c7fb08 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -46,12 +46,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/ClientGUI/ClientGUI.csproj b/ClientGUI/ClientGUI.csproj index ff2edf7ec..b84a222d9 100644 --- a/ClientGUI/ClientGUI.csproj +++ b/ClientGUI/ClientGUI.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DTAConfig/DTAConfig.csproj b/DTAConfig/DTAConfig.csproj index f1e633bfb..6a4ec55fc 100644 --- a/DTAConfig/DTAConfig.csproj +++ b/DTAConfig/DTAConfig.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 0ce19f848..86c02304b 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -37,7 +37,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 901f3442c..cc1d9c219 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -61,10 +61,8 @@ public static CnCNetTunnel Parse(string str, bool hasIPv6Internet, bool hasIPv4I } else { - Logger.Log($""" - No supported IP address/connection found ({nameof(NetworkHelper.HasIPv6Internet)}={hasIPv6Internet}, - {nameof(NetworkHelper.HasIPv4Internet)}={hasIPv4Internet}) for {primaryIpAddress} - {secondaryIpAddress}. - """); + Logger.Log($"No supported IP address/connection found ({nameof(NetworkHelper.HasIPv6Internet)}={hasIPv6Internet}, " + + $"{nameof(NetworkHelper.HasIPv4Internet)}={hasIPv4Internet}) for {primaryIpAddress} - {secondaryIpAddress}."); return null; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index a847b2488..cf18dfa79 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -21,13 +21,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal sealed record InternetGatewayDevice( IEnumerable Locations, string Server, - string CacheControl, - string Ext, - string SearchTarget, - string UniqueServiceName, UPnPDescription UPnPDescription, - Uri PreferredLocation, - IReadOnlyCollection LocalIpAddresses) + Uri PreferredLocation) { private const uint IpLeaseTimeInSeconds = 4 * 60 * 60; private const ushort IanaUdpProtocolNumber = 17; @@ -135,8 +130,9 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance Logger.Log($"P2P: Received external IPv4 address."); #endif } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } return ipAddress; @@ -172,8 +168,9 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance Logger.Log($"P2P: Received NAT status {natEnabled}."); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } return natEnabled; @@ -194,6 +191,8 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance } catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { + ProgramConstants.LogException(ex); + return (null, null); } } @@ -238,9 +237,7 @@ private async ValueTask DoSoapActionAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - ProgramConstants.LogException(ex, $"P2P: {action} error/not supported using {addressFamily}."); - - throw; + throw new($"P2P: {action} error/not supported using {addressFamily}.", ex); } } @@ -321,7 +318,9 @@ private static async ValueTask ExecuteSoapAction { AddressFamily.InterNetwork when Locations.Any(q => q.HostNameType is UriHostNameType.IPv4) => Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv4), - AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6) => + AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6 && !NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) => + Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6), + AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6 && NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) => Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6), _ => PreferredLocation }; @@ -330,7 +329,7 @@ AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNa Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPConstants.UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); string serviceType = $"{UPnPConstants.UPnPServiceNamespace}:{wanConnectionDeviceService}"; ServiceListItem wanIpConnectionService = wanConnectionDevice.ServiceList.Single(q => q.ServiceType.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); - var serviceUri = new Uri(FormattableString.Invariant($"{location.Scheme}://{(location.HostNameType is UriHostNameType.IPv6 ? '[' : null)}{location.IdnHost}{(location.HostNameType is UriHostNameType.IPv6 ? ']' : null)}:{location.Port}{wanIpConnectionService.ControlUrl}")); + Uri serviceUri = NetworkHelper.FormatUri(location.Scheme, location, (ushort)location.Port, wanIpConnectionService.ControlUrl); return new(wanIpConnectionService, serviceUri, serviceType); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs deleted file mode 100644 index 50d0e5181..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; - -internal enum AddressType -{ - Unknown, - - IpV4SiteLocal, - - IpV6LinkLocal, - - IpV6SiteLocal -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs index f0c62e257..e1701ea98 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs @@ -3,4 +3,4 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn, IPAddress LocalIpAddress); \ No newline at end of file +internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string Usn, IPAddress LocalIpAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 8bc06da08..a120b7e07 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -70,14 +70,6 @@ internal static class UPnPHandler DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - private static IReadOnlyDictionary SsdpMultiCastAddresses - => new Dictionary - { - [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), - [AddressType.IpV6LinkLocal] = IPAddress.Parse("[FF02::C]"), - [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") - }.AsReadOnly(); - public static async ValueTask<( InternetGatewayDevice InternetGatewayDevice, List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, @@ -113,10 +105,8 @@ private static async Task GetInternetGatewayDeviceAsync(C foreach (InternetGatewayDevice internetGatewayDevice in internetGatewayDevices) { - Logger.Log($""" - P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} - {internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server}). - """); + Logger.Log($"P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + + $"{internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server})."); } InternetGatewayDevice selectedInternetGatewayDevice = GetInternetGatewayDeviceByVersion(internetGatewayDevices, 2); @@ -125,10 +115,8 @@ private static async Task GetInternetGatewayDeviceAsync(C if (selectedInternetGatewayDevice is not null) { - Logger.Log($""" - P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} - {selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server}). - """); + Logger.Log($"P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + + $"{selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server})."); } else { @@ -295,11 +283,15 @@ private static async ValueTask> GetDevicesAsy private static IEnumerable> GetGroupedDeviceResponses( IEnumerable<(IPAddress LocalIpAddress, IEnumerable> Responses)> formattedDeviceResponses) => formattedDeviceResponses - .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["CACHE-CONTROL"], r["EXT"], r["ST"], r["USN"], q.LocalIpAddress))) + .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["USN"], q.LocalIpAddress))) .GroupBy(q => q.Usn); private static Uri GetPreferredLocation(IReadOnlyCollection locations) - => locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6) ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); + { + return locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6 && !NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) + ?? locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6 && NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) + ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); + } private static IEnumerable> GetFormattedDeviceResponses(IEnumerable responses) { @@ -317,23 +309,18 @@ private static IEnumerable> GetFormattedDeviceRespons StringComparer.OrdinalIgnoreCase)); } - private static async Task<(IPAddress LocalIpAddress, IEnumerable Responses)> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken) + private static async Task<(IPAddress LocalIpAddress, IEnumerable Responses)> SearchDevicesAsync(IPAddress localAddress, IPAddress multicastAddress, CancellationToken cancellationToken) { var responses = new List(); - AddressType addressType = GetAddressType(localAddress); - - if (addressType is AddressType.Unknown) - return new(localAddress, responses); - - using var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); var localEndPoint = new IPEndPoint(localAddress, 0); - var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); + var multiCastIpEndPoint = new IPEndPoint(multicastAddress, UPnPConstants.UPnPMultiCastPort); try { socket.Bind(localEndPoint); - string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: {ReceiveTimeoutInSeconds}\r\n\r\n"); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {NetworkHelper.FormatUri(multiCastIpEndPoint).Authority}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: {ReceiveTimeoutInSeconds}\r\n\r\n"); const int charSize = sizeof(char); int bufferSize = request.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -356,20 +343,6 @@ private static IEnumerable> GetFormattedDeviceRespons return new(localAddress, responses); } - private static AddressType GetAddressType(IPAddress localAddress) - { - if (localAddress.AddressFamily is AddressFamily.InterNetwork) - return AddressType.IpV4SiteLocal; - - if (localAddress.IsIPv6LinkLocal) - return AddressType.IpV6LinkLocal; - - if (localAddress.IsIPv6SiteLocal) - return AddressType.IpV6SiteLocal; - - return AddressType.Unknown; - } - private static async ValueTask ReceiveAsync(Socket socket, ICollection responses, CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); @@ -406,9 +379,10 @@ private static async ValueTask GetDescriptionAsync(Uri uri, Can private static async ValueTask Responses)>> DetectDevicesAsync(CancellationToken cancellationToken) { - IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); + IEnumerable unicastAddresses = NetworkHelper.GetLocalAddresses(); + IEnumerable multicastAddresses = NetworkHelper.GetMulticastAddresses(); (IPAddress LocalIpAddress, IEnumerable Responses)[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( - localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))).ConfigureAwait(false); + multicastAddresses.SelectMany(q => unicastAddresses.Where(r => r.AddressFamily == q.AddressFamily).Select(r => SearchDevicesAsync(r, q, cancellationToken)))).ConfigureAwait(false); return localAddressesDeviceResponses.Where(q => q.Responses.Any(r => r.Any())).Select(q => (q.LocalIpAddress, q.Responses)).Distinct(); } @@ -447,13 +421,8 @@ private static async Task ParseDeviceAsync( return new( locations, internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), - internetGatewayDeviceResponses.Key, uPnPDescription, - preferredLocation, - internetGatewayDeviceResponses.Select(r => r.LocalIpAddress).Distinct().ToList().AsReadOnly()); + preferredLocation); } catch (Exception ex) { @@ -465,18 +434,9 @@ private static async Task ParseDeviceAsync( private static Uri ParseLocation((IPAddress LocalIpAddress, Uri Location) location) { - if (location.Location.HostNameType is not UriHostNameType.IPv6 || !IPAddress.TryParse(location.Location.Host, out IPAddress ipAddress) - || !NetworkHelper.IsPrivateIpAddress(ipAddress)) - { + if (location.Location.HostNameType is not UriHostNameType.IPv6 || !IPAddress.TryParse(location.Location.IdnHost, out IPAddress ipAddress) || !NetworkHelper.IsPrivateIpAddress(ipAddress)) return location.Location; - } - - var uriBuilder = new UriBuilder(location.Location); - - uriBuilder.Host = FormattableString.Invariant($"[{uriBuilder.Host - .Replace("[", null, StringComparison.OrdinalIgnoreCase) - .Replace("]", null, StringComparison.OrdinalIgnoreCase)}%{location.LocalIpAddress.ScopeId}]"); - return uriBuilder.Uri; + return NetworkHelper.FormatUri(new(IPAddress.Parse(FormattableString.Invariant($"{location.Location.IdnHost}%{location.LocalIpAddress.ScopeId}")), location.Location.Port), location.Location.Scheme, location.Location.PathAndQuery); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index d7ea8ecb1..8bb27a7de 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -34,7 +34,7 @@ public static bool HasIPv4Internet() public static IEnumerable GetLocalAddresses() => GetUniCastIpAddresses() - .Select(q => q.UnicastIPAddressInformation.Address); + .Select(q => q.Address); public static IEnumerable GetPublicIpAddresses() => GetLocalAddresses() @@ -54,12 +54,38 @@ public static IEnumerable GetLanUniCastIpAddresses( .SelectMany(q => q.UnicastAddresses) .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); - private static IEnumerable<(UnicastIPAddressInformation UnicastIPAddressInformation, GatewayIPAddressInformation GatewayIPAddressInformation)> GetUniCastIpAddresses() + public static IEnumerable GetMulticastAddresses() + => GetIpInterfaces() + .SelectMany(q => q.MulticastAddresses.Select(r => r.Address)) + .Where(q => SupportedAddressFamilies.Contains(q.AddressFamily)); + + public static Uri FormatUri(string scheme, Uri uri, ushort port, string path) + { + string[] pathAndQuery = path.Split('?'); + var uriBuilder = new UriBuilder(uri) + { + Scheme = scheme, + Host = uri.IdnHost, + Port = port, + Path = pathAndQuery.First(), + Query = pathAndQuery.Skip(1).SingleOrDefault() + }; + + return uriBuilder.Uri; + } + + public static Uri FormatUri(IPEndPoint ipEndPoint, string scheme = null, string path = null) + { + var uriBuilder = new UriBuilder(scheme ?? Uri.UriSchemeHttps, ipEndPoint.Address.ToString(), ipEndPoint.Port, path); + + return uriBuilder.Uri; + } + + private static IEnumerable GetUniCastIpAddresses() => GetIpInterfaces() .Where(q => q.GatewayAddresses.Any()) - .SelectMany(q => q.UnicastAddresses.Select( - r => (UnicastIPAddressInformation: r, GatewayIPAddressInformation: q.GatewayAddresses.FirstOrDefault(s => s.Address.AddressFamily == r.Address.AddressFamily)))) - .Where(q => SupportedAddressFamilies.Contains(q.UnicastIPAddressInformation.Address.AddressFamily)); + .SelectMany(q => q.UnicastAddresses) + .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); private static IEnumerable GetIpInterfaces() => NetworkInterface.GetAllNetworkInterfaces() @@ -69,8 +95,8 @@ private static IEnumerable GetIpInterfaces() [SupportedOSPlatform("windows")] private static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() => GetUniCastIpAddresses() - .Where(q => !IsPrivateIpAddress(q.UnicastIPAddressInformation.Address)) - .Select(q => (q.UnicastIPAddressInformation.Address, q.UnicastIPAddressInformation.PrefixOrigin, q.UnicastIPAddressInformation.SuffixOrigin)); + .Where(q => !IsPrivateIpAddress(q.Address)) + .Select(q => (q.Address, q.PrefixOrigin, q.SuffixOrigin)); public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIpAddressInformation) { @@ -257,7 +283,6 @@ public static bool IsPrivateIpAddress(IPAddress ipAddress) || ipAddress.IsIPv6UniqueLocal || ipAddress.IsIPv6LinkLocal, AddressFamily.InterNetwork => IsInRange("10.0.0.0", "10.255.255.255", ipAddress) - || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) || IsInRange("192.168.0.0", "192.168.255.255", ipAddress) || IsInRange("169.254.0.0", "169.254.255.255", ipAddress) diff --git a/Localization/Localization.csproj b/Localization/Localization.csproj index cdc40c544..314407ed6 100644 --- a/Localization/Localization.csproj +++ b/Localization/Localization.csproj @@ -14,7 +14,7 @@ Localization - + all runtime; build; native; contentfiles; analyzers; buildtransitive