Loading...
Loading...
Android VPN client that tunnels SOCKS5 traffic through Google Apps Script to a VPS exit server with AES-256-GCM encryption and domain fronting
npx skill4agent add aradotso/devtools-skills gooserelayvpn-android-clientSkill by ara.so — Devtools Skills collection.
goose-serverapps_script/Code.gsscripts/gen-key.shgomobile# From project root
bash android/build_go_mobile.shgomobileandroid/app/libs/gooserelay.aargo install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
gomobile bind \
-target=android \
-androidapi=21 \
-o android/app/libs/gooserelay.aar \
-v \
./mobilecd android
./gradlew :app:assembleDebug
# Output: android/app/build/outputs/apk/debug/app-debug.apkcd android
./gradlew :app:assembleRelease
# Requires signing configuration in android/app/build.gradle{
"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 | Type | Description |
|---|---|---|
| bool | Enable timing debug logs |
| string | Local SOCKS5 bind address (usually 127.0.0.1) |
| int | Local SOCKS5 port (default 1080) |
| string | Google IP for domain fronting (216.239.38.120 is common) |
| []string | SNI hostnames for TLS handshake rotation |
| []string | Apps Script deployment IDs (one or more for redundancy) |
| string | Base64 AES-256 key (must match server-side key) |
# From GooseRelayVPN main repo
bash scripts/gen-key.sh
# Outputs base64-encoded key
# Example: a3d7f9e2b1c4...tunnel_keygoose-serverVpnService// 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
)
}
}<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>gomobilepackage 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)
}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
}
}
}// 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)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)// 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
}// 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 writerdata 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"
}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.apkname: 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.apkANDROID_KEYSTORE_BASE64ANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORDscript_keystunnel_key# 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// 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()
}// 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()# 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";{
"debug_timing": false,
"sni": [
"www.google.com"
],
"script_keys": [
"PRIMARY_DEPLOYMENT_ID",
"BACKUP_DEPLOYMENT_ID"
]
}script_keysgoogle_hostdebug_timingbuild_go_mobile.sh# 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// 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 testingtunnel_keyscript_keysdebug_timing: truegoose-serverapps_script/Code.gsserver/