Skip to content

Latest commit

 

History

History
344 lines (282 loc) · 17.8 KB

File metadata and controls

344 lines (282 loc) · 17.8 KB

Stealth build QA and release policy

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.

Build profiles

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-ci

Direct 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.

Static checks

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.

Garble string-literal obfuscation blind spot

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_leakage pass + raw-grep-0 is NECESSARY but NOT SUFFICIENT for garbled Go packages.

Brand removal in garbled Go packages must be verified by:

  1. 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.
  2. nm -D symbol-absence check. After rebuild, confirm that the exported cgo/gomobile JNI symbols for the excluded feature are gone from libgojni.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.

Standing nm -D brand-symbol gate

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 empty

Important: the bare string oauth legitimately remains in libgojni.so as the RFC 7635 ICE credential-type constant from pion/webrtc/v4 (iceCredentialTypeOauthStr). Do NOT add oauth to 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.

Signing certificate policy

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 empty

The 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 VpnService declaration 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.

Dynamic Android validation

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 connectivity or equivalent evidence showing VPN/TUN state for stealth-vpn and absence of VPN/TUN state for stealth-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.

Hostile app detection matrix

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-vpn do 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-novpn never 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.

Release package rotation policy

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.

Manual update implications

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 mapping and runbook

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:

  1. Ask the user for the visible app label, version name, version code when available, the distribution channel, and an installation date.
  2. If diagnostics are available, collect the build ID and artifact channel but do not ask the user to post private profile names publicly.
  3. Resolve the build ID to the private profile in the private support mapping.
  4. Use the profile runbook to answer feature availability, update, migration, entitlement, and rollback questions.
  5. Escalate to release and QA when a user report suggests detector exposure, package collision, signing mismatch, or update failure.
  6. Record any hostile-app detection report against the private profile and app version tested.

Acceptance criteria

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, and stealth-novpn Android artifacts and runs automated static manifest/profile checks.
  • Manual dynamic Android validation is complete for stealth-vpn and stealth-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-novpn exposes 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.