app: an IMS network is not a blocked measurement
rmnet_data1 on the OnePlus is the carrier's IMS/VoLTE network: it carries IMS&MMTEL but neither INTERNET nor NOT_RESTRICTED, so binding needs CONNECTIVITY_USE_RESTRICTED_NETWORKS - signature-level, unobtainable. EPERM there is permanent and says nothing about any network's health, but the constraint detector counted it, so a phone with VoLTE reported 'measurement blocked' on every run and every verdict came out INCONCLUSIVE. Capabilities now decide: networks an app may never bind are recorded as app_usable:false in networks[] and skipped, rather than reported as something the run failed to do. Also retires the 'while it tears down' wording, which was a guess that turned out to be wrong - the refusal outlasts the VPN indefinitely, so the text now names the VPN as the usual cause without asserting a timeline it cannot know. App version 0.2.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d420a71a01
commit
ab6e278272
@@ -111,12 +111,24 @@ A run may exercise several networks simultaneously (Wi-Fi + cellular + USB ether
|
||||
"changes": [
|
||||
{ "at_mono_ns": 91000000000, "kind": "lost | gained | link_changed",
|
||||
"detail": { /* new link snapshot or diff */ } }
|
||||
]
|
||||
],
|
||||
"app_usable": true
|
||||
}
|
||||
```
|
||||
|
||||
`routes[].proto` and lifetime fields are Shizuku-tier data (`ip route`/`ip addr`); app-tier snapshots leave them absent — absence means "not observed", never "not present".
|
||||
|
||||
`app_usable` records whether an ordinary app may send on this network at all. Android lists the
|
||||
carrier's special-purpose networks — IMS/VoLTE, MMS, XCAP — alongside the real ones, and they
|
||||
carry neither `INTERNET` nor `NOT_RESTRICTED`; binding to one needs
|
||||
`CONNECTIVITY_USE_RESTRICTED_NETWORKS`, which is signature-level and unobtainable for a normal
|
||||
app. Those networks are therefore permanently unmeasurable, and that is a property of Android's
|
||||
permission model rather than of the link. They stay in `networks[]` because they are genuinely
|
||||
present — an interface silently missing from the inventory is its own kind of lie — but a
|
||||
consumer must not read the absence of tests against them as a fault, and they are **not**
|
||||
`constraints.unmeasured_networks` (§3): nothing was prevented, the run was never entitled to
|
||||
measure them.
|
||||
|
||||
## 5. `server_sessions[]`
|
||||
|
||||
```json
|
||||
|
||||
@@ -15,7 +15,7 @@ plugins {
|
||||
//
|
||||
// major*1_000_000 + minor*10_000 + patch*10 leaves room for 9 patch-level rebuilds (the trailing
|
||||
// digit) without disturbing the mapping, and stays inside the 2_100_000_000 ceiling until major 2100.
|
||||
val appVersionName = "0.2.3"
|
||||
val appVersionName = "0.2.4"
|
||||
|
||||
fun versionCodeOf(semver: String): Int {
|
||||
val (major, minor, patch) = semver.substringBefore('-').split(".").map(String::toInt)
|
||||
|
||||
@@ -432,9 +432,9 @@ private fun Results(doc: MeasurementDocument) {
|
||||
"Disconnect the VPN and run again to measure the networks themselves.")
|
||||
constraints.perNetworkBlocked ->
|
||||
"Some networks could not be measured" to
|
||||
("Android refused sends on $blocked — the restriction a VPN leaves in place " +
|
||||
"while it tears down. These networks went unmeasured; wait a few " +
|
||||
"seconds and run again.")
|
||||
("Android refused this app permission to send on $blocked, so they went " +
|
||||
"unmeasured. A connected VPN is the usual cause; the refusal can also " +
|
||||
"outlast one. Everything else in this run is unaffected.")
|
||||
else ->
|
||||
"A VPN holds the default route" to
|
||||
("Default-route results describe the tunnel; per-network measurements " +
|
||||
|
||||
@@ -594,10 +594,11 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
||||
runConstraints.perNetworkBlocked ->
|
||||
"The OS refused sends on $blocked" to
|
||||
("Android denied this app permission to send on $blocked (EPERM on " +
|
||||
"bind). That is the restriction a VPN imposes on the networks " +
|
||||
"beneath it — a tunnel disconnected moments ago can still leave it " +
|
||||
"in place while it tears down. Nothing in this run says anything " +
|
||||
"about $blocked; wait a few seconds and run again.")
|
||||
"bind), so nothing in this run says anything about it. The usual " +
|
||||
"cause is a VPN — Android walls off the networks beneath a tunnel — " +
|
||||
"but the refusal can also outlast one, and some networks are " +
|
||||
"reserved for the system entirely. If no VPN is connected, this is " +
|
||||
"a restriction on the app rather than a fault in the network.")
|
||||
else ->
|
||||
"A VPN holds the default route" to
|
||||
("Everything using the default route in this run describes the tunnel, " +
|
||||
|
||||
@@ -19,6 +19,17 @@ data class Network(
|
||||
val cellular: Cellular? = null,
|
||||
val changes: List<NetworkChange> = emptyList(),
|
||||
@SerialName("system_verdict") val systemVerdict: SystemVerdict? = null,
|
||||
/**
|
||||
* Whether an ordinary app may send on this network at all.
|
||||
*
|
||||
* False for the carrier's special-purpose networks — IMS/VoLTE, MMS, XCAP — which appear
|
||||
* beside the real ones in Android's list and carry neither `INTERNET` nor `NOT_RESTRICTED`.
|
||||
* Binding to those needs `CONNECTIVITY_USE_RESTRICTED_NETWORKS`, a privileged permission no
|
||||
* normal app can hold, so the refusal is permanent and says nothing about the network's
|
||||
* health. Recorded rather than hidden: a reader seeing an interface with no measurements
|
||||
* against it deserves to know the OS forbade them, instead of concluding the link is dead.
|
||||
*/
|
||||
@SerialName("app_usable") val appUsable: Boolean? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,12 @@ object ConstraintDetector {
|
||||
for (e in entries) {
|
||||
// The tunnel itself stays bindable — it is the underlying networks the OS walls off.
|
||||
if (e.model.transport == Transport.VPN) continue
|
||||
// Networks an app may never bind (carrier IMS/VoLTE, MMS, XCAP — no INTERNET, no
|
||||
// NOT_RESTRICTED) refuse with EPERM permanently, VPN or no VPN. Counting them as a
|
||||
// constraint is what made a phone with VoLTE report "measurement blocked" forever
|
||||
// and forced every run INCONCLUSIVE. They are recorded in networks[] as
|
||||
// app_usable:false; they are not something a run failed to do.
|
||||
if (e.model.appUsable == false) continue
|
||||
val err = try {
|
||||
DatagramSocket().use { s -> e.handle.bindSocket(s) }
|
||||
null
|
||||
|
||||
@@ -65,6 +65,20 @@ object NetworkInventory {
|
||||
/** @SystemApi NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY, API 28+. */
|
||||
private const val NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24
|
||||
|
||||
/**
|
||||
* Can an ordinary app send on this network?
|
||||
*
|
||||
* The carrier's special-purpose networks (IMS/VoLTE, MMS, XCAP) sit in `allNetworks` next to
|
||||
* the real ones, carrying `IMS`/`MMS` but neither `INTERNET` nor `NOT_RESTRICTED`. Binding
|
||||
* to them fails with EPERM forever, because it needs `CONNECTIVITY_USE_RESTRICTED_NETWORKS`
|
||||
* — signature-level, unobtainable for this app. Asking the capabilities up front is what
|
||||
* separates "the OS will never let us measure this" from "something is blocking us", which
|
||||
* a bind attempt alone cannot distinguish and which the constraint logic must not confuse.
|
||||
*/
|
||||
private fun appUsable(caps: NetworkCapabilities): Boolean =
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
|
||||
private fun toModel(id: String, caps: NetworkCapabilities, lp: LinkProperties): MNetwork {
|
||||
val transport = when {
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> Transport.WIFI
|
||||
@@ -99,6 +113,7 @@ object NetworkInventory {
|
||||
id = id, transport = transport, iface = lp.interfaceName,
|
||||
link = Link(mtu = lp.mtu.takeIf { it > 0 }, addresses = addresses, routes = routes, dns = dns),
|
||||
systemVerdict = systemVerdict(caps),
|
||||
appUsable = appUsable(caps),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user