The checks engine, the path and the cards: me-web? 0.1 reads the connection
MeWeb.Core runs seven checks concurrently and reports each as it settles:
online or behind a login page, this connection, the public IPv4 and IPv6
seen from v4./v6.meweb, the provider from a built-in map, the router
(UPnP IGD, NAT-PMP and PCP, read-only), address sharing (router WAN
against the echo, TTL hops only as a hint), and the history kept on the
device. The app draws the path from this device to the internet and lights
it node by node, with a card per check and a sheet listing every host it
contacts.
The provider map is iptoasn's table compressed to 2.1 MB by
tools/knowledge.cs and built before every app release; eleven Italian
provider profiles carry each fact with who said it, the source and the
month. The server gains a download page (en, it) and /download/{platform}
from the latest Gitea release.
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
16a1cecaae
commit
0dffbc27d6
61 files changed
+3750
-149
No files matched your search
@@ -0,0 +1,255 @@
|
||||
using System.Globalization;
|
||||
|
||||
using MeWeb.Core.History;
|
||||
using MeWeb.Core.Knowledge;
|
||||
using MeWeb.Core.Net;
|
||||
using MeWeb.Core.Router;
|
||||
|
||||
namespace MeWeb.Core.Checks;
|
||||
|
||||
public static class Verdicts
|
||||
{
|
||||
public static Finding Online(OnlineReport report, LocalSnapshot local)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.Online };
|
||||
switch (report.State)
|
||||
{
|
||||
case OnlineState.Online:
|
||||
finding.Outcome = Outcome.Good;
|
||||
finding.Summary = "OnlineGood";
|
||||
break;
|
||||
case OnlineState.Rewritten:
|
||||
finding.Outcome = Outcome.Caution;
|
||||
finding.Summary = "OnlineRewritten";
|
||||
finding.With("FactRedirectedTo", report.Portal);
|
||||
break;
|
||||
case OnlineState.Captive:
|
||||
finding.Outcome = Outcome.Caution;
|
||||
finding.Summary = "OnlineCaptive";
|
||||
finding.With("FactRedirectedTo", report.Portal);
|
||||
break;
|
||||
case OnlineState.NoNames when local.Active is not null:
|
||||
finding.Outcome = Outcome.Bad;
|
||||
finding.Summary = "OnlineNoNames";
|
||||
break;
|
||||
default:
|
||||
finding.Outcome = Outcome.Bad;
|
||||
finding.Summary = local.Active is null ? "OnlineNoNetwork" : "OnlineOffline";
|
||||
break;
|
||||
}
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static Finding Connection(LocalSnapshot local)
|
||||
{
|
||||
var active = local.Active;
|
||||
if (active is null) return new Finding { Id = CheckId.Connection, Outcome = Outcome.Bad, Summary = "ConnectionNone" };
|
||||
var finding = new Finding
|
||||
{
|
||||
Id = CheckId.Connection,
|
||||
Outcome = Outcome.Info,
|
||||
Summary = active.Kind switch
|
||||
{
|
||||
LinkKind.Wifi => "ConnectionWifi",
|
||||
LinkKind.Cable => "ConnectionCable",
|
||||
LinkKind.Mobile => "ConnectionMobile",
|
||||
_ => "ConnectionOther",
|
||||
},
|
||||
};
|
||||
finding.With("FactAdapter", active.Name);
|
||||
if (active.SpeedMbps > 0) finding.WithKey("FactLinkSpeed", "ValueMbps", active.SpeedMbps.ToString(CultureInfo.InvariantCulture));
|
||||
finding.With("FactLocalAddress", local.LocalV4);
|
||||
var v6 = local.GlobalV6;
|
||||
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)));
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static Finding Addresses(string v4, string v6, LocalSnapshot local)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.Addresses };
|
||||
(finding.Outcome, finding.Summary) = (v4, v6) switch
|
||||
{
|
||||
(not null, not null) => (Outcome.Good, "AddressesBoth"),
|
||||
(not null, null) => (Outcome.Caution, "AddressesV4Only"),
|
||||
(null, not null) => (Outcome.Caution, "AddressesV6Only"),
|
||||
_ => (Outcome.Unknown, "AddressesNone"),
|
||||
};
|
||||
if (v4 is not null)
|
||||
{
|
||||
finding.With("FactPublicV4", v4);
|
||||
var own = local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v4)) == true;
|
||||
finding.WithKey("FactWhoHasIt", own ? "ValueOnThisDevice" : "ValueSharedAtHome");
|
||||
}
|
||||
if (v6 is not null)
|
||||
{
|
||||
finding.With("FactPublicV6", v6);
|
||||
var own = local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v6)) == true;
|
||||
finding.WithKey("FactWhoHasIpv6", own ? "ValueOnThisDevice" : "ValueIpv6NotThisDevice");
|
||||
}
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static Finding Provider(KnowledgeBase knowledge, AsnEntry v4, AsnEntry v6, bool anyAddress)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.Provider };
|
||||
if (!anyAddress)
|
||||
{
|
||||
finding.Outcome = Outcome.Unknown;
|
||||
finding.Summary = "ProviderNoAddress";
|
||||
return finding;
|
||||
}
|
||||
if (!knowledge.HasAsnTable)
|
||||
{
|
||||
finding.Outcome = Outcome.Unknown;
|
||||
finding.Summary = "ProviderNoMap";
|
||||
return finding;
|
||||
}
|
||||
var main = v4 ?? v6;
|
||||
if (main is null)
|
||||
{
|
||||
finding.Outcome = Outcome.Unknown;
|
||||
finding.Summary = "ProviderNotFound";
|
||||
return finding;
|
||||
}
|
||||
var profile = knowledge.ProviderFor(main.Number);
|
||||
finding.Outcome = Outcome.Info;
|
||||
finding.Summary = profile is null ? "ProviderUnlisted" : "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));
|
||||
if (profile is null) return finding;
|
||||
AddFact(finding, "FactProviderIpv4", "ProviderIpv4", profile.Ipv4);
|
||||
AddFact(finding, "FactProviderPublic", "ProviderPublic", profile.PublicAddress);
|
||||
AddFact(finding, "FactProviderIpv6", "ProviderIpv6", profile.Ipv6);
|
||||
AddFact(finding, "FactProviderPort25", "ProviderPort25", profile.Port25);
|
||||
if (profile.Sheet is not null) finding.Facts.Add(new Fact { Label = "FactProviderSheet", Key = "ValueOpenSheet", Link = profile.Sheet });
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static Finding Router(RouterReport router, LocalSnapshot local)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.Router };
|
||||
if (router.Gateway is null)
|
||||
{
|
||||
finding.Outcome = Outcome.Info;
|
||||
finding.Summary = local.Active?.Kind == LinkKind.Mobile ? "RouterMobile" : "RouterNone";
|
||||
return finding;
|
||||
}
|
||||
(finding.Outcome, finding.Summary) = router switch
|
||||
{
|
||||
{ AnyMapping: true } => (Outcome.Good, "RouterOpens"),
|
||||
{ AnyAnswer: true } => (Outcome.Caution, "RouterRefuses"),
|
||||
_ => (Outcome.Caution, "RouterSilent"),
|
||||
};
|
||||
finding.With("FactRouter", Join(router.Make, router.Label));
|
||||
finding.With("FactGateway", router.Gateway);
|
||||
finding.WithKey("FactUpnp", AnswerKey(router.Upnp), router.Upnp == Answer.Refused ? router.UpnpError.ToString(CultureInfo.InvariantCulture) : router.UpnpVersion.ToString(CultureInfo.InvariantCulture));
|
||||
finding.WithKey("FactNatPmp", AnswerKey(router.NatPmp), router.NatPmpResult.ToString(CultureInfo.InvariantCulture));
|
||||
finding.WithKey("FactPcp", AnswerKey(router.Pcp), router.PcpResult.ToString(CultureInfo.InvariantCulture));
|
||||
finding.With("FactRouterWan", router.WanAddress);
|
||||
return finding;
|
||||
}
|
||||
|
||||
public static (Finding Finding, NatKind Kind) Nat(LocalSnapshot local, RouterReport router, string v4, string v6, List<Hop> hops)
|
||||
{
|
||||
var kind = NatOf(local, router, v4);
|
||||
var finding = new Finding { Id = CheckId.Nat };
|
||||
(finding.Outcome, finding.Summary) = kind switch
|
||||
{
|
||||
NatKind.Direct => (Outcome.Good, "NatDirect"),
|
||||
NatKind.HomeRouter => (Outcome.Good, "NatHomeRouter"),
|
||||
NatKind.DoubleNat => (Outcome.Caution, "NatDouble"),
|
||||
NatKind.Shared => (Outcome.Bad, "NatShared"),
|
||||
NatKind.Translated => (Outcome.Bad, "NatTranslated"),
|
||||
NatKind.NoIpv4 => (Outcome.Info, "NatNoIpv4"),
|
||||
_ => (Outcome.Unknown, "NatUnknown"),
|
||||
};
|
||||
finding.With("FactRouterWan", router?.WanAddress);
|
||||
finding.With("FactPublicV4", v4);
|
||||
var before = Route.BeforeInternet(hops);
|
||||
if (hops.Count > 0) finding.With("FactRoute", string.Join(" → ", before.Select(h => h.Address ?? "*").Append(hops.FirstOrDefault(h => h.Kind == AddressKind.Public)?.Address ?? "…")));
|
||||
if (kind == NatKind.Unknown && before.Any(h => h.Kind == AddressKind.Shared)) finding.WithKey("FactHint", "HintSharedHop");
|
||||
|
||||
var v6Direct = v6 is not null && local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v6)) == true;
|
||||
if (v6 is not null) finding.WithKey("FactIpv6", v6Direct ? "ValueIpv6Direct" : "ValueIpv6Translated");
|
||||
if (v6Direct && finding.Outcome is Outcome.Bad or Outcome.Unknown && kind != NatKind.NoIpv4)
|
||||
{
|
||||
finding.Outcome = Outcome.Caution;
|
||||
finding.WithKey("FactWayOut", "ValueIpv6Way");
|
||||
}
|
||||
return (finding, kind);
|
||||
}
|
||||
|
||||
public static NatKind NatOf(LocalSnapshot local, RouterReport router, string v4)
|
||||
{
|
||||
if (v4 is null) return NatKind.NoIpv4;
|
||||
if (local.Active?.Addresses.Any(a => Net.Addresses.SameAddress(a, v4)) == true) return NatKind.Direct;
|
||||
var localV4 = Net.Addresses.Parse(local.LocalV4);
|
||||
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) return NatKind.Unknown;
|
||||
if (Net.Addresses.SameAddress(router.WanAddress, v4)) return NatKind.HomeRouter;
|
||||
return Net.Addresses.Classify(wan) switch
|
||||
{
|
||||
AddressKind.Shared => NatKind.Shared,
|
||||
AddressKind.Private => NatKind.DoubleNat,
|
||||
AddressKind.Public => NatKind.Translated,
|
||||
_ => NatKind.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static Finding History(HistorySummary summary)
|
||||
{
|
||||
var finding = new Finding { Id = CheckId.History, Outcome = Outcome.Info };
|
||||
var since = summary.Since.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
if (summary.Checks <= 1)
|
||||
{
|
||||
finding.Summary = "HistoryFirst";
|
||||
return finding;
|
||||
}
|
||||
finding.Summary = summary.Changes == 0 ? "HistoryStable" : "HistoryChanged";
|
||||
finding.Args = [summary.Checks.ToString(CultureInfo.InvariantCulture), since, summary.Changes.ToString(CultureInfo.InvariantCulture)];
|
||||
finding.WithKey("FactChecks", "ValueCount", summary.Checks.ToString(CultureInfo.InvariantCulture));
|
||||
finding.Facts.Add(new Fact { Label = "FactSince", Date = since });
|
||||
finding.WithKey("FactAddressesSeen", "ValueCount", summary.AddressesSeen.ToString(CultureInfo.InvariantCulture));
|
||||
return finding;
|
||||
}
|
||||
|
||||
static void AddFact(Finding finding, string label, string prefix, ProviderFact fact)
|
||||
{
|
||||
if (fact?.Value is null) return;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
static string AnswerKey(Answer answer) => answer switch
|
||||
{
|
||||
Answer.Answered => "ValueAnswered",
|
||||
Answer.Refused => "ValueRefused",
|
||||
_ => "ValueSilent",
|
||||
};
|
||||
|
||||
static string Display(AsnEntry entry) => string.IsNullOrWhiteSpace(entry.Name) ? $"AS{entry.Number}" : entry.Name;
|
||||
|
||||
static string Describe(AsnEntry entry) => $"{Display(entry)} (AS{entry.Number}{(string.IsNullOrEmpty(entry.Country) || entry.Country == "None" ? "" : ", " + entry.Country)})";
|
||||
|
||||
static string Join(string make, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(make)) return label;
|
||||
if (string.IsNullOrWhiteSpace(label) || label.Contains(make.Split(' ')[0], StringComparison.OrdinalIgnoreCase)) return label ?? make;
|
||||
return $"{make} {label}";
|
||||
}
|
||||
|
||||
static string Pascal(string value) => string.Concat(value.Split('-').Select(p => p.Length == 0 ? p : char.ToUpperInvariant(p[0]) + p[1..]));
|
||||
}
|
||||
Reference in new issue
Block a user