Skip to content

Commit

Permalink
P2P: UPnP SSDP on all interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
Rans4ckeR committed Jan 20, 2023
1 parent 65a0329 commit 8a8b2cc
Show file tree
Hide file tree
Showing 12 changed files with 77 additions and 107 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,7 +27,7 @@ jobs:
run: ./BuildScripts/Build-${{matrix.Game}}.ps1
shell: pwsh

- uses: actions/[email protected].1
- uses: actions/[email protected].2
name: Upload Artifacts
with:
name: artifacts-${{matrix.Game}}
Expand Down
6 changes: 3 additions & 3 deletions ClientCore/ClientCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@
<EmbeddedResource Include="Resources\yricon.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.11.1">
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Rampastring.XNAUI.$(Engine)" Version="2.3.2" Condition="'!$(Configuration.Contains(Debug))'" />
<PackageReference Include="Rampastring.XNAUI.$(Engine).Debug" Version="2.3.2" Condition="'$(Configuration.Contains(Debug))'" />
<PackageReference Include="Rampastring.XNAUI.$(Engine)" Version="2.3.3" Condition="'!$(Configuration.Contains(Debug))'" />
<PackageReference Include="Rampastring.XNAUI.$(Engine).Debug" Version="2.3.3" Condition="'$(Configuration.Contains(Debug))'" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion ClientGUI/ClientGUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<ProjectReference Include="..\ClientCore\ClientCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.11.1">
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion DTAConfig/DTAConfig.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<Import Project="$(MSBuildThisFileDirectory)..\build\WinForms.props" />
<ItemGroup>
<PackageReference Include="CnCNet.ClientUpdater" Version="1.0.5" />
<PackageReference Include="GitVersion.MsBuild" Version="5.11.1">
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion DXMainClient/DXMainClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<PackageReference Include="OpenMcdf" Version="2.2.1.12" />
<PackageReference Include="System.Management" Version="7.0.0" />
<PackageReference Include="System.DirectoryServices" Version="7.0.0" />
<PackageReference Include="GitVersion.MsBuild" Version="5.11.1">
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
6 changes: 2 additions & 4 deletions DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP;
internal sealed record InternetGatewayDevice(
IEnumerable<Uri> Locations,
string Server,
string CacheControl,
string Ext,
string SearchTarget,
string UniqueServiceName,
UPnPDescription UPnPDescription,
Uri PreferredLocation,
IReadOnlyCollection<IPAddress> LocalIpAddresses)
Uri PreferredLocation)
{
private const uint IpLeaseTimeInSeconds = 4 * 60 * 60;
private const ushort IanaUdpProtocolNumber = 17;
Expand Down Expand Up @@ -135,8 +130,9 @@ public async Task<IPAddress> GetExternalIpV4AddressAsync(CancellationToken cance
Logger.Log($"P2P: Received external IPv4 address.");
#endif
}
catch
catch (Exception ex)
{
ProgramConstants.LogException(ex);
}

return ipAddress;
Expand Down Expand Up @@ -172,8 +168,9 @@ public async Task<IPAddress> GetExternalIpV4AddressAsync(CancellationToken cance

Logger.Log($"P2P: Received NAT status {natEnabled}.");
}
catch
catch (Exception ex)
{
ProgramConstants.LogException(ex);
}

return natEnabled;
Expand All @@ -194,6 +191,8 @@ public async Task<IPAddress> GetExternalIpV4AddressAsync(CancellationToken cance
}
catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
{
ProgramConstants.LogException(ex);

return (null, null);
}
}
Expand Down Expand Up @@ -238,9 +237,7 @@ private async ValueTask<TResponse> DoSoapActionAsync<TRequest, TResponse>(
}
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);
}
}

Expand Down Expand Up @@ -321,7 +318,9 @@ private static async ValueTask<TResponse> ExecuteSoapAction<TRequest, TResponse>
{
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
};
Expand All @@ -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);
}
Expand Down
12 changes: 0 additions & 12 deletions DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string Usn, IPAddress LocalIpAddress);
80 changes: 20 additions & 60 deletions DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,6 @@ internal static class UPnPHandler
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
};

private static IReadOnlyDictionary<AddressType, IPAddress> SsdpMultiCastAddresses
=> new Dictionary<AddressType, IPAddress>
{
[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,
Expand Down Expand Up @@ -113,10 +105,8 @@ private static async Task<InternetGatewayDevice> 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);
Expand All @@ -125,10 +115,8 @@ private static async Task<InternetGatewayDevice> 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
{
Expand Down Expand Up @@ -295,11 +283,15 @@ private static async ValueTask<IEnumerable<InternetGatewayDevice>> GetDevicesAsy
private static IEnumerable<IGrouping<string, InternetGatewayDeviceResponse>> GetGroupedDeviceResponses(
IEnumerable<(IPAddress LocalIpAddress, IEnumerable<Dictionary<string, string>> 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<Uri> 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<Dictionary<string, string>> GetFormattedDeviceResponses(IEnumerable<string> responses)
{
Expand All @@ -317,23 +309,18 @@ private static IEnumerable<Dictionary<string, string>> GetFormattedDeviceRespons
StringComparer.OrdinalIgnoreCase));
}

private static async Task<(IPAddress LocalIpAddress, IEnumerable<string> Responses)> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken)
private static async Task<(IPAddress LocalIpAddress, IEnumerable<string> Responses)> SearchDevicesAsync(IPAddress localAddress, IPAddress multicastAddress, CancellationToken cancellationToken)
{
var responses = new List<string>();
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<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize);
Expand All @@ -356,20 +343,6 @@ private static IEnumerable<Dictionary<string, string>> 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<string> responses, CancellationToken cancellationToken)
{
using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(4096);
Expand Down Expand Up @@ -406,9 +379,10 @@ private static async ValueTask<UPnPDescription> GetDescriptionAsync(Uri uri, Can

private static async ValueTask<IEnumerable<(IPAddress LocalIpAddress, IEnumerable<string> Responses)>> DetectDevicesAsync(CancellationToken cancellationToken)
{
IEnumerable<IPAddress> localAddresses = NetworkHelper.GetLocalAddresses();
IEnumerable<IPAddress> unicastAddresses = NetworkHelper.GetLocalAddresses();
IEnumerable<IPAddress> multicastAddresses = NetworkHelper.GetMulticastAddresses();
(IPAddress LocalIpAddress, IEnumerable<string> 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();
}
Expand Down Expand Up @@ -447,13 +421,8 @@ private static async Task<InternetGatewayDevice> 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)
{
Expand All @@ -465,18 +434,9 @@ private static async Task<InternetGatewayDevice> 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);
}
}
Loading

0 comments on commit 8a8b2cc

Please sign in to comment.