Commit Graph
66 Commits
Author SHA1 Message Date
mrambossekandClaude Fable 5 7a94c9a3d7 chore: ignore the VSCodium Java extension's bin/ output
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 28s
server-release / release (push) Successful in 29s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 10:26:30 +02:00
mrambossekandClaude Fable 5 2521d39989 server: DF-mode big_send + uploaded-run storage with an operator policy
big_send now forces the Don't-Fragment bit for the whole burst by default, so
the largest size that arrives IS the downstream path MTU rather than "fragments
got through" — two different measurements the schema already separates. Sizes
above our own egress MTU (from the startup self-test) are refused up front and
reported as max_df_bytes, because absence caused by our kernel must not be read
as a limit of the client's path.

Uploads: one JSON file per run under the state dir, with the policy the operator
actually cares about — who may upload (off / anonymous / account), how large,
how long to keep, and the least anonymization accepted. The profile advertises
all of it so the app can present the switch honestly instead of discovering the
rules by failing. `account` refuses today rather than falling back to anonymous:
picking the strict setting before OIDC lands must not silently mean the loose one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 10:26:19 +02:00
mrambossekandClaude Opus 5 7e1015c211 server: §3.4 asymmetric grants + downtrain and big_send actions
server-test / test (push) Successful in 29s
server-release / image (push) Successful in 34s
server-release / release (push) Successful in 29s
The grant is the keystone that makes server->client sends safe: created
only by an authenticated control-plane action, bound at creation to the
session's OBSERVED data-plane source (so it can never be aimed at a third
party), and bounded by bytes, average rate and expiry. Sends stop the
moment the budget runs out, so a buggy action cannot become a flood.

Two granted actions on top of it:
- downtrain: N packets at a given size/interval toward the client, with
  seq + send-timestamp in the payload — downstream loss/reorder/jitter,
  which an upstream-only train cannot measure.
- big_send: one datagram per requested size, echoing the intended size in
  the payload — downstream MTU / black-hole evidence the client cannot
  produce for itself (only the far end can emit a large packet toward it).

Tests cover the security properties: no grant without a verified
destination, client requests clamped to server limits, byte budget stops
sending exactly, expiry refuses, and the rate ceiling throttles a burst.
Capabilities gain downtrain + big-send.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:09:13 +02:00
mrambossekandClaude Opus 5 c75a9f5eb7 app: accurate Shizuku handoff — name the steps, add developer-options shortcut
Verified against Shizuku 13.6's manifest (pulled APK, aapt2 xmltree): its
wireless-debugging entry points (AdbPairingTutorialActivity,
AdbPairingService, StarterActivity) have no intent filters, so they are
not exported and cannot be launched externally; MainActivity answers only
MAIN/LAUNCHER with no deep link. Starting wireless debugging from another
app is therefore not possible, which is why the handoff lands on the
root-start screen.

Instead the hint now names the exact steps inside Shizuku ("Pairing", then
"Start"), and a second tap opens Developer options — that action IS public
and exported, and Wireless debugging has to be on before Shizuku's
wireless start works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:03:03 +02:00
mrambossekandClaude Opus 5 1d9e2063bf app: make the Shizuku banner actionable (open Shizuku / request permission)
A third-party app cannot start Shizuku — the wireless-debugging pairing
flow is privileged and lives in Shizuku's own app — so the banner
deep-links there when it is installed but stopped, and fires the
permission request directly when it is running but unauthorised. The hint
line says which.

Verified on-device together with the earlier UX work: progress bar showing
"test 4 of 8 · icmp.ping6 · ~33s left", Cancel beside the disabled Run
button, cutout-safe title, and the banner live-updating from
not-running to needs-permission via the binder listener.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:58:39 +02:00
mrambossekandClaude Opus 5 d7dda40e4e app: tell the user before the run when Shizuku is installed but not started
Distinguishes not-installed (say nothing — don't nag users who don't use
Shizuku) from installed-but-stopped (amber banner: start it to include
shell-tier tests), plus running-unauthorised and ready. Detection is
listener-based since pingBinder() only becomes truthful once
ShizukuProvider delivers the binder; a launch-time poll would show a false
"not running". Installed-vs-not needs the <queries> entry on Android 11+.

Verified on-device: with shizuku_server stopped, the banner shows before
pressing Run; the title also now clears the status bar/cutout after the
safeDrawingPadding fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:51:01 +02:00
mrambossekandClaude Opus 5 217818f7b3 app: progress bar with ETA, cancel button, and cutout-safe layout
- Probe.estimatedMs (measured per probe; timeout-bound ones dominate)
  drives a determinate progress bar and "test N of M · ~Xs left",
  including the Shizuku battery in the total.
- Cancel stops the run and shows the partial results as a normal document
  (findings + verdict over what was collected) but never uploads them.
- safeDrawingPadding() on the root column: Android 15 is edge-to-edge by
  default and the title was colliding with the status-bar clock and the
  camera cutout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:48:18 +02:00
mrambossekandClaude Opus 5 2e34463c0a app: router identification verified on-device — named a MikroTik RouterOS 7.23.2
The probe identified the LAN's IPv6 RA sender end to end from an
unprivileged app: EUI-64 MAC recovery (78:9A:18:54:B8:F9, matching the
Shizuku neighbor table) -> MikroTik by OUI, corroborated by the UPnP
device description (RouterOS/7.23.2, MikroTik Router) and reverse DNS
(router.hudelist.local). Cellular's RFC 7217 privacy RA source is
correctly reported as not-EUI-64 instead of guessed. The SSDP sweep also
inventoried a Synology DS1522+ and a Sky gateway — the raw material for
future LLDP/mDNS cross-matching.

Fixes found by running it: added the confirmed MikroTik OUI 78:9A:18 (+
other RouterBOARD ranges) and an elvis-operator bug that printed "no UPnP
response" alongside valid UPnP data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:42:06 +02:00
mrambossekandClaude Opus 5 38f8036252 app: link.ra_source router identification + brand icons + DEV build variant
link.ra_source answers "who advertises IPv6 here, and which box is it":
RA source per network, MAC recovered from the modified-EUI-64 link-local
(privacy addresses reported as such, not guessed), vendor via a curated
OUI table, UPnP/SSDP M-SEARCH for the gateway's server banner + device
description (manufacturer/model/friendly name), and reverse DNS. All SSDP
responders are recorded so a rogue RA sender that isn't the gateway can
still be matched; the MAC accompanies every identity source as the hook
for future LLDP/mDNS cross-matching. UI gains a "Router / IPv6 advertiser"
panel.

Icons: branding adaptive icon converted to vector drawables (+ PNG
mipmaps, monochrome layer). The debug build is now a separate app —
applicationIdSuffix .dev, label "Echolot DEV", DEV-badged icon — so it
installs alongside a production build and can't be confused with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:35:54 +02:00
mrambossekandClaude Opus 5 dc094d1631 app: autorun mode (unattended run + upload + auto-exit); IPv6 severity rework
IPv6: absence is no longer a defect. If the network never provisioned v6
(no global address, no ::/0 route) the finding is ipv6.not_offered at INFO
(green) — most networks are still IPv4-only. If v6 IS advertised but
doesn't work, it's ipv6.broken at MEDIUM (yellow), because half-working v6
stalls connections. Verified on-device: our LAN advertises a v6 default
route with no path, and now reports ipv6.broken.

Autorun: `am start ... --ez autorun true` runs the suite immediately,
POSTs the report to the collection endpoint, shows the result for 3s and
finishes the activity (stays open if the upload failed). receiver.py gains
POST /report + GET /reports + GET /report/<name>. Verified end to end: one
adb command, report retrieved over HTTP, app closed itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:27:04 +02:00
mrambossekandClaude Opus 5 483de5ca54 app: nat.stun_5780 — NAT mapping/filtering discovery, verified vs live server
Hand-rolled RFC 5389/5780 STUN client (stdlib only) that exercises the
server's stun-5780 capability: one socket, three binding requests
(primary, OTHER-ADDRESS alternate IP, CHANGE-REQUEST port) — the
comparison classifies NAT mapping and filtering behavior.

Verified on the OnePlus: local 10.13.102.124 -> mapped
178.191.120.247:53259 (behind_nat true), alternate address answered from
the server's second IP, mapping endpoint-independent, filtering
address/port-dependent. Finding nat.symmetric (medium) for the
P2P-hostile case.

Two real bugs found by running it: port preservation was misread as "no
NAT" (compare addresses, not ports), and an unbound socket reports the
wildcard local address (resolve via a throwaway connected socket).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:15:29 +02:00
mrambossekandClaude Opus 5 dd9ecf5032 app: Shizuku shell tier verified on-device — 7/7 via UserService
Ran on the OnePlus with Shizuku started: tiers.shizuku=true, test ok,
commands_ok 7/7, exec_path=UserService (dual-path executor picked the
right path for this device). Evidence includes the live neighbor table,
per-table v6 routes, a real [NEIGH] netlink event, IpClient DHCP logs with
APF caps, and the wifi dump — as shell(2000).

Both v1 privilege tiers (app + shizuku) now verified end to end in the
production app on hardware. Report archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:06:24 +02:00
mrambossekandClaude Opus 5 59ba1c16bc app: dns.canary probe — client half of the canary measurement, verified live
Resolves the server's canary zone through the platform resolver and
compares against the spec-frozen ground truth (probe-protocol §6.1):
reference records detect answers rewritten in flight, and a per-run nonce
name (uncacheable) proves the query reached the authoritative server.
Findings: dns.answer_rewritten (high), dns.authoritative_unreachable
(medium).

Verified on the OnePlus against the deployed fmr zone: 4/4 reference
records matched exactly, nonce name answered 192.0.2.21 with
reached_authoritative=true. First full client<->server measurement loop
on real hardware; report archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:02:43 +02:00
mrambossekandClaude Opus 5 cf5cd2dc68 CLAUDE.md: record the beacon's inherent mDNS noise limitation
Network churn (SSID jump/roam) makes adbd re-publish its advertisement
repeatedly; each resolve re-arms the connection and posts a notification,
so the resolve-once guard can't fully prevent spam on the OnePlus. Noted
the alternatives (manual port, or Shizuku `ss` with no mDNS involved).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 08:55:44 +02:00
mrambossekandClaude Opus 5 68a6bcb9e3 CLAUDE.md: record the beacon's resolve-once rule and verified rotation behavior
Re-resolving adbd's own mDNS advertisement is what caused the notification
spam; resolving once per service instance (guard cleared on loss) keeps
rotation tracking intact — verified live, a 37089->33667 rotation was
reported 6s later and reconnected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 08:50:48 +02:00
mrambossekandClaude Opus 5 b751ca771e app: net.captive_portal verified on-device; archive first app run report
OnePlus 15 run: Android's generate_204 logic reproduced correctly —
default+wifi 204/204 -> validated, cellular -1/-1 -> no_internet (a
per-network asymmetry the OS itself hides). Shizuku tier degraded
correctly to UNSUPPORTED with tiers.shizuku=false since Shizuku isn't
running there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:17:35 +02:00
mrambossekandClaude Opus 5 4c51bd2aad beacon: stop causing "wireless debugging connected" notification spam
Root cause of the spam the user kept seeing: the service resolved adbd's
own mDNS advertisement repeatedly (every discovery callback, plus a 20s
heartbeat). Resolving that service makes adbd re-arm the connection, and
Android posts a "wireless debugging connected" notification each time —
so the beacon itself was the noise source, independent of the PC-side
connector loops.

Now: resolve each discovered service instance exactly ONCE (guard set,
cleared on onServiceLost so a genuine rotation re-resolves once), and the
heartbeat only re-POSTs the cached port (60s, no mDNS traffic).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:12:14 +02:00
mrambossekandClaude Opus 5 ee4031b086 tools: beacon receiver can serve a staged APK on /apk (443)
A test device that can only reach fmr on 443 (LAN blocks other outbound
ports) can pull an APK via its own downloader — more resilient than adb's
sustained transport over flaky wifi. GET /apk serves APK_PATH. (Doesn't
help the Lenovo tablet, which ships no curl; kept for devices that do.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:08:09 +02:00
mrambossekandClaude Opus 5 6e269d424b app: net.captive_portal probe — reproduce Android's internet/portal checks
Mirrors NetworkMonitor: per active network, fetch the AOSP default
generate_204 endpoints and check for HTTP 204 No Content.
- HTTPS https://www.google.com/generate_204 == 204 -> validated internet
- HTTP http://connectivitycheck.gstatic.com/generate_204: 204 -> clean;
  an unfollowed 3xx or a 200-with-body -> captive portal (Location captured)
- both fail -> no_internet
Per-network verdicts (bound via Network.openConnection), redirects not
followed (the 3xx IS the evidence). Findings: captive_portal (medium) and
no_internet (high). New test type net.captive_portal (net family ->
connectivity category). App gains usesCleartextTraffic (a network
diagnostic that intentionally probes plain HTTP).

Builds; measurement verdict tests still green. On-device verification
deferred with the rest (flaky test devices).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:01:22 +02:00
mrambossekandClaude Opus 5 aaed22dd3f app: core-shizuku — dual-path shell-tier executor + probe, wired into the app
Ports the prober's validated Shizuku tier: AIDL UserService, the build-4
dual-path ShizukuRunner (UserService bind where it works, legacy
newProcess reflection fallback where it doesn't — exec_path records
which), and ShizukuProbe running the shell command battery, emitting a
shizuku-tier link.ip_monitor Test with per-device dumps as evidence.
Self-degrades to UNSUPPORTED without Shizuku.

Wired into RunViewModel (sets tiers.shizuku); app APK assembles. On-device
verification deferred — no device reachable at build time (flaky LAN
dropped the tablet, phone debugging off). Expect UserService on OnePlus,
newProcess on Lenovo per the prober.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:37:48 +02:00
mrambossekandClaude Opus 5 9809ae57b4 tools: connector must not chase port rotation on a live connection
The connector tore down a working adb link whenever the beacon reported a
new port, reconnecting every loop and spamming the phone with "wireless
debugging connected" notifications (~every 8s). Existing connections
survive rotation, so now: if any device-state entry exists for the IP,
leave it; only (dis)connect when there's no working link at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:29:03 +02:00
mrambossekandClaude Opus 5 9bb3ac9df4 tools: version the beacon PC-connector; confirm Shizuku-toggle recovery
connect.sh polls the fmr beacon map and keeps `adb connect` current for
every device, now handling offline/stale entries and port changes
(disconnect+reconnect). Verified end to end: after Shizuku start + a
wireless-debugging toggle, the tablet's port rotated 38309->46667, the
beacon caught it, the connector reconnected, and Shizuku kept running as
an independent shell(2000) process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:26:09 +02:00
mrambossekandClaude Opus 5 96422dd58c CLAUDE.md: document the beacon + the Shizuku-kills-adb finding
Verified live: starting Shizuku (non-root, via wireless debugging) hijacks
the debug channel — adb drops and adbd advertises a stale mDNS port, so the
beacon can't auto-recover. Workaround: toggle wireless debugging off/on
after starting Shizuku (it keeps running). Baked into the dev notes so the
Shizuku-tier build loop plans around it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:23:58 +02:00
mrambossekandClaude Opus 5 16836ea7b1 build-status: production app verified on both devices via beacon-managed adb
Overall YELLOW on OnePlus 15 (A16) + Lenovo TB330FU (A15), driven by the
real broken-LAN IPv6 finding — full probe→schema→verdict→UI vertical on
hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:16:23 +02:00
mrambossekandClaude Opus 5 7b9b312dcd beacon: own-IP filter (shared-LAN fix) + wireless-debugging-off warning
Live multi-device test exposed two things:
- On a shared LAN, NsdManager discovers EVERY device's
  _adb-tls-connect._tcp advertisement, so a phone reported the tablet's
  port for its own IP (crossed). Now only accept the resolved service
  whose host matches this device's own wlan0 IP.
- When Wireless debugging is turned off, adbd drops its mDNS
  advertisement (onServiceLost) — the app now says so plainly in the
  status line and the ongoing notification ("Wireless debugging appears
  OFF — re-enable it"), instead of a vague "waiting".

Verified with phone + tablet on the same LAN: correct per-device ports,
both auto-connected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:13:13 +02:00
mrambossekandClaude Opus 5 38d4440412 tools: beacon fixes — cleartext, multi-device, validated-net POST, status URL
Debugging against a real restricted LAN surfaced three fixes:
- Android blocks app cleartext HTTP by default (targetSdk 36) — the port
  discovery + reachability were fine (phone curl reached fmr), only the
  app POST was denied. Added usesCleartextTraffic for this dev tool.
- Multi-device: report + receiver are keyed by device (Build.MODEL) so a
  phone and tablet don't clobber each other; connector connects each.
- POST over a VALIDATED internet network (prefer cellular) since the
  wireless-debug wifi is often a restricted LAN.
- Status/notification now show the target beacon URL + which network, per
  the request to surface what it's connecting to.
Verified live: beacon tracks the (frequently rotating) port via mDNS and
self-reports the current endpoint within seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:07:35 +02:00
mrambossekandClaude Opus 5 52748ac853 tools: wireless-adb beacon — self-healing bridge across drops + port rotation
The phone's wireless-debug port rotates and the bridge drops; this makes
adb reconnect automatically. Three pieces:
- adb-beacon (Android dev app): reads adbd's own mDNS advertisement
  (_adb-tls-connect._tcp) via NsdManager for the live connect port — no
  root, no Shizuku — plus the wlan0 IPv4, and POSTs {ip,port} to fmr every
  time it changes (continuous NSD discovery catches rotation in seconds).
  Foreground service (specialUse) so it survives backgrounding.
- tools/adb-beacon/receiver.py: ~30-line rendezvous on fmr:9099 (secret-
  gated POST stores the latest endpoint; GET returns it). Deployed as
  echolot-adb-beacon.service.
- PC connector polls the endpoint and keeps `adb connect` current.

Dev tooling, separate from the product. Bootstrap: sideload the beacon
APK once (no adb needed); thereafter adb self-heals for everything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:50:33 +02:00
mrambossekandClaude Opus 5 bc220f950e app: installable APK — core-probe (device-tier) + Compose UI
First assembling build of the production app. Android toolchain mirrors
the prober (AGP 9 built-in Kotlin; applying kotlin.android too
double-registers the kotlin extension — the one gotcha).

core-probe (Android lib): Probe→core-measurement Test abstraction;
NetworkInventory (LinkProperties→networks[]), LinkSnapshotProbe,
per-network IcmpProbe (ported from the prober's validated logic).

app (Compose): RunViewModel orchestrates probes into a MeasurementDocument
with a §7.3 summary + first-pass findings; UI renders traffic lights,
networks, tests, findings; JSON export. Rotation-safe (ViewModel).
App-tier only; server-facing (core-engine) + Shizuku are additive
follow-ups. Debug APK 9.5 MB, assembles clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:40:42 +02:00
mrambossekandClaude Opus 5 49c6197aff app: core-engine — run engine; full server-facing vertical proven vs fmr
Composes core-protocol probes into core-measurement documents. Injected
clock/UUID source keeps it pure and unit-testable. Runs a server ECHO
train and derives RTT distribution, loss, and NAT-rebinding detection
(from the server's observed source port) as train.udp_updown, then
findings + a §7.3 summary.

Verified end-to-end against fmr: 20-packet train, 0% loss, RTT
1.7/2.5/6.9ms, no rebinding → valid MeasurementDocument (2.3kB), overall
GREEN. The whole server-facing stack (protocol → engine → schema →
verdict) now produces the real product artifact against the live server,
no device required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:28:09 +02:00
mrambossekandClaude Opus 5 1f8860f7f8 app: core-measurement — the measurement-schema.md document model
Pure Kotlin/JVM, faithful to the schema contract: two-clock (wall RFC3339 +
*_mono_ns), units in field names, observation/interpretation split
(tests[] vs findings[]), columnar train evidence (nulls preserved per
index), the full v1 test-type registry, the anonymization logical types as
field notes, and a finding-requires-evidence invariant.

The one piece with real logic — §7.3 deterministic verdict derivation
(category = worst finding light; >50% failed/unsupported → inconclusive;
overall = worst category, inconclusive only if all are) — is implemented
in Verdicts and fully unit-tested. Document JSON round-trips (snake_case
wire names, null-in-columns), typed builders for train/traceroute/resolver
evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:24:20 +02:00
mrambossekandClaude Opus 5 3520eabd21 app: scaffold echolot-app + core-protocol — client spine verified live vs fmr
Multi-module Android app, built bottom-up from a verifiable core.
core-protocol is pure Kotlin/JVM (no Android SDK): SPKI-pinned control
plane (enroll/profile/session over HttpsURLConnection — API-1 compatible,
hostname verification off, trust is the pin), HKDF-SHA256 session keys,
ELT1 UDP data plane (HMAC gate, ECHO+observation, MTU probe) —
byte-compatible with the Go server.

Unit tests incl. the RFC 5869 HKDF vector (key derivation provably matches
the server). LiveServerTest + scripts/test-fmr.sh prove the client
end-to-end against the deployed fmr server: profile (8 caps), session,
ECHO rtt~11ms with the observation block returning our observed NAT port,
MTU 1400->1400, observations. Live test self-skips without ECHOLOT_LIVE_*.

Two client bugs caught live: java.net.http hostname verification (→
HttpsURLConnection, also the Android-minSdk-26 choice) and ECHO padding
needed for the observation to survive anti-amplification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:16:41 +02:00
mrambossekandClaude Opus 5 b229eeb674 build-status: tls-echo/JA4 live on fmr — spec §4 complete
Cross-client verified (openssl vs python ssl yield distinct JA4s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:01:19 +02:00
mrambossekandClaude Opus 5 1472a86508 server: tls-echo — ClientHello capture + JA4 on the TCP-echo port (§4 complete)
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 28s
server-release / release (push) Successful in 28s
A connection opening with a TLS handshake (first byte 0x16) and ALPN
elt-echo gets the ClientHello it sent back raw (b64) and as a JA4
fingerprint (sec.clienthello_echo), then a TLS byte-echo; plain
connections are unchanged. One port, multiplexed by a timed peek:
plain echo is server-speaks-first, so a silent client (peek timeout) is
greeted, while a TLS client's immediate ClientHello (0x16) routes to the
TLS path — 500ms tolerates ~1s RTT before misdetection.

JA4 (FoxIO): full ClientHello parser (ciphers, extensions, ALPN,
supported_versions, sig algs) with GREASE exclusion; a_b_c fingerprint,
unit-tested for structure + GREASE invariance. Live-verified: elt-echo
negotiated, JA4 t13d1712eo computed, 1530-byte ClientHello returned.
Capability tls-echo. This completes spec §4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:59:44 +02:00
mrambossekandClaude Opus 5 8a854141c5 build-status: server self-test live; fmr proven good (sysctl+MTU clean)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:49:05 +02:00
mrambossekandClaude Opus 5 d5e15816b5 server: fix egress-MTU probe — connect the socket before reading IP_MTU
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 27s
IP_MTU getsockopt returns ENOTCONN on an unconnected socket; the v0.3.4
probe set IP_MTU_DISCOVER and Sendto but never Connect'd, so every probe
errored. UDP-connect (no handshake) pins the route so IP_MTU reflects the
path; switched to Write (two return values). Sysctl audit already flagged
the four real fmr issues in v0.3.4; this makes the MTU proof report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:46:47 +02:00
mrambossekandClaude Opus 5 4ae744aae5 server: self-test — sysctl audit + egress-MTU self-proof ("server proven good")
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 28s
A measurement server must prove its own host isn't distorting results:
- sysctl audit (/proc/sys): flags accept_ra on a static host, ICMP
  redirects, ICMP rate-limiting of the server's own errors, and disabled
  TCP options — each a measurement-fidelity hazard, with the "why".
- egress-MTU self-proof: DF PMTUD probe (IP_MTU_DISCOVER + getsockopt
  IP_MTU, no root — Linux-only, stub elsewhere) to external anchors. If the
  server's own uplink is below 1500, client MTU tests measure THIS server,
  so we say so.
Exposed at GET /admin/selftest (full report) and as server_selftest
{mtu_ok, sysctl_ok} in the profile so clients can trust or skip MTU tests.
Recommended deploy/99-echolot-sysctl.conf + README section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:44:38 +02:00
mrambossekandClaude Opus 5 c9e0d06ea2 server: MTU probe (MTU_PROBE/MTU_ACK) — path-MTU / black-hole measurement
server-release / image (push) Successful in 14s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 27s
Server ACKs each DF-flagged probe with a tiny MTU_ACK carrying the size it
received; the client binary-searches the path MTU. Non-amplifying by
construction. Tested.

Also records: v0.3.2 (http-echo + tls-reference) verified live on fmr, and
the finding that upstream trains are already observable via the
observations API (dedicated TRAIN_REPORT deferred — needs an
anti-amplification grant + columnar encoding).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:37:20 +02:00
mrambossekandClaude Opus 5 38fb73c34e server: HTTP echo + TLS reference (control-plane security measurements)
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 28s
- POST /v1/echo: returns the received request head + body (base64) and the
  observed TLS parameters (version, cipher, SNI, ALPN, resumed). The client
  diffs against what it sent to detect header injection/stripping,
  transparent proxying, or TLS interception (sec.http_echo). http-echo
  added to the capability set.
- GET /v1/tls-reference: the served leaf-first DER chain + pin, so the app
  can compare an out-of-band copy against its own handshake (sec.tls_reference).
  Always available, no auth — public handshake info.
- Optional CLEARTEXT http-echo listener (ECHOLOT_HTTP_ECHO_LISTEN, default
  off) exposing only /v1/echo for the plaintext-path tampering test.

Live-smoke-tested (HTTPS echo reflected an injected header + observed
TLS1.3; cleartext variant reports tls:none); httptest unit tests added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:34:03 +02:00
mrambossekandClaude Opus 5 379153219e build-status: canary DNS live on fmr — session attribution + 0x20 finding
Zone delegated + authoritative, verified via public recursion; per-session
nonce queries attributed in the observations API. First test caught
Google's 0x20 case randomization vs Cloudflare's plain case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:18:50 +02:00
mrambossekandClaude Opus 5 35baf70cdb server: canary DNS — authoritative zone with frozen §6.1 reference records
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 26s
server-release / release (push) Successful in 27s
Stdlib DNS responder (no external deps): parses single-question queries
with EDNS OPT (bufsize, DO, ECS), serves the spec's frozen reference
records (ttl-{5,60,3600,86400} A/AAAA/TXT, many-rr 8×A in order, big-txt
~1800B), and per-query <nonce>.<session>.<zone> answers in 192.0.2.0/24.
UDP truncation sets TC past 512 (or the EDNS bufsize); TCP never
truncates — the EDNS-bufsize / TCP-fallback test. Every query is logged
(qname, resolver, transport, EDNS, ECS, case) and surfaced per session
prefix in GET /v1/sessions/{id}/observations as dns_canary. Profile gains
canary_zone + the canary-dns capability when configured.

Wire format validated against an independent client (correct rcodes,
answer counts, TC behavior, full EDNS response); unit tests cover
references, truncation-vs-EDNS, logging, NXDOMAIN.

Versioning: patch-first convention recorded in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:08:30 +02:00
mrambossekandClaude Opus 5 4f5499198b build-status: server v0.3.0 live on fmr — STUN/TCP-echo/observations/actions verified
Deployed via self-update (first real run). External checks: stun-5780
advertised, STUN binding OK v4+v6 with OTHER-ADDRESS, TCP echo mss=1440
over IPv6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:56:22 +02:00
mrambossekandClaude Opus 5 7b676e666e server: STUN, TCP echo, observations API, delayed-echo + connect-back actions
server-test / test (push) Successful in 27s
server-release / image (push) Successful in 14s
server-release / release (push) Successful in 27s
- stun: RFC 5389 binding responder + RFC 5780 attributes (OTHER-ADDRESS,
  RESPONSE-ORIGIN, CHANGE-REQUEST) on a primary/alt-port socket grid per
  address; advertises stun-5780 with >=2 same-family addrs, else
  stun-basic. Unmodified framing for tooling interop. Tested.
- tcpecho: JSON greeting with observed src + TCP_INFO MSS/options
  (Linux getsockopt; zeroed elsewhere via build tags), then byte echo.
- session: per-packet UDP observations + connect-back results, ByID lookup.
- control: GET /v1/sessions/{id}/observations, POST .../actions
  (delayed_echo → DELAYED_ECHO at the observed data-plane source;
  connect_back → dial the control-plane source, record connected/refused/
  timeout+rtt). Capabilities computed from what is actually wired.
- config/main: comma-separated STUN listeners; all planes bind explicit
  addresses; graceful shutdown of the new listeners.

Full flow smoke-tested; go test green (stun binding/change-port,
dataplane wire format).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:53:36 +02:00
mrambossekandClaude Opus 5 507a8bfc1f build-status: production server v0.2.0 live on the fmr VM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:42:56 +02:00
mrambossekandClaude Opus 5 43e1ba778a server: multi-address listeners, env-file config, self-update timer + checksums
server-test / test (push) Successful in 24s
server-release / image (push) Successful in 5s
server-release / release (push) Successful in 26s
- Comma-separated ECHOLOT_{CONTROL,UDP,TCP}_LISTEN; one listener/socket per
  address. Explicit binds matter on multi-IP hosts (a wildcard would also
  claim the SSH-only management address) and per-address UDP sockets are
  the substrate stun-5780 needs.
- systemd unit reads /etc/echolot-server.env (seeded once, never
  overwritten); --install-systemd with --self-update-api also installs a
  daily randomized update timer that try-restarts the service.
- selfupdate: SHA256SUMS verification is now mandatory before the atomic
  replace (integrity, not authenticity — signing still TODO).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:40:36 +02:00
mrambossekandClaude Opus 5 cae3c00990 server: document deployment requirements — no proxy, no 80/443, and why
server-test / test (push) Successful in 24s
Ports table, host-IP/second-IP/canary-zone requirements, and the two
protocol properties (SPKI pinning, observed-source fidelity) that a
reverse proxy would break. Coexists with traefik by never touching its
ports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:54:52 +02:00
mrambossekandClaude Opus 5 b7658a6777 server: selfupdate must normalize namespaced tags; build-status: pipeline green
server-test / test (push) Successful in 24s
Release tags are server-v1.2.3 but binaries are stamped v1.2.3 — the raw
comparison would re-download the current version on every check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:38:08 +02:00
mrambossekandClaude Opus 5 ebcb012450 ci: registry login via REGISTRY_TOKEN secret — Actions token is rejected
server-release / image (push) Successful in 25s
server-release / release (push) Successful in 26s
docker was present on the echolot runner; the login failed because
Gitea's container registry does not accept the ephemeral Actions token.
Requires a PAT with package read/write scope as the REGISTRY_TOKEN repo
secret (REGISTRY_USER optional, defaults to the actor); fails with a
self-explanatory error when missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:33:01 +02:00
mrambossekandClaude Opus 5 234622549c ci: image job on the dedicated 'echolot' runner; public registry host default
server-release / image (push) Failing after 4s
server-release / release (push) Successful in 26s
compilesau-echolot (repo runner, label echolot) handles the docker image
build. Registry host defaults to git.rambossek.at (runner-internal
GITHUB_SERVER_URL is http://app:3000). Asset re-uploads on tag re-runs
warn instead of failing the completed release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:29:27 +02:00
mrambossekandClaude Opus 5 5f258a38b7 build-status: server-v0.1.0 shipped with assets; only the image job remains
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:17:16 +02:00
mrambossekandClaude Opus 5 33db502a76 ci: fix release-id extraction (greedy sed grabbed the wrong id) + re-run safety
server-release / release (push) Successful in 57s
server-release / image (push) Failing after 5s
The job log showed uploads 404ing: sed's greedy .* matched the LAST "id"
in the release JSON (a nested user id), not the release's. First-match
grep now. Release creation falls back to GET-by-tag on re-runs, and the
image job strips any scheme from the internal server URL (http://app:3000)
with an ECHOLOT_REGISTRY_HOST override for the public registry host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:15:46 +02:00
mrambossekandClaude Opus 5 725443c72c build-status: record pipeline shakeout state for the next session
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:02:21 +02:00
mrambossekandClaude Opus 5 8d15a318fb ci: split server release into independent binaries and image jobs
server-release / release (push) Failing after 40s
server-release / image (push) Failing after 5s
First tag run failed as one monolithic job. Binaries + Gitea release now
need only Go and curl (jq dropped too) and succeed on any runner; the
docker image build is a separate job that can fail without killing the
release self-update depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:00:23 +02:00
mrambossekandClaude Opus 5 9d4b41da7f CLAUDE.md: device-testing lessons for future sessions; ignore web/.wrangler
server-release / release (push) Failing after 41s
The wireless-adb findings, report-collection workflow, build-number
convention, per-device Shizuku matrix, and the JDK constraint now live in
the repo so a session on any machine starts with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:43:32 +02:00
mrambossekandClaude Opus 5 0c988df7e8 ci: split server test and release workflows — mixed paths+tags never fired
server-test / test (push) Successful in 1m3s
A push trigger that declares tags: stops matching branch pushes, and the
paths filter doesn't apply to tag pushes — so the combined workflow could
effectively never run. test-server.yml now covers branch pushes touching
server/; build-server.yml is tags-only (server-v*).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:41:25 +02:00
mrambossekandClaude Opus 5 676a4d1eb8 prober: build-4 tablet report — newProcess fallback works where UserService won't
exec_path confirms the Lenovo/A15 ran the whole battery via the legacy
Shizuku.newProcess path: 6/7 commands with real data, incl. Lenovo's
IpClient dump format (distinct from OnePlus's — the per-vendor parser
sample the prober exists to collect). Design consequence recorded:
core-shizuku needs a dual-path executor. ip_monitor times out under
newProcess; bind-wait shortening noted as future tuning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:34:10 +02:00
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
mrambossekandClaude Opus 5 0ed36d9103 prober build 3: raise Shizuku bind window for slow first spawn; archive build-2 reports
Build-2 matrix (OnePlus 15/A16 + Lenovo TB330FU/A15) delivered the big
answer: traceroute.udp4 reads real hops via pure-Kotlin MSG_ERRQUEUE on
BOTH devices — the C-over-JNI shim is dead for Android 15+. Per-network
ICMP works as designed (phone ping6: cellular-only with topology
evidence). mDNS meta-query confirmed broken on both devices while
concrete types find real services — production must enumerate types.
Network finding: hudeWLAN RAs advertise a v6 default route but no global
prefix — the LAN's IPv6 is half-broken (nice dogfood case).

Tablet Shizuku bound 0/7 (binder alive, permission granted): first-spawn
dex extraction on slow storage suspected — bind window now 25s + one
retry. versionCode 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:32:11 +02:00
mrambossekandClaude Opus 5 8a80026d49 server: Go skeleton — control plane, UDP data plane, Docker + systemd modes
Pure stdlib. Implements the spec's core: enrollment (single-use tokens),
profile (SPKI pin, only real capabilities advertised), sessions with the
§2.4 HKDF-SHA256 key schedule; UDP data plane with the 32-byte ELT1
header, 4-byte HMAC gate, 1024-wide anti-replay window, ECHO_RESP with
observation block, TIMESYNC, and the §3.4 anti-amplification cap. Wire
format has tests (roundtrip + silent-drop cases); enroll→profile→session
smoke-tested live.

Modes: container (autodetect /.dockerenv|/run/.containerenv|cgroup, or
--docker/ECHOLOT_DOCKER=1; config via ECHOLOT_* env; distroless image;
network_mode host required — Docker NAT would falsify observed sources)
and native (--install-systemd/--uninstall-systemd with a hardened unit,
opt-in --self-update from Gitea releases; refused in containers).

CI: tests on any server/ push; server-v* tags build+push the image to the
Gitea registry and attach linux amd64/arm64 binaries + SHA256SUMS to a
release — the artifact self-update consumes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:09:08 +02:00
mrambossekandClaude Opus 5 ee66648e3c prober: on-screen + in-report build number (versionCode, now 2)
Ascending integer bumped per deployed change; shown in the header and as
proberBuild in the JSON report so a report is attributable to an exact
build. buildConfig feature enabled for BuildConfig.VERSION_CODE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:03:50 +02:00
mrambossekandClaude Opus 5 e695fe7aee prober: per-network ICMP, real errqueue traceroute, sharper mDNS/ip-monitor
- IcmpProbe: attempt the echo on the default network AND each active
  network (Network.bindSocket + per-network DNS). Run 1's ping6 EAGAIN was
  topology (v6 only on cellular), not capability — now the report shows
  which networks carry which family instead of a bare ERROR.
- New traceroute.udp4 (TracerouteProbe): actual UDP traceroute reading
  ICMP time-exceeded via Os.recvmsg(MSG_ERRQUEUE) with cmsg parsing —
  reflection per repo convention (API surface exists ~34+). If this
  returns hops, the C-over-JNI shim is unnecessary on that device.
  sock_extended_err layout constants documented in OsAbi.
- MulticastProbe: 10 s window; meta-query PLUS concrete types
  (_http._tcp, _googlecast._tcp) — run 1 showed the meta-query alone
  returning 0 on a network with live services; capture names + failure
  codes as evidence.
- ShizukuProbe: ip monitor window 2s -> 5s, gateway ping in background to
  provoke a NEIGH transition instead of hoping for ambient churn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:59:43 +02:00
mrambossekandClaude Opus 5 81d13ebb08 prober: archive run 2 — all three report-driven fixes verified on device
multinetwork now SUPPORTED (wifi+cellular bound concurrently; the cellular
request surfaced a second dual-stack PDN), Shizuku battery 7/7 in 2.9s
(was 4/7 in 34s), rtt_ms locale-clean. Remaining probe improvements
(per-network ping6, longer mDNS/ip-monitor windows) noted in build-status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:50:13 +02:00
mrambossekandClaude Opus 5 4c61c9bba8 prober: fix three bugs exposed by the first device report (OnePlus 15/A16)
- AndroidManifest: add CHANGE_NETWORK_STATE — requestNetwork threw
  SecurityException, multinetwork.request_and_bind could never run.
- IcmpProbe: format rtt_ms with Locale.ROOT — Austrian locale produced
  "38,1" in the JSON report.
- ShizukuRunner/ShizukuProbe: bind the UserService once per battery
  (execBatch) instead of per command; the per-command bind/unbind raced
  Shizuku and 3/7 commands died on SHIZUKU_BIND_TIMEOUT.

Archive the report at echolot-prober/reports/, record findings in
build-status.md. Notable: errqueue path fully reachable on Android 16 —
the native shim may be unnecessary on modern devices.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:19:14 +02:00
mrambossekandClaude Opus 5 6c29d0c039 prober: survive configuration changes (rotation wiped a finished run)
Found on the OnePlus 15 (CPH2747): rotating after a run emptied the result
list — results lived in a MainActivity field and the run in lifecycleScope,
so a config change dropped collected results and cancelled remaining probes.
Both now live in ProberViewModel (viewModelScope + application context; no
probe needs an Activity). A lost run means a lost export, so this matters
for a collection tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:55:32 +02:00
mrambossekandClaude Opus 5 d3e35ecead Add branding: Focus mark, wordmark, banner, social preview
- assets/branding/: icon (SVG + 512px PNG for Gitea avatar), adaptive-icon
  foreground/background layers, path-only wordmark for dark/light grounds,
  1200x300 README banner, 1280x640 social preview (SVG + PNG)
- README: banner hero
- build-status: branding decision log (includes the pending website section
  from the parallel web/ session, interleaved in the same file)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:52:12 +02:00
mrambossekandClaude Opus 5 6ddf013dfe License the project; add root README, website, and parked release CI
- Code: GPL-3.0-or-later (LICENSE, SPDX headers on all .kt/.aidl).
  Specs in docs/: CC-BY-4.0 (docs/LICENSE). Rationale in build-status.md;
  server decided GPL (not AGPL).
- Root README for the public repo.
- web/: Cloudflare Worker site for echo-lot.app. /apk resolves the newest
  APK from the Gitea latest-release API at request time (edge-cached 5 min),
  so tagging a release is the only publish step. /fdroid, /source, and a
  manual DOWNLOAD_URL fallback are wrangler vars.
- .gitea/workflows/release.yml: tag-driven (v*) signed semver APK builds for
  the future production app in echolot-app/. Parked; the prober is
  deliberately not CI-built.
- Fix UserService.kt: drop the explicit secondary constructor that
  conflicted with the implicit primary (never compiled before — first
  local build caught it). Prober now builds: :app:assembleDebug OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:15:35 +02:00
mrambossekandClaude Opus 5 e3840f54fe Initial commit: capability prober + design docs
Monorepo root for Echolot. Contains the no-root capability prober
(Kotlin/Compose, app.echo_lot.prober) and the four design docs that act
as the contract for the production app and the Go server.

LICENSE is deliberately absent — still undecided, see docs/build-status.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 08:54:28 +02:00