Files
MeWeb/src/MeWeb.Core/Knowledge/UpstreamTally.cs
T
thepraandClaude Opus 5.5 d18caed157
Build / Desktop, server and tools (push) Successful in 1m17s
Release / Every platform, the server, the release (push) Successful in 8m13s
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
2026-10-15 19:44:18 +02:00

128 lines
4.2 KiB
C#

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;
}