Skip to content

Commit 94a9913

Browse files
AlbertoDeckowclaude
andcommitted
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>
2 parents 8390f8b + 8ad60fa commit 94a9913

162 files changed

Lines changed: 129077 additions & 945 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/upstream-drift.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ jobs:
2020
- uses: actions/checkout@v7
2121

2222
- name: Clone upstream
23-
run: git clone --quiet https://github.com/NodePassProject/Nowhere.git upstream-nowhere
23+
run: |
24+
git clone --quiet https://github.com/NodePassProject/Nowhere.git upstream-nowhere
25+
git clone --quiet https://github.com/NodePassProject/Nowhere-Protocol.git upstream-nowhere-protocol
2426
2527
- name: Drift check
2628
id: drift
2729
run: |
2830
set +e
29-
OUT="$(bash conformance/scripts/drift-check.sh "$GITHUB_WORKSPACE/upstream-nowhere" 2>&1)"
31+
OUT="$(bash conformance/scripts/drift-check.sh "$GITHUB_WORKSPACE/upstream-nowhere" "$GITHUB_WORKSPACE/upstream-nowhere-protocol" 2>&1)"
3032
CODE=$?
3133
echo "$OUT"
3234
{

‎CLAUDE.md‎

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ comments, everything. Do not emit Chinese in project files.
1919
carriers, so the protocol layer now has two carriers: TLS, one connection per
2020
flow, and QUIC, one connection for all of them, each with Morph on or off.
2121
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
2425
device: every device result in this repository is from an emulator. The
2526
README's roadmap carries the stage view and the conformance row counts.
2627

2728
## Layout
2829

2930
| Path | Purpose |
3031
|---|---|
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 |
3233
| `app/src/main/jni/` | Vendored lwIP and its JNI bridge, inherited from the donor, plus the QUIC bridge and the library's export map |
3334
| `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 |
3435
| `conformance/` | Byte-level vectors, self-verifying checker, end-to-end smoke test, drift detection, test matrix |
35-
| `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` |
3638
| `docs/` | Architecture, the design system, i18n, brand, privacy, store policy, and the ADRs: TLS exporter, QUIC certificate verification, Morph |
3739

3840
The lwIP layer is inherited from `NodePassProject/Anywhere-Android` (GPL-3.0) at
@@ -50,7 +52,8 @@ it goes wrong.
5052
## Conventions
5153

5254
- 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`).
5457
- New protocol code lands with a matching known-answer or behavioural case.
5558
Protocol-layer coverage gate: 90%.
5659
- Keep `conformance/PROTOCOL_BASELINE` current: upstream commit plus the KAT
@@ -79,14 +82,14 @@ python3 conformance/scripts/verify-vectors.py
7982
| Gate | What it enforces |
8083
|---|---|
8184
| `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 |
8386
| `lintDebug` | Android lint |
8487
| `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 |
8588
| `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 |
8689
| `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` |
8790
| `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 |
8891
| `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) |
9093

9194
Both gates that can silently pass were verified to actually fail. The coverage
9295
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.
121124

122125
## Design and language
123126

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
125128
crashes when they are violated:
126129

127130
- [`docs/design-system.md`](docs/design-system.md) — colour tokens for both
128131
themes with measured contrast, type, controls, and the four rules the UI has to
129132
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.
130136
- [`docs/i18n.md`](docs/i18n.md) — English, Simplified and Traditional Chinese,
131137
matched by script (`values-b+zh+Hans`) rather than by region.
132138
- [`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:
404410
Google APIs image; four were the device's. Exact counts belong on a host
405411
(`oracle-diff.sh`); on a device the assertion is "at least one per flow".
406412

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+
407442
## Already verified — do not redo
408443

409444
- The donor project `Anywhere-Android` **builds clean** on this toolchain
410445
(JDK 21 / android-36 / NDK 28.2 / CMake 3.22.1), 1m50s cold, one harmless lwIP
411446
macro warning. Inheriting its shell is not a risk.
412-
- `cargo build --release --locked` succeeds on Linux x86_64 at Nowhere v3.0.1
447+
- `cargo build --release --locked` succeeds on Linux x86_64 at Nowhere v3.1.0
413448
(and did on macOS/aarch64 at v1.8.2).
414449
- **End-to-end works** over TLS and QUIC, Morph on: SOCKS5 → the oracle's
415450
Vector → Portal → target. Re-run `conformance/scripts/smoke-local.sh`.
416451
- **Authentication and Morph derivations agree three ways** — spec prose, an
417452
independent implementation, and upstream's fixed vectors, byte for byte.
418-
Re-run `python3 conformance/scripts/verify-vectors.py` (86 checks).
453+
Re-run `python3 conformance/scripts/verify-vectors.py` (99 checks, the
454+
Authenticated Prelude's four vectors among them).
419455
- **The dialer's certificate handling is verified against a live Portal**: the
420456
pin is the fingerprint the Portal itself logged, exported by
421457
`conformance/scripts/portal-for-tests.sh`, so a passing pin test cannot be the
422458
dialer agreeing with itself.
423-
- **The oracle differential passes against v3.0.1** on every case over TLS and
459+
- **The oracle differential passes against v3.1.0** on every case over TLS and
424460
QUIC, Morph on and off. Re-run `conformance/scripts/oracle-diff.sh`; it needs
425461
a host QUIC bridge (`conformance/scripts/build-host-quic.sh`, which needs Go).

‎CONTRIBUTING.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ conformance/scripts/portal-for-tests.sh --stop
4343
classpath gate and the design-rule suite were each broken on purpose once to
4444
confirm they notice.
4545

46+
## Translations
47+
48+
New languages are welcome. Three files change and a test says when they
49+
agree — see [Adding a language](docs/i18n.md#adding-a-language).
50+
4651
## Things that are easy to get wrong
4752

4853
`CLAUDE.md` carries a numbered list of them — verified facts about the protocol,

0 commit comments

Comments
 (0)