me-web? 0.6.0: provider notes from shared checks, and the internet seen from eight places
- Notes on providers in any country: findings people choose to share are counted on the publisher's server, one vote per network. A total appears on the provider card only after a person has reviewed it, and only when at least five different networks of that provider agree four times in five; the card says how many agree. Notes researched by hand still come first, and on mobile data the totals for the mobile network replace the notes about home lines. - "Connected to the internet through" now comes from eight RouteViews collectors (Oregon, Ashburn, London, Sao Paulo, Johannesburg, Dubai, Singapore, Sydney) instead of one in Oregon, each counting the same, so networks far from the United States are described from nearby. A network that only exchanges traffic with a collector's neighbour is no longer listed as one of its upstreams. - Ready for measuring servers outside Europe: once the publisher adds one, the app times each server and runs the speed test and the test from outside against the closest. None exists yet, so nothing changes today. - When me-web? has no notes on a provider, it says that findings shared at the end of a check help write them. - The privacy page states the rule for publishing totals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
c72a6be433
commit
d18caed157
139 files changed
+1403
-287
No files matched your search
@@ -26,6 +26,7 @@ public class RunReport
|
||||
public DeviceReport DeviceReport { get; set; }
|
||||
public CellularReport Cellular { get; set; }
|
||||
public bool OwnNetwork { get; set; }
|
||||
public Shared.MeasuringSite Site { get; set; }
|
||||
public SpeedReport Speed { get; set; }
|
||||
public List<UseFinding> Uses { get; set; } = [];
|
||||
public HistorySummary History { get; set; }
|
||||
@@ -65,6 +66,7 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
|
||||
var online = server.Online(ct);
|
||||
var v4 = server.Echo(false, ct);
|
||||
var v6 = server.Echo(true, ct);
|
||||
var site = SpeedCheck.Nearest(ct);
|
||||
var route = Task.Run(async () =>
|
||||
{
|
||||
var target = await ServerProbe.ResolveV4(ct);
|
||||
@@ -165,6 +167,7 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
|
||||
var speed = Step(CheckId.Speed, async () =>
|
||||
{
|
||||
await Task.WhenAll(onlineDone, nat, behaviour, mail, names);
|
||||
run.Site = await site;
|
||||
if (run.Online?.State is not (OnlineState.Online or OnlineState.Rewritten))
|
||||
{
|
||||
Settle(new Finding { Id = CheckId.Speed, Outcome = Outcome.Unknown, Summary = "SpeedOffline" });
|
||||
@@ -175,8 +178,8 @@ public class CheckRunner(IPlatform platform, ServerProbe server, KnowledgeBase k
|
||||
Settle(new Finding { Id = CheckId.Speed, Outcome = Outcome.Unknown, Summary = "ReachOwnNetwork" });
|
||||
return;
|
||||
}
|
||||
run.Speed = await SpeedCheck.Run(https, run.Local?.Active?.Kind ?? LinkKind.Unknown, ct);
|
||||
Settle(Verdicts.Speed(run.Speed));
|
||||
run.Speed = await SpeedCheck.Run(https, run.Local?.Active?.Kind ?? LinkKind.Unknown, run.Site, ct);
|
||||
Settle(Verdicts.Speed(run.Speed, run.Site));
|
||||
});
|
||||
|
||||
await Step(CheckId.History, async () =>
|
||||
|
||||
@@ -49,6 +49,8 @@ public class Fact
|
||||
public string Currency { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string Basis { get; set; }
|
||||
public int? Agree { get; set; }
|
||||
public int? Of { get; set; }
|
||||
public string Date { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ public class Reach(ServerProbe server, Leftovers leftovers)
|
||||
var report = new ReachReport { Choice = choice };
|
||||
if (choice == ReachChoice.Skip) return report;
|
||||
|
||||
var ticket4 = run.PublicV4 is null ? (null, null) : await server.Ticket(false, ct);
|
||||
var ticket6 = run.PublicV6 is null ? (null, null) : await server.Ticket(true, ct);
|
||||
var ticket4 = run.PublicV4 is null ? (null, null) : await server.Ticket(run.Site ?? Endpoints.Main, false, ct);
|
||||
var ticket6 = run.PublicV6 is null ? (null, null) : await server.Ticket(run.Site ?? Endpoints.Main, true, ct);
|
||||
report.V4Refusal = ticket4.Refusal?.Reason;
|
||||
report.V6Refusal = ticket6.Refusal?.Reason;
|
||||
report.RetryAfterSeconds = Math.Max(ticket4.Refusal?.RetryAfterSeconds ?? 0, ticket6.Refusal?.RetryAfterSeconds ?? 0);
|
||||
@@ -92,8 +92,8 @@ public class Reach(ServerProbe server, Leftovers leftovers)
|
||||
: [.. Wellknown, answerer.Port];
|
||||
ports4 = ports4.Where(p => p is 80 or 443 or (>= 49152 and <= 65535)).Distinct().Take(3).ToList();
|
||||
|
||||
var knock4 = ticket4.Ticket is null ? Task.FromResult<(ViewProbe, ViewRefusal)>((null, null)) : server.Knock(false, ticket4.Ticket.Id, ports4, ct);
|
||||
var knock6 = ticket6.Ticket is null ? Task.FromResult<(ViewProbe, ViewRefusal)>((null, null)) : server.Knock(true, ticket6.Ticket.Id, [answerer.Port], ct);
|
||||
var knock4 = ticket4.Ticket is null ? Task.FromResult<(ViewProbe, ViewRefusal)>((null, null)) : server.Knock(run.Site ?? Endpoints.Main, false, ticket4.Ticket.Id, ports4, ct);
|
||||
var knock6 = ticket6.Ticket is null ? Task.FromResult<(ViewProbe, ViewRefusal)>((null, null)) : server.Knock(run.Site ?? Endpoints.Main, true, ticket6.Ticket.Id, [answerer.Port], ct);
|
||||
var (probe4, refusal4) = await knock4;
|
||||
var (probe6, refusal6) = await knock6;
|
||||
report.V4 = probe4;
|
||||
|
||||
@@ -56,15 +56,15 @@ public class ServerProbe(HttpClient https, HttpClient plain)
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(ViewProbeTicket Ticket, ViewRefusal Refusal)> Ticket(bool v6, CancellationToken ct)
|
||||
public async Task<(ViewProbeTicket Ticket, ViewRefusal Refusal)> Ticket(MeasuringSite site, bool v6, CancellationToken ct)
|
||||
{
|
||||
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
|
||||
var origin = v6 ? site.BaseV6 : site.BaseV4;
|
||||
return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_Ticket, null, MeWebJsonContext.Default.ViewProbeTicket, ct);
|
||||
}
|
||||
|
||||
public async Task<(ViewProbe Probe, ViewRefusal Refusal)> Knock(bool v6, string id, List<int> ports, CancellationToken ct)
|
||||
public async Task<(ViewProbe Probe, ViewRefusal Refusal)> Knock(MeasuringSite site, bool v6, string id, List<int> ports, CancellationToken ct)
|
||||
{
|
||||
var origin = v6 ? Endpoints.BaseV6 : Endpoints.BaseV4;
|
||||
var origin = v6 ? site.BaseV6 : site.BaseV4;
|
||||
var form = JsonContent.Create(new ProbeForm { Ports = ports }, MeWebJsonContext.Default.ProbeForm);
|
||||
return await Send(HttpMethod.Post, origin + Endpoints.Probe.POST_M_Run(id), form, MeWebJsonContext.Default.ViewProbe, ct, TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
@@ -120,29 +120,29 @@ public static class Verdicts
|
||||
return finding;
|
||||
}
|
||||
var profile = knowledge.ProviderFor(main.Number);
|
||||
var observed = knowledge.ObservedFor(main.Number, mobile);
|
||||
var hand = mobile && observed is not null ? null : profile;
|
||||
finding.Outcome = Outcome.Info;
|
||||
finding.Summary = profile is null ? "ProviderUnlisted" : mobile ? "ProviderListedMobile" : "ProviderListed";
|
||||
finding.Summary = profile is null && observed is null ? "ProviderUnlisted" : mobile && observed is null ? "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));
|
||||
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);
|
||||
var usual = profile.Ipv4?.Value is null ? null : Pascal(profile.Ipv4.Value);
|
||||
if (hand is null && observed is null) return finding;
|
||||
var usual = AddFact(finding, "FactProviderIpv4", "ProviderIpv4", hand?.Ipv4, observed?.Ipv4, observed?.To);
|
||||
var sharedHere = run?.Nat is NatKind.Shared or NatKind.Translated;
|
||||
var ownHere = run?.Nat is NatKind.HomeRouter or NatKind.Direct or NatKind.DoubleNat;
|
||||
if ((usual is "Shared" or "SharedPorts" && ownHere) || (usual is "PublicDynamic" && sharedHere))
|
||||
if ((usual is "Shared" or "SharedPorts" && ownHere) || (usual is "PublicDynamic" or "Public" && sharedHere))
|
||||
finding.WithKey("FactThisLine", Holder(run.Nat));
|
||||
AddFact(finding, "FactProviderPublic", "ProviderPublic", profile.PublicAddress);
|
||||
AddFact(finding, "FactProviderIpv6", "ProviderIpv6", profile.Ipv6);
|
||||
AddFact(finding, "FactProviderPort25", "ProviderPort25", profile.Port25);
|
||||
var port25 = profile.Port25?.Value is null ? null : Pascal(profile.Port25.Value);
|
||||
AddFact(finding, "FactProviderPublic", "ProviderPublic", hand?.PublicAddress, null, null);
|
||||
AddFact(finding, "FactProviderIpv6", "ProviderIpv6", hand?.Ipv6, observed?.Ipv6, observed?.To);
|
||||
var port25 = AddFact(finding, "FactProviderPort25", "ProviderPort25", hand?.Port25, observed?.Port25, observed?.To);
|
||||
if (run?.Mail?.Port25 == Core.Mail.PortState.Open && port25 is "Blocked" or "BlockedOnRequest" or "Closed")
|
||||
finding.WithKey("FactThisLine", "ValuePort25Open", run.Mail.Banner ?? "");
|
||||
else if (run?.Mail?.Port25 == Core.Mail.PortState.Blocked && port25 is "Open")
|
||||
finding.WithKey("FactThisLine", "ValuePort25Blocked");
|
||||
if (profile.Sheet is not null) finding.Facts.Add(new Fact { Label = "FactProviderSheet", Key = "ValueOpenSheet", Link = profile.Sheet });
|
||||
if (hand?.Sheet is not null) finding.Facts.Add(new Fact { Label = "FactProviderSheet", Key = "ValueOpenSheet", Link = hand.Sheet });
|
||||
return finding;
|
||||
}
|
||||
|
||||
@@ -313,7 +313,9 @@ public static class Verdicts
|
||||
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 ?? "*")));
|
||||
var site = run?.Site ?? Endpoints.Main;
|
||||
if (route.Count > 0) finding.With(site == Endpoints.Main ? "FactRouteBack" : "FactRouteBackNode", string.Join(" → ", route.Select(h => h.Name ?? h.Address ?? "*")));
|
||||
if (Endpoints.Nodes.Count > 0) finding.With("FactMeasuredFrom", $"{site.Place} ({site.Host})");
|
||||
if (reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.OpenNotYou && p.Port is 80 or 443) == true && !mapped)
|
||||
finding.WithKey("FactHint", "HintSomethingAnswers");
|
||||
return finding;
|
||||
@@ -390,7 +392,7 @@ public static class Verdicts
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static Finding Speed(SpeedReport speed)
|
||||
public static Finding Speed(SpeedReport speed, MeasuringSite site)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.Speed };
|
||||
if (speed.Limited)
|
||||
@@ -411,7 +413,8 @@ 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));
|
||||
if (speed.RoundTripMs > 0) finding.WithKey(site == Endpoints.Main ? "FactRoundTrip" : "FactRoundTripNode", "ValueMilliseconds", speed.RoundTripMs.ToString(CultureInfo.InvariantCulture));
|
||||
if (Endpoints.Nodes.Count > 0) finding.With("FactMeasuredFrom", $"{site.Place} ({site.Host})");
|
||||
return finding;
|
||||
}
|
||||
|
||||
@@ -451,19 +454,25 @@ public static class Verdicts
|
||||
return finding;
|
||||
}
|
||||
|
||||
static void AddFact(Finding finding, string label, string prefix, ProviderFact fact)
|
||||
static string AddFact(Finding finding, string label, string prefix, ProviderFact fact, ObservedFact seen, string month)
|
||||
{
|
||||
if (fact?.Value is null) return;
|
||||
finding.Facts.Add(new Fact
|
||||
if (fact?.Value is not null)
|
||||
{
|
||||
Label = label,
|
||||
Key = prefix + Pascal(fact.Value),
|
||||
Amount = fact.Price,
|
||||
Currency = fact.Currency,
|
||||
Link = fact.Source,
|
||||
Basis = fact.Basis is null ? null : "Basis" + Pascal(fact.Basis),
|
||||
Date = fact.Date,
|
||||
});
|
||||
finding.Facts.Add(new Fact
|
||||
{
|
||||
Label = label,
|
||||
Key = prefix + Pascal(fact.Value),
|
||||
Amount = fact.Price,
|
||||
Currency = fact.Currency,
|
||||
Link = fact.Source,
|
||||
Basis = fact.Basis is null ? null : "Basis" + Pascal(fact.Basis),
|
||||
Date = fact.Date,
|
||||
});
|
||||
return Pascal(fact.Value);
|
||||
}
|
||||
if (seen?.Value is null) return null;
|
||||
finding.Facts.Add(new Fact { Label = label, Key = prefix + Pascal(seen.Value), Basis = "BasisChecks", Agree = seen.Agree, Of = seen.Of, Date = month });
|
||||
return Pascal(seen.Value);
|
||||
}
|
||||
|
||||
static string AnswerKey(Answer answer) => answer switch
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MeWeb.Core.Knowledge;
|
||||
|
||||
public class ObservedFact
|
||||
{
|
||||
public const int MinNetworks = 5;
|
||||
|
||||
public string Value { get; set; }
|
||||
public int Agree { get; set; }
|
||||
public int Of { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool Ready => Value is not null && Of >= MinNetworks && Agree * 5 >= Of * 4;
|
||||
|
||||
public static ObservedFact Tally(IEnumerable<string> values)
|
||||
{
|
||||
var counts = values.Where(v => v is not null).GroupBy(v => v).Select(g => (Value: g.Key, Count: g.Count())).ToList();
|
||||
if (counts.Count == 0) return null;
|
||||
var top = counts.OrderByDescending(c => c.Count).ThenBy(c => c.Value, StringComparer.Ordinal).First();
|
||||
return new ObservedFact { Value = top.Value, Agree = top.Count, Of = counts.Sum(c => c.Count) };
|
||||
}
|
||||
}
|
||||
|
||||
public class ObservedNetwork
|
||||
{
|
||||
public const string Home = "home";
|
||||
public const string Mobile = "mobile";
|
||||
|
||||
public uint Asn { get; set; }
|
||||
public string Line { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Networks { get; set; }
|
||||
public string From { get; set; }
|
||||
public string To { get; set; }
|
||||
public ObservedFact Ipv4 { get; set; }
|
||||
public ObservedFact Ipv6 { get; set; }
|
||||
public ObservedFact Port25 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Key => $"AS{Asn}/{Line}";
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class ProviderProfile
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
[JsonSerializable(typeof(ProviderProfile))]
|
||||
[JsonSerializable(typeof(List<Carrier>))]
|
||||
[JsonSerializable(typeof(List<ObservedNetwork>))]
|
||||
public partial class KnowledgeJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -38,6 +39,7 @@ public sealed class KnowledgeBase
|
||||
readonly Lazy<List<ProviderProfile>> providers = new(LoadProviders);
|
||||
readonly Lazy<NetworkTable> networks = new(LoadNetworks);
|
||||
readonly Lazy<Dictionary<string, string>> carriers = new(LoadCarriers);
|
||||
readonly Lazy<List<ObservedNetwork>> observed = new(LoadObserved);
|
||||
|
||||
public bool HasAsnTable => typeof(KnowledgeBase).Assembly.GetManifestResourceInfo("ip2asn.bin") is not null;
|
||||
|
||||
@@ -47,6 +49,9 @@ public sealed class KnowledgeBase
|
||||
|
||||
public ProviderProfile ProviderFor(uint number) => number == 0 ? null : Providers.FirstOrDefault(p => p.Asns.Contains(number));
|
||||
|
||||
public ObservedNetwork ObservedFor(uint number, bool mobile) =>
|
||||
number == 0 ? null : observed.Value.FirstOrDefault(o => o.Asn == number && o.Line == (mobile ? ObservedNetwork.Mobile : ObservedNetwork.Home));
|
||||
|
||||
public NetworkInfo NetworkFor(uint number) => number == 0 ? null : networks.Value?.Find(number);
|
||||
|
||||
public string CarrierFor(string mccMnc) => string.IsNullOrEmpty(mccMnc) ? null : carriers.Value.GetValueOrDefault(mccMnc);
|
||||
@@ -67,6 +72,12 @@ public sealed class KnowledgeBase
|
||||
return list.Where(c => c.MccMnc is not null && c.Name is not null).GroupBy(c => c.MccMnc).ToDictionary(g => g.Key, g => g.First().Name);
|
||||
}
|
||||
|
||||
static List<ObservedNetwork> LoadObserved()
|
||||
{
|
||||
using var stream = typeof(KnowledgeBase).Assembly.GetManifestResourceStream("observed.json");
|
||||
return stream is null ? [] : JsonSerializer.Deserialize(stream, KnowledgeJsonContext.Default.ListObservedNetwork) ?? [];
|
||||
}
|
||||
|
||||
static AsnTable LoadAsn()
|
||||
{
|
||||
using var stream = typeof(KnowledgeBase).Assembly.GetManifestResourceStream("ip2asn.bin");
|
||||
|
||||
@@ -46,31 +46,6 @@ public static class Sources
|
||||
return seen.ToDictionary(p => p.Key, p => p.Value.MaxBy(n => n.Value).Key);
|
||||
}
|
||||
|
||||
public static Dictionary<uint, List<uint>> Upstreams(Stream mrt, int keep = 4, double share = 0.05)
|
||||
{
|
||||
var counts = new Dictionary<uint, Dictionary<uint, int>>();
|
||||
var header = new byte[12];
|
||||
var body = new byte[1 << 16];
|
||||
while (Fill(mrt, header))
|
||||
{
|
||||
var type = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4));
|
||||
var subtype = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(6));
|
||||
var length = (int)BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(8));
|
||||
if (body.Length < length) body = new byte[length];
|
||||
if (!Fill(mrt, body.AsSpan(0, length))) break;
|
||||
if (type != 13 || subtype is not (2 or 4)) continue;
|
||||
Rib(body.AsSpan(0, length), counts);
|
||||
}
|
||||
var result = new Dictionary<uint, List<uint>>();
|
||||
foreach (var (origin, neighbours) in counts)
|
||||
{
|
||||
var total = (double)neighbours.Values.Sum();
|
||||
var chosen = neighbours.Where(n => n.Value / total >= share).OrderByDescending(n => n.Value).ThenBy(n => n.Key).Take(keep).Select(n => n.Key).ToList();
|
||||
if (chosen.Count > 0) result[origin] = chosen;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<Carrier> Carriers(TextReader textpb)
|
||||
{
|
||||
var list = new List<Carrier>();
|
||||
@@ -113,82 +88,7 @@ public static class Sources
|
||||
return start >= 0 && end > start ? text[(start + 1)..end] : null;
|
||||
}
|
||||
|
||||
static void Rib(ReadOnlySpan<byte> rib, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
if (rib.Length < 5) return;
|
||||
var at = 4;
|
||||
var bits = rib[at++];
|
||||
at += (bits + 7) / 8;
|
||||
if (at + 2 > rib.Length) return;
|
||||
var entries = BinaryPrimitives.ReadUInt16BigEndian(rib[at..]);
|
||||
at += 2;
|
||||
for (var e = 0; e < entries && at + 8 <= rib.Length; e++)
|
||||
{
|
||||
at += 6;
|
||||
var size = BinaryPrimitives.ReadUInt16BigEndian(rib[at..]);
|
||||
at += 2;
|
||||
if (at + size > rib.Length) return;
|
||||
Attributes(rib.Slice(at, size), counts);
|
||||
at += size;
|
||||
}
|
||||
}
|
||||
|
||||
static void Attributes(ReadOnlySpan<byte> attributes, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
var at = 0;
|
||||
while (at + 3 <= attributes.Length)
|
||||
{
|
||||
var flags = attributes[at];
|
||||
var type = attributes[at + 1];
|
||||
var extended = (flags & 0x10) != 0;
|
||||
if (extended && at + 4 > attributes.Length) return;
|
||||
var length = extended ? BinaryPrimitives.ReadUInt16BigEndian(attributes[(at + 2)..]) : attributes[at + 2];
|
||||
var start = at + (extended ? 4 : 3);
|
||||
if (start + length > attributes.Length) return;
|
||||
if (type == 2)
|
||||
{
|
||||
Path(attributes.Slice(start, length), counts);
|
||||
return;
|
||||
}
|
||||
at = start + length;
|
||||
}
|
||||
}
|
||||
|
||||
static void Path(ReadOnlySpan<byte> path, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
uint last = 0;
|
||||
uint before = 0;
|
||||
var endsInSet = false;
|
||||
var at = 0;
|
||||
while (at + 2 <= path.Length)
|
||||
{
|
||||
var segment = path[at];
|
||||
var count = path[at + 1];
|
||||
at += 2;
|
||||
if (at + count * 4 > path.Length) return;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var number = BinaryPrimitives.ReadUInt32BigEndian(path[(at + i * 4)..]);
|
||||
if (segment != 2)
|
||||
{
|
||||
endsInSet = true;
|
||||
continue;
|
||||
}
|
||||
endsInSet = false;
|
||||
if (number == last) continue;
|
||||
before = last;
|
||||
last = number;
|
||||
}
|
||||
at += count * 4;
|
||||
}
|
||||
if (endsInSet || last == 0 || before == 0 || Reserved(before) || Reserved(last)) return;
|
||||
if (!counts.TryGetValue(last, out var neighbours)) counts[last] = neighbours = [];
|
||||
neighbours[before] = neighbours.GetValueOrDefault(before) + 1;
|
||||
}
|
||||
|
||||
static bool Reserved(uint number) => number is 23456 or (>= 64496 and <= 131071) or >= 4200000000;
|
||||
|
||||
static bool Fill(Stream stream, Span<byte> buffer)
|
||||
internal static bool Fill(Stream stream, Span<byte> buffer)
|
||||
{
|
||||
var read = 0;
|
||||
while (read < buffer.Length)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace MeWeb.Core.Knowledge;
|
||||
|
||||
public sealed class UpstreamTally
|
||||
{
|
||||
readonly Dictionary<uint, Dictionary<uint, double>> weights = [];
|
||||
readonly Dictionary<uint, int> seen = [];
|
||||
|
||||
public int Collectors { get; private set; }
|
||||
|
||||
public int Add(Stream mrt)
|
||||
{
|
||||
var counts = new Dictionary<uint, Dictionary<uint, int>>();
|
||||
var header = new byte[12];
|
||||
var body = new byte[1 << 16];
|
||||
while (Sources.Fill(mrt, header))
|
||||
{
|
||||
var type = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4));
|
||||
var subtype = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(6));
|
||||
var length = (int)BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(8));
|
||||
if (body.Length < length) body = new byte[length];
|
||||
if (!Sources.Fill(mrt, body.AsSpan(0, length))) break;
|
||||
if (type != 13 || subtype is not (2 or 4 or 8 or 10)) continue;
|
||||
Rib(body.AsSpan(0, length), subtype is 8 or 10, counts);
|
||||
}
|
||||
foreach (var (origin, neighbours) in counts)
|
||||
{
|
||||
var total = (double)neighbours.Values.Sum();
|
||||
if (!weights.TryGetValue(origin, out var sum)) weights[origin] = sum = [];
|
||||
foreach (var (neighbour, count) in neighbours) sum[neighbour] = sum.GetValueOrDefault(neighbour) + count / total;
|
||||
seen[origin] = seen.GetValueOrDefault(origin) + 1;
|
||||
}
|
||||
Collectors++;
|
||||
return counts.Count;
|
||||
}
|
||||
|
||||
public Dictionary<uint, List<uint>> Upstreams(int keep = 4, double share = 0.05)
|
||||
{
|
||||
var result = new Dictionary<uint, List<uint>>();
|
||||
foreach (var (origin, neighbours) in weights)
|
||||
{
|
||||
var collectors = (double)seen[origin];
|
||||
var chosen = neighbours.Where(n => n.Value / collectors >= share).OrderByDescending(n => n.Value).ThenBy(n => n.Key).Take(keep).Select(n => n.Key).ToList();
|
||||
if (chosen.Count > 0) result[origin] = chosen;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Rib(ReadOnlySpan<byte> rib, bool addPath, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
if (rib.Length < 5) return;
|
||||
var at = 4;
|
||||
var bits = rib[at++];
|
||||
at += (bits + 7) / 8;
|
||||
if (at + 2 > rib.Length) return;
|
||||
var entries = BinaryPrimitives.ReadUInt16BigEndian(rib[at..]);
|
||||
at += 2;
|
||||
var head = addPath ? 10 : 6;
|
||||
for (var e = 0; e < entries && at + head + 2 <= rib.Length; e++)
|
||||
{
|
||||
at += head;
|
||||
var size = BinaryPrimitives.ReadUInt16BigEndian(rib[at..]);
|
||||
at += 2;
|
||||
if (at + size > rib.Length) return;
|
||||
Attributes(rib.Slice(at, size), counts);
|
||||
at += size;
|
||||
}
|
||||
}
|
||||
|
||||
static void Attributes(ReadOnlySpan<byte> attributes, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
var at = 0;
|
||||
while (at + 3 <= attributes.Length)
|
||||
{
|
||||
var flags = attributes[at];
|
||||
var type = attributes[at + 1];
|
||||
var extended = (flags & 0x10) != 0;
|
||||
if (extended && at + 4 > attributes.Length) return;
|
||||
var length = extended ? BinaryPrimitives.ReadUInt16BigEndian(attributes[(at + 2)..]) : attributes[at + 2];
|
||||
var start = at + (extended ? 4 : 3);
|
||||
if (start + length > attributes.Length) return;
|
||||
if (type == 2)
|
||||
{
|
||||
Path(attributes.Slice(start, length), counts);
|
||||
return;
|
||||
}
|
||||
at = start + length;
|
||||
}
|
||||
}
|
||||
|
||||
static void Path(ReadOnlySpan<byte> path, Dictionary<uint, Dictionary<uint, int>> counts)
|
||||
{
|
||||
uint last = 0;
|
||||
uint before = 0;
|
||||
var distinct = 0;
|
||||
var endsInSet = false;
|
||||
var at = 0;
|
||||
while (at + 2 <= path.Length)
|
||||
{
|
||||
var segment = path[at];
|
||||
var count = path[at + 1];
|
||||
at += 2;
|
||||
if (at + count * 4 > path.Length) return;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var number = BinaryPrimitives.ReadUInt32BigEndian(path[(at + i * 4)..]);
|
||||
if (segment != 2)
|
||||
{
|
||||
endsInSet = true;
|
||||
continue;
|
||||
}
|
||||
endsInSet = false;
|
||||
if (number == last) continue;
|
||||
before = last;
|
||||
last = number;
|
||||
distinct++;
|
||||
}
|
||||
at += count * 4;
|
||||
}
|
||||
if (endsInSet || distinct < 3 || Reserved(before) || Reserved(last)) return;
|
||||
if (!counts.TryGetValue(last, out var neighbours)) counts[last] = neighbours = [];
|
||||
neighbours[before] = neighbours.GetValueOrDefault(before) + 1;
|
||||
}
|
||||
|
||||
static bool Reserved(uint number) => number is 0 or 23456 or (>= 64496 and <= 131071) or >= 4200000000;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\..\data\isps\*.json" LogicalName="isp.%(Filename).json" />
|
||||
<EmbeddedResource Include="..\..\data\observed.json" LogicalName="observed.json" />
|
||||
<EmbeddedResource Include="$(KnowledgeFolder)ip2asn.bin" LogicalName="ip2asn.bin" Condition="Exists('$(KnowledgeFolder)ip2asn.bin')" />
|
||||
<EmbeddedResource Include="$(KnowledgeFolder)networks.bin" LogicalName="networks.bin" Condition="Exists('$(KnowledgeFolder)networks.bin')" />
|
||||
<EmbeddedResource Include="$(KnowledgeFolder)carriers.json" LogicalName="carriers.json" Condition="Exists('$(KnowledgeFolder)carriers.json')" />
|
||||
|
||||
@@ -18,17 +18,17 @@ public class SpeedReport
|
||||
|
||||
public static class SpeedCheck
|
||||
{
|
||||
public static async Task<SpeedReport> Run(HttpClient https, LinkKind kind, CancellationToken ct)
|
||||
public static async Task<SpeedReport> Run(HttpClient https, LinkKind kind, MeasuringSite site, CancellationToken ct)
|
||||
{
|
||||
var mobile = kind == LinkKind.Mobile;
|
||||
var report = new SpeedReport { DownBytes = mobile ? 2 * 1024 * 1024 : 12 * 1024 * 1024, UpBytes = mobile ? 1024 * 1024 : 4 * 1024 * 1024 };
|
||||
report.RoundTripMs = await RoundTrip(ct);
|
||||
report.RoundTripMs = await RoundTrip(site.Host, ct);
|
||||
try
|
||||
{
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
var clock = Stopwatch.StartNew();
|
||||
using (var response = await https.GetAsync(Endpoints.Base + Endpoints.Speed.GET_M_Down(report.DownBytes), HttpCompletionOption.ResponseHeadersRead, timeout.Token))
|
||||
using (var response = await https.GetAsync(site.Base + Endpoints.Speed.GET_M_Down(report.DownBytes), HttpCompletionOption.ResponseHeadersRead, timeout.Token))
|
||||
{
|
||||
if ((int)response.StatusCode == 429)
|
||||
{
|
||||
@@ -59,7 +59,7 @@ public static class SpeedCheck
|
||||
var payload = new byte[report.UpBytes];
|
||||
Random.Shared.NextBytes(payload);
|
||||
clock.Restart();
|
||||
using (var response = await https.PostAsync(Endpoints.Base + Endpoints.Speed.POST_Up, new ByteArrayContent(payload), timeout.Token))
|
||||
using (var response = await https.PostAsync(site.Base + Endpoints.Speed.POST_Up, new ByteArrayContent(payload), timeout.Token))
|
||||
{
|
||||
if (response.IsSuccessStatusCode) report.UpMbps = Mbps(payload.Length, clock.Elapsed);
|
||||
}
|
||||
@@ -70,11 +70,20 @@ public static class SpeedCheck
|
||||
return report;
|
||||
}
|
||||
|
||||
static async Task<int> RoundTrip(CancellationToken ct)
|
||||
public static async Task<MeasuringSite> Nearest(CancellationToken ct)
|
||||
{
|
||||
if (Endpoints.Nodes.Count == 0) return Endpoints.Main;
|
||||
var sites = Endpoints.Nodes.Prepend(Endpoints.Main).ToList();
|
||||
var times = await Task.WhenAll(sites.Select(site => RoundTrip(site.Host, ct)));
|
||||
var best = Enumerable.Range(1, sites.Count - 1).Where(i => times[i] > 0).OrderBy(i => times[i]).FirstOrDefault();
|
||||
return best > 0 && (times[0] == 0 || times[best] * 10 < times[0] * 7) ? sites[best] : Endpoints.Main;
|
||||
}
|
||||
|
||||
static async Task<int> RoundTrip(string host, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(Endpoints.Host, ct);
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, ct);
|
||||
if (addresses.Length == 0) return 0;
|
||||
var samples = new List<double>();
|
||||
for (var i = 0; i < 3; i++)
|
||||
|
||||
Reference in new issue
Block a user