You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 94a9913
Browse filesBrowse the repository at this point in the historyBrowse files
Merge pre-users: what a first real user needs, and Nowhere v3.1.0
Closes#3.
Four first-day defects fixed, each with a device test that failed first;
QUIC falling back to TLS; Nowhere v3.1.0 and its Authenticated Prelude,
with the drift check watching the standalone specification too;
mainland-China rules on a rebuilt rule engine; base64 subscriptions; the
update check; the language on every Android version; docs/ux.md and the
documents brought up to date. Full gate green, live-Portal tests run,
99 vector checks, 70 of 70 on an API 35 emulator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+47-11Lines changed: 47 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,20 +19,22 @@ comments, everything. Do not emit Chinese in project files.
19
19
carriers, so the protocol layer now has two carriers: TLS, one connection per
20
20
flow, and QUIC, one connection for all of them, each with Morph on or off.
21
21
A bare `nowhere://key@host:port` is a TLS node with Morph on; `?net=udp` is
22
-
QUIC. The oracle differential agrees with v3.0.1 on every case over both
23
-
carriers and both Morph settings. What has *not* happened is a physical
22
+
QUIC. Morph TCP carries upstream's Authenticated Prelude (v3.1.0). The
23
+
oracle differential agrees with v3.1.0 on every case over both carriers and
24
+
both Morph settings. What has *not* happened is a physical
24
25
device: every device result in this repository is from an emulator. The
25
26
README's roadmap carries the stage view and the conformance row counts.
26
27
27
28
## Layout
28
29
29
30
| Path | Purpose |
30
31
|---|---|
31
-
|`app/`| The client. Kotlin, Compose, `minSdk` 26, `compileSdk` 36. Nine screens, three locales, and the protocol, TUN, DNS, routing and per-app layers |
32
+
|`app/`| The client. Kotlin, Compose, `minSdk` 26, `compileSdk` 36. Ten screens, three locales, and the protocol, TUN, DNS, routing and per-app layers |
32
33
|`app/src/main/jni/`| Vendored lwIP and its JNI bridge, inherited from the donor, plus the QUIC bridge and the library's export map |
33
34
|`tools/quic/`| The QUIC stack's pinned commits and the script that fetches and builds them. Not vendored (D-17); the conformance probe reads the same pin |
|`app/src/main/assets/rules/`| The bundled rule set (D-14), network-structural tier only, both address families, carrying a provenance header |
36
+
|`app/src/main/assets/rules/`| The bundled rule sets, each carrying a provenance header: the network-structural tier at the top level (D-14), and `policy/mainland-china.list`, the one policy set (D-18), generated — never edited |
37
+
|`tools/rules/`| The mainland-China set's generator and its inputs, pinned by URL and checksum in `SOURCES`|
36
38
|`docs/`| Architecture, the design system, i18n, brand, privacy, store policy, and the ADRs: TLS exporter, QUIC certificate verification, Morph |
37
39
38
40
The lwIP layer is inherited from `NodePassProject/Anywhere-Android` (GPL-3.0) at
@@ -50,7 +52,8 @@ it goes wrong.
50
52
## Conventions
51
53
52
54
- Every protocol implementation detail must be traceable to a specific section of
53
-
the upstream `docs/protocol.md`.
55
+
the Core specification (`Nowhere-Protocol``PROTOCOL.md`, standalone since
56
+
v3.1.0) or of a Nowhere extension it uses (`docs/prelude.md`).
54
57
- New protocol code lands with a matching known-answer or behavioural case.
55
58
Protocol-layer coverage gate: 90%.
56
59
- Keep `conformance/PROTOCOL_BASELINE` current: upstream commit plus the KAT
|`ktlintCheck`| Style. `@Composable` PascalCase is exempted through ktlint's own `.editorconfig` switch, not suppressed |
82
-
|`koverVerifyDebug`|**90% line coverage on `eu.nodepass.somewhere.protocol.*`, `.subscription.*`, `.dns.*`, `.apps.*`and `.routing.*`** — the last three joined the protocol layer's gate because they are the same kind of code: a wrong answer is wrong silently, and in `routing` it is wrong in the worst direction, with traffic leaving the device somewhere the user did not ask it to. Kover 0.9 filters only at report level, so the report *is* the protocol layer — deliberately, because one number applied to the whole app just gets gamed with trivial tests |
85
+
|`koverVerifyDebug`|**90% line coverage on `eu.nodepass.somewhere.protocol.*`, `.subscription.*`, `.dns.*`, `.apps.*`, `.routing.*`and `.nodes.*`** — the last four joined the protocol layer's gate because they are the same kind of code: a wrong answer is wrong silently, and in `routing` it is wrong in the worst direction, with traffic leaving the device somewhere the user did not ask it to. Kover 0.9 filters only at report level, so the report *is* the protocol layer — deliberately, because one number applied to the whole app just gets gamed with trivial tests |
83
86
|`lintDebug`| Android lint |
84
87
|`checkClasspathConsistency`|**Compile and runtime resolving different versions of the same module.** Added after `FlowRow` compiled against `foundation-layout` 1.7.2 and shipped against 1.9.2: the signature differed, so the screen crashed with `NoSuchMethodError` the first time it was opened, with the build, ktlint, lint and 253 tests all green. Re-running the gate against that BOM reports **29** skewed modules, not one — the crash we hit was one of many latent ones |
85
88
|`verify-vectors.py`| 86 known-answer checks, recomputed from the spec prose with a stdlib-only ChaCha20 checked against RFC 8439 first. References no upstream code and needs no clone |
86
89
|`checkNativeLibraries`|**What the shipped `.so` files export, how they are aligned, and what they need.** Reads the APK rather than a build intermediate. Verified to fail: removing the version script reports 1,684 non-JNI exports on arm64-v8a, starting with `AES_CMAC`|
87
90
|`checkNativeBridge`|**That the QUIC bridge cannot open a socket.** A source rule over `app/src/main/jni/quic/`: no `socket`, `bind`, `connect`, `sendto` or `recvfrom`. Addresses and packets cross the JNI boundary as bytes, so there is no path by which a datagram escapes `VpnService.protect()`, and no way for one to grow later without the gate saying so |
88
91
|`checkReleaseArtifact`|**What R8 left in the APK that ships.** R8 renames the JNI callbacks C resolves by name, and the failure is not a crash but a tunnel that comes up and answers nothing. The gate reads the artifact rather than running instrumentation against a second R8 output — two R8 runs produce two name sets, so that test would be checking a build nobody ships |
89
-
|`drift-check.sh`| Upstream normative change. Runs daily in CI and opens an issue; see `conformance/PROTOCOL_BASELINE` for when the pin is held and when it moves — it fired on 2026-08-26 (pin to v1.8.2) and on 2026-10-21 for v3, the first change to reach the wire (pin to v3.0.1, D-21) |
92
+
|`drift-check.sh`| Upstream normative change, in both the implementation repository and the standalone specification (`Nowhere-Protocol`). Runs daily in CI and opens an issue; see `conformance/PROTOCOL_BASELINE` for when the pin is held and when it moves — it fired on 2026-08-26 (pin to v1.8.2), on 2026-10-21 for v3, the first change to reach the wire (pin to v3.0.1, D-21), and on 2026-10-22 for v3.1.0, the Prelude Extension (pin to v3.1.0, issue #3) |
90
93
91
94
Both gates that can silently pass were verified to actually fail. The coverage
92
95
gate: a protocol class with no tests produces `lines covered percentage is
@@ -121,12 +124,15 @@ New protocol code lands with a matching known-answer or behavioural case.
121
124
122
125
## Design and language
123
126
124
-
Two documents in `docs/` carry the UI decisions. Both exist because nothing
127
+
The documents in `docs/`that carry the UI decisions exist because nothing
125
128
crashes when they are violated:
126
129
127
130
-[`docs/design-system.md`](docs/design-system.md) — colour tokens for both
128
131
themes with measured contrast, type, controls, and the four rules the UI has to
129
132
keep. Copy values from there rather than from a mockup.
133
+
-[`docs/ux.md`](docs/ux.md) — information architecture (which tab a feature
134
+
belongs on), theme modes, screen states, interaction, accessibility and
135
+
writing rules.
130
136
-[`docs/i18n.md`](docs/i18n.md) — English, Simplified and Traditional Chinese,
131
137
matched by script (`values-b+zh+Hans`) rather than by region.
132
138
-[`docs/brand.md`](docs/brand.md) — the brand hue, which carries no protocol
@@ -404,22 +410,52 @@ Two rules from those documents that reach into protocol code:
404
410
Google APIs image; four were the device's. Exact counts belong on a host
405
411
(`oracle-diff.sh`); on a device the assertion is "at least one per flow".
406
412
413
+
48.**A lookup by name on the main thread fails as "cannot be looked up".**
414
+
StrictMode refuses it with `NetworkOnMainThreadException`, whose message is
415
+
null, and the service once ran `startTunnel` there: every node named by host
416
+
failed on a device, logged as `cannot resolve <host>: null`, while every
417
+
emulator suite passed because it used addresses. Tunnels start on the
418
+
service's control thread. `FailoverOnDeviceTest` resolves `localhost` to
419
+
keep it so.
420
+
49.**`connectedAndroidTest` reinstalls the app, and that drops VPN consent.**
421
+
Granting it with `cmd appops` before the task does nothing; every tunnel
422
+
case then fails on consent. `e2e-android.sh` installs, grants, and runs
423
+
with `am instrument`.
424
+
50.**A 3.1 Portal answers a Morph bootstrap it cannot unmask with silence.**
425
+
A wrong key, or Morph on one side only, used to fail the TLS handshake; it
426
+
now holds the connection to its deadline and resets it. That is
427
+
`DialReason.Silent`, treated as unreachable so that an interfering path
428
+
still fails over.
429
+
51.**Unit tests that read files by path need those files declared as task
430
+
inputs.**`StringResourceTest` reads `src/main/res`; with nothing declared,
431
+
deleting a translation left `testDebugUnitTest` up to date and green. The
432
+
resources, assets, native sources and vectors are inputs in
433
+
`app/build.gradle.kts` now; a test that reads another directory adds it.
434
+
52.**The policy rule set cannot be loaded the way an import is.** Through
435
+
`RuleDocument.parse` and a tree of HashMaps it took 4.5 s and about 31 MiB
436
+
on an emulator, per connect. It is parsed once per process, only in Rules
437
+
mode, by a single-pass compiler that copies the generated text into one
438
+
packed byte array (about 0.6 s cold, 4-5 MiB). Keep the generator's output
439
+
sorted, lower-case and DIRECT-only, or the fast path declines and the slow
440
+
one returns.
441
+
407
442
## Already verified — do not redo
408
443
409
444
- The donor project `Anywhere-Android`**builds clean** on this toolchain
0 commit comments