This document defines the verification, release, and support policy for Android
stealth artifacts. It covers the normal, stealth-vpn, and stealth-novpn
profiles used by the Stealth Lantern epic.
Target Android stealth artifacts must be built through the Makefile release path
so the gomobile AAR, Flutter artifacts, generated profile, and manifest
filtering are produced from the same inputs. The command shape below becomes
actionable only after the companion build-profile and manifest-selection PRs are
integrated; this policy PR does not make STEALTH_MODE runnable by itself.
STEALTH_MODE=vpn make android-release-ci
STEALTH_MODE=novpn make android-release-ciDirect Gradle invocations are only acceptable for local manifest experiments after the matching AAR has already been generated by the Makefile.
Android stealth manifest minimization is also available to local Gradle
experiments through -PSTEALTH_MODE=vpn or -PSTEALTH_MODE=novpn. The Gradle
task uses -PstealthPython, then PYTHON, then python3 to generate the
filtered manifest. -PstealthNoVpn=true is kept as a compatibility switch for
older automation: when STEALTH_MODE is unset it selects novpn, and when
combined with STEALTH_MODE=vpn Gradle fails fast because the inputs conflict.
Prefer STEALTH_MODE=novpn for new scripts.
vpn keeps the Android VpnService surface but removes verified app links,
public custom-scheme filters, broad package visibility, write-settings access,
payment query declarations, wallet metadata, and cleartext traffic allowance
from the generated manifest.
novpn applies the same filtering and also removes Android VPN service
components, quick-tile VPN controls, boot receiver, and VPN-related permissions.
The companion no-VPN runtime PR routes startup through a normal Android
Service, which configures radiance with:
RADIANCE_USE_SOCKS_PROXY=true
RADIANCE_SOCKS_ADDRESS=127.0.0.1:<profile-specific-port>
At the pinned radiance version, that env pair replaces the TUN inbound with a loopback mixed HTTP/SOCKS inbound. The listener must have local access control; a profile-specific port only reduces cross-artifact stability and is not enough to pass hostile-app probing by itself. If the runtime cannot enforce local proxy access control, the no-VPN artifact must remain experimental and fail release qualification when hostile apps can identify the listener signature. Build Flutter with the no-VPN profile's generated Dart defines so VPN controls, full-device routing, and split tunneling are hidden.
Every stealth release candidate must publish the normal Android artifact plus the two stealth Android artifacts from the same source revision. CI should fail the release candidate when any of these checks fail.
The leakage scanner (check_leakage.py) and raw strings | grep scans operate
on the post-garble binary. When a Go package is in GARBLE_GOGARBLE scope,
garble -literals encrypts string literals at compile time and reconstructs
them at runtime from an embedded decryption table. A brand string encoded this
way is absent from the binary as readable bytes — both check_leakage and
strings | grep return 0 — yet the string exists at runtime and is used (for
example, as an OAuth redirect URI or a deep-link scheme). This means:
check_leakagepass + raw-grep-0 is NECESSARY but NOT SUFFICIENT for garbled Go packages.
Brand removal in garbled Go packages must be verified by:
- Source-level build-tag exclusion. The file or function that produces the
brand string must carry a
//go:build !stealth(or narrower) constraint so it is not compiled into the stealth variant at all. Garble-hiding a string that is still compiled in does not satisfy the brand-removal requirement. nm -Dsymbol-absence check. After rebuild, confirm that the exported cgo/gomobile JNI symbols for the excluded feature are gone fromlibgojni.so, not just string-hidden. See "Standing nm -D gate" below.
When investigating a suspected garble false-pass, compare strings libgojni.so | grep <token> (should be 0) against the Go source build tags for
the file that contains the literal. If the file is still compiled in, the 0 is
garble-obfuscated, not a genuine exclusion. The correct fix is always to
broaden the build tag, never to rely on garble encryption alone.
In addition to check_leakage, run the following nm -D checks on
lib/arm64-v8a/libgojni.so extracted from each stealth artifact. All commands
must produce empty output (exit 0 with no matches). These gates are hard
failures.
OAuth2 / deep-link symbols (brand-bearing Lantern OAuth flow; excluded via
//go:build !stealth on account/oauth.go, backend/oauth.go,
mobile_oauth.go):
nm -D libgojni.so | grep -iE 'GetOAuthProvider|OAuthLogin(Url|Callback)|IsOAuthLogin'
# must be emptyImportant: the bare string
oauthlegitimately remains inlibgojni.soas the RFC 7635 ICE credential-type constant frompion/webrtc/v4(iceCredentialTypeOauthStr). Do NOT addoauthto the nm -D gate; the gate targets only the Lantern OAuth flow symbols listed above.
When adding new stealth-excluded features, audit the gomobile-exported symbol
names in the excluded file and add a corresponding nm -D | grep line to this
gate before shipping.
A stealth release APK signed with a Lantern-identifying certificate leaks
identity through the APK signature, certificate pinning checks, and
PackageManager.getPackageInfo certificate inspection.
Release signing requirement: stealth release builds must use a neutral release signing key — a key whose certificate subject/issuer DN contains no Lantern, getlantern, Brave New Software, or other brand-identifying strings.
After signing, verify with:
apksigner verify --print-certs path/to/stealth-release.apk \
| grep -iE 'lantern|getlantern|bns|brave.new'
# must be emptyThe debug certificate (CN=Android Debug, O=Android, C=US) is neutral and
acceptable for local verification builds only. It must not be used for
distributed stealth releases. When rotating the package name or distribution
channel, verify the new signing key independently before distributing.
| Check | Normal | Stealth VPN | Stealth No-VPN |
|---|---|---|---|
| Build mode | normal Makefile release path | Makefile release path with STEALTH_MODE=vpn |
Makefile release path with STEALTH_MODE=novpn |
| Package identity | canonical package | private profile package name | private profile package name |
| Manifest parser | baseline manifest is valid | no app links, broad queries, wallet metadata, write-settings, or cleartext allowance | same as VPN plus no VpnService, quick tile, boot receiver, or VPN permission |
| Dart defines | default | default | no-VPN profile defines required |
| Asset policy | public assets allowed | private profile assets only | private profile assets only |
| Dependency policy | normal allowlist | no new detector-facing SDKs without review | no new detector-facing SDKs without review |
| Symbol and metadata review | standard release checks | no public profile names in artifact metadata | no public profile names in artifact metadata |
Minimum automated CI coverage:
- Build the three Android release variants from a clean checkout.
- Decode each generated APK or AAB manifest and compare it with the expected profile allowlist.
- Verify the no-VPN artifact has no
android.permission.BIND_VPN_SERVICE,android.net.VpnService, quick-settings VPN tile, boot-time VPN startup path, split-tunneling UI entry point, or full-device-routing UI entry point. - Verify the VPN artifact still has the expected
VpnServicedeclaration and excluded-app configuration path. - Verify package name, app label, icon, supported schemes, exported components, permissions, queries, services, receivers, providers, and metadata match the private profile manifest contract.
- Save the manifest diff and build metadata as CI artifacts for support traceability.
Run dynamic validation on a physical Android device for every stealth release candidate. Repeat the full matrix whenever a release changes Android networking, manifest generation, startup, payments, account flows, updates, support metadata, package rotation, or stealth profile inputs.
| Scenario | Normal | Stealth VPN | Stealth No-VPN |
|---|---|---|---|
| Fresh install | installs from official channel | installs from private channel | installs from private channel |
| First launch | public app identity visible | private app identity visible | private app identity visible |
| Connect | full-device VPN can connect | full-device VPN can connect | local proxy mode can connect |
| Android VPN indicator | visible when connected | visible when connected | never visible |
| TUN interface | present when connected | present when connected | absent |
| Split tunneling | available when supported | excluded-app behavior works | hidden or disabled |
| Non-excluded traffic | routed through Lantern | routed through Lantern | routes only through app-local proxy integrations |
| Excluded traffic | follows normal split rules | does not see Lantern-routed network state | not applicable |
| Reboot | expected normal behavior | no unexpected public identity exposure | no VPN startup exposure |
| Uninstall/reinstall | normal state cleanup | private profile cleanup | private profile cleanup |
Required device evidence:
- Android version, device model, build fingerprint, profile name, package name, version code, version name, source commit, artifact hash, and tester.
- Screenshots or command output showing VPN indicator behavior for both stealth profiles.
adb shell dumpsys package <package>output confirming package and component exposure.adb shell dumpsys connectivityor equivalent evidence showing VPN/TUN state forstealth-vpnand absence of VPN/TUN state forstealth-novpn.- Traffic evidence from at least one non-excluded browser or test app for
stealth-vpn. - Traffic evidence from each hostile excluded app listed for the profile.
- Local loopback probing evidence for the no-VPN proxy listener showing that hostile apps cannot identify a stable unauthenticated proxy signature.
Each private profile owns a hostile-app list. The list is private support metadata and must not be embedded in public release notes. QA must validate the apps that are active for the profile and record the exact app versions tested.
| Detector class | Example observation | Stealth VPN expectation | Stealth No-VPN expectation |
|---|---|---|---|
| VPN state readers | ConnectivityManager, active network, VPN transport |
excluded hostile apps do not see Lantern-routed network state | no VPN transport is exposed |
| TUN/interface readers | /proc/net, network interfaces, local routing table |
excluded hostile apps do not observe Lantern TUN routing | no TUN interface is created |
| Package scanners | installed packages, launcher labels, signatures | rotated private package and labels are used | rotated private package and labels are used |
| Intent/app-link scanners | supported schemes, app links, exported components | public Lantern app links are absent | public Lantern app links and VPN components are absent |
| Permission scanners | requested permissions and metadata | only profile-approved permissions remain | VPN-related permissions are absent |
| Traffic probes | DNS, HTTP, TCP route checks from hostile app | hostile app traffic bypasses Lantern | hostile app traffic is not routed by a VPN |
| Non-hostile control app | browser or QA traffic probe | traffic routes through Lantern | only explicitly proxy-aware flows use Lantern |
Pass criteria:
- Every hostile app in the profile is tested on a clean install.
- Hostile apps excluded from
stealth-vpndo not see Lantern-routed network state and their traffic does not traverse Lantern. - At least one non-excluded app still routes through Lantern in
stealth-vpn. stealth-novpnnever exposes Android VPN state, TUN interfaces, VPN permissions, VPN service components, or VPN controls.- Any detector result that differs from the private profile contract blocks the release until the profile owner approves a documented exception.
Stealth artifacts are direct-distribution builds. They are not uploaded to public app stores unless a profile explicitly says otherwise.
Package-name rotation rules:
- Rotate the private package name when a hostile app begins matching the current package, label, icon, app-link surface, metadata, or other artifact fingerprint.
- If a hostile app matches the signing certificate or APK signature lineage, package-name rotation alone is not sufficient. Rotate to a profile-specific signing lineage when policy and distribution constraints allow it; otherwise the profile owner must approve a documented exception before release.
- Rotate when a distribution channel, support channel, or telemetry path leaks a private profile identifier.
- Rotate when a profile owner intentionally changes the user population, distribution partner, or risk model.
- Do not rotate only to ship routine bug fixes; unnecessary rotation increases support and migration risk.
Release package requirements:
- Each private package name maps to one profile, one distribution channel, one release train, and one support retention policy.
- Release notes for recipients must explicitly state feature sacrifices, update behavior, migration behavior, and rollback expectations.
- Public release notes must not disclose private profile names, hostile-app lists, distribution partners, or package-name mappings.
- The release owner must retain artifact hashes, signing certificate lineage, source commit, CI run, build profile, package name, version code, version name, distribution channel, and rollout window.
- Rollback means distributing the previous approved artifact for the same private package. Cross-package rollback is a migration and requires support approval.
Direct-distribution stealth builds cannot assume public store update behavior.
- Users installed on one private package name will not receive updates for a new package name through Android package replacement. A package-name rotation is a parallel install or manual migration unless a profile-specific migration path is implemented.
- Automatic in-app update prompts must be reviewed for each profile because update URLs, app labels, package names, and support copy can expose profile identity.
- A direct APK update can replace an installed app only when package name and signing lineage are compatible. AABs are not directly installable device updates; they require store delivery or conversion to APK/split APK artifacts.
- Manual migration instructions must explain whether account login, Pro status, private-server configuration, diagnostics, and local settings carry over.
- If account or purchase flows are disabled or hidden in a stealth profile, the release notes must tell support how the user receives entitlement or recovery help.
- Users must be told when a new stealth package is a replacement, a parallel install, or a rollback.
Support needs to map build IDs to private profiles without exposing that mapping inside the artifact or in public issue trackers.
Retain the mapping in a private support source of truth with access limited to release, QA, and support leads. The mapping must include:
- Private profile identifier.
- Package name.
- App label and icon set identifier.
- Version code and version name.
- Source commit and CI run.
- Build ID.
- Artifact hashes.
- Signing certificate lineage.
- Distribution channel and rollout window.
- Hostile-app list version.
- Feature sacrifices and disabled flows.
- Migration, rollback, and uninstall guidance.
- Support escalation owner.
Support workflow:
- Ask the user for the visible app label, version name, version code when available, the distribution channel, and an installation date.
- If diagnostics are available, collect the build ID and artifact channel but do not ask the user to post private profile names publicly.
- Resolve the build ID to the private profile in the private support mapping.
- Use the profile runbook to answer feature availability, update, migration, entitlement, and rollback questions.
- Escalate to release and QA when a user report suggests detector exposure, package collision, signing mismatch, or update failure.
- Record any hostile-app detection report against the private profile and app version tested.
This policy is only satisfied by a release candidate after the companion implementation and CI PRs have merged. The document alone is not a release gate.
A stealth release candidate is acceptable only when all of the following are true:
- Release automation builds the normal,
stealth-vpn, andstealth-novpnAndroid artifacts and runs automated static manifest/profile checks. - Manual dynamic Android validation is complete for
stealth-vpnandstealth-novpn. - Hostile excluded apps do not see Lantern-routed network state in
stealth-vpn. - Non-excluded apps still route through Lantern in
stealth-vpn. stealth-novpnexposes no VPN/TUN state, VPN controls, VPN permissions, or VPN service components.- Release notes describe feature sacrifices, direct-distribution behavior, update behavior, package-name rotation implications, migration behavior, and rollback expectations.
- Support can map build IDs to private profiles through private metadata without exposing that mapping in the artifact or public channels.
- Artifact hashes, source commit, CI run, package name, version code, version name, signing lineage, distribution channel, and rollout window are retained.