Files
MeWeb/src/MeWeb.Core/Router/PortMapper.cs
T

189 lines
7.0 KiB
C#
Raw Normal View History

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<MappingAttempt> 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",
$"<NewRemoteHost></NewRemoteHost><NewExternalPort>{mapping.ExternalPort}</NewExternalPort><NewProtocol>TCP</NewProtocol>", 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",
$"<NewRemoteHost></NewRemoteHost><NewExternalPort>{external}</NewExternalPort><NewProtocol>TCP</NewProtocol>" +
$"<NewInternalPort>{@internal}</NewInternalPort><NewInternalClient>{localAddress}</NewInternalClient><NewEnabled>1</NewEnabled>" +
$"<NewPortMappingDescription>me-web? test</NewPortMappingDescription><NewLeaseDuration>{lease}</NewLeaseDuration>", 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<byte[]> 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);
}
}