Skip to content

feat(client): host auto-discovery and proofed routing - #75

Open
Florious95 wants to merge 23 commits into
pr/foreground-resume-refreshfrom
pr/host-auto-discovery-client
Open

feat(client): host auto-discovery and proofed routing#75
Florious95 wants to merge 23 commits into
pr/foreground-resume-refreshfrom
pr/host-auto-discovery-client

Conversation

@Florious95

@Florious95 Florious95 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Goal

Implement the client side of bounded host auto-discovery from literal host records, NSD, and typed tsnet peers, proving an endpoint before WebSocket auth. This PR is based on frozen eaaa7d47d88e7e2de6c82988fe462e7adf29f86d and targets pr/foreground-resume-refresh; it does not touch server/**, merge, release, deploy, or push main.

Changes

  • Add literal IPv4 routing, host /pair/whoami and HMAC-bound /pair/identify seams, bounded discovery, proof-before-WS pairing, legacy exact-404 compatibility, and deterministic candidate proof before fallback dialing.
  • Keep TS peer discovery bounded and typed (PeerSnapshot(knownId,cursor)), with complete-table known-ID lookup and non-overlapping 256-row cursor pages; add Go behavior coverage for 9/33/>256 fairness.
  • Stage TS key changes while READY, apply only at the next connection generation, and disable stale state on an empty key.
  • Keep one session upload VM on the live LAN/tailnet base; add discovery, key-rotation, and live-upload behavior coverage.
  • Add a concrete HostIdentityVerifier test seam; production remains HostIdentifyClient with literal-IP/no-DNS/no-redirect guards.

Validation (Grok Bot)

  • Final exact source HEAD 381281b194bf663b92618c99d37f297d866b9164.
  • cd tools/tsnetbind && go test -count=1 -v ./...: PASS, 1 package / 5 tests, 0 failures, 1191 ms; /workspace/host-discovery-android-runner/logs/go-test-381281b1.log.
  • ANDROID_HOME=/home/box/Android/Sdk ANDROID_NDK_HOME=/home/box/Android/Sdk/ndk/27.2.12479018 bash tools/tsnetbind/build.sh: PASS for Go source revision 13f8b2f31da381a4bfcbefb21dd11dd13143f29e; artifact Git commit 51d4a08fe8e90d3e13bcf72fd619ee7a1538552d; AAR SHA-256 1bd5f924c1d53f477d33eee0a6c6b9d14a15786b7032e281f52b711cf2222dd7 (7,974,616 bytes), sources JAR SHA-256 6df63e789abd537912473def74b93d4af2eb164ac23f4b7abd29f069b5a13a0e (8,986 bytes); /workspace/host-discovery-android-runner/logs/aar-13f8b2f3.log.
  • Final AAR libgojni.so LOAD segments all report p_align=0x4000; verification receipt is in the evidence file.
  • ./gradlew --rerun-tasks -Pkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin: PASS, 23 tasks, 32,577 ms; /workspace/host-discovery-android-runner/logs/compile-381281b1-final.log.
  • Full affected inner loop with --rerun-tasks and -Pkotlin.compiler.execution.strategy=in-process: PASS, app testDebugUnitTest 171 tests / 0 failures plus core-conn:test 3 tests / 0 failures, 60,548 ms; /workspace/host-discovery-android-runner/logs/inner-381281b1.log. Exact filters and class set are recorded in EVIDENCE.md.

Risk / boundary

This is source/AAR/inner-JVM validation only. No device, NSD hardware path, real tailnet, paired server/client acceptance, instrumentation, performance A/B, signing, install-over, production process, or credential access is claimed. Architecture tooling remains unknown because its declared directory is absent. Final paired/device/performance acceptance remains a follow-up after both endpoint implementations are available.


Repair (identify HTTP 200 → WS READY) — execution evidence, not independent acceptance

Goal. After HTTP 200 /pair/identify, the App must HostIdentifyResult.Proven → one transport.create → AuthFrame → READY. Do not treat server 200 or a host Python replica as App Proven.

Root cause (device DiagLog, not guess). OkHttpHostHttpTransport.execute called Okio readByteArray(1025). Real identify JSON is ~163–177 bytes, so Okio threw EOFException. The App logged http_code=599 body_len=0 verdict=Rejected reason=identify rejected proved=0, exhausted the generation, and never connectTarget. Fixture/Darwin still saw HTTP 200. Probe SHA ae5e125dda43876891b8c94560c28195439c9108.

Fix. request(1025) then read remaining bytes; oversized bodies still fail closed. Common path only (OkHttpHostHttpTransport.execute). No HMAC/LocalAddr/host_id/nonce relaxation, no Proxy.NO_PROXY change. Fix SHA 1c9ed5d977a17f0d1ff0314340a030da8d9f2085 on pr/host-auto-discovery-client.

Tests (Grok Bot, --rerun-tasks, in-process Kotlin). pairing testDebugUnitTest 67 / 0 fail including OkHttpHostIdentifyBodyCapTest; core-conn:test 3 / 0 fail. Full :app:testDebugUnitTest 654 with 11 fails is not claimed as 171-green: failures are RemapThroughputTest (host path), ColdStartReconnectTest, ForegroundServiceWiringTest, DiagLogExportDirBoundedTest — outside this inner loop / environment. Original owning 171 list was not re-run as that exact filter.

Device (own emulator-5590, synthetic fixture :8765 and isolated Darwin :19918). Before: EOF 599, WS=0. After: Proven + connectTarget create=true + auth_frame + auth_ack ok=true ready=true on both handlers. DESIGN 17 OK (17 tests) 52.91s. Foreground: READY chrome 会话/设置/收藏, resume/rapid keep 会话, MISS 重连中; 快捷键/查看 MISS because discovery allowlist empty (前置未满足). AAR unchanged 1bd5f924c1d53f477d33eee0a6c6b9d14a15786b7032e281f52b711cf2222dd7. Debug APK sha256 d7e379289422c2e05f11ea88b753c32af85c01487d19ae90e41be1dd9451290a (40474947 bytes) from source 1c9ed5d.

This is owning-PR execution evidence. It is not a seal / independent acceptance PASS.


Issue 79 (cold-start / foreground gate)

#79not closed.

Two roots, not nine product bugs:

  1. Fixture (7 tests, 0 connections): seedConfig omitted schema_version=2, so load() treated prefs as v1 and turned on the identify coordinator. JVM fake transport never create()d. Current pairing save() writes schema 2. Tests now seed schema 2; user assertion unchanged (one connection on paired cold start).
  2. Product (2 tests): URL-only tailnet cold start must wait for tsnet Up/Error before first dial (eaaa). Restored whenSettled + start generation only when no host coordinator. HostId records still let the coordinator own LAN/TS timing. R2 key staging unchanged.

HEAD 72117404cc1c7624d474dfb92466d30fc24f34ee. Full :app:testDebugUnitTest 655 / 1 fail (RemapThroughputTest Issue 81 only). The nine Issue 79 names executed and passed. Issue 80/81 not fixed. EOF body-cap not reverted. Execution evidence, not independent acceptance.


Issue 82 (READY drop reconnect) — retained, not re-fixed here

#82not closed. Owning fix remains f53f91b6a3ac188b5cb84875f8510f6592b3af43. This Issue 83 commit re-ran CoordinatorReadyDropReconnectTest (4) and ForegroundResumeTest (4): 0 failures. Independent acceptance PASS on f53 is not overwritten.


Issue 83 (unbound skip → quiet empty workspace)

#83still OPEN. Not merged, not signed, not performance-gated.

Goal. New install / pm clear with no pairing record: tap 以后再说 must enter a quiet empty workspace, not endless 连接中 / 正在连接主机. Configured cold start must still dial. Healthy READY must not rebuild. Disconnect must still recover. Do not fake READY or relax identify.

Root cause. WorkspaceViewModel defaulted to CONNECTING. Unbound skip never calls startPersistentConnection, so no manager ever leaves CONNECTING. isLoading stayed true (LoadingContent + banner) and enterLevel1 hung refreshing. NSD is a separate apparatus gap.

Fix. Unbound initial state is ConnectionUi.UNBOUND (not READY, not STOPPED). Skip shows title + empty body, no spinner/copy. Configured cold start still CONNECTING and starts the manager. AAR unchanged.

HEAD. d99c02b15614691d56d013008d1410020bf20aa1 on pr/host-auto-discovery-client. Base still eaaa7d47. Ancestor of frozen f53.

Tests (Grok Bot, --rerun-tasks --no-build-cache, in-process Kotlin). START 2026-09-05T19:21:20Z END 19:22:19Z, 59s, exit 0. XML 34 tests / 0 failures / 6 classes: WorkspaceViewModelTest 12, WorkspaceUnboundSkipTest 2, ColdStartReconnectTest 9, CoordinatorReadyDropReconnectTest 4, ForegroundResumeTest 4, ConnReconnectPolicyTest 3. Issue 80/81 not run, not claimed, not fixed. Performance not run.

APK. debug app-debug.apk 40474947 B, SHA-256 48e88183e64d8e47ebd7b157e08cc5e2937b05c0be1a95c2dc7f9dd3a7599471 from source d99c02b1. assembleDebug 35s, 44 executed, exit 0.

Device (own emulator-5572, AVD empty-ws-api35-20260906). Did not use 5580/5590/8876/8765/19928/19918 or production 9900/10361.

  • RED f53 APK c35e2aba…: pm clear, prefs absent, skip; after 15s still 连接中 / 正在连接主机; XML SHA ebbdb3bf346d878db92c22abc8a15aef6817487459b2c529d46f325d0881a22a matches the Issue 83 field.
  • GREEN this APK 48e88183…: same steps; quiet empty 工作区, no 连接中 / 正在连接主机 / 暂无工作区; XML SHA 9e4745d324d5c30b3ff22982dfeb3d68fbf47f859cec7f39e24b3e04b491fa57 stable across immediate and +15s.

Execution evidence, not independent acceptance. Issue 80/81 remain FAIL. NSD discovery remains a separate case.

Alauda added 18 commits September 5, 2026 13:52
Log http_code/body_len/parse/host_id/bound/mac match booleans, generation
deadline/cancel, Proven, offer/emitted, transport.create, AuthFrame/READY.
No token or MAC values. Behavior unchanged.
Okio readByteArray(1025) throws EOFException on short /pair/identify JSON,
so the App mapped server HTTP 200 to 599 and never proved or created WS.
Cap with request(1025) then read remaining bytes. Regression talks to a
real short JSON responder on a literal LAN IPv4.
Alauda added 2 commits September 6, 2026 00:07
Refs #79. Current pairing save() writes schema_version=2. Tests that
omit it look like v1 prefs, so load() synthesizes legacyBootstrapUrl
and the identify coordinator never create()s the JVM fake transport.
Refs #79. URL-only (no host coordinator) tailnet targets must not dial
before Up/Error; Starting direct-dial can stick in RECONNECTING without
a clock pump. HostId records still let the coordinator own LAN/TS timing.
Alauda added 2 commits September 6, 2026 02:01
Issue 82: HostDialCoordinator.onReady cancels the round, so a later
non-permanent peer close must not stay on a dead READY socket.
HostDialCoordinator.onReady cancels the round. A later non-permanent
close must scheduleReconnect (new identify generation) instead of
onTargetFailed, which is a no-op once round is null. Pre-READY still
advances the next proven target. Refs #82
Unbound installs have no pairing record and never start a manager, so the
default CONNECTING workspace stayed on 连接中/正在连接主机. Keep configured
cold start on CONNECTING. Refs #83
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant