Files
echolot/docs/build-status.md
T
mrambossekandClaude Opus 5 60989b67ef prober build 4: newProcess fallback when the Shizuku UserService won't bind
Build 3 settled it: the Lenovo TB330FU/A15 never spawns the UserService
(two 25s bind attempts, binder alive, permission granted). Build 4 falls
back to the legacy Shizuku.newProcess remote-process API via reflection
and records exec_path in the evidence — whether that path works per
device is itself the capability question core-shizuku needs answered.

Also archives both build-3 reports: phone 7/7 incl. provoked NEIGH
transitions in ip_monitor; traceroute.udp4 handled a silent hop ("*")
correctly on both devices.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:10:58 +02:00

11 KiB
Raw Blame History

Echolot — build status & next steps

Last updated: 2026-07-29.

Decided

  • Name Echolot; domain echo-lot.app; scheme echolot://; namespace app.echo_lot.* (hyphen→underscore; appIds/packages can't contain hyphens). Prober appId app.echo_lot.prober.
  • Stack: native Kotlin + Jetpack Compose, no Flutter.
  • Tiers: app (no root), shizuku in v1 (wireless-ADB pairing), root future.
  • License (decided 2026-07-30): all code GPL-3.0-or-later; the specs in docs/ CC-BY-4.0. Rationale: the moat is the no-root platform research, which is trivially liftable into a proprietary repackage — copyleft is the only option that prevents that, and the F-Droid / network-engineer audience reads GPL as a trust signal rather than friction. The specs go permissive on purpose: a wire protocol only becomes a standard if anyone can implement it. Not AGPL for the server — the ASP loophole it closes is speculative here, while blanket corporate AGPL bans would hit exactly the enterprise network teams most likely to self-host. Sole copyright holder, so relicensing the server to AGPL later stays possible. Open: substitute a real legal copyright holder for "Echolot contributors" in the SPDX headers.

Specs (in docs/, alongside this file)

  • feature-catalog-and-feasibility.md, measurement-schema.md, probe-protocol.md. Considered draft-complete and reviewed by the user.

Capability prober — DELIVERED (as zip, 2026-07-29)

Full Kotlin/Compose project scaffolded: app.echo_lot.prober, minSdk 26 / target+compile 35, AGP 8.7.3, Kotlin 2.0.21, Compose BOM 2024.10, Shizuku api+provider 13.1.5, kotlinx-serialization. Probes implemented: link.snapshot, icmp.ping4/6 (unprivileged ICMP datagram), sockopt.matrix (TTL/TOS/RECVERR/MTU_DISCOVER), trace.errqueue_reachable, multinetwork.request_and_bind, local.mdns_discover, peer.ble_advertise, shizuku.command_battery (ip neigh / ip -6 route / ip addr / ip monitor / dumpsys network_stack DHCP+IpClient / dumpsys wifi). JSON export via share intent; results carry verdict + raw evidence.

Could NOT be compiled in the cloud sandbox: dl.google.com (Google Maven) and services.gradle.org are proxy-blocked, and no device is reachable for on-device runs. Build + iterate locally (Android Studio / Claude Code). Wrapper is pinned to Gradle 8.14.3.

Website — web/ (2026-07-30, LIVE at https://echo-lot.app)

Single-page site for echo-lot.app on Cloudflare Workers: static public/ from the edge, a small Worker for the stable short URLs (/apk, /apk.sha256, /fdroid, /source) and /api/latest. Version info is resolved from the Gitea releases API at request time (edge-cached 5 min), so tagging a release is the only publish step — the homepage shows the latest version/date/size via progressive-enhancement JS and falls back to pre-release copy otherwise. Auto light/dark via prefers-color-scheme: dark = sonar display, light = chart-recorder printout, same amber-accent token system. Deploy: npx wrangler deploy from web/, or CI via .gitea/workflows/deploy-site.yml (needs CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID secrets); full story in web/README.md. Branding applied (2026-07-30): real favicon/wordmark copied into web/public/assets/ (copies — assets/branding/ is the source of truth), palette switched to abyss/teal/amber with teal-as-instrument semantics, sonar-sweep hero replaced by a Focus-lattice panel (rogue-DHCP finding vignette), tagline "measure, don't guess".

Branding — assets/branding/ (2026-07-30)

Identity chosen: the Focus mark — a quiet lattice of nodes with exactly one under examination (amber) inside teal viewfinder brackets. Tone is surgical measurement, not scanning (sonar-sweep visuals and the "sound out your network" tagline were explicitly rejected); tagline is "measure, don't guess". Palette: abyss #071522, tile #0E2433, instrument teal #35E0C4, finding amber #FFB454, foam #E8F4F2 — teal is always the instrument, the single amber point is the finding. The wordmark is hand-drawn monoline SVG paths (zero font dependency); the second "o" is the signature teal ring + amber ping. Assets: icon.svg (+ icon.png 512² — Gitea avatars reject SVG), icon-adaptive-{foreground,background}.svg (108dp adaptive-icon layers, art inside the 66dp safe circle), wordmark-on-{dark,light}.svg, banner.svg (1200×300, wired into the repo README), social-preview.svg/.png (1280×640, for the Gitea/GitHub social-preview slot). The website's placeholder favicon/wordmark can now be replaced from these.

First device report — OnePlus 15, Android 16/SDK 36 (2026-07-30)

Archived at echolot-prober/reports/CPH2747-android16-sdk36.json. Collection workflow that works: deploy over (wireless) adb, but the user starts the run and exports the JSON manually — probe runs reliably kill the adb-over-wifi session (multinetwork/multicast churn the link adb rides on).

OS findings:

  • link.snapshot, icmp.ping4 (38 ms), sockopt.matrix 4/4, peer.ble_advertise: SUPPORTED.
  • trace.errqueue_reachable SUPPORTED — full Os-API errqueue path exists on Android 16; the C-over-JNI shim (next-steps #2) may be unnecessary on modern devices. Still needs a real errqueue round-trip test, and older devices may differ.
  • icmp.ping6 EAGAIN — topology, not capability: wifi was v4-only, v6 only on cellular, socket used the default network. Probe should bind per-network (also covers the ping4 case properly).
  • shizuku.command_battery SUPPORTED, shell(2000): real ip -6 route RA data (lifetimes, per-table), ip monitor NEIGH events, wifi dump with full DhcpResults. Vendor formats look parseable.
  • local.mdns_discover: lock acquired, discovery ran, 0 services in the 4 s window — needs a longer window / known-good reference network before calling it more than formally SUPPORTED.

Prober bugs the run exposed (all fixed same day):

  • rotation wiped results + cancelled runs → state moved to a ViewModel;
  • multinetwork.request_and_bind ERROR → missing CHANGE_NETWORK_STATE in the manifest;
  • rtt_ms: "38,1" → device locale leaked into report numbers, now Locale.ROOT;
  • 3/7 Shizuku commands SHIZUKU_BIND_TIMEOUT → bind/unbind-per-command raced Shizuku; now one bind per battery (execBatch). Run 2 (same device, fixed APK — …-run2.json) confirmed all three fixes:
  • multinetwork.request_and_bind SUPPORTED — wifi + cellular acquired and bound concurrently (downKbps 39094 / 13231); requesting cellular even brought up a second, dual-stack PDN (rmnet_data2, private v4 /30 + carrier-NAT DNS) next to the IPv6-only default PDN — exactly the per-network behavior the production app needs.
  • Shizuku battery 7/7 in 2.9 s (was 4/7 in 34 s): full ARP/NDP neighbor table, ip addr, IpClient ProvisioningConfiguration incl. SSID/BSSID and APF caps (v6000, 4096 B).
  • rtt_ms: "32.0" — locale fix holds. Still open (probe improvements, not blockers): ping6 should bind per-network (still EAGAIN via v4-only default wifi); mDNS window too short to see real services (0 found on a network that has them); ip monitor 2 s window caught nothing on a quiet net — consider longer/event-triggered capture; IP_MTU read via getsockoptInt reflection unavailable — reader belongs in the errqueue shim if that lands.

Go server skeleton — server/ (2026-07-30)

Pure-Go stdlib implementation of the spec's core: control plane (enroll with single-use tokens, profile with SPKI pin, sessions with the §2.4 HKDF key schedule) + UDP data plane (ELT1 header, HMAC gate, 1024-wide anti-replay, ECHO_RESP with observation block, TIMESYNC, §3.4 anti-amplification). Wire format covered by go test (roundtrip, replay/bad-HMAC/unknown-prefix silent drops); full enroll→profile→session flow smoke-tested live. Two run modes: container (autodetected via /.dockerenv etc. or --docker; config from ECHOLOT_* env; distroless image, network_mode: host required — Docker NAT would falsify the observed sources the protocol measures) and native (--install-systemd/--uninstall-systemd, opt-in --self-update against a Gitea releases API). CI: .gitea/workflows/build-server.yml — tests on push; server-v* tags push the image to the Gitea registry + attach linux amd64/arm64 binaries that self-update consumes. Not yet: TCP/TLS echo, STUN, canary DNS, actions, observations API, real admin UI, self-update signature verification.

Prober build numbering: ascending versionCode shown on screen + as proberBuild in the JSON report; current deployed build = 3. Bump on every deployed change.

Build-2 reports: OnePlus 15 (A16) + Lenovo TB330FU (A15) — 2026-07-30

Archived as …-build2.json in echolot-prober/reports/. The findings:

  • traceroute.udp4 SUPPORTED on BOTH devices (SDK 35 + 36): 6 real hops via pure-Kotlin Os.recvmsg(MSG_ERRQUEUE) in ~250 ms, identical paths (A1 → Cloudflare). The C-over-JNI errqueue shim is dead for Android 15+; keep it in mind only if pre-15 devices matter.
  • Per-network ICMP works as designed: phone ping6 = "cellular only, default has no v6 path" (SUPPORTED with topology evidence instead of the old bare ERROR); per-network RTTs (wifi 20 ms vs cellular 201 ms on v4).
  • Network finding (dogfood!): hudeWLAN advertises a v6 default route via RA but hands out no global v6 address — both devices have only link-local + a ::/0 route on wifi. That's a half-broken IPv6 config on the LAN, exactly the class of issue the product should flag.
  • mDNS: meta-query returns 0 on BOTH devices (NsdManager limitation, now confirmed twice); concrete _http._tcp finds 4 real services (3× "Magic 2 LAN", EPSON WF-7840) on both. Production inventory must enumerate concrete types, not trust the meta-query.
  • Tablet Shizuku: binder alive, permission granted, but UserService bind timed out (0/7) — first-spawn dex extraction on slow storage suspected; build 3 raises the bind window to 25 s + one retry. Phone stays 7/7 with rich neighbor/RA/DHCP evidence. Build-3 verdict: not timing. Both 25 s attempts timed out (50 s total) — the UserService spawn genuinely fails on the Lenovo/A15. Build 4 adds a reflection fallback to the legacy Shizuku.newProcess remote-process API when the bind fails; exec_path in the evidence says which path ran. Whatever the outcome, core-shizuku must not assume UserService works everywhere. (Phone build 3: still 7/7; ip_monitor now catches provoked NEIGH PROBE→REACHABLE transitions, and a mid-path router dropping one TTL round showed the "*" hop path works in traceroute.udp4.)
  • ip_monitor returned no events this run even with the provoked gateway ping (gateway was already REACHABLE, so no NEIGH transition happened). Evidence-dependent, not a bug.

Next steps

  1. Build locally, run on several physical devices (varied Android versions/vendors), collect the JSON reports — especially the real per-device Shizuku dump formats.
  2. If trace.errqueue_reachable = PARTIAL, add a C-over-JNI errqueue shim (recvmsg+cmsg parse) as a :native module and a real traceroute.udp4 probe.
  3. Start the Go server skeleton (enrollment + profile + sessions + UDP echo with observation blocks + canary-DNS reference records) per probe-protocol.md.
  4. Fold confirmed capabilities into the production core-probe / core-shizuku modules.