Files
MeWeb/src/MeWeb.Core/Router/PortMapper.cs
T
thepraandClaude Opus 5.5 20f8b2149f
Build / Desktop, server and tools (push) Successful in 1m31s
Release / Every platform, the server, the release (push) Successful in 9m21s
me-web? 0.4: no network failure takes the app down; mobile data read right; the route on the path
- Every network call returns a result whatever fails, not only the exceptions .NET
  usually throws: on Android the HTTPS client is the platform's own stack, and a network
  switch mid-request throws others. The outside test as a whole cannot throw; sharing the
  link falls back to copying it. A last guard (the UI thread, unobserved tasks, Android's
  raiser) writes errors.log in the app folder and keeps the app running; nothing is sent.
- On mobile data the app no longer probes the operator's gateway as if it were a home
  router: the router card says there is none, a private address on the phone with another
  public one is the operator's address sharing (CGNAT) and is drawn as its shared exit, the
  public IPv4 is held by that exit, and the provider's notes are labelled as being about its
  home lines. These were "router not answering" and "can't tell" on an Iliad phone.
- The route's routers appear as stops on the path's wires: the operator's internal ones
  before its exit, the public ones with their network number towards the internet.
- The download speed is timed after the first quarter of the sample, once the connection
  has ramped up; the path no longer shows the address block's registry country.
- Two new strings, drafted in every language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-15 09:10:37 +02:00

190 lines
6.9 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) when (!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) when (!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);
}
}