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 580bcfb
Browse filesBrowse the repository at this point in the historyBrowse files
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.
Copy file name to clipboardExpand all lines: CLAUDE.md
+62-6Lines changed: 62 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,21 +16,30 @@ comments, everything. Do not emit Chinese in project files.
16
16
cross-compiled for Android.
17
17
-**Delivered in layers**: L1 TLS/TCP → L2 Mux → L3 QUIC. Each layer ships to
18
18
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.
19
26
20
27
## Layout
21
28
22
29
| Path | Purpose |
23
30
|---|---|
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|
25
32
|`app/src/main/jni/`| Vendored lwIP and its JNI bridge, inherited from the donor, plus the QUIC bridge and the library's export map |
26
33
|`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 |
|`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 |
71
81
|`lintDebug`| Android lint |
72
82
|`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 |
73
83
|`verify-vectors.py`| 45 known-answer checks, recomputed from the spec prose. References no upstream code and needs no clone |
74
84
|`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 |
75
87
|`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 |
76
88
77
89
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:
269
281
declares it. This cost a whole debugging round: the fix was right, the
270
282
tests failed the same way, and the library on the device predated the edit.
271
283
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
+
272
328
## Already verified — do not redo
273
329
274
330
- The donor project `Anywhere-Android`**builds clean** on this toolchain
Copy file name to clipboardExpand all lines: README.md
+41Lines changed: 41 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,47 @@ which is a QUIC node, because the specification's default for both directions is
37
37
both.
38
38
-**No release has been published**, and no signing key exists.
39
39
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
+
40
81
## Protocol baseline
41
82
42
83
Pinned in [`conformance/PROTOCOL_BASELINE`](conformance/PROTOCOL_BASELINE):
0 commit comments