Phases two and three ship together, because their code grew in the same files. With consent the app asks the router to open 80, 443 and a high port for ten minutes (UPnP, NAT-PMP or PCP), and the server, holding a single-use ticket bound to the caller's own address, connects back and must get an HMAC only the app can give; the mappings are removed after the run and remembered until then, so a crash cannot leave one open. Hairpin is tried from inside. Two STUN servers, the outside one first, tell easy from hard NAT. Mail gets port 25 to Gmail and Outlook, the reverse name and five blocklists through the user's own resolver, network-wide UCEPROTECT listings counted as a caution. DNS, speed against the server (four at a time, twelve an hour per address) and the device follow, and six uses (website, home from outside, files, game server, fediverse, mail) close the run as possible, possible with a named workaround, or can't tell. Findings can be shared after reading the exact JSON: a file per submission under the unit's state directory, ten a day per address, deletable by the token the app keeps, swept after twelve months; /privacy says all of it in English and Italian. QR codes lead to the downloads. Every language carries every string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
190 lines
7.0 KiB
C#
190 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|