Skip to content

Commit 580bcfb

Browse files
committed
Give the README a roadmap, and bring the status documents to today
The README said what works and what does not, but not what stage the project is at or how much of the plan is behind it. That is the question a reader arrives with, and the answer existed only in a private repository. The roadmap is the layered plan the project has actually followed — M0, L1, L2, L3, the release tail, then the device pass, L4 and publication — with a state per stage. Under it, the count that makes 'done' checkable: 107 of 111 conformance rows covered by a test, 4 partly with the gap named, none blocked, and a link to the matrix those come from. The numbers are counted from the table rather than written by hand, because the last hand-written total was wrong and survived two runs. CLAUDE.md had drifted in three places: it still said the TUN and routing layers were not present, listed two of the five packages inside the coverage gate, and omitted checkNativeBridge and checkReleaseArtifact from the gate command. Eight facts join the easy-to-get-wrong list, all paid for during L3 — the single stream credited before authentication, a closed stream left in the write round-robin, one datagram per loop pass, this client being forced out of its own tunnel so an in-process fetch proves nothing, loopback never entering a TUN, Kotlin delegation erasing the interface it was not told about, an unauthenticated QUIC connection dying at the handshake deadline, and UDP framing belonging to a direction rather than a flow.
1 parent f723edf commit 580bcfb

2 files changed

Lines changed: 103 additions & 6 deletions

File tree

‎CLAUDE.md‎

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,30 @@ comments, everything. Do not emit Chinese in project files.
1616
cross-compiled for Android.
1717
- **Delivered in layers**: L1 TLS/TCP → L2 Mux → L3 QUIC. Each layer ships to
1818
internal testing and leaves a fix-back window. No big-bang release.
19+
- **All three layers are delivered** (L1 and L2 on 2026-08-27, L3 on 2026-08-29),
20+
together with the release tail. A bare `nowhere://key@host:port` — a QUIC node,
21+
because the specification defaults both directions to `udp` — connects and
22+
carries traffic, and the oracle differential agrees on every case over every
23+
carrier combination. What has *not* happened is a physical device: every device
24+
result in this repository is from an emulator. The README's roadmap carries the
25+
stage view and the conformance row counts behind it.
1926

2027
## Layout
2128

2229
| Path | Purpose |
2330
|---|---|
24-
| `app/` | The client. Kotlin, Compose, `minSdk` 26, `compileSdk` 36 |
31+
| `app/` | The client. Kotlin, Compose, `minSdk` 26, `compileSdk` 36. Nine screens, three locales, and the protocol, TUN, DNS, routing and per-app layers |
2532
| `app/src/main/jni/` | Vendored lwIP and its JNI bridge, inherited from the donor, plus the QUIC bridge and the library's export map |
2633
| `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 |
2734
| `conformance/` | Byte-level vectors, self-verifying checker, end-to-end smoke test, drift detection, test matrix |
28-
| `docs/` | Architecture, the design system, i18n, brand, and the TLS exporter ADR |
35+
| `app/src/main/assets/rules/` | The bundled rule set (D-14), network-structural tier only, carrying a provenance header |
36+
| `docs/` | Architecture, the design system, i18n, brand, privacy, store policy, and the TLS exporter ADR |
2937

3038
The lwIP layer is inherited from `NodePassProject/Anywhere-Android` (GPL-3.0) at
3139
**e9a9274, 2026-04-28** — recorded because the donor is under active development
32-
and fixes landing there will not arrive on their own. The TUN and routing layers
33-
are not present yet.
40+
and fixes landing there will not arrive on their own. The TUN, routing and
41+
per-app layers are present, and follow the donor's shape rather than copying its
42+
code beyond the lwIP bridge.
3443

3544
**BLAKE3 and libyaml were deliberately not inherited.** The donor's
3645
`CMakeLists.txt` builds both; BLAKE3 serves a protocol this client does not
@@ -60,18 +69,21 @@ automation from the first protocol commit rather than retrofitted later.
6069

6170
```sh
6271
./gradlew ktlintCheck testDebugUnitTest koverVerifyDebug lintDebug \
63-
checkClasspathConsistency assembleDebug checkNativeLibraries
72+
checkClasspathConsistency assembleDebug \
73+
checkNativeLibraries checkNativeBridge checkReleaseArtifact
6474
python3 conformance/scripts/verify-vectors.py
6575
```
6676

6777
| Gate | What it enforces |
6878
|---|---|
6979
| `ktlintCheck` | Style. `@Composable` PascalCase is exempted through ktlint's own `.editorconfig` switch, not suppressed |
70-
| `koverVerifyDebug` | **90% line coverage on `eu.nodepass.somewhere.protocol.*` and `.subscription.*`.** 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 |
80+
| `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 |
7181
| `lintDebug` | Android lint |
7282
| `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 |
7383
| `verify-vectors.py` | 45 known-answer checks, recomputed from the spec prose. References no upstream code and needs no clone |
7484
| `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` |
85+
| `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 |
86+
| `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 |
7587
| `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 for the first time on 2026-08-26 and the pin moved to v1.8.2 |
7688

7789
Both gates that can silently pass were verified to actually fail. The coverage
@@ -269,6 +281,50 @@ Two rules from those documents that reach into protocol code:
269281
declares it. This cost a whole debugging round: the fix was right, the
270282
tests failed the same way, and the library on the device predated the edit.
271283

284+
25. **Exactly one bidirectional stream is credited before authentication**
285+
(NW-P-19). Sixteen concurrent opens meet `ERR_STREAM_ID_BLOCKED`, which is
286+
the protocol working rather than a failure. The wait for credit has to
287+
happen **outside** the connection's owner thread: the owner is what pumps,
288+
and the peer's extension cannot arrive while it is blocked waiting for it.
289+
26. **A closed QUIC stream must leave the write round-robin.** `writev` handed
290+
an id ngtcp2 no longer knows answers `ERR_STREAM_NOT_FOUND`, which reads
291+
like a connection failure and takes every other flow on that connection
292+
down with it. Streams carry a `gone` flag for exactly this.
293+
27. **One datagram per loop pass hangs a large transfer, silently.** Twenty
294+
megabytes is fourteen thousand datagrams; the kernel buffer overflows, QUIC
295+
retransmits, the window collapses, and **nothing reports an error** — the
296+
transfer simply stops making progress while looking healthy. Bursts of
297+
sixty-four into a 4 MB receive buffer take the same transfer to six seconds.
298+
28. **This client is forced out of its own tunnel in every mode**
299+
(`AppSelection.ruleFor` — a VPN inside its own tunnel is a routing loop), so
300+
**a fetch made from inside the app's own process never enters the TUN**.
301+
Three device test classes passed for two runs while proving only that their
302+
destination was reachable some other way, and one of them was the evidence
303+
for a conformance row. The decisive check is a target only the Portal can
304+
reach: the case fails instantly and the Portal's counters stay put.
305+
`TunnelHarness` now refuses a target the device can reach directly.
306+
29. **Loopback never enters a VPN's TUN.** The kernel routes 127.0.0.0/8 to `lo`
307+
rather than to the default route, so the trick that works from inside the app
308+
— choosing an address only the Portal can reach — proves nothing when the
309+
fetch is driven from a shell. The evidence there is the Portal's own byte
310+
counters, which move **on a timer**, so a check must poll until they move
311+
rather than read them once. And a **truncated transfer is non-empty**: a
312+
retry loop that accepts any non-empty file reports a corrupt tunnel when what
313+
it had was a hold window expiring mid-transfer.
314+
30. **Kotlin's `by` delegation erases every interface you did not name.**
315+
`class TrackedFlow(flow: Flow) : Flow by flow` wrapping a `PacketFlow` is no
316+
longer a `PacketFlow`, so an `is` check downstream falls through to the
317+
stream path and UDP quietly loses its packet boundaries. `NowhereSession`
318+
keeps two wrappers and picks between them.
319+
31. **An unauthenticated QUIC connection dies at the handshake deadline**, about
320+
five seconds in. A keep-alive test that never opens a flow therefore measures
321+
nothing: the connection was going to close on its own regardless of whether
322+
a PING was ever sent.
323+
32. **UDP framing belongs to a direction, not to a flow.** A split flow has two
324+
carriers, and section 9 carriage has to be applied per direction — the
325+
oracle differential caught split-UDP diverging when the framing was attached
326+
to the flow instead. `DatagramLane` is the shared piece both directions use.
327+
272328
## Already verified — do not redo
273329

274330
- The donor project `Anywhere-Android` **builds clean** on this toolchain

‎README.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@ which is a QUIC node, because the specification's default for both directions is
3737
both.
3838
- **No release has been published**, and no signing key exists.
3939

40+
## Roadmap
41+
42+
Delivery is layered, and each layer is a working client rather than a stage of
43+
one: it ships to internal testing and leaves a window to fix what testing finds.
44+
There is no big-bang release.
45+
46+
| Stage | What it delivers | State |
47+
|---|---|---|
48+
| **M0** · foundations | Public repository, CI, the quality gates, and the conformance suite the later layers are judged by | **Done**, 2026-08-24 |
49+
| **L1** · TLS over TCP | Authentication, FlowHeader, Target, the seven setup outcomes, the dedicated lane, UDP over a stream — and, pulled forward because a client nobody can feed nodes to cannot be tested, the TUN, DNS with fake-IP, node storage and subscription import | **Done**, 2026-08-27 |
50+
| **L2** · TLS Mux | The Mux carrier at upstream's own shard density, and concurrency measured over it | **Done**, 2026-08-27 |
51+
| **L3** · QUIC | ngtcp2 over aws-lc, authentication, TCP over reliable streams, UDP over DATAGRAM, split flows across two carriers, keep-alive, and every carrier combination compared against the reference implementation | **Done**, 2026-08-29 |
52+
| **Release tail** | A bundled rule set, a connection log that shows what the Portal actually said, a launcher icon, a release build that has been run rather than only produced, and the documents a VPN-class listing requires | **Done**, 2026-08-29 |
53+
| **Device pass** | The first run on physical hardware: Private DNS, a real per-app list, a Wi-Fi to cellular change, Doze, a path MTU below 1500 | **Not started.** No physical device has ever been attached to this project |
54+
| **L4** · product surface | Subscription refresh, latency testing, encrypted DNS. Scope deliberately not fixed before a device pass says what the product actually needs | **Not started** |
55+
| **Publication** | A signing key, a distribution channel, a first release | **Waiting on decisions rather than on code** |
56+
57+
### How much of it is done
58+
59+
[`conformance/cases/l1-coverage.md`](conformance/cases/l1-coverage.md) is the
60+
measure. It carries one row per specification obligation, and every row either
61+
names the test that covers it or says what it is waiting for — so these counts
62+
can be re-derived by reading it rather than taken on trust. They were wrong once,
63+
carried forward through two runs by hand, and that is why they are now counted
64+
from the table.
65+
66+
| Layer | Rows | Covered by a test | Partly, with the gap named | Blocked |
67+
|---|---|---|---|---|
68+
| L1 | 63 | 62 | 1 | 0 |
69+
| L2 | 21 | 20 | 1 | 0 |
70+
| L3 | 27 | 25 | 2 | 0 |
71+
| **Total** | **111** | **107** | **4** | **0** |
72+
73+
The four partial rows are each half of a pair: this client's half is tested, and
74+
the other half needs something no repository can contain — a running dashboard, a
75+
Portal that refuses stream credit, a phone.
76+
77+
**The protocol is finished and the product is not.** Between this repository and
78+
a release stand a physical device, a decision about certificate verification on
79+
the QUIC carrier, and a signing key that will never live here.
80+
4081
## Protocol baseline
4182

4283
Pinned in [`conformance/PROTOCOL_BASELINE`](conformance/PROTOCOL_BASELINE):

0 commit comments

Comments
 (0)