2026-10-15 05:41:44 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-10-15 09:10:37 +02:00
|
|
|
catch (Exception) when (!ct.IsCancellationRequested)
|
2026-10-15 05:41:44 +02:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|