app: fold the prober's validated capabilities into core-probe
traceroute.udp4 lands as TracerouteProbe: ICMP time-exceeded read off the socket error queue via Os.recvmsg(MSG_ERRQUEUE) through the reflection facade the prober validated on both devices - no root, no raw socket, and the C-over-JNI shim stays dead. Emits the schema's TracerouteEvidence with rtt_ns. OsAbi carries the hardcoded sockopt ABI numbers across, including the measured fact that Os.getsockoptInt exists on neither device, so PMTU must come from the errqueue, never getsockopt(IP_MTU). local.mdns_inventory lands as MdnsInventoryProbe with both hardware-bought lessons intact: the meta-query lies (0 results beside live services on both devices), and 4s of listening misses what 10s catches. App version 0.2.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d98a43866
commit
f6849f8e6a
@@ -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.1"
|
||||
val appVersionName = "0.2.2"
|
||||
|
||||
fun versionCodeOf(semver: String): Int {
|
||||
val (major, minor, patch) = semver.substringBefore('-').split(".").map(String::toInt)
|
||||
|
||||
@@ -403,6 +403,10 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
||||
RouterIdentityProbe(entries),
|
||||
IcmpProbe(entries, v6 = false),
|
||||
IcmpProbe(entries, v6 = true),
|
||||
// Folded from the prober after hardware validation: errqueue traceroute (no root,
|
||||
// no JNI) and the mDNS service inventory / VLAN-leakage detector.
|
||||
app.echo_lot.probe.TracerouteProbe(),
|
||||
app.echo_lot.probe.MdnsInventoryProbe(),
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app.echo_lot.probe
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import app.echo_lot.measurement.Test
|
||||
import app.echo_lot.measurement.TestStatus
|
||||
import app.echo_lot.measurement.TestType
|
||||
import app.echo_lot.measurement.Tier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* local.mdns_inventory — what answers mDNS on this network (MulticastLock + NSD discovery).
|
||||
* The service inventory doubles as the VLAN-leakage detector: a chromecast answering on the
|
||||
* guest wifi is a segmentation fault made visible. Folded from the prober, validated on both
|
||||
* known devices (4 services each).
|
||||
*
|
||||
* Two hardware-bought lessons are load-bearing here:
|
||||
* - The `_services._dns-sd._udp.` meta-query returned 0 on BOTH devices while concrete types
|
||||
* found live services — NsdManager's meta-query support is unreliable across builds, so the
|
||||
* concrete types are the measurement and the meta-query result is itself evidence.
|
||||
* - 4 s of listening missed services that 10 s catches; mDNS answers straggle.
|
||||
*/
|
||||
class MdnsInventoryProbe : Probe {
|
||||
override val type = TestType.LOCAL_MDNS_INVENTORY
|
||||
override val tier = Tier.APP
|
||||
override val estimatedMs = 10_500L
|
||||
|
||||
/** Meta-query + common concrete types (HTTP covers HA/printers/NAS; googlecast is ubiquitous). */
|
||||
private val queries = listOf(
|
||||
"meta" to "_services._dns-sd._udp.",
|
||||
"http" to "_http._tcp.",
|
||||
"googlecast" to "_googlecast._tcp.",
|
||||
)
|
||||
|
||||
private class Recorder : NsdManager.DiscoveryListener {
|
||||
val names: MutableList<String> = Collections.synchronizedList(mutableListOf())
|
||||
@Volatile var started = false
|
||||
@Volatile var startFailCode: Int? = null
|
||||
override fun onStartDiscoveryFailed(t: String?, code: Int) { startFailCode = code }
|
||||
override fun onStopDiscoveryFailed(t: String?, code: Int) {}
|
||||
override fun onDiscoveryStarted(t: String?) { started = true }
|
||||
override fun onDiscoveryStopped(t: String?) {}
|
||||
override fun onServiceFound(s: NsdServiceInfo?) { s?.serviceName?.let { names.add(it) } }
|
||||
override fun onServiceLost(s: NsdServiceInfo?) {}
|
||||
}
|
||||
|
||||
override suspend fun run(ctx: Context, ids: ProbeIds): Test = withContext(Dispatchers.IO) {
|
||||
val b = TestBuilder(type, tier, ids)
|
||||
val wifi = ctx.getSystemService(WifiManager::class.java)
|
||||
val lock = wifi?.createMulticastLock("echolot")?.apply {
|
||||
setReferenceCounted(false)
|
||||
runCatching { acquire() }
|
||||
}
|
||||
val nsd = ctx.getSystemService(NsdManager::class.java)
|
||||
?: return@withContext b.build(
|
||||
TestStatus.UNSUPPORTED,
|
||||
evidence = buildJsonObject { put("reason", "NsdManager unavailable") },
|
||||
)
|
||||
|
||||
val recorders = queries.map { (label, type) ->
|
||||
val r = Recorder()
|
||||
runCatching { nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, r) }
|
||||
.onFailure { r.startFailCode = -1 }
|
||||
Triple(label, type, r)
|
||||
}
|
||||
try {
|
||||
delay(10_000)
|
||||
var total = 0
|
||||
var anyStarted = false
|
||||
val evidence = buildJsonObject {
|
||||
put("multicast_lock", lock?.isHeld == true)
|
||||
for ((label, type, r) in recorders) {
|
||||
runCatching { nsd.stopServiceDiscovery(r) }
|
||||
anyStarted = anyStarted || r.started
|
||||
val names = r.names.distinct()
|
||||
total += names.size
|
||||
putJsonObject(label) {
|
||||
put("query", type)
|
||||
put("started", r.started)
|
||||
r.startFailCode?.let { put("start_fail_code", it) }
|
||||
put("found", names.size)
|
||||
if (names.isNotEmpty()) put("names", names.joinToString(", ").take(300))
|
||||
}
|
||||
}
|
||||
}
|
||||
val metrics = buildJsonObject { put("services_found", total) }
|
||||
// Zero services on a started discovery is a legitimate result (an empty or properly
|
||||
// isolated network), not a failure — only discovery refusing to start is one.
|
||||
b.build(if (anyStarted) TestStatus.OK else TestStatus.FAILED,
|
||||
evidence = evidence, metrics = metrics)
|
||||
} finally {
|
||||
recorders.forEach { (_, _, r) -> runCatching { nsd.stopServiceDiscovery(r) } }
|
||||
runCatching { lock?.release() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app.echo_lot.probe
|
||||
|
||||
import android.system.Os
|
||||
import java.io.FileDescriptor
|
||||
|
||||
/**
|
||||
* Linux socket-option ABI numbers that android.system.OsConstants does NOT reliably expose.
|
||||
* Stable across Android's supported ABIs at the IP/IPv6 protocol levels, which is why they can
|
||||
* be hardcoded: if setsockoptInt with one of these succeeds, the kernel accepted the option; if
|
||||
* it throws ErrnoException, it did not. Either outcome is data. Do not "fix" these to
|
||||
* OsConstants names — they don't exist there (validated in the prober; see its OsAbi.kt).
|
||||
*
|
||||
* Measured fact worth keeping: `Os.getsockoptInt` is absent on both known devices (OnePlus 15
|
||||
* A16, Lenovo TB330FU A15), so path-MTU values must be read from the errqueue (`ee_info`), never
|
||||
* from getsockopt(IP_MTU).
|
||||
*/
|
||||
object OsAbi {
|
||||
// IP level
|
||||
const val IP_TTL = 2
|
||||
const val IP_MTU_DISCOVER = 10
|
||||
const val IP_MTU = 14
|
||||
const val IP_RECVERR = 11
|
||||
const val IP_PMTUDISC_DO = 2 // set DF, honor PMTU
|
||||
const val IP_PMTUDISC_PROBE = 3 // set DF, ignore PMTU (for probing)
|
||||
|
||||
// IPv6 level
|
||||
const val IPV6_MTU_DISCOVER = 23
|
||||
const val IPV6_MTU = 24
|
||||
const val IPV6_RECVERR = 25
|
||||
const val IPV6_UNICAST_HOPS = 16
|
||||
const val IPV6_PMTUDISC_PROBE = 3
|
||||
|
||||
// recv flags — not in OsConstants on any current API level
|
||||
const val MSG_ERRQUEUE = 0x2000
|
||||
const val MSG_DONTWAIT = 0x40
|
||||
|
||||
// struct sock_extended_err (uapi/linux/errqueue.h), fixed layout on all Android ABIs:
|
||||
// u32 ee_errno; u8 ee_origin; u8 ee_type; u8 ee_code; u8 ee_pad; u32 ee_info; u32 ee_data;
|
||||
// followed directly by the offender sockaddr (SO_EE_OFFENDER).
|
||||
const val SOCK_EE_SIZE = 16
|
||||
const val SO_EE_ORIGIN_ICMP = 2
|
||||
const val ICMP_TIME_EXCEEDED = 11
|
||||
const val ICMP_DEST_UNREACH = 3
|
||||
|
||||
/** Try setsockoptInt; return null on success, or the errno name on failure. */
|
||||
fun trySetIntOpt(fd: FileDescriptor, level: Int, opt: Int, value: Int): String? =
|
||||
try {
|
||||
Os.setsockoptInt(fd, level, opt, value)
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
e.message ?: e.javaClass.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* getsockoptInt is not part of the stable public Os surface on every API level, so it is
|
||||
* reached via reflection; callers must treat failure as "unreadable", not as an error.
|
||||
*/
|
||||
fun tryGetIntOpt(fd: FileDescriptor, level: Int, opt: Int): Result<Int> = runCatching {
|
||||
val m = Os::class.java.getMethod(
|
||||
"getsockoptInt",
|
||||
FileDescriptor::class.java,
|
||||
Int::class.javaPrimitiveType,
|
||||
Int::class.javaPrimitiveType,
|
||||
)
|
||||
m.invoke(null, fd, level, opt) as Int
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,10 @@ class TestBuilder(
|
||||
evidence: JsonObject? = null,
|
||||
metrics: JsonObject? = null,
|
||||
error: TestError? = null,
|
||||
params: JsonObject? = null,
|
||||
): Test = Test(
|
||||
id = id, type = type, networkRef = networkRef, sessionRef = sessionRef, tier = tier,
|
||||
startedMonoNs = startedMonoNs, endedMonoNs = ids.monoNs(),
|
||||
status = status, error = error, evidence = evidence, metrics = metrics,
|
||||
status = status, error = error, params = params, evidence = evidence, metrics = metrics,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app.echo_lot.probe
|
||||
|
||||
import android.content.Context
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import app.echo_lot.measurement.Flow
|
||||
import app.echo_lot.measurement.Hop
|
||||
import app.echo_lot.measurement.HopProbe
|
||||
import app.echo_lot.measurement.Test
|
||||
import app.echo_lot.measurement.TestError
|
||||
import app.echo_lot.measurement.TestStatus
|
||||
import app.echo_lot.measurement.TestType
|
||||
import app.echo_lot.measurement.Tier
|
||||
import app.echo_lot.measurement.TracerouteEvidence
|
||||
import app.echo_lot.measurement.toEvidence
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.io.FileDescriptor
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* traceroute.udp4 — UDP traceroute reading ICMP time-exceeded off the socket error queue via
|
||||
* Os.recvmsg(MSG_ERRQUEUE): no root, no raw socket, no native code. Folded from the prober,
|
||||
* which validated real hop addresses on both known devices (6 hops on the OnePlus 15, 5 on the
|
||||
* Lenovo) and thereby retired the planned C-over-JNI errqueue shim.
|
||||
*
|
||||
* StructMsghdr/StructCmsghdr/recvmsg are reached via reflection (repo convention for uncertain
|
||||
* OS paths): present since roughly API 34, absent before, and the probe must run — and report —
|
||||
* on both. An absent API is UNSUPPORTED with the reason, never a crash.
|
||||
*/
|
||||
class TracerouteProbe(
|
||||
private val targetHost: String = "1.1.1.1",
|
||||
private val maxHops: Int = 6,
|
||||
) : Probe {
|
||||
override val type = TestType.TRACEROUTE_UDP4
|
||||
override val tier = Tier.APP
|
||||
// Validated wall clock is ~250 ms on a healthy path; the ceiling is maxHops silent hops at
|
||||
// 900 ms each, which only a blackholing path produces.
|
||||
override val estimatedMs = 1_500L
|
||||
|
||||
private companion object {
|
||||
const val BASE_PORT = 33434
|
||||
/** ICMP errors take one RTT to surface on the errqueue; poll briefly, never block. */
|
||||
const val HOP_DEADLINE_NS = 900_000_000L
|
||||
const val POLL_INTERVAL_MS = 40L
|
||||
}
|
||||
|
||||
override suspend fun run(ctx: Context, ids: ProbeIds): Test = withContext(Dispatchers.IO) {
|
||||
val b = TestBuilder(type, tier, ids)
|
||||
val params = buildJsonObject {
|
||||
put("target", targetHost); put("max_hops", maxHops); put("base_port", BASE_PORT)
|
||||
}
|
||||
|
||||
val api = ErrqueueApi.resolve()
|
||||
?: return@withContext b.build(
|
||||
TestStatus.UNSUPPORTED,
|
||||
params = params,
|
||||
error = TestError(
|
||||
"no_recvmsg",
|
||||
"StructMsghdr/Os.recvmsg not on this API level — errqueue unreadable",
|
||||
),
|
||||
)
|
||||
|
||||
var fd: FileDescriptor? = null
|
||||
try {
|
||||
fd = Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP)
|
||||
OsAbi.trySetIntOpt(fd, OsConstants.IPPROTO_IP, OsAbi.IP_RECVERR, 1)?.let {
|
||||
return@withContext b.build(
|
||||
TestStatus.UNSUPPORTED,
|
||||
params = params,
|
||||
error = TestError("ip_recverr_rejected", it),
|
||||
)
|
||||
}
|
||||
val target = InetAddress.getByName(targetHost)
|
||||
|
||||
val hops = ArrayList<Hop>(maxHops)
|
||||
var hopsSeen = 0
|
||||
var reachedTarget = false
|
||||
var srcPort = 0
|
||||
for (ttl in 1..maxHops) {
|
||||
OsAbi.trySetIntOpt(fd, OsConstants.IPPROTO_IP, OsAbi.IP_TTL, ttl)
|
||||
val t0 = System.nanoTime()
|
||||
val sent = runCatching {
|
||||
Os.sendto(fd, ByteArray(32), 0, 32, 0, target, BASE_PORT + ttl)
|
||||
}
|
||||
if (sent.isFailure) {
|
||||
hops.add(Hop(ttl, listOf(HopProbe(icmp = "sendto failed: " +
|
||||
(sent.exceptionOrNull()?.message ?: "?")))))
|
||||
continue
|
||||
}
|
||||
if (srcPort == 0) {
|
||||
// Only readable after the implicit bind the first send performs.
|
||||
srcPort = runCatching {
|
||||
(Os.getsockname(fd) as? InetSocketAddress)?.port ?: 0
|
||||
}.getOrDefault(0)
|
||||
}
|
||||
|
||||
var hop: ErrqueueApi.ErrEvent? = null
|
||||
val deadline = System.nanoTime() + HOP_DEADLINE_NS
|
||||
while (hop == null && System.nanoTime() < deadline) {
|
||||
hop = api.pollErrqueue(fd)
|
||||
if (hop == null) delay(POLL_INTERVAL_MS)
|
||||
}
|
||||
val rttNs = System.nanoTime() - t0
|
||||
when {
|
||||
hop == null -> hops.add(Hop(ttl, listOf(HopProbe()))) // silent hop: all null
|
||||
hop.parseError != null ->
|
||||
hops.add(Hop(ttl, listOf(HopProbe(icmp = "unparsed: ${hop.parseError}"))))
|
||||
else -> {
|
||||
hopsSeen++
|
||||
hops.add(Hop(ttl, listOf(HopProbe(
|
||||
replyFrom = hop.offender,
|
||||
rttNs = rttNs,
|
||||
icmp = when (hop.icmpType) {
|
||||
OsAbi.ICMP_TIME_EXCEEDED -> "time_exceeded"
|
||||
OsAbi.ICMP_DEST_UNREACH -> "dest_unreachable"
|
||||
else -> "type_${hop.icmpType}"
|
||||
},
|
||||
))))
|
||||
if (hop.icmpType == OsAbi.ICMP_DEST_UNREACH) reachedTarget = true
|
||||
}
|
||||
}
|
||||
if (reachedTarget) break
|
||||
}
|
||||
|
||||
// dst_port varies per TTL (classic traceroute, and what was validated on hardware),
|
||||
// so this flow is explicitly NOT fixed-tuple; base_port is in params.
|
||||
val evidence = TracerouteEvidence(
|
||||
flow = Flow(srcPort = srcPort, dstPort = BASE_PORT, fixedTuple = false),
|
||||
hops = hops,
|
||||
).toEvidence()
|
||||
val metrics = buildJsonObject {
|
||||
put("hops_seen", hopsSeen)
|
||||
put("reached_target", reachedTarget)
|
||||
}
|
||||
val status = when {
|
||||
hopsSeen > 0 -> TestStatus.OK
|
||||
// API present, sends succeeded, nothing surfaced: a fact about this path or
|
||||
// kernel, not proof the mechanism is missing.
|
||||
else -> TestStatus.PARTIAL
|
||||
}
|
||||
b.build(status, params = params, evidence = evidence, metrics = metrics)
|
||||
} catch (e: Throwable) {
|
||||
b.build(
|
||||
TestStatus.FAILED,
|
||||
params = params,
|
||||
error = TestError("uncaught", e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
} finally {
|
||||
fd?.let { runCatching { Os.close(it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflection facade over android.system.{StructMsghdr, StructCmsghdr, Os.recvmsg}.
|
||||
* Resolved once; null if any piece is missing on this API level.
|
||||
*/
|
||||
internal class ErrqueueApi private constructor(
|
||||
private val msghdrCtor: java.lang.reflect.Constructor<*>,
|
||||
private val recvmsg: java.lang.reflect.Method,
|
||||
private val cmsgLevel: java.lang.reflect.Field,
|
||||
private val cmsgType: java.lang.reflect.Field,
|
||||
private val cmsgData: java.lang.reflect.Field,
|
||||
private val msgControl: java.lang.reflect.Field,
|
||||
) {
|
||||
class ErrEvent(
|
||||
val offender: String?,
|
||||
val icmpType: Int,
|
||||
val origin: Int,
|
||||
val parseError: String? = null,
|
||||
)
|
||||
|
||||
/** One non-blocking MSG_ERRQUEUE read; null when the queue is empty. */
|
||||
fun pollErrqueue(fd: FileDescriptor): ErrEvent? {
|
||||
return try {
|
||||
val iov = arrayOf(ByteBuffer.allocate(512))
|
||||
// (SocketAddress msg_name, ByteBuffer[] msg_iov, StructCmsghdr[] msg_control, flags)
|
||||
val msghdr = msghdrCtor.newInstance(
|
||||
InetSocketAddress(0), iov, null, 0,
|
||||
)
|
||||
recvmsg.invoke(null, fd, msghdr, OsAbi.MSG_ERRQUEUE or OsAbi.MSG_DONTWAIT)
|
||||
val control = msgControl.get(msghdr) as? Array<*>
|
||||
?: return ErrEvent(null, -1, -1, "msg_control empty after recvmsg")
|
||||
for (cmsg in control.filterNotNull()) {
|
||||
val level = cmsgLevel.getInt(cmsg)
|
||||
val type = cmsgType.getInt(cmsg)
|
||||
if (level == OsConstants.IPPROTO_IP && type == OsAbi.IP_RECVERR) {
|
||||
return parseSockExtendedErr(cmsgData.get(cmsg))
|
||||
}
|
||||
}
|
||||
ErrEvent(null, -1, -1, "no IP_RECVERR cmsg among ${control.size}")
|
||||
} catch (e: Throwable) {
|
||||
// The single most load-bearing line: reflection wraps errno in
|
||||
// InvocationTargetException, and EAGAIN there means "queue empty", not failure.
|
||||
val cause = (e as? java.lang.reflect.InvocationTargetException)?.cause ?: e
|
||||
val msg = cause.message ?: cause.javaClass.simpleName
|
||||
if ("EAGAIN" in msg || "EWOULDBLOCK" in msg) null
|
||||
else ErrEvent(null, -1, -1, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/** cmsg_data = struct sock_extended_err + offender sockaddr_in (see OsAbi). */
|
||||
private fun parseSockExtendedErr(data: Any?): ErrEvent {
|
||||
val bytes: ByteArray = when (data) {
|
||||
is ByteArray -> data
|
||||
is ByteBuffer -> ByteArray(data.remaining()).also { data.duplicate().get(it) }
|
||||
else -> return ErrEvent(null, -1, -1, "cmsg_data is ${data?.javaClass?.name}")
|
||||
}
|
||||
if (bytes.size < OsAbi.SOCK_EE_SIZE) {
|
||||
return ErrEvent(null, -1, -1, "cmsg_data too short: ${bytes.size}")
|
||||
}
|
||||
val origin = bytes[4].toInt() and 0xFF
|
||||
val icmpType = bytes[5].toInt() and 0xFF
|
||||
// SO_EE_OFFENDER: sockaddr_in directly after the fixed struct; family is in native
|
||||
// byte order, sin_addr at offset +4 within the sockaddr.
|
||||
val offender = if (bytes.size >= OsAbi.SOCK_EE_SIZE + 8) {
|
||||
val family = ByteBuffer.wrap(bytes, OsAbi.SOCK_EE_SIZE, 2)
|
||||
.order(ByteOrder.nativeOrder()).short.toInt()
|
||||
if (family == OsConstants.AF_INET) {
|
||||
val a = bytes.copyOfRange(OsAbi.SOCK_EE_SIZE + 4, OsAbi.SOCK_EE_SIZE + 8)
|
||||
InetAddress.getByAddress(a).hostAddress
|
||||
} else null
|
||||
} else null
|
||||
return ErrEvent(offender, icmpType, origin)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun resolve(): ErrqueueApi? = runCatching {
|
||||
val msghdrCls = Class.forName("android.system.StructMsghdr")
|
||||
val cmsghdrCls = Class.forName("android.system.StructCmsghdr")
|
||||
ErrqueueApi(
|
||||
// Picked by shape, not by position: the 4-arg form is
|
||||
// (SocketAddress, ByteBuffer[], StructCmsghdr[], int) on every level that has it.
|
||||
msghdrCtor = msghdrCls.constructors.first { it.parameterCount == 4 },
|
||||
recvmsg = Os::class.java.getMethod(
|
||||
"recvmsg", FileDescriptor::class.java, msghdrCls, Int::class.javaPrimitiveType,
|
||||
),
|
||||
cmsgLevel = cmsghdrCls.getField("cmsg_level"),
|
||||
cmsgType = cmsghdrCls.getField("cmsg_type"),
|
||||
cmsgData = cmsghdrCls.getField("cmsg_data"),
|
||||
msgControl = msghdrCls.getField("msg_control"),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user