me-web? 0.5: the shared exit measured, the route from the server, the network's upstreams
Build / Desktop, server and tools (push) Successful in 59s
Release / Every platform, the server, the release (push) Successful in 8m42s

- Behind your provider's shared address, me-web? now shows how its exit hands out ports
  (kept, one after another, from one block, or at random), the range it gave this device,
  and how many customers may share the address at most; the estimate sharpens with each
  check on the same address.
- It tests whether two devices behind the same address can reach each other (hairpin), and
  how long an unused connection stays open: the card says "measuring" for about two minutes
  after the check, then the answer.
- With the test from outside, the publisher's server also traces the route back to your
  address: your provider's routers and the networks in between appear on the path, with
  their names.
- The provider card says which networks your provider reaches the internet through.
- On mobile data, the path shows the operator and the type of network (5G, 4G) read from the
  phone; on Android 13 and later the phone asks once for permission to read the network type.
- The speed card adds the round trip to the publisher's server.
- The network data now comes from the publisher's server, refreshed there from open sources:
  iptoasn.com, DB-IP Lite (CC BY 4.0), the University of Oregon Route Views Project (CC BY 4.0)
  and the Android Open Source Project's list of mobile operators (Apache 2.0); the release
  notes list each source's date and checksum.
- Source links in the cards wrap instead of being cut off.
- 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 11:11:24 +02:00
1 parent 8caed6b28d
commit eb8be87284
145 files changed
+3704 -193

No files matched your search

+20 -3
View File
@@ -24,6 +24,7 @@ public class RunReport
public Mail.MailReport Mail { get; set; }
public DnsReport Dns { get; set; }
public DeviceReport DeviceReport { get; set; }
public CellularReport Cellular { get; set; }
public SpeedReport Speed { get; set; }
public List<UseFinding> Uses { get; set; } = [];
public HistorySummary History { get; set; }
@@ -39,7 +40,11 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
void Settle(Finding finding)
{
lock (run.Findings) run.Findings.Add(finding);
lock (run.Findings)
{
run.Findings.RemoveAll(f => f.Id == finding.Id);
run.Findings.Add(finding);
}
report(finding, run);
}
@@ -68,7 +73,8 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
var connection = Step(CheckId.Connection, async () =>
{
run.Local = await local;
Settle(Verdicts.Connection(run.Local));
if (run.Local?.Active?.Kind == LinkKind.Mobile) run.Cellular = await platform.ReadCellular(ct);
Settle(Verdicts.Connection(run.Local, run.Cellular, knowledge));
});
var onlineDone = Step(CheckId.Online, async () =>
@@ -116,7 +122,16 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
{
await connection;
run.Stun = await Stun.Compare((Shared.Endpoints.SecondStunHost, Shared.Endpoints.SecondStunPort), (Shared.Endpoints.StunHost, Shared.Endpoints.StunPort), run.Local, ct);
Settle(Verdicts.Behaviour(run.Stun));
run.Stun.Ports = await Stun.Ports((Shared.Endpoints.StunHost, Shared.Endpoints.StunPort), 16, ct);
await nat;
var (low, high) = history.PortRange(run.PublicV4);
Stun.Classify(run.Stun.Ports, low, high);
Settle(Verdicts.Behaviour(run.Stun, run.Nat));
_ = Task.Run(async () =>
{
await Stun.Lifetime((Shared.Endpoints.StunHost, Shared.Endpoints.StunPort), run.Stun.Ports, [30, 120], ct);
Settle(Verdicts.Behaviour(run.Stun, run.Nat));
}, ct);
});
var mail = Step(CheckId.Mail, async () =>
@@ -165,6 +180,8 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
PublicV6 = run.PublicV6,
Asn = number,
Nat = run.Nat.ToString(),
PortLow = run.Stun?.Ports?.Pattern is PortPattern.InOrder or PortPattern.Block ? run.Stun.Ports.Low : 0,
PortHigh = run.Stun?.Ports?.Pattern is PortPattern.InOrder or PortPattern.Block ? run.Stun.Ports.High : 0,
});
Settle(Verdicts.History(run.History));
run.Uses = Uses.Decide(run);
+36 -2
View File
@@ -41,7 +41,7 @@ public static class Verdicts
return finding;
}
public static Finding Connection(LocalSnapshot local)
public static Finding Connection(LocalSnapshot local, CellularReport cellular, KnowledgeBase knowledge)
{
var active = local.Active;
if (active is null) return new Finding { Id = CheckId.Connection, Outcome = Outcome.Bad, Summary = "ConnectionNone" };
@@ -64,6 +64,12 @@ public static class Verdicts
if (v6.Count > 0) finding.With("FactLocalIpv6", string.Join("\n", v6));
finding.With("FactGateway", local.GatewayV4 ?? active.Gateways.FirstOrDefault());
if (local.Dns.Count > 0) finding.With("FactDns", string.Join(", ", local.Dns.Take(3)));
if (cellular is null) return finding;
var name = !string.IsNullOrWhiteSpace(cellular.Operator) ? cellular.Operator.Trim() : knowledge.CarrierFor(cellular.NetworkCode) ?? cellular.Carrier;
var code = cellular.NetworkCode is { Length: >= 5 } network ? $"{network[..3]} {network[3..]}" : null;
finding.With("FactOperator", code is null ? name : name is null ? code : $"{name} ({code})");
finding.With("FactRadio", cellular.Radio);
if (cellular.Roaming is not null) finding.WithKey("FactRoaming", cellular.Roaming == true ? "ValueYes" : "ValueNo");
return finding;
}
@@ -121,6 +127,8 @@ public static class Verdicts
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));
var upstreams = knowledge.NetworkFor(main.Number)?.Upstreams ?? [];
if (upstreams.Count > 0) finding.With("FactUpstreams", string.Join("\n", upstreams.Select(u => $"{knowledge.NameOf(u) ?? "AS" + u} (AS{u})")));
if (profile is null) return finding;
AddFact(finding, "FactProviderIpv4", "ProviderIpv4", profile.Ipv4);
AddFact(finding, "FactProviderPublic", "ProviderPublic", profile.PublicAddress);
@@ -204,7 +212,7 @@ public static class Verdicts
};
}
public static Finding Behaviour(StunReport stun)
public static Finding Behaviour(StunReport stun, NatKind nat)
{
var finding = new Finding { Id = CheckId.Behaviour };
(finding.Outcome, finding.Summary) = stun.Behaviour switch
@@ -217,6 +225,29 @@ public static class Verdicts
finding.With("FactStunFirst", stun.FirstMapped is null ? null : $"{stun.FirstServer} → {stun.FirstMapped}");
finding.With("FactStunSecond", stun.SecondMapped is null ? null : $"{stun.SecondServer} → {stun.SecondMapped}");
finding.With("FactLocalPort", stun.LocalPort > 0 ? stun.LocalPort.ToString(CultureInfo.InvariantCulture) : null);
var ports = stun.Ports;
if (ports is null) return finding;
if (ports.Pattern != PortPattern.Unknown)
{
var low = ports.Low.ToString(CultureInfo.InvariantCulture);
var high = ports.High.ToString(CultureInfo.InvariantCulture);
finding.WithKey("FactPortSlice", ports.Pattern switch
{
PortPattern.Kept => "ValuePortsKept",
PortPattern.InOrder => "ValuePortsInOrder",
PortPattern.Block => "ValuePortsBlock",
_ => "ValuePortsScattered",
}, low, high);
if (nat == NatKind.Shared && ports.BlockSize >= 64)
finding.WithKey("FactSharers", "ValueUpTo", (64512 / ports.BlockSize).ToString(CultureInfo.InvariantCulture));
}
if (ports.Hairpin is not null) finding.WithKey("FactHairpinExit", ports.Hairpin == true ? "ValueYes" : "ValueNo");
if (!ports.LifetimeDone) finding.WithKey("FactMappingLife", "ValueMeasuring");
else if (ports.KeptAtLeast > 0 && ports.LostBefore > ports.KeptAtLeast)
finding.WithKey("FactMappingLife", "ValueBetweenSeconds", ports.KeptAtLeast.ToString(CultureInfo.InvariantCulture), ports.LostBefore.ToString(CultureInfo.InvariantCulture));
else if (ports.KeptAtLeast > 0) finding.WithKey("FactMappingLife", "ValueAtLeastSeconds", ports.KeptAtLeast.ToString(CultureInfo.InvariantCulture));
else if (ports.LostBefore > 0) finding.WithKey("FactMappingLife", "ValueUnderSeconds", ports.LostBefore.ToString(CultureInfo.InvariantCulture));
if (ports.LifetimeDone && ports.LostBefore is > 0 and <= 30 && finding.Outcome == Outcome.Good) finding.Outcome = Outcome.Caution;
return finding;
}
@@ -265,6 +296,8 @@ public static class Verdicts
if (reach.V4 is null && reach.V4Refusal is not null) finding.WithKey("FactReachV4", "Refusal" + Pascal(reach.V4Refusal));
if (reach.V6 is null && reach.V6Refusal is not null) finding.WithKey("FactReachV6", "Refusal" + Pascal(reach.V6Refusal));
if (reach.Hairpin is not null) finding.WithKey("FactHairpin", reach.Hairpin == true ? "ValueHairpinYes" : "ValueHairpinNo");
var route = reach.V4?.Route ?? reach.V6?.Route ?? [];
if (route.Count > 0) finding.With("FactRouteBack", string.Join(" → ", route.Select(h => h.Name ?? h.Address ?? "*")));
if (reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.OpenNotYou && p.Port is 80 or 443) == true && !mapped)
finding.WithKey("FactHint", "HintSomethingAnswers");
return finding;
@@ -362,6 +395,7 @@ public static class Verdicts
finding.WithKey("FactDownload", "ValueMbpsDecimal", speed.DownMbps.ToString("0.#", CultureInfo.InvariantCulture));
finding.WithKey("FactUpload", "ValueMbpsDecimal", speed.UpMbps.ToString("0.#", CultureInfo.InvariantCulture));
finding.WithKey("FactSample", "ValueSample", (speed.DownBytes / 1048576).ToString(CultureInfo.InvariantCulture), (speed.UpBytes / 1048576).ToString(CultureInfo.InvariantCulture));
if (speed.RoundTripMs > 0) finding.WithKey("FactRoundTrip", "ValueMilliseconds", speed.RoundTripMs.ToString(CultureInfo.InvariantCulture));
return finding;
}