Open-source VPN for speed, privacy, and censorship circumvention. Free to download on Android, iOS, Windows, macOS, and Linux.
# AGENTS.md
The Lantern client: Flutter UI in `lib/`, Go core in `lantern-core/` (FFI on desktop, gomobile on mobile, both into `getlantern/radiance`), and native code in `macos/`, `ios/`, `android/`, `windows/`, `linux/`.
## Build and test
- Go: `CGO_ENABLED=1 go test -tags with_gvisor,with_quic,with_wireguard,with_utls,with_grpc ./...`
- Flutter: `make pubget && make gen`, then `flutter analyze` and `flutter test`.
- FFI bindings: `make ffigen` regenerates `lib/lantern/lantern_generated_bindings.dart`. Never hand-edit it.
- Native prerequisites: `make install-gomobile`, `make pubget && make gen`, and an `app.env` at the repo root (from 1Password; an empty placeholder is enough for compile checks and unit tests, as CI does).
- Swift: `make macos-unit-tests`, `make ios-compile-check`.
- Android: `make install-android-deps && make android-debug-ci`, then `cd android && ./gradlew :app:testDebugUnitTest`. Only `gradle-wrapper.properties` is tracked; the Flutter build generates `android/gradlew`.
- Dependency bumps (radiance, lantern-box, sing-box-minimal): run `go mod tidy` and commit `go.mod` and `go.sum` together.
## Review focus
These bug classes have shipped here, most frequent and severe first. Check a diff against each class that applies to the files it touches.
1. **Crashes at the CGo/gomobile boundary** (`lantern-core/ffi/`, `lantern-core/mobile/`, `lantern-core/utils/gostack.go`). Pointer-rich values on the cgo callback stack panic. Invalid UTF-8 or a typed-nil error reaching gomobile aborts the app in the ObjC bridge (#8541, #8635, #8989).
- Check that every new `//export` returning `*C.char` runs its body in `runOnGoStack`.
- Check that every gomobile export goes through `withCore`/`withCoreR` or `utils.RunOffCgoStack`, and that no raw upstream error crosses the boundary.
- Check that recovery keeps `errors.Is`/`errors.As` working.
2. **Network Extension wedges and blackholes** (`macos/PacketTunnel/`, `ios/Tunnel/`, `macos/Runner/VPN/`). Semaphore bridges around `setTunnelNetworkSettings` deadlocked. A second utun opened over a live one. A failed restart was swallowed while the system showed "connected" (#8515, #8965, #9007, #9080).
- Check that `semaphore.wait()` on an NE or cooperative queue has a deadline.
- Check that errors are thrown or propagated, not logged and returned.
- Check that start paths tear down the existing tunnel first and that old monitors are cancelled.
- Fixes are usually needed in both `ios/Tunnel` and `macos/PacketTunnel`, so question a diff that changes only one.
3. **iOS start deadline and NE memory cap.** iOS kills the provider if `startTunnel` hasn't returned in about 7.5s, and the extension has a jetsam cap of roughly 50 MB (#8993, #8971).
- Check that there is no network or config fetch on the `startTunnel` path.
- Check for unbounded allocation in the extension (archives, outbound lists).
- Check that new periodic work in the extension is rate-bounded, because battery cost is a regression too.
4. **Android VPN service lifecycle** (`android/.../service/LanternVpnService.kt`, `VpnStartGate.kt`, `PlatformInterfaceWrapper.kt`). Crash loops on Connect, JNI aborts, ANRs in `onDestroy`, and start/stop races that left the VPN latched (#8947, #8948, #9085).
- Check that Kotlin implementations of Go callbacks without an `error` return never throw (return null or a default instead). Callbacks whose Go signature returns `error`, such as `RestartService`, should throw on failure so Go sees it.
- Check that every `launch {}` has a `CoroutineExceptionHandler` and that nothing blocks the main thread.
- Check that every start or stop entry point goes through `VpnStartGate`.
- Blocking JNI calls can't be cancelled: `withTimeout`, with or without `async`, only bounds the caller's wait. Check that they run off the main thread and that a timeout doesn't release `VpnStartGate` early. `VpnStartGate.Attempt.connect` reports the deadline but holds the gate until JNI returns, so a new start or a teardown can't overlap native work.
5. **Dart parsing that throws on upstream JSON** (`lib/core/models/`). A lantern-box shape change threw out of `fromJson` and wiped the whole server list (#8929, #8412).
- Check that radiance and lantern-box fields are parsed tolerantly, per entry.
- A dependency bump that changes a JSON shape needs the Dart change in the same PR.
6. **UI state drifting from VPN and account state** (`lib/features/vpn/provider/`, `lib/features/home/`). The toggle acted on a different state than the one shown, and account data was lost on refresh (#8887, #9091, #9102).
- Check that UI state is read from the status stream, not inferred locally.
- Check that Pro checks use `UserDataProX.isPro`/`isExpired` only.
7. **OS, ABI, and toolchain floors.** Missing ABIs crashed at launch, and Go or Sparkle bumps raised the minimum macOS above the declared target (#8726, #8821, #9083).
- Check that ABI lists in the `Makefile` and `android/app/build.gradle` agree.
- Check that new Android APIs are guarded by `SDK_INT`.
- Check that toolchain and framework bumps are checked against the declared minimum OS.
8. **macOS system-extension upgrades** (`macos/Runner/VPN/SystemExtensionManager.swift`, `release.yml`). Check that `CFBundleVersion` only increases and that every `OSSystemExtension` result, including `willCompleteAfterReboot`, leads to activation (#8628, #8614).
9. **Version stamping** (`Makefile`, workflows). An empty version caused config 400s, so check any change to `-X` ldflags paths or how the version is derived (#8677, #8539, #8865).
10. **Windows installer and service lifecycle** (`windows/packaging/exe/inno_setup.iss`; the service is radiance's `cmd/lanternd`, built through `LANTERND_SRC` in the `Makefile`). Lost legacy installs, ARM64 VCRedist detection, and service install, IPC startup, and child-recovery failures (#8576, #9127, #8623, #9015).
## Contracts a diff can silently break
- **Report-issue keys.** The dropdown keys in `lib/features/report_issue/report_issue.dart` must match `parseIssueType` in `lantern-core/core.go` and `assets/locales/en.po`. `TestIssueOptionsMatchParser` enforces this (#9023).
- **FFI.** `//export` signatures in `lantern-core/ffi/` must match the generated bindings. Dart callers free returned C strings.
- **Platform channels.** Method and event channel names (`org.getlantern.lantern/method`, `/status`, `/logs`, `/app_events`, โฆ) are string literals duplicated across `lib/lantern/lantern_platform_service.dart`, the Android handlers, and the iOS/macOS Runners. Nothing checks that they match.
- **radiance.** A radiance bump can change the IPC API and JSON shapes. On iOS the extension sends an empty device ID so radiance resolves the shared one (#8936). Smart-routing rule detours must match outbound tags (`ai-domains` vs `sr-ai-domains`).
- **Purchases** (`lib/core/utils/store_utils.dart`, `lib/core/services/purchase/`, `lib/features/plans/`). Payment routing follows the build channel, not the region: `resolvePlayBillingAvailability` offers Play Billing on every Android store build in every country, and non-store builds never get it (#9028). `store_utils_test.dart` covers this.