Operating context for AI agents (opencode, Cursor, Claude Code, etc.) working on this repository. Read this file first; it covers what the project is, how to build/verify it, and the conventions to follow.
GooseRelayVPN-AndroidClient is the Android client for the
GooseRelayVPN project. It wraps the upstream Go core (in internal/,
shared with the server) in an Android VpnService and exposes a
Jetpack Compose UI for VPN lifecycle, profile management, logs, and
settings.
Upstream Go core: https://github.com/kianmhz/GooseRelayVPN This client: https://github.com/ArashAfkandeh/GooseRelayVPN-AndroidClient
┌─────────────────────────────────────────────┐
│ Android UI Layer (Compose) │
│ Home │ Profiles │ Settings │ Logs │ Info │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ ViewModels & Repository Layer │
│ Room │ DataStore │ Hilt DI │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ VPN Service & Go Core │
│ tun2socks │ GooseRelay Core (Go mobile) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Network Layer │
│ SOCKS5 → Google Apps Script → VPS exit │
└─────────────────────────────────────────────┘
android/— Gradle project (settings.gradle.kts,build.gradle.kts,app/). Kotlin 2.1.0, AGP 8.13.0,minSdk=21,targetSdk=36,compileSdk=36, JVM 17.android/app/src/main/java/com/gooserelay/gooserelayvpn/— Kotlin source:service/—GooseRelayVpnService(the VpnService),VpnTileService(Quick Settings tile),BootReceiver(boot, currently a no-op).ui/— Compose screens and ViewModels, split by feature (home/,profiles/,settings/,logs/,info/,navigation/).data/local/— Room (AppDatabasev3,ProfileEntity,ProfileDao).data/repository/—ProfileRepository(Hilt@Singleton).dns/— Kotlin FakeDNS interceptor (separate from the Go-side FakeDNS inmobile/tun/).di/— HiltAppModule.util/—VpnManager(singleton bridge between UI and Go core),ConfigGenerator(profile → JSON config),GlobalSettingsStore(DataStore-backed).
mobile/— Go mobile bridge, gomobile bind target:mobile/mobile.go—StartClient/StopClient/StartTun/StopTun/StartTunBridge/StopTunBridgeexported to Kotlin viamobile.Mobile.*.mobile/tun/— FakeDNS proxy (fakedns_proxy.go), DNS mapper (dns_mapper.go),tun_api.go(exportedStartFakeDNSProxyetc.).
internal/— upstream Go core (carrier, session, socks, config, etc.). Treat as read-only when working on the Android client.apps_script/— Google Apps Script deployment that fronts the carrier traffic over Google infrastructure.
# 1. Build the Go mobile AAR (requires Go 1.25+, NDK installed).
# On Windows: use build_go_mobile.bat; on Unix: build_go_mobile.sh.
bash ./android/build_go_mobile.sh
# Output: android/app/libs/gooserelayvpn.aar
# 2. Build the debug APK.
cd android
./gradlew :app:assembleDebug
# Output: android/app/build/outputs/apk/debug/GooseRelayVPN.apk
# 3. Build the release AAB (requires signing config in local.properties).
./gradlew :app:bundleReleasecd android
# Unit tests (JVM, no emulator needed).
./gradlew :app:testDebugUnitTest --stacktrace
# Lint.
./gradlew :app:lintDebug
# Compile-only fast check.
./gradlew :app:compileDebugKotlin --stacktrace
# Go-side vet + format.
go vet ./mobile/...
gofmt -l mobile/Instrumented tests (./gradlew :app:connectedDebugAndroidTest)
require a connected emulator or device; CI does not run them.
- Style:
kotlin.code.style=official(seeandroid/gradle.properties). - ViewModels:
@HiltViewModel+ constructor injection. Seeui/profiles/ProfilesViewModel.ktas the canonical example. - Singletons:
objectdeclarations for cross-cutting state (VpnManager,ConfigGenerator,GlobalSettingsStore). - Error handling:
runCatching { ... }.onFailure { ... }for non-fatal failures;try { ... } catch (_: Exception) {}only for true background noise (e.g. closing sockets during shutdown). - Coroutines:
CoroutineScope(SupervisorJob() + Dispatchers.X). UsewithContext(Dispatchers.IO)for thread hops; avoid nestedlaunchblocks inside a coroutine. Guard withisActiveafter suspending operations. - Logging:
android.util.Logfor system logcat;VpnManager.appendLogfor user-visible log lines (shown in the Logs screen, with a 2000-line ring buffer). Never log credentials —ProfileEntity.socksPass,tunnelKey,scriptKeysText(and their JSON-serialized forms inConfigGenerator.exportProfileJson) must not appear in logs.
gofmt-clean;go vet ./mobile/...returns no findings.- Exported functions PascalCase (gomobile convention). Logger uses
log.Printf("[prefix] ...")with brackets, e.g.[TUN-API],[client],[socks]. - Mutex discipline:
muguards app state (running,tunActive,tunBridgeRunning,cancelFn,socksLn,clientDone).engineMuguards the tun2socks engine Start/Stop. Take them in the same order every time to avoid deadlocks. - Errors wrapped with
fmt.Errorf("...: %w", err).
- Commit style:
type(scope): subject(conventional commits). Examples fromgit log:fix(android): ...,feat(android): ...,perf(android): ...,style(ui): ...,chore(mobile): ...,docs(readme): ...,ci(android): .... - Don't push or open PRs unless the operator instructed it.
- Don't commit secrets. Signing keys live in
$ANDROID_KEYSTORE_PATHenv var or in CI secrets.
- Credentials in logs. Before adding any
VpnManager.appendLog(...)orLog.d(...)call that includes profile data, redactsocksUser,socksPass,tunnelKey, andscriptKeysText. The exported JSON config object contains all four. fallbackToDestructiveMigration. Don't add new Room schema versions without a realMigrationentry inProfileMigrations.ALL(seedata/local/ProfileMigrations.kt). A missing migration will crash the app at launch for any user whose DB schema version is lower than the new one.engine.Stop()panics. Therecover()blocks inmobile.go'sStopTun/StopTunBridgeare load-bearing — they mask a tun2socks panic on stop that previously SIGSEGV'd the app (commit41c3eef). Don't remove them.network_security_config.xmlallows cleartext globally. Tightening this is on the roadmap; don't add newhttp://fetches to production paths without revisiting this config.- Go mobile rebuild. After any
mobile/*.gochange, runbash ./android/build_go_mobile.shbefore./gradlew :app:assembleDebug— the Gradle build loadsandroid/app/libs/gooserelayvpn.aaras a file dependency and will silently test against a stale AAR if the AAR isn't refreshed.
The plans/ directory contains self-contained implementation plans.
Each plan's filename is NNN-short-slug.md; plans/README.md is the
index (priority order, dependencies, status). Read the full plan
before starting, honor the STOP conditions, update the status row in
plans/README.md when done. Don't improvise when reality doesn't
match the plan — report back to the operator.