|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Operating context for AI agents (opencode, Cursor, Claude Code, etc.) |
| 4 | +working on this repository. Read this file first; it covers what the |
| 5 | +project is, how to build/verify it, and the conventions to follow. |
| 6 | + |
| 7 | +## What this repo is |
| 8 | + |
| 9 | +`GooseRelayVPN-AndroidClient` is the Android client for the |
| 10 | +GooseRelayVPN project. It wraps the upstream Go core (in `internal/`, |
| 11 | +shared with the server) in an Android `VpnService` and exposes a |
| 12 | +Jetpack Compose UI for VPN lifecycle, profile management, logs, and |
| 13 | +settings. |
| 14 | + |
| 15 | +Upstream Go core: <https://github.com/kianmhz/GooseRelayVPN> |
| 16 | +This client: <https://github.com/ArashAfkandeh/GooseRelayVPN-AndroidClient> |
| 17 | + |
| 18 | +## Architecture (Android side) |
| 19 | + |
| 20 | +``` |
| 21 | +┌─────────────────────────────────────────────┐ |
| 22 | +│ Android UI Layer (Compose) │ |
| 23 | +│ Home │ Profiles │ Settings │ Logs │ Info │ |
| 24 | +└─────────────────────────────────────────────┘ |
| 25 | + ↓ |
| 26 | +┌─────────────────────────────────────────────┐ |
| 27 | +│ ViewModels & Repository Layer │ |
| 28 | +│ Room │ DataStore │ Hilt DI │ |
| 29 | +└─────────────────────────────────────────────┘ |
| 30 | + ↓ |
| 31 | +┌─────────────────────────────────────────────┐ |
| 32 | +│ VPN Service & Go Core │ |
| 33 | +│ tun2socks │ GooseRelay Core (Go mobile) │ |
| 34 | +└─────────────────────────────────────────────┘ |
| 35 | + ↓ |
| 36 | +┌─────────────────────────────────────────────┐ |
| 37 | +│ Network Layer │ |
| 38 | +│ SOCKS5 → Google Apps Script → VPS exit │ |
| 39 | +└─────────────────────────────────────────────┘ |
| 40 | +``` |
| 41 | + |
| 42 | +- `android/` — Gradle project (`settings.gradle.kts`, `build.gradle.kts`, `app/`). Kotlin 2.1.0, |
| 43 | + AGP 8.13.0, `minSdk=21`, `targetSdk=36`, `compileSdk=36`, JVM 17. |
| 44 | +- `android/app/src/main/java/com/gooserelay/gooserelayvpn/` — Kotlin source: |
| 45 | + - `service/` — `GooseRelayVpnService` (the VpnService), |
| 46 | + `VpnTileService` (Quick Settings tile), `BootReceiver` (boot, |
| 47 | + currently a no-op). |
| 48 | + - `ui/` — Compose screens and ViewModels, split by feature |
| 49 | + (`home/`, `profiles/`, `settings/`, `logs/`, `info/`, |
| 50 | + `navigation/`). |
| 51 | + - `data/local/` — Room (`AppDatabase` v3, `ProfileEntity`, |
| 52 | + `ProfileDao`). |
| 53 | + - `data/repository/` — `ProfileRepository` (Hilt `@Singleton`). |
| 54 | + - `dns/` — Kotlin FakeDNS interceptor (separate from the Go-side |
| 55 | + FakeDNS in `mobile/tun/`). |
| 56 | + - `di/` — Hilt `AppModule`. |
| 57 | + - `util/` — `VpnManager` (singleton bridge between UI and Go |
| 58 | + core), `ConfigGenerator` (profile → JSON config), |
| 59 | + `GlobalSettingsStore` (DataStore-backed). |
| 60 | +- `mobile/` — Go mobile bridge, gomobile bind target: |
| 61 | + - `mobile/mobile.go` — `StartClient` / `StopClient` / |
| 62 | + `StartTun` / `StopTun` / `StartTunBridge` / `StopTunBridge` |
| 63 | + exported to Kotlin via `mobile.Mobile.*`. |
| 64 | + - `mobile/tun/` — FakeDNS proxy (`fakedns_proxy.go`), |
| 65 | + DNS mapper (`dns_mapper.go`), `tun_api.go` (exported |
| 66 | + `StartFakeDNSProxy` etc.). |
| 67 | +- `internal/` — upstream Go core (carrier, session, socks, config, |
| 68 | + etc.). **Treat as read-only when working on the Android client.** |
| 69 | +- `apps_script/` — Google Apps Script deployment that fronts the |
| 70 | + carrier traffic over Google infrastructure. |
| 71 | + |
| 72 | +## How to build |
| 73 | + |
| 74 | +```bash |
| 75 | +# 1. Build the Go mobile AAR (requires Go 1.25+, NDK installed). |
| 76 | +# On Windows: use build_go_mobile.bat; on Unix: build_go_mobile.sh. |
| 77 | +bash ./android/build_go_mobile.sh |
| 78 | +# Output: android/app/libs/gooserelayvpn.aar |
| 79 | + |
| 80 | +# 2. Build the debug APK. |
| 81 | +cd android |
| 82 | +./gradlew :app:assembleDebug |
| 83 | +# Output: android/app/build/outputs/apk/debug/GooseRelayVPN.apk |
| 84 | + |
| 85 | +# 3. Build the release AAB (requires signing config in local.properties). |
| 86 | +./gradlew :app:bundleRelease |
| 87 | +``` |
| 88 | + |
| 89 | +## How to verify |
| 90 | + |
| 91 | +```bash |
| 92 | +cd android |
| 93 | + |
| 94 | +# Unit tests (JVM, no emulator needed). |
| 95 | +./gradlew :app:testDebugUnitTest --stacktrace |
| 96 | + |
| 97 | +# Lint. |
| 98 | +./gradlew :app:lintDebug |
| 99 | + |
| 100 | +# Compile-only fast check. |
| 101 | +./gradlew :app:compileDebugKotlin --stacktrace |
| 102 | + |
| 103 | +# Go-side vet + format. |
| 104 | +go vet ./mobile/... |
| 105 | +gofmt -l mobile/ |
| 106 | +``` |
| 107 | + |
| 108 | +Instrumented tests (`./gradlew :app:connectedDebugAndroidTest`) |
| 109 | +require a connected emulator or device; CI does not run them. |
| 110 | + |
| 111 | +## Conventions |
| 112 | + |
| 113 | +### Kotlin |
| 114 | + |
| 115 | +- Style: `kotlin.code.style=official` (see |
| 116 | + `android/gradle.properties`). |
| 117 | +- ViewModels: `@HiltViewModel` + constructor injection. See |
| 118 | + `ui/profiles/ProfilesViewModel.kt` as the canonical example. |
| 119 | +- Singletons: `object` declarations for cross-cutting state |
| 120 | + (`VpnManager`, `ConfigGenerator`, `GlobalSettingsStore`). |
| 121 | +- Error handling: `runCatching { ... }.onFailure { ... }` for |
| 122 | + non-fatal failures; `try { ... } catch (_: Exception) {}` only |
| 123 | + for true background noise (e.g. closing sockets during shutdown). |
| 124 | +- Coroutines: `CoroutineScope(SupervisorJob() + Dispatchers.X)`. |
| 125 | + Use `withContext(Dispatchers.IO)` for thread hops; avoid nested |
| 126 | + `launch` blocks inside a coroutine. Guard with `isActive` after |
| 127 | + suspending operations. |
| 128 | +- Logging: `android.util.Log` for system logcat; `VpnManager.appendLog` |
| 129 | + for user-visible log lines (shown in the Logs screen, with a |
| 130 | + 2000-line ring buffer). **Never log credentials** — |
| 131 | + `ProfileEntity.socksPass`, `tunnelKey`, `scriptKeysText` (and their |
| 132 | + JSON-serialized forms in `ConfigGenerator.exportProfileJson`) must |
| 133 | + not appear in logs. |
| 134 | + |
| 135 | +### Go (in `mobile/`) |
| 136 | + |
| 137 | +- `gofmt`-clean; `go vet ./mobile/...` returns no findings. |
| 138 | +- Exported functions PascalCase (gomobile convention). Logger uses |
| 139 | + `log.Printf("[prefix] ...")` with brackets, e.g. `[TUN-API]`, |
| 140 | + `[client]`, `[socks]`. |
| 141 | +- Mutex discipline: `mu` guards app state (`running`, `tunActive`, |
| 142 | + `tunBridgeRunning`, `cancelFn`, `socksLn`, `clientDone`). `engineMu` |
| 143 | + guards the tun2socks engine Start/Stop. Take them in the same |
| 144 | + order every time to avoid deadlocks. |
| 145 | +- Errors wrapped with `fmt.Errorf("...: %w", err)`. |
| 146 | + |
| 147 | +### Build / CI |
| 148 | + |
| 149 | +- Commit style: `type(scope): subject` (conventional commits). |
| 150 | + Examples from `git log`: `fix(android): ...`, |
| 151 | + `feat(android): ...`, `perf(android): ...`, `style(ui): ...`, |
| 152 | + `chore(mobile): ...`, `docs(readme): ...`, `ci(android): ...`. |
| 153 | +- Don't push or open PRs unless the operator instructed it. |
| 154 | +- Don't commit secrets. Signing keys live in |
| 155 | + `$ANDROID_KEYSTORE_PATH` env var or in CI secrets. |
| 156 | + |
| 157 | +## Things that are easy to get wrong |
| 158 | + |
| 159 | +- **Credentials in logs.** Before adding any |
| 160 | + `VpnManager.appendLog(...)` or `Log.d(...)` call that includes |
| 161 | + profile data, redact `socksUser`, `socksPass`, `tunnelKey`, and |
| 162 | + `scriptKeysText`. The exported JSON config object contains all |
| 163 | + four. |
| 164 | +- **`fallbackToDestructiveMigration`.** Don't add new Room schema |
| 165 | + versions without a real `Migration` entry in |
| 166 | + `ProfileMigrations.ALL` (see `data/local/ProfileMigrations.kt`). |
| 167 | + A missing migration will crash the app at launch for any user |
| 168 | + whose DB schema version is lower than the new one. |
| 169 | +- **`engine.Stop()` panics.** The `recover()` blocks in |
| 170 | + `mobile.go`'s `StopTun`/`StopTunBridge` are load-bearing — they |
| 171 | + mask a tun2socks panic on stop that previously SIGSEGV'd the app |
| 172 | + (commit `41c3eef`). Don't remove them. |
| 173 | +- **`network_security_config.xml` allows cleartext globally.** |
| 174 | + Tightening this is on the roadmap; don't add new `http://` |
| 175 | + fetches to production paths without revisiting this config. |
| 176 | +- **Go mobile rebuild.** After any `mobile/*.go` change, run |
| 177 | + `bash ./android/build_go_mobile.sh` *before* `./gradlew :app:assembleDebug` |
| 178 | + — the Gradle build loads `android/app/libs/gooserelayvpn.aar` as a |
| 179 | + file dependency and will silently test against a stale AAR if the |
| 180 | + AAR isn't refreshed. |
| 181 | + |
| 182 | +## Running the plans in `plans/` |
| 183 | + |
| 184 | +The `plans/` directory contains self-contained implementation plans. |
| 185 | +Each plan's filename is `NNN-short-slug.md`; `plans/README.md` is the |
| 186 | +index (priority order, dependencies, status). Read the full plan |
| 187 | +before starting, honor the STOP conditions, update the status row in |
| 188 | +`plans/README.md` when done. Don't improvise when reality doesn't |
| 189 | +match the plan — report back to the operator. |
0 commit comments