me-web? 0.4: no network failure takes the app down; mobile data read right; the route on the path
Build / Desktop, server and tools (push) Successful in 1m31s
Release / Every platform, the server, the release (push) Successful in 9m21s

- 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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-15 09:10:37 +02:00
1 parent 4b4b37ad8e
commit 20f8b2149f
131 files changed
+416 -37

No files matched your search

+2 -2
View File
@@ -93,13 +93,13 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
run.NetworkV4 = table?.Find(Addresses.Parse(run.PublicV4));
run.NetworkV6 = table?.Find(Addresses.Parse(run.PublicV6));
run.Provider = knowledge.ProviderFor((run.NetworkV4 ?? run.NetworkV6)?.Number ?? 0);
Settle(Verdicts.Provider(knowledge, run.NetworkV4, run.NetworkV6, run.PublicV4 is not null || run.PublicV6 is not null));
Settle(Verdicts.Provider(knowledge, run.NetworkV4, run.NetworkV6, run.PublicV4 is not null || run.PublicV6 is not null, run.Local?.Active?.Kind == LinkKind.Mobile));
});
var router = Step(CheckId.Router, async () =>
{
await connection;
run.Router = await Gateway.Inspect(platform, run.Local.GatewayV4, ct);
run.Router = run.Local?.Active?.Kind == LinkKind.Mobile ? new RouterReport() : await Gateway.Inspect(platform, run.Local?.GatewayV4, ct);
Settle(Verdicts.Router(run.Router, run.Local));
});
+13 -1
View File
@@ -33,6 +33,18 @@ public class Reach(ServerProbe server, Leftovers leftovers)
static readonly int[] Wellknown = [80, 443];
public async Task<ReachReport> Run(RunReport run, ReachChoice choice, CancellationToken ct)
{
try
{
return await Attempt(run, choice, ct);
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return new ReachReport { Choice = choice, V4Refusal = run.PublicV4 is null ? null : "unreachable", V6Refusal = run.PublicV6 is null ? null : "unreachable" };
}
}
async Task<ReachReport> Attempt(RunReport run, ReachChoice choice, CancellationToken ct)
{
var report = new ReachReport { Choice = choice };
if (choice == ReachChoice.Skip) return report;
@@ -118,7 +130,7 @@ public class Reach(ServerProbe server, Leftovers leftovers)
var read = await stream.ReadAsync(buffer, timeout.Token);
return Encoding.ASCII.GetString(buffer, 0, read).Trim() == ProbeProof.For(Convert.FromBase64String(ticket.Key), ticket.Nonce, port);
}
catch (Exception ex) when (ex is SocketException or IOException or OperationCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return false;
}
+7 -7
View File
@@ -50,7 +50,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
var answer = await https.GetFromJsonAsync(origin + Endpoints.Server.GET_Ip, MeWebJsonContext.Default.ViewIp, timeout.Token);
return answer?.Address;
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return null;
}
@@ -86,7 +86,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
using var response = await https.SendAsync(request, timeout.Token);
return response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NotFound;
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return false;
}
@@ -108,7 +108,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
: null;
return (null, refusal ?? new ViewRefusal { Reason = $"http-{(int)response.StatusCode}" });
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return (null, new ViewRefusal { Reason = "unreachable" });
}
@@ -121,7 +121,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
var addresses = await Dns.GetHostAddressesAsync(Endpoints.HostV4, AddressFamily.InterNetwork, ct);
return addresses.FirstOrDefault();
}
catch (SocketException)
catch (Exception) when (!ct.IsCancellationRequested)
{
return null;
}
@@ -133,7 +133,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
{
return (await Dns.GetHostAddressesAsync(host, ct)).Length > 0;
}
catch (SocketException)
catch (Exception) when (!ct.IsCancellationRequested)
{
return false;
}
@@ -147,7 +147,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
timeout.CancelAfter(Patience);
return await https.GetFromJsonAsync(Endpoints.Base + Endpoints.Server.GET_Info, MeWebJsonContext.Default.ViewInfo, timeout.Token);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return null;
}
@@ -170,7 +170,7 @@ public class ServerProbe(HttpClient https, HttpClient plain)
if (response.IsSuccessStatusCode && response.Content.Headers.ContentType?.MediaType == "application/json") return (true, null);
return (false, null);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return (null, null);
}
+5 -3
View File
@@ -81,7 +81,8 @@ public static class Verdicts
{
finding.With("FactPublicV4", v4);
var own = local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v4)) == true;
finding.WithKey("FactWhoHasIt", own ? "ValueOnThisDevice" : "ValueSharedAtHome");
var shared = local.Active?.Kind == LinkKind.Mobile || Net.Addresses.Parse(local.LocalV4) is { } address && Net.Addresses.Classify(address) == AddressKind.Shared;
finding.WithKey("FactWhoHasIt", own ? "ValueOnThisDevice" : shared ? "ValueSharedProvider" : "ValueSharedAtHome");
}
if (v6 is not null)
{
@@ -92,7 +93,7 @@ public static class Verdicts
return finding;
}
public static Finding Provider(KnowledgeBase knowledge, AsnEntry v4, AsnEntry v6, bool anyAddress)
public static Finding Provider(KnowledgeBase knowledge, AsnEntry v4, AsnEntry v6, bool anyAddress, bool mobile)
{
var finding = new Finding { Id = CheckId.Provider };
if (!anyAddress)
@@ -116,7 +117,7 @@ public static class Verdicts
}
var profile = knowledge.ProviderFor(main.Number);
finding.Outcome = Outcome.Info;
finding.Summary = profile is null ? "ProviderUnlisted" : "ProviderListed";
finding.Summary = profile is null ? "ProviderUnlisted" : mobile ? "ProviderListedMobile" : "ProviderListed";
finding.Args = [profile?.Name ?? Display(main)];
finding.With("FactNetwork", Describe(main));
if (v6 is not null && v4 is not null && v6.Number != v4.Number) finding.With("FactNetworkIpv6", Describe(v6));
@@ -191,6 +192,7 @@ public static class Verdicts
if (router?.Gateway is null && localV4 is not null && Net.Addresses.Classify(localV4) is AddressKind.Shared or AddressKind.Private && local.Active?.Kind == LinkKind.Mobile)
return NatKind.Shared;
var wan = Net.Addresses.Parse(router?.WanAddress);
if (wan is null && localV4 is not null && Net.Addresses.Classify(localV4) == AddressKind.Shared) return NatKind.Shared;
if (wan is null) return NatKind.Unknown;
if (Net.Addresses.SameAddress(router.WanAddress, v4)) return NatKind.HomeRouter;
return Net.Addresses.Classify(wan) switch
+1 -1
View File
@@ -98,7 +98,7 @@ public static partial class MailChecks
}
anyAnswer = true;
}
catch (Exception ex) when (ex is SocketException or IOException or OperationCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
}
}
+1 -1
View File
@@ -55,7 +55,7 @@ public static class Route
IsDestination = reply.Status == IPStatus.Success,
};
}
catch (Exception ex) when (ex is PingException or PlatformNotSupportedException or InvalidOperationException or NotSupportedException)
catch (Exception) when (!ct.IsCancellationRequested)
{
return null;
}
+15 -3
View File
@@ -34,10 +34,22 @@ public static class SpeedCheck
response.EnsureSuccessStatusCode();
await using var body = await response.Content.ReadAsStreamAsync(timeout.Token);
var buffer = new byte[64 * 1024];
var warm = report.DownBytes / 4;
long total = 0;
long warmBytes = 0;
var warmAt = TimeSpan.Zero;
int read;
while ((read = await body.ReadAsync(buffer, timeout.Token)) > 0) total += read;
report.DownMbps = Mbps(total, clock.Elapsed);
while ((read = await body.ReadAsync(buffer, timeout.Token)) > 0)
{
total += read;
if (warmBytes > 0 || total < warm) continue;
warmBytes = total;
warmAt = clock.Elapsed;
}
var steady = clock.Elapsed - warmAt;
report.DownMbps = warmBytes > 0 && total > warmBytes && steady > TimeSpan.FromMilliseconds(50)
? Mbps(total - warmBytes, steady)
: Mbps(total, clock.Elapsed);
}
var payload = new byte[report.UpBytes];
@@ -48,7 +60,7 @@ public static class SpeedCheck
if (response.IsSuccessStatusCode) report.UpMbps = Mbps(payload.Length, clock.Elapsed);
}
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
}
return report;
+1 -1
View File
@@ -57,7 +57,7 @@ public static class Gateway
report.UpnpService = service.Type;
await ExternalAddress(control, service.Type, report, ct);
}
catch (Exception ex) when (ex is HttpRequestException or XmlException or TaskCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
}
}
+2 -2
View File
@@ -85,7 +85,7 @@ public static class PortMapper
break;
}
}
catch (Exception ex) when (ex is HttpRequestException or XmlException or SocketException or FormatException or TaskCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
}
}
@@ -116,7 +116,7 @@ public static class PortMapper
}, 0);
if (error != 725) return (null, error);
}
catch (Exception ex) when (ex is HttpRequestException or XmlException or TaskCanceledException && !ct.IsCancellationRequested)
catch (Exception) when (!ct.IsCancellationRequested)
{
return (null, -1);
}