using System.Buffers.Binary; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Xml; using MeWeb.Core.Net; namespace MeWeb.Core.Router; public enum MapVia { None, Upnp, NatPmp, Pcp, } public class Mapping { public MapVia Via { get; set; } public string Gateway { get; set; } public int ExternalPort { get; set; } public int InternalPort { get; set; } public string InternalClient { get; set; } public string Control { get; set; } public string Service { get; set; } public string Nonce { get; set; } public DateTimeOffset Until { get; set; } } public class MappingAttempt { public int ExternalPort { get; set; } public Mapping Mapping { get; set; } public MapVia Via { get; set; } public int Error { get; set; } public bool Done => Mapping is not null; } public static class PortMapper { public const int LeaseSeconds = 600; const int PortMappingPort = 5351; public static async Task Map(RouterReport router, string localAddress, int external, int @internal, CancellationToken ct) { var attempt = new MappingAttempt { ExternalPort = external }; if (router.UpnpControl is not null) { attempt.Via = MapVia.Upnp; (attempt.Mapping, attempt.Error) = await Upnp(router, localAddress, external, @internal, ct); if (attempt.Done) return attempt; } if (router.NatPmp == Answer.Answered) { attempt.Via = MapVia.NatPmp; (attempt.Mapping, attempt.Error) = await NatPmp(router.Gateway, external, @internal, ct); if (attempt.Done) return attempt; } if (router.Pcp != Answer.Silent) { attempt.Via = MapVia.Pcp; (attempt.Mapping, attempt.Error) = await Pcp(router.Gateway, localAddress, external, @internal, ct); } return attempt; } public static async Task Remove(Mapping mapping, CancellationToken ct) { try { switch (mapping.Via) { case MapVia.Upnp: await Gateway.Soap(new Uri(mapping.Control), mapping.Service, "DeletePortMapping", $"{mapping.ExternalPort}TCP", ct); break; case MapVia.NatPmp: await Gateway.Exchange(IPAddress.Parse(mapping.Gateway), NatPmpRequest(mapping.InternalPort, 0, 0), ct); break; case MapVia.Pcp: await PcpExchange(mapping.Gateway, mapping.InternalClient, Convert.FromBase64String(mapping.Nonce), mapping.ExternalPort, mapping.InternalPort, 0, ct); break; } } catch (Exception ex) when (ex is HttpRequestException or XmlException or SocketException or FormatException or TaskCanceledException && !ct.IsCancellationRequested) { } } static async Task<(Mapping, int)> Upnp(RouterReport router, string localAddress, int external, int @internal, CancellationToken ct) { var control = new Uri(router.UpnpControl); foreach (var lease in new[] { LeaseSeconds, 0 }) { try { var (answer, status) = await Gateway.Soap(control, router.UpnpService, "AddPortMapping", $"{external}TCP" + $"{@internal}{localAddress}1" + $"me-web? test{lease}", ct); var error = status == 200 ? 0 : Gateway.ErrorCode(answer, status); if (error == 0) return (new Mapping { Via = MapVia.Upnp, Gateway = router.Gateway, ExternalPort = external, InternalPort = @internal, InternalClient = localAddress, Control = router.UpnpControl, Service = router.UpnpService, Until = lease == 0 ? DateTimeOffset.MaxValue : DateTimeOffset.UtcNow.AddSeconds(lease), }, 0); if (error != 725) return (null, error); } catch (Exception ex) when (ex is HttpRequestException or XmlException or TaskCanceledException && !ct.IsCancellationRequested) { return (null, -1); } } return (null, 725); } static async Task<(Mapping, int)> NatPmp(string gateway, int external, int @internal, CancellationToken ct) { var answer = await Gateway.Exchange(IPAddress.Parse(gateway), NatPmpRequest(@internal, external, LeaseSeconds), ct); if (answer is null || answer.Length < 16 || answer[0] != 0 || answer[1] != 130) return (null, -1); var result = BinaryPrimitives.ReadUInt16BigEndian(answer.AsSpan(2)); if (result != 0) return (null, result); var mapped = BinaryPrimitives.ReadUInt16BigEndian(answer.AsSpan(10)); var lifetime = BinaryPrimitives.ReadUInt32BigEndian(answer.AsSpan(12)); return (new Mapping { Via = MapVia.NatPmp, Gateway = gateway, ExternalPort = mapped, InternalPort = @internal, Until = DateTimeOffset.UtcNow.AddSeconds(lifetime), }, 0); } static async Task<(Mapping, int)> Pcp(string gateway, string localAddress, int external, int @internal, CancellationToken ct) { var nonce = RandomNumberGenerator.GetBytes(12); var answer = await PcpExchange(gateway, localAddress, nonce, external, @internal, LeaseSeconds, ct); if (answer is null || answer.Length < 60 || answer[0] != 2 || answer[1] != 0x81) return (null, -1); if (answer[3] != 0) return (null, answer[3]); var lifetime = BinaryPrimitives.ReadUInt32BigEndian(answer.AsSpan(4)); var mapped = BinaryPrimitives.ReadUInt16BigEndian(answer.AsSpan(42)); return (new Mapping { Via = MapVia.Pcp, Gateway = gateway, ExternalPort = mapped, InternalPort = @internal, InternalClient = localAddress, Nonce = Convert.ToBase64String(nonce), Until = DateTimeOffset.UtcNow.AddSeconds(lifetime), }, 0); } static byte[] NatPmpRequest(int @internal, int external, uint lifetime) { var request = new byte[12]; request[1] = 2; BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(4), (ushort)@internal); BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(6), (ushort)external); BinaryPrimitives.WriteUInt32BigEndian(request.AsSpan(8), lifetime); return request; } static async Task PcpExchange(string gateway, string localAddress, byte[] nonce, int external, int @internal, uint lifetime, CancellationToken ct) { var request = new byte[60]; request[0] = 2; request[1] = 1; BinaryPrimitives.WriteUInt32BigEndian(request.AsSpan(4), lifetime); (Addresses.Parse(localAddress) ?? IPAddress.Any).MapToIPv6().GetAddressBytes().CopyTo(request, 8); nonce.CopyTo(request, 24); request[36] = 6; BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(40), (ushort)@internal); BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(42), (ushort)external); IPAddress.Any.MapToIPv6().GetAddressBytes().CopyTo(request, 44); return await Gateway.Exchange(IPAddress.Parse(gateway), request, ct); } }