A native Android client for the Nowhere protocol, written in Kotlin.
Nowhere protocol only. No VLESS, Trojan, Shadowsocks or Naive. The protocol engine is hand-written in Kotlin rather than wrapping an existing core.
Pre-release, and not published anywhere. The client speaks Nowhere v3
(nw3) and nothing older: a share link nowhere://key@host:port — TLS, Morph
on, the v3 defaults — or the same with ?net=udp for QUIC, connects and carries
traffic.
| Protocol | v3 authentication with role keys, the one-byte FlowStart, the five setup outcomes, UDP over a stream, stream-derived QUIC flow ids, UDP over DATAGRAM with fragmentation |
| Morph | The v3 wire mask, on by default: ChaCha20 under TLS with the fade prelude carrying upstream's Authenticated Prelude (v3.1.0), and per datagram under QUIC |
| Carriers | TLS (one connection per flow) and QUIC (one connection for all of them), each with Morph on and off, compared case by case against the reference implementation, which agrees on every one. A node is checked before it is called Connected, and a QUIC node that does not answer is tried over TLS on the same port |
| Certificates | Verified on both carriers, always: a pin, or this device's trust store and the hostname. v3 has no unverified mode |
| Tunnel | VpnService + lwIP, both address families, TCP with back-pressure, UDP, DNS interception with fake-IP over IPv4 and IPv6, live throughput |
| Routing | Everything or Rules mode; TCP and UDP decided by the same rules; a bundled network-structural rule set, and a mainland-China set (Rules mode, switchable) generated reproducibly from APNIC and dnsmasq-china-list; an import path, a direct path, a reject path; local networks kept out of the tunnel by default; MTU and the DNS server for relayed queries |
| Per-app | A real installed list, a persisted selection, system apps shown or hidden, select all or none, and the list copied or pasted as text; this client structurally outside its own tunnel |
| Nodes | Chosen by the user or left automatic (failover only when automatic), imported from a link, a batch, a QR code or a picture, shared as a link or a QR code, probed over their own carrier, sorted and filtered |
| Subscription | Several, each with its own quota, expiry, refresh and failure, using upstream's actual quota semantics; plain or base64 feeds, with unreadable lines counted rather than dropped; a periodic refresh, every 1 to 24 hours, that is off until asked for |
| Activity | Live connections with the app that opened each, closable; the DNS queries; the Portal's answers with setup time; an export for an issue |
| Outside the app | A notification with live rates and Disconnect, a quick-settings tile, launcher shortcuts, and START/STOP/TOGGLE for automation apps behind a switch that is off |
| UI | Ten screens, three themes (light, dark, automatic), three locales chosen in the app on every supported Android version, an undoable node removal, and a plaintext backup with a warning |
| Updates | A daily check of this repository's GitHub Releases, on by default and listed in docs/privacy.md with its exact request; a new release is offered once and opened in the browser, never installed by the app |
What is not done, stated as plainly as the rest:
- Nothing has ever run on physical hardware. Every device result is from an emulator. Private DNS over DoT, IPv6-only and NAT64 networks, a real Wi-Fi to cellular change, Doze and a path MTU below 1500 are all things an emulator cannot represent, and all things a VPN meets on the first real device.
- Start on boot is deliberately absent. Android's always-on VPN starts the tunnel before applications run and can block traffic while it is down; an in-app boot receiver would be a worse version of it. Settings links to the system switch instead.
- Verification by name has only been seen to refuse on a device. Accepting a chain this device trusts has been exercised on a JVM and against a live Portal with a test anchor, not on Android, because that needs a CA whose key a test can sign with. See ADR-0002.
- The dashboard does not emit v3 links yet. NowhereDash targets Nowhere
2.1.1; its
up/downlinks are read asnetwhere the two agree, and its carrier-path form is refused with a reason. - No release has been published, and no signing key exists.
Delivery is layered, and each layer is a working client rather than a stage of one: it ships to internal testing and leaves a window to fix what testing finds. There is no big-bang release.
| Stage | What it delivers | State |
|---|---|---|
| M0 · foundations | Public repository, CI, the quality gates, and the conformance suite the later layers are judged by | Done, 2026-08-24 |
| L1 · TLS over TCP | Authentication, flow setup, Target, the 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, against v1.8 |
| L2 · TLS Mux | The Mux carrier at upstream's own shard density | Done, 2026-08-27 — and removed with v3, which deleted Mux |
| L3 · QUIC | ngtcp2 over aws-lc, authentication, TCP over reliable streams, UDP over DATAGRAM, keep-alive, every carrier compared against the reference implementation | Done, 2026-08-29, against v1.8 |
| 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 |
| L4 · product surface | IPv6 end to end, pin on the QUIC carrier, node health and failover, a scheduled subscription refresh and a quick-settings tile |
Done, 2026-08-29 |
| v3 · following upstream | Nowhere v3.0.1: the new authentication, FlowStart and setup outcomes; Mux and split carriers removed; stream-derived QUIC flow ids; Morph on both carriers; chain verification by name on QUIC; every conformance claim re-established against the v3 oracle and on an emulator | Done, 2026-10-21 |
| L5 · product surface | Everything a FlClash user reaches for that is not Clash configuration: node choice and automatic selection, QR and batch import, several subscriptions, live connections and DNS, notification, shortcuts and automation, theme and language, per-app tools, network settings, backup. Each part reviewed adversarially before the next began | Done on emulators, 2026-10-21 |
| Before users | Following upstream to v3.1.0 and its Authenticated Prelude; four defects a first user would have met (a Portal named by host could not be reached, a dead TLS node read as Connected, UDP ignored the rules, the LAN went to the Portal); falling back from QUIC to TLS; mainland-China rules; base64 feeds; the update check; the language on every Android version; docs/ux.md |
Done on emulators, 2026-10-24 |
| Device pass | The first run on physical hardware, now one pass for everything: docs/device-checklist.md and conformance/scripts/device-acceptance.sh |
Not started. No physical device has ever been attached to this project |
| L6 · what the device pass asks for | Whatever a real phone turns out to need | Not started, deliberately: scope fixed after a device pass, not before |
| Publication | A signing key, a distribution channel, a first release | Waiting on decisions rather than on code |
conformance/cases/coverage.md is the
measure. It carries one row per specification obligation, and every row either
names the test that covers it or says why it has none — so these counts can be
re-derived by reading it rather than taken on trust. They were wrong once,
carried forward through two runs by hand, and that is why they are now counted
from the table.
| Rows | Covered by a test | of which with a gap named | Manual, with the reason | Blocked |
|---|---|---|---|---|
| 91 | 90 | 2 | 1 | 0 |
The two rows with a gap are each half of a pair: this client's half is tested, and the other half needs something no repository should contain — a dashboard that emits v3 links, and a CA key a test could sign with.
The protocol is finished and the product is not. Between this repository and a release stand a physical device and a signing key that will never live here.
Pinned in conformance/PROTOCOL_BASELINE:
Nowhere v3.1.0 (0000065), with the Core specification at
Nowhere-Protocol
0000001. Every protocol implementation detail is traceable to a section of
that specification, or of Nowhere's docs/prelude.md for the Authenticated
Prelude this client sends. v3 is not wire-compatible with v1: a v3 client
cannot reach a v1 Portal, or the reverse.
conformance/ holds byte-level test vectors, a self-verifying
checker, an end-to-end smoke test and upstream drift detection. It is published
deliberately: third-party implementations need it to align, dashboard and Portal
interop checks need it, and community bug reports need it to be meaningful.
python3 conformance/scripts/verify-vectors.py # 99 known-answer checks
conformance/scripts/drift-check.sh /path/to/Nowhere /path/to/Nowhere-Protocol
conformance/scripts/oracle-diff.sh # this client against upstream's own
conformance/scripts/e2e-android.sh "$PWD" # the whole instrumentation suite on a deviceoracle-diff.sh is the one worth running. It puts the same case list through
this implementation and the reference client, over both carriers with Morph on
and off, and diffs the outcomes. Against v1 it found a rejection that one
carrier reported in a form a caller could not read; on its first v3 run it found
a refused AuthFrame over QUIC reported as a transport failure, where the oracle
and the TLS carrier both read it as no answer.
The suite makes no assumption about where the upstream clones live; point
NOWHERE_CLONE at Nowhere and NOWHERE_SPEC_CLONE at Nowhere-Protocol, or pass
both to drift-check.sh. Since v3.1.0 the Core specification is its own
repository, and the drift check watches both.
Many tests dial a real Portal rather than a fake, because the failures that matter most are the ones no fixture predicts. The first run against v3 found the clearest example: a Portal ends a flow whose first payload arrives before READY, which no vector and no fake would have shown. They skip cleanly when no Portal is running, so an environment that cannot run them reports honestly instead of passing vacuously.
eval "$(conformance/scripts/portal-for-tests.sh)"
./gradlew testDebugUnitTest
conformance/scripts/portal-for-tests.sh --stop| Document | Carries |
|---|---|
docs/design-system.md |
Colour tokens for both themes with measured contrast, the type scale, and the rules the UI has to keep |
docs/ux.md |
Which tab a feature belongs on, the theme modes, the states every screen handles, interaction, accessibility and writing |
docs/brand.md |
The brand hue, which carries no protocol meaning and is derived rather than picked |
docs/i18n.md |
The three shipping locales, how to add one, and the rule that machine identifiers stay English while sentences translate |
docs/adr-0001-tls-exporter.md |
Where the TLS exporter comes from, and why minSdk is 26 |
docs/adr-0002-quic-certificate-verification.md |
How certificates are verified on the QUIC carrier: pin in the handshake, a name against the platform's trust store after it |
docs/adr-0003-morph.md |
How Morph is applied under each carrier, and why TLS is now Conscrypt in engine mode everywhere |
docs/privacy.md |
What is stored, every address the app contacts and what it sends, and the longer list of what is not collected |
docs/device-checklist.md |
Every feature, what to do on a phone, and what should happen — the device pass, numbered for reporting |
docs/store-policy.md |
What a VPN-class listing requires, what is already satisfied, and what is still someone's decision |
docs/architecture.md covers the layers, which way they
depend, and the rules that keep them apart.
CONTRIBUTING.md covers the gates and what a change is
expected to carry.
Every contrast figure in those documents is asserted by a test rather than claimed, because measuring has already caught defects the eye did not.
./gradlew ktlintCheck testDebugUnitTest koverVerifyDebug lintDebug \
checkClasspathConsistency assembleDebug \
checkNativeLibraries checkNativeBridge checkReleaseArtifact
python3 conformance/scripts/verify-vectors.pyThe protocol layer carries a 90% line-coverage gate. Every other gate here was
added after something got through: checkClasspathConsistency after a module
resolved to different versions on the compile and runtime classpaths and crashed
a screen with everything else green; checkNativeLibraries after linking aws-lc
exported 1,684 of the crypto library's own symbols into a process that already
runs a second BoringSSL; checkNativeBridge because the QUIC bridge must have no
way to open a socket that escapes VpnService.protect(); checkReleaseArtifact
because 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.
Each was verified to fail before it was trusted. CI runs all of the above on every pull request, and checks upstream for normative protocol changes daily.
Requires JDK 21, the Android SDK with platform 36, the NDK pinned in
app/build.gradle.kts, and CMake 3.22.1. The QUIC stack additionally needs Go
and perl on the build host — aws-lc generates assembly with perl and its build
tooling with Go — and it is fetched at pinned commits rather than vendored, so a
first build has a network step. It takes about half a minute per ABI and nothing
thereafter.
echo "sdk.dir=/path/to/android-sdk" > local.properties
./gradlew assembleDebugA release build works without a keystore and produces an unsigned APK; with one
configured in local.properties it is signed. Signing material lives nowhere in
this repository.
./gradlew assembleRelease
tools/quic/build-deps.sh --bundle-source corresponding-source.tar.gzThe second command produces the corresponding source for the statically linked ngtcp2 and aws-lc. A GPL-3.0 binary that links them must be accompanied by it.
Somewhere carries user traffic; security defects are the highest-priority class of issue. Do not report them in a public issue — see SECURITY.md for the private reporting channel and what is in scope.
Two properties worth stating plainly:
- Every node's certificate is verified, by a pin or by this device's trust store and the hostname. v3 removed the mode that skipped it; a Portal on its generated certificate needs a pin.
- Morph masks the wire; it does not protect anything. Upstream says so — "wire masking, not authentication or integrity" — and makes no claim that Nowhere resists censorship. This client must not imply otherwise.
GPL-3.0-only. See LICENSE.
Bundled fonts are OFL-1.1, icon path geometry is ISC, lwIP is BSD-3-Clause, ngtcp2 is MIT and aws-lc is Apache-2.0 OR ISC; the mainland-China rule set is generated from APNIC's statistics and dnsmasq-china-list (WTFPL-2.0); see THIRD-PARTY-NOTICES.md, which also records what was deliberately not taken from each.
Copyright (C) 2026 The Somewhere Authors. The lwIP layer and its bridge are inherited from Anywhere-Android (GPL-3.0) and carry their original attribution.