me-web? 0.2: the test from outside, calls and games, mail, and what you can host
Build / Desktop, server and tools (push) Successful in 59s
Release / Every platform, the server, the release (push) Successful in 7m27s

Phases two and three ship together, because their code grew in the same
files. With consent the app asks the router to open 80, 443 and a high
port for ten minutes (UPnP, NAT-PMP or PCP), and the server, holding a
single-use ticket bound to the caller's own address, connects back and
must get an HMAC only the app can give; the mappings are removed after the
run and remembered until then, so a crash cannot leave one open. Hairpin
is tried from inside. Two STUN servers, the outside one first, tell easy
from hard NAT.

Mail gets port 25 to Gmail and Outlook, the reverse name and five
blocklists through the user's own resolver, network-wide UCEPROTECT
listings counted as a caution. DNS, speed against the server (four at a
time, twelve an hour per address) and the device follow, and six uses
(website, home from outside, files, game server, fediverse, mail) close
the run as possible, possible with a named workaround, or can't tell.

Findings can be shared after reading the exact JSON: a file per submission
under the unit's state directory, ten a day per address, deletable by the
token the app keeps, swept after twelve months; /privacy says all of it in
English and Italian. QR codes lead to the downloads. Every language carries
every string.

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 05:41:44 +02:00
1 parent f79682eaef
commit 7cfc8b6de5
161 files changed
+23159 -305

No files matched your search

+181
View File
@@ -4,6 +4,7 @@ using MeWeb.Core.History;
using MeWeb.Core.Knowledge;
using MeWeb.Core.Net;
using MeWeb.Core.Router;
using MeWeb.Shared;
namespace MeWeb.Core.Checks;
@@ -201,6 +202,186 @@ public static class Verdicts
};
}
public static Finding Behaviour(StunReport stun)
{
var finding = new Finding { Id = CheckId.Behaviour };
(finding.Outcome, finding.Summary) = stun.Behaviour switch
{
NatBehaviour.None => (Outcome.Good, "BehaviourNone"),
NatBehaviour.Easy => (Outcome.Good, "BehaviourEasy"),
NatBehaviour.Hard => (Outcome.Caution, "BehaviourHard"),
_ => (Outcome.Unknown, "BehaviourUnknown"),
};
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);
return finding;
}
public static Finding Reach(ReachReport reach, RunReport run)
{
var finding = new Finding { Id = CheckId.Reach };
if (reach is null || reach.Choice == ReachChoice.Skip)
{
finding.Outcome = Outcome.Unknown;
finding.Summary = "ReachSkipped";
return finding;
}
var refusal = reach.V4Refusal ?? reach.V6Refusal;
if (reach.V4 is null && reach.V6 is null)
{
finding.Outcome = Outcome.Unknown;
finding.Summary = refusal switch
{
"own-network" => "ReachOwnNetwork",
"too-many" => "ReachTooMany",
"no-listener" => "ReachNoListener",
_ => "ReachUnavailable",
};
if (refusal == "too-many") finding.Args = [Math.Max(1, (int)Math.Ceiling(reach.RetryAfterSeconds / 60.0)).ToString(CultureInfo.InvariantCulture)];
return finding;
}
var reached4 = reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.Reached) == true;
var reached6 = reach.V6?.Ports.Any(p => p.Outcome == ProbeOutcome.Reached) == true;
var mapped = reach.Mappings.Any(m => m.Done);
(finding.Outcome, finding.Summary) = (reached4, reached6) switch
{
(true, true) => (Outcome.Good, "ReachBoth"),
(true, false) => (Outcome.Good, "ReachV4"),
(false, true) => (Outcome.Caution, "ReachV6Only"),
_ when reach.Choice == ReachChoice.OpenPorts && mapped => (Outcome.Bad, "ReachBlockedAfterMapping"),
_ when reach.Choice == ReachChoice.OpenPorts => (Outcome.Bad, "ReachNoMapping"),
_ => (Outcome.Caution, "ReachNothingAsIs"),
};
foreach (var attempt in reach.Mappings)
finding.WithKey("FactMapping", attempt.Done ? "ValueMapped" : "ValueNotMapped", attempt.ExternalPort.ToString(CultureInfo.InvariantCulture), attempt.Via.ToString(), attempt.Error.ToString(CultureInfo.InvariantCulture));
foreach (var port in reach.V4?.Ports ?? [])
finding.WithKey("FactReachV4", "Probe" + port.Outcome, port.Port.ToString(CultureInfo.InvariantCulture));
foreach (var port in reach.V6?.Ports ?? [])
finding.WithKey("FactReachV6", "Probe" + port.Outcome, port.Port.ToString(CultureInfo.InvariantCulture));
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");
if (reach.V4?.Ports.Any(p => p.Outcome == ProbeOutcome.OpenNotYou && p.Port is 80 or 443) == true && !mapped)
finding.WithKey("FactHint", "HintSomethingAnswers");
return finding;
}
public static Finding Mail(Mail.MailReport mail, bool hasV4)
{
var finding = new Finding { Id = CheckId.Mail };
if (!hasV4)
{
finding.Outcome = Outcome.Unknown;
finding.Summary = "MailNoIpv4";
return finding;
}
var listed = mail.Listings.Where(l => l.State == Core.Mail.ListingState.Listed && !l.Residential && !l.Wide).ToList();
var wide = mail.Listings.Where(l => l.State == Core.Mail.ListingState.Listed && l.Wide).ToList();
(finding.Outcome, finding.Summary) = mail switch
{
{ Port25: Core.Mail.PortState.Blocked } => (Outcome.Bad, "MailBlocked"),
_ when listed.Count > 0 => (Outcome.Bad, "MailListed"),
_ when mail.Listings.Any(l => l.Residential) => (Outcome.Caution, "MailResidential"),
_ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"),
{ ReverseName: null } => (Outcome.Caution, "MailNoName"),
{ ReverseLooksDynamic: true } => (Outcome.Caution, "MailDynamicName"),
{ Port25: Core.Mail.PortState.Open } => (Outcome.Good, "MailClean"),
_ => (Outcome.Unknown, "MailUnknown"),
};
if (listed.Count > 0) finding.Args = [string.Join(", ", listed.Select(l => l.List))];
else if (wide.Count > 0) finding.Args = [string.Join(", ", wide.Select(l => l.List))];
finding.WithKey("FactPort25", mail.Port25 switch
{
Core.Mail.PortState.Open => "ValuePort25Open",
Core.Mail.PortState.Blocked => "ValuePort25Blocked",
_ => "ValueCantTell",
}, mail.Banner ?? "");
if (mail.ReverseName is null) finding.WithKey("FactReverseName", "ValueNoReverse");
else
{
finding.With("FactReverseName", mail.ReverseName);
finding.WithKey("FactReverseLooks", mail.ReverseLooksDynamic ? "ValueLooksHome" : "ValueLooksServer");
finding.WithKey("FactReverseConfirmed", mail.ReverseConfirmed ? "ValueYes" : "ValueNo");
}
foreach (var listing in mail.Listings)
finding.Facts.Add(new Fact
{
Label = "FactList",
Key = listing.State switch
{
Core.Mail.ListingState.Listed when listing.Residential => "ValueListedHome",
Core.Mail.ListingState.Listed when listing.Wide => "ValueListedNetwork",
Core.Mail.ListingState.Listed => "ValueListed",
Core.Mail.ListingState.Clean => "ValueNotListed",
_ => "ValueCantTellResolver",
},
Args = [listing.List, listing.Code ?? ""],
});
return finding;
}
public static Finding Dns(DnsReport dns)
{
var finding = new Finding { Id = CheckId.Dns };
(finding.Outcome, finding.Summary) = dns switch
{
{ Hijacks: true } => (Outcome.Bad, "DnsHijack"),
{ Validates: false } => (Outcome.Caution, "DnsNoValidation"),
{ Validates: true, Hijacks: false } => (Outcome.Good, "DnsGood"),
_ => (Outcome.Unknown, "DnsUnknown"),
};
if (dns.Resolvers.Count > 0) finding.With("FactDns", string.Join(", ", dns.Resolvers.Take(3)));
if (dns.Validates is not null) finding.WithKey("FactDnssec", dns.Validates == true ? "ValueDnssecYes" : "ValueDnssecNo");
if (dns.Hijacks is not null) finding.WithKey("FactMissingNames", dns.Hijacks == true ? "ValueHijacked" : "ValueAnsweredMissing");
if (dns.Ipv6Names is not null) finding.WithKey("FactIpv6Names", dns.Ipv6Names == true ? "ValueYes" : "ValueNo");
return finding;
}
public static Finding Speed(SpeedReport speed)
{
var finding = new Finding { Id = CheckId.Speed };
if (speed.Limited)
{
finding.Outcome = Outcome.Unknown;
finding.Summary = "SpeedLimited";
return finding;
}
if (speed.DownMbps <= 0)
{
finding.Outcome = Outcome.Unknown;
finding.Summary = "SpeedFailed";
return finding;
}
finding.Outcome = Outcome.Info;
finding.Summary = "SpeedResult";
finding.Args = [speed.DownMbps.ToString("0.#", CultureInfo.InvariantCulture), speed.UpMbps.ToString("0.#", CultureInfo.InvariantCulture)];
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));
return finding;
}
public static Finding Device(DeviceReport device)
{
var finding = new Finding { Id = CheckId.Device };
var small = device.Cores < 2 || device.MemoryMb < 1800 || device.FreeDiskGb is > 0 and < 20;
(finding.Outcome, finding.Summary) = device switch
{
{ Kind: DeviceKind.Phone } => (Outcome.Caution, "DevicePhone"),
_ when small => (Outcome.Caution, "DeviceSmall"),
{ OnBattery: true } => (Outcome.Info, "DeviceLaptop"),
_ => (Outcome.Good, "DeviceGood"),
};
finding.With("FactSystem", device.System);
finding.WithKey("FactCores", "ValueCount", device.Cores.ToString(CultureInfo.InvariantCulture));
finding.WithKey("FactMemory", "ValueGb", Math.Round(device.MemoryMb / 1024.0, 1).ToString("0.#", CultureInfo.InvariantCulture));
if (device.FreeDiskGb > 0) finding.WithKey("FactDisk", "ValueGbFree", device.FreeDiskGb.ToString(CultureInfo.InvariantCulture));
if (device.OnBattery is not null) finding.WithKey("FactBattery", device.OnBattery == true ? "ValueYes" : "ValueNo");
return finding;
}
public static Finding History(HistorySummary summary)
{
var finding = new Finding { Id = CheckId.History, Outcome = Outcome.Info };