app: probe the server this device is enrolled with, not ours
The canary-DNS zone and the STUN host were compiled in as c.echo-lot.app and fmr-1.echo-lot.app, so every copy of the app measured against this particular deployment whatever server its owner had enrolled with. On someone else's install those two tests describe our infrastructure and report the result as a fact about their network. The zone comes from the server's own profile, which has advertised canary_zone all along — the app simply never read it. It is cached in settings because the canary probe runs at device tier, before anything has contacted the control plane, and a probe that had to make a call first would fail on exactly the networks worth measuring. The STUN host is derived from the configured server URL rather than stored, since a second copy of the server's name goes stale the moment someone re-enrolls elsewhere. With no server configured both now report SKIPPED. StunProbe previously would have reported FAILED on a blank host, which reads as a finding about the network when the truth is that no packet was ever sent — the same conflation between "measured nothing" and "measured a fault" that the ICMPv6 finding had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e0428b4c84
commit
4aaaa5f5d4
@@ -90,6 +90,8 @@ class RunStore(context: Context, private val settings: Settings) {
|
||||
if (!settings.serverConfigured) return "Fill in the server URL, pin and credential first."
|
||||
return try {
|
||||
val profile = client().profile(settings.serverCredential)
|
||||
// Learned here so the next run's canary probe knows what to ask for.
|
||||
profile.canaryZone.takeIf { it.isNotBlank() }?.let { settings.canaryZone = it }
|
||||
val compat = Compat.check(profile, BuildConfig.APP_SEMVER)
|
||||
val head = "${profile.name} · server ${profile.serverVersion} · " +
|
||||
"protocol ${profile.compat.protocolVersion.ifBlank { "unstated" }}"
|
||||
@@ -150,6 +152,7 @@ class RunStore(context: Context, private val settings: Settings) {
|
||||
return try {
|
||||
val client = client()
|
||||
val profile = client.profile(settings.serverCredential)
|
||||
profile.canaryZone.takeIf { it.isNotBlank() }?.let { settings.canaryZone = it }
|
||||
|
||||
// Compatibility before policy: an incompatible server may well advertise an upload
|
||||
// policy it would never actually apply to us.
|
||||
|
||||
@@ -314,8 +314,12 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
||||
CaptivePortalProbe(entries),
|
||||
// Canary zone served by the Echolot probe server (probe-protocol §6.1). Hardcoded to
|
||||
// the reference deployment until profiles/enrollment land in the UI.
|
||||
DnsCanaryProbe(canaryZone = "c.echo-lot.app", sessionPrefix = "adhoc"),
|
||||
StunProbe(serverHost = "fmr-1.echo-lot.app"),
|
||||
// Both target whatever server this device is enrolled with, not the deployment the
|
||||
// app happened to be developed against. With no server configured they get blank
|
||||
// strings and report themselves skipped, which is the honest outcome — the
|
||||
// alternative measures someone else's infrastructure and calls it your network.
|
||||
DnsCanaryProbe(canaryZone = settings.canaryZone, sessionPrefix = "adhoc"),
|
||||
StunProbe(serverHost = settings.serverHost()),
|
||||
)
|
||||
|
||||
// Plan the run first: the Shizuku battery is counted alongside the app-tier probes so
|
||||
|
||||
@@ -104,6 +104,28 @@ class Settings(context: Context) {
|
||||
val serverConfigured: Boolean
|
||||
get() = serverUrl.isNotBlank() && serverPin.isNotBlank() && serverCredential.isNotBlank()
|
||||
|
||||
/**
|
||||
* The DNS zone this server is authoritative for, learned from its profile.
|
||||
*
|
||||
* Cached because the canary probe runs at device tier, before anything has talked to the
|
||||
* server, and a probe that had to make a control-plane call first would fail on exactly the
|
||||
* networks worth measuring. Empty means "not known yet", and the probe reports itself as
|
||||
* skipped rather than inventing a zone.
|
||||
*/
|
||||
var canaryZone: String
|
||||
get() = prefs.getString(CANARY_ZONE, "") ?: ""
|
||||
set(v) = prefs.edit().putString(CANARY_ZONE, v.trim()).apply()
|
||||
|
||||
/**
|
||||
* Host part of the configured server URL, for probes that address it directly (STUN).
|
||||
*
|
||||
* Derived rather than stored: a second copy of the server's name is a second thing to keep in
|
||||
* step, and it would go stale the moment someone re-enrolled against a different server.
|
||||
*/
|
||||
fun serverHost(): String = runCatching {
|
||||
java.net.URI(serverUrl).host?.takeIf { it.isNotBlank() }
|
||||
}.getOrNull() ?: ""
|
||||
|
||||
// ---- account ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -150,6 +172,7 @@ class Settings(context: Context) {
|
||||
const val SERVER_URL = "server_url"
|
||||
const val SERVER_PIN = "server_pin"
|
||||
const val SERVER_CRED = "server_credential"
|
||||
const val CANARY_ZONE = "server_canary_zone"
|
||||
const val PENDING_VERIFIER = "pending_auth_verifier"
|
||||
const val PENDING_STATE = "pending_auth_state"
|
||||
const val ACCOUNT_NAME = "account_name"
|
||||
|
||||
Reference in New Issue
Block a user