| name | gooserelayvpn-android-client |
| description | Android VPN client that tunnels SOCKS5 traffic through Google Apps Script to a VPS exit server with AES-256-GCM encryption and domain fronting |
| triggers | ["how do I build the GooseRelayVPN Android client","configure GooseRelay Android VPN profile","setup Android VPN with Google Apps Script tunnel","GooseRelayVPN Android profile JSON format","build GooseRelay mobile AAR library","troubleshoot GooseRelay Android connection","import GooseRelay VPN profile on Android","setup domain-fronted VPN on Android"] |
GooseRelayVPN Android Client Skill
Skill by ara.so — Devtools Skills collection.
Overview
GooseRelayVPN Android Client is an Android application that provides a VPN service tunneling TCP traffic through Google Apps Script to a VPS exit server. It uses:
- Local SOCKS5 proxy for app/browser traffic
- AES-256-GCM encryption for tunnel security
- Domain fronting via Google infrastructure (Apps Script)
- Android VpnService integration with tun2socks
- Profile-based configuration with JSON import/export
Architecture flow:
- Android app traffic → SOCKS5 (127.0.0.1:1080)
- GooseRelay core encrypts with AES key
- HTTPS transport through Google Apps Script deployment
- VPS exit server decrypts and forwards to target
Prerequisites
Before using the Android client, you must set up upstream infrastructure:
- VPS server running
goose-server (from main GooseRelayVPN project)
- Google Apps Script deployment with
apps_script/Code.gs
- Tunnel encryption key generated via
scripts/gen-key.sh
- Deployment ID(s) from Apps Script
Upstream project: https://github.com/kianmhz/GooseRelayVPN
Building the Android Client
Requirements
- Android Studio
- JDK 17
- Go 1.22+
- Android SDK with NDK
gomobile tool
Build Go Mobile AAR
The core GooseRelay logic is compiled to an Android AAR library:
bash android/build_go_mobile.sh
This script:
- Installs
gomobile if needed
- Compiles Go code to AAR for arm64-v8a, armeabi-v7a, x86, x86_64
- Outputs to
android/app/libs/gooserelay.aar
Manual AAR build:
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
gomobile bind \
-target=android \
-androidapi=21 \
-o android/app/libs/gooserelay.aar \
-v \
./mobile
Build Debug APK
cd android
./gradlew :app:assembleDebug
Build Release APK
cd android
./gradlew :app:assembleRelease
Profile Configuration
Profile JSON Structure
{
"debug_timing": false,
"socks_host": "127.0.0.1",
"socks_port": 1080,
"google_host": "216.239.38.120",
"sni": [
"www.google.com",
"mail.google.com",
"accounts.google.com"
],
"script_keys": [
"DEPLOYMENT_ID_FROM_APPS_SCRIPT",
"OPTIONAL_SECOND_DEPLOYMENT_ID"
],
"tunnel_key": "BASE64_ENCODED_AES_KEY_FROM_GEN_KEY_SCRIPT"
}
Field Descriptions
| Field | Type | Description |
|---|
debug_timing | bool | Enable timing debug logs |
socks_host | string | Local SOCKS5 bind address (usually 127.0.0.1) |
socks_port | int | Local SOCKS5 port (default 1080) |
google_host | string | Google IP for domain fronting (216.239.38.120 is common) |
sni | []string | SNI hostnames for TLS handshake rotation |
script_keys | []string | Apps Script deployment IDs (one or more for redundancy) |
tunnel_key | string | Base64 AES-256 key (must match server-side key) |
Generating Tunnel Key
Use the upstream script to generate a secure key:
bash scripts/gen-key.sh
Important: The same tunnel_key must be used on both the Android client and the VPS goose-server.
In-App Profile Management
Create new profile:
- Open app → Profiles tab
- Tap "+" button
- Enter profile name and configuration
- Save
Import profile from JSON:
- Profiles tab → menu → Import
- Select JSON file from storage
- Profile is added to list
Export profile to JSON:
- Profiles tab → long-press profile
- Select Export
- JSON saved to Downloads
Android VPN Service Integration
VpnService Implementation
The app uses Android's VpnService API to capture device traffic:
class GooseVpnService : VpnService() {
private val socksHost = "127.0.0.1"
private val socksPort = 1080
fun startVpn(profile: Profile) {
GooseRelay.start(profile.toJson())
val builder = Builder()
.setSession("GooseRelayVPN")
.addAddress("10.0.0.2", 24)
.addRoute("0.0.0.0", 0)
.addDnsServer("8.8.8.8")
if (profile.splitTunnel) {
profile.allowedApps.forEach { pkg ->
builder.addAllowedApplication(pkg)
}
}
val vpnInterface = builder.establish()
Tun2Socks.start(
vpnInterface.fd,
socksHost,
socksPort
)
}
}
Permissions Required
AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<service
android:name=".service.GooseVpnService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>
Go Mobile Bridge
The Go core is exposed to Android via gomobile:
Mobile Package Interface
mobile/mobile.go:
package mobile
import (
"encoding/json"
"github.com/Hidden-Node/GooseRelayVPN/client"
)
type Config struct {
DebugTiming bool `json:"debug_timing"`
SocksHost string `json:"socks_host"`
SocksPort int `json:"socks_port"`
GoogleHost string `json:"google_host"`
SNI []string `json:"sni"`
ScriptKeys []string `json:"script_keys"`
TunnelKey string `json:"tunnel_key"`
}
var relayClient *client.Client
func Start(configJSON string) error {
var cfg Config
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
return err
}
relayClient = client.NewClient(client.Config{
DebugTiming: cfg.DebugTiming,
SocksAddr: fmt.Sprintf("%s:%d", cfg.SocksHost, cfg.SocksPort),
GoogleHost: cfg.GoogleHost,
SNI: cfg.SNI,
ScriptKeys: cfg.ScriptKeys,
TunnelKey: cfg.TunnelKey,
})
return relayClient.Start()
}
func Stop() error {
if relayClient != nil {
relayClient.Stop()
}
}
{
relayClient == {
}
stats := relayClient.GetStats()
data, _ := json.Marshal(stats)
(data)
}
Calling from Android (Kotlin)
import gooserelay.Gooserelay
class RelayManager {
fun startRelay(profile: Profile) {
val configJson = profile.toJson()
try {
Gooserelay.start(configJson)
Log.i("GooseRelay", "Started successfully")
} catch (e: Exception) {
Log.e("GooseRelay", "Start failed: ${e.message}")
}
}
fun stopRelay() {
try {
Gooserelay.stop()
} catch (e: Exception) {
Log.e("GooseRelay", "Stop failed: ${e.message}")
}
}
fun getStats(): Stats? {
return try {
val json = Gooserelay.getStats()
Json.decodeFromString<Stats>(json)
} catch (e: Exception) {
null
}
}
}
Common Usage Patterns
Full Device VPN
Route all device traffic through the tunnel:
val profile = Profile(
name = "Full VPN",
socksPort = 1080,
scriptKeys = listOf(System.getenv("APPS_SCRIPT_DEPLOYMENT_ID")),
tunnelKey = System.getenv("GOOSE_TUNNEL_KEY"),
splitTunnel = false,
excludeLocalNetwork = true
)
vpnService.startVpn(profile)
Split Tunneling
Route only specific apps through the tunnel:
val profile = Profile(
name = "Split Tunnel",
socksPort = 1080,
scriptKeys = listOf(System.getenv("APPS_SCRIPT_DEPLOYMENT_ID")),
tunnelKey = System.getenv("GOOSE_TUNNEL_KEY"),
splitTunnel = true,
allowedApps = listOf(
"com.android.chrome",
"org.telegram.messenger"
)
)
vpnService.startVpn(profile)
Manual SOCKS5 Proxy
Use without VpnService (manual app configuration):
fun startSocksOnly(profile: Profile) {
GooseRelay.start(profile.toJson())
}
Logging and Telemetry
Real-time Logs
The app provides a Logs tab showing Android and Go core logs:
object LogCollector {
private val logs = mutableListOf<LogEntry>()
fun addLog(level: String, tag: String, message: String) {
logs.add(LogEntry(
timestamp = System.currentTimeMillis(),
level = level,
tag = tag,
message = message
))
notifyLogListeners()
}
}
Telemetry Stats
Real-time connection statistics:
data class TelemetryStats(
val bytesUploaded: Long,
val bytesDownloaded: Long,
val activeConnections: Int,
val successRate: Float,
val avgLatency: Long
)
fun updateTelemetry() {
val statsJson = Gooserelay.getStats()
val stats = Json.decodeFromString<TelemetryStats>(statsJson)
uploadCard.text = formatBytes(stats.bytesUploaded)
downloadCard.text = formatBytes(stats.bytesDownloaded)
latencyCard.text = "${stats.avgLatency}ms"
}
CI/CD and Release
GitHub Actions Workflows
Debug CI (.github/workflows/android-ci.yml):
name: Android CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Build Go Mobile AAR
run: bash android/build_go_mobile.sh
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build Debug APK
run: |
cd android
./gradlew assembleDebug
Release Workflow (.github/workflows/release.yml):
name: Release Build
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build AAR
run: bash android/build_go_mobile.sh
- name: Decode Keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/keystore.jks
- name: Build Release APK
env:
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
cd android
./gradlew assembleRelease
- name: Create Release
Required secrets:
ANDROID_KEYSTORE_BASE64: Base64-encoded keystore file
ANDROID_KEYSTORE_PASSWORD: Keystore password
ANDROID_KEY_ALIAS: Key alias in keystore
ANDROID_KEY_PASSWORD: Key password
Troubleshooting
Connection Stuck in "Preparing" State
Symptoms: VPN shows "Preparing" but never connects.
Causes:
- Invalid
script_keys (deployment IDs)
- Mismatched
tunnel_key between client and server
- Apps Script deployment not accessible
Solution:
SOCKS Port Busy Error
Symptoms: "bind: address already in use" in logs.
Solution:
fun reconnect() {
vpnService.stop()
Thread.sleep(2000)
profile.socksPort = 1081
vpnService.start()
}
No Traffic Flowing Through VPN
Symptoms: VPN connected but no internet access.
Checklist:
- VPN permission granted
- Split tunnel app selection correct
- DNS servers configured
- Local network excluded if needed
Solution:
val builder = Builder()
.addAddress("10.0.0.2", 24)
.addRoute("0.0.0.0", 0)
.addDnsServer("8.8.8.8")
.addDnsServer("1.1.1.1")
if (profile.excludeLocalNetwork) {
builder.addRoute("0.0.0.0", 5)
builder.addRoute("8.0.0.0", 7)
builder.addRoute("11.0.0.0", 8)
}
val vpnInterface = builder.establish()
Apps Script Deployment Issues
Symptoms: "Script execution failed" in logs.
Solution:
curl -L "https://script.google.com/macros/s/DEPLOYMENT_ID/exec?action=ping"
High Latency or Slow Speeds
Symptoms: Connected but slow performance.
Optimization:
{
"debug_timing": false,
"sni": [
"www.google.com"
],
"script_keys": [
"PRIMARY_DEPLOYMENT_ID",
"BACKUP_DEPLOYMENT_ID"
]
}
Tips:
- Use multiple
script_keys for load balancing
- Choose closer Google IP in
google_host
- Optimize VPS server configuration
- Enable
debug_timing temporarily to identify bottlenecks
AAR Build Failures
Symptoms: build_go_mobile.sh fails.
Solution:
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/25.1.8937393
bash android/build_go_mobile.sh
Certificate/TLS Errors
Symptoms: "x509: certificate signed by unknown authority"
Solution:
const ALLOW_SELF_SIGNED = false;
Best Practices
- Key Management: Store
tunnel_key in Android Keystore, not plaintext
- Multiple Deployments: Use 2-3
script_keys for redundancy
- Battery Optimization: Exclude VPN service from battery optimization
- Testing: Test with
debug_timing: true during setup
- Backup Profiles: Export profiles before app updates
- Server Monitoring: Monitor VPS
goose-server logs alongside Android logs
Additional Resources