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
gomobiletool
Build Go Mobile AAR
The core GooseRelay logic is compiled to an Android AAR library:
# From project root
bash android/build_go_mobile.sh
This script:
- Installs
gomobileif 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
# Output: android/app/build/outputs/apk/debug/app-debug.apk
Build Release APK
cd android
./gradlew :app:assembleRelease
# Requires signing configuration in android/app/build.gradle
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:
# From GooseRelayVPN main repo
bash scripts/gen-key.sh
# Outputs base64-encoded key
# Example: a3d7f9e2b1c4...
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:
// Simplified example from Android codebase
class GooseVpnService : VpnService() {
private val socksHost = "127.0.0.1"
private val socksPort = 1080
fun startVpn(profile: Profile) {
// 1. Start GooseRelay core (via JNI to Go AAR)
GooseRelay.start(profile.toJson())
// 2. Configure VPN interface
val builder = Builder()
.setSession("GooseRelayVPN")
.addAddress("10.0.0.2", 24)
.addRoute("0.0.0.0", 0)
.addDnsServer("8.8.8.8")
// 3. Exclude apps if split tunneling enabled
if (profile.splitTunnel) {
profile.allowedApps.forEach { pkg ->
builder.addAllowedApplication(pkg)
}
}
val vpnInterface = builder.establish()
// 4. Start tun2socks to forward traffic to SOCKS5
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"
)
// Config matches Android profile JSON structure
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
// Start initializes and starts the GooseRelay 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()
}
// Stop gracefully stops the relay client
func Stop() error {
if relayClient != nil {
return relayClient.Stop()
}
return nil
}
// GetStats returns JSON statistics
func GetStats() string {
if relayClient == nil {
return "{}"
}
stats := relayClient.GetStats()
data, _ := json.Marshal(stats)
return string(data)
}
Calling from Android (Kotlin)
import gooserelay.Gooserelay // Generated from AAR
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:
// In profile configuration
val profile = Profile(
name = "Full VPN",
socksPort = 1080,
scriptKeys = listOf(System.getenv("APPS_SCRIPT_DEPLOYMENT_ID")),
tunnelKey = System.getenv("GOOSE_TUNNEL_KEY"),
splitTunnel = false, // All apps
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):
// Start only the SOCKS5 server, no VPN
fun startSocksOnly(profile: Profile) {
GooseRelay.start(profile.toJson())
// Apps must be configured to use 127.0.0.1:1080 as SOCKS5 proxy
}
Logging and Telemetry
Real-time Logs
The app provides a Logs tab showing Android and Go core logs:
// Android side logger forwarding to UI
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
))
// Notify UI observers
notifyLogListeners()
}
}
// Go side: logs are captured via custom writer
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)
// Update UI cards
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
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-debug
path: android/app/build/outputs/apk/debug/app-debug.apk
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
uses: softprops/action-gh-release@v1
with:
files: android/app/build/outputs/apk/release/app-release.apk
Required secrets:
ANDROID_KEYSTORE_BASE64: Base64-encoded keystore fileANDROID_KEYSTORE_PASSWORD: Keystore passwordANDROID_KEY_ALIAS: Key alias in keystoreANDROID_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_keybetween client and server - Apps Script deployment not accessible
Solution:
# Check logs tab in app for specific errors
# Common log patterns:
# "Failed to connect to script" → Check deployment ID
# "Decryption failed" → Check tunnel_key matches server
# "Connection timeout" → Verify VPS server is running
# Verify server-side key matches:
# On VPS: cat /etc/goose-server/config.json | jq .tunnel_key
# In Android profile: check tunnel_key field
SOCKS Port Busy Error
Symptoms: "bind: address already in use" in logs.
Solution:
// Ensure clean disconnect before reconnect
fun reconnect() {
// Stop VPN service
vpnService.stop()
// Wait for port release
Thread.sleep(2000)
// Check no other app uses port 1080
// Change profile socks_port if needed
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:
// Verify VPN builder configuration
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")
// Exclude local network
if (profile.excludeLocalNetwork) {
builder.addRoute("0.0.0.0", 5)
builder.addRoute("8.0.0.0", 7)
builder.addRoute("11.0.0.0", 8)
// ... exclude 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
}
val vpnInterface = builder.establish()
Apps Script Deployment Issues
Symptoms: "Script execution failed" in logs.
Solution:
# Verify deployment ID format (should be long alphanumeric string)
# Example: AKfycbzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# Test deployment manually:
curl -L "https://script.google.com/macros/s/DEPLOYMENT_ID/exec?action=ping"
# Ensure Apps Script has correct VPS endpoint in Code.gs:
# const VPS_ENDPOINT = "https://your-vps-ip:8443";
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_keysfor load balancing - Choose closer Google IP in
google_host - Optimize VPS server configuration
- Enable
debug_timingtemporarily to identify bottlenecks
AAR Build Failures
Symptoms: build_go_mobile.sh fails.
Solution:
# Ensure gomobile is installed
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
# Check Android NDK is installed
# In Android Studio: Tools → SDK Manager → SDK Tools → NDK
# Set environment variables
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/25.1.8937393
# Retry build
bash android/build_go_mobile.sh
Certificate/TLS Errors
Symptoms: "x509: certificate signed by unknown authority"
Solution:
// If using self-signed cert on VPS, Apps Script must trust it
// Better: Use Let's Encrypt on VPS
// In Code.gs, configure:
const ALLOW_SELF_SIGNED = false; // Set true only for testing
Best Practices
- Key Management: Store
tunnel_keyin Android Keystore, not plaintext - Multiple Deployments: Use 2-3
script_keysfor redundancy - Battery Optimization: Exclude VPN service from battery optimization
- Testing: Test with
debug_timing: trueduring setup - Backup Profiles: Export profiles before app updates
- Server Monitoring: Monitor VPS
goose-serverlogs alongside Android logs
Additional Resources
- Main project: https://github.com/kianmhz/GooseRelayVPN
- Apps Script setup: See upstream
apps_script/Code.gs - Server setup: See upstream
server/directory - Issues: Check open issues at https://github.com/Hidden-Node/GooseRelayVPN-AndroidClient/issues