gooserelayvpn-android-client

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GooseRelayVPN Android Client Skill

GooseRelayVPN Android客户端技能

Skill by ara.so — Devtools Skills collection.
ara.so提供的技能 — 开发工具技能合集。

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:
  1. Android app traffic → SOCKS5 (127.0.0.1:1080)
  2. GooseRelay core encrypts with AES key
  3. HTTPS transport through Google Apps Script deployment
  4. VPS exit server decrypts and forwards to target
GooseRelayVPN Android客户端是一款提供VPN服务的Android应用,可通过Google Apps Script将TCP流量隧道传输至VPS出口服务器。它采用以下技术:
  • 本地SOCKS5代理:用于应用/浏览器流量
  • AES-256-GCM加密:保障隧道安全
  • 域名前置:通过Google基础设施(Apps Script)实现
  • Android VpnService集成:搭配tun2socks使用
  • 基于配置文件的设置:支持JSON导入/导出
架构流程:
  1. Android应用流量 → SOCKS5(127.0.0.1:1080)
  2. GooseRelay核心模块使用AES密钥加密流量
  3. 通过Google Apps Script部署的HTTPS传输通道
  4. VPS出口服务器解密并转发至目标地址

Prerequisites

前提条件

Before using the Android client, you must set up upstream infrastructure:
  1. VPS server running
    goose-server
    (from main GooseRelayVPN project)
  2. Google Apps Script deployment with
    apps_script/Code.gs
  3. Tunnel encryption key generated via
    scripts/gen-key.sh
  4. Deployment ID(s) from Apps Script
使用Android客户端前,您必须先搭建上游基础设施:
  1. VPS服务器:运行
    goose-server
    (来自GooseRelayVPN主项目)
  2. Google Apps Script部署:配置
    apps_script/Code.gs
  3. 隧道加密密钥:通过
    scripts/gen-key.sh
    生成
  4. 部署ID:来自Apps Script的部署标识

Building the Android Client

构建Android客户端

Requirements

环境要求

  • Android Studio
  • JDK 17
  • Go 1.22+
  • Android SDK with NDK
  • gomobile
    tool
  • Android Studio
  • JDK 17
  • Go 1.22+
  • 包含NDK的Android SDK
  • gomobile
    工具

Build Go Mobile AAR

构建Go Mobile AAR

The core GooseRelay logic is compiled to an Android AAR library:
bash
undefined
GooseRelay核心逻辑会被编译为Android AAR库:
bash
undefined

From project root

从项目根目录执行

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:**

```bash
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
bash android/build_go_mobile.sh

该脚本会:
- 按需安装`gomobile`
- 将Go代码编译为支持arm64-v8a、armeabi-v7a、x86、x86_64架构的AAR
- 输出至`android/app/libs/gooserelay.aar`

**手动构建AAR:**

```bash
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

构建Debug APK

bash
cd android
./gradlew :app:assembleDebug
bash
cd android
./gradlew :app:assembleDebug

Output: android/app/build/outputs/apk/debug/app-debug.apk

输出路径: android/app/build/outputs/apk/debug/app-debug.apk

undefined
undefined

Build Release APK

构建Release APK

bash
cd android
./gradlew :app:assembleRelease
bash
cd android
./gradlew :app:assembleRelease

Requires signing configuration in android/app/build.gradle

需要在android/app/build.gradle中配置签名信息

undefined
undefined

Profile Configuration

配置文件设置

Profile JSON Structure

配置文件JSON结构

json
{
  "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"
}
json
{
  "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

字段说明

FieldTypeDescription
debug_timing
boolEnable timing debug logs
socks_host
stringLocal SOCKS5 bind address (usually 127.0.0.1)
socks_port
intLocal SOCKS5 port (default 1080)
google_host
stringGoogle IP for domain fronting (216.239.38.120 is common)
sni
[]stringSNI hostnames for TLS handshake rotation
script_keys
[]stringApps Script deployment IDs (one or more for redundancy)
tunnel_key
stringBase64 AES-256 key (must match server-side key)
字段类型描述
debug_timing
bool启用计时调试日志
socks_host
string本地SOCKS5绑定地址(通常为127.0.0.1)
socks_port
int本地SOCKS5端口(默认1080)
google_host
string用于域名前置的Google IP(常用216.239.38.120)
sni
[]stringTLS握手轮换使用的SNI主机名
script_keys
[]stringApps Script部署ID(可配置多个实现冗余)
tunnel_key
stringBase64编码的AES-256密钥(必须与服务器端密钥一致)

Generating Tunnel Key

生成隧道密钥

Use the upstream script to generate a secure key:
bash
undefined
使用上游脚本生成安全密钥:
bash
undefined

From GooseRelayVPN main repo

从GooseRelayVPN主仓库执行

bash scripts/gen-key.sh
bash scripts/gen-key.sh

Outputs base64-encoded key

输出Base64编码的密钥

Example: a3d7f9e2b1c4...

示例: a3d7f9e2b1c4...


**Important:** The same `tunnel_key` must be used on both the Android client and the VPS `goose-server`.

**重要提示:** Android客户端和VPS上的`goose-server`必须使用相同的`tunnel_key`。

In-App Profile Management

应用内配置文件管理

Create new profile:
  1. Open app → Profiles tab
  2. Tap "+" button
  3. Enter profile name and configuration
  4. Save
Import profile from JSON:
  1. Profiles tab → menu → Import
  2. Select JSON file from storage
  3. Profile is added to list
Export profile to JSON:
  1. Profiles tab → long-press profile
  2. Select Export
  3. JSON saved to Downloads
创建新配置文件:
  1. 打开应用 → 配置文件标签页
  2. 点击"+"按钮
  3. 输入配置文件名称和参数
  4. 保存
从JSON导入配置文件:
  1. 配置文件标签页 → 菜单 → 导入
  2. 从存储中选择JSON文件
  3. 配置文件将被添加至列表
导出配置文件为JSON:
  1. 配置文件标签页 → 长按目标配置文件
  2. 选择导出
  3. JSON文件将保存至下载目录

Android VPN Service Integration

Android VPN服务集成

VpnService Implementation

VpnService实现

The app uses Android's
VpnService
API to capture device traffic:
kotlin
// 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
        )
    }
}
应用使用Android的
VpnService
API捕获设备流量:
kotlin
// 来自Android代码库的简化示例
class GooseVpnService : VpnService() {
    private val socksHost = "127.0.0.1"
    private val socksPort = 1080
    
    fun startVpn(profile: Profile) {
        // 1. 启动GooseRelay核心(通过JNI调用Go AAR)
        GooseRelay.start(profile.toJson())
        
        // 2. 配置VPN接口
        val builder = Builder()
            .setSession("GooseRelayVPN")
            .addAddress("10.0.0.2", 24)
            .addRoute("0.0.0.0", 0)
            .addDnsServer("8.8.8.8")
        
        // 3. 如果启用拆分隧道则排除指定应用
        if (profile.splitTunnel) {
            profile.allowedApps.forEach { pkg ->
                builder.addAllowedApplication(pkg)
            }
        }
        
        val vpnInterface = builder.establish()
        
        // 4. 启动tun2socks将流量转发至SOCKS5
        Tun2Socks.start(
            vpnInterface.fd,
            socksHost,
            socksPort
        )
    }
}

Permissions Required

所需权限

AndroidManifest.xml:
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>
AndroidManifest.xml:
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

Go Mobile桥接层

The Go core is exposed to Android via
gomobile
:
Go核心逻辑通过
gomobile
暴露给Android:

Mobile Package Interface

移动包接口

mobile/mobile.go:
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)
}
mobile/mobile.go:
go
package mobile

import (
    "encoding/json"
    "github.com/Hidden-Node/GooseRelayVPN/client"
)

// Config与Android配置文件JSON结构匹配
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初始化并启动GooseRelay客户端
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优雅停止中继客户端
func Stop() error {
    if relayClient != nil {
        return relayClient.Stop()
    }
    return nil
}

// GetStats返回JSON格式的统计数据
func GetStats() string {
    if relayClient == nil {
        return "{}"
    }
    stats := relayClient.GetStats()
    data, _ := json.Marshal(stats)
    return string(data)
}

Calling from Android (Kotlin)

Android端调用(Kotlin)

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
        }
    }
}
kotlin
import gooserelay.Gooserelay // 从AAR生成

class RelayManager {
    fun startRelay(profile: Profile) {
        val configJson = profile.toJson()
        try {
            Gooserelay.start(configJson)
            Log.i("GooseRelay", "启动成功")
        } catch (e: Exception) {
            Log.e("GooseRelay", "启动失败: ${e.message}")
        }
    }
    
    fun stopRelay() {
        try {
            Gooserelay.stop()
        } catch (e: Exception) {
            Log.e("GooseRelay", "停止失败: ${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

全设备VPN

Route all device traffic through the tunnel:
kotlin
// 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)
将所有设备流量通过隧道传输:
kotlin
// 配置文件设置
val profile = Profile(
    name = "全设备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:
kotlin
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)
仅让指定应用通过隧道传输:
kotlin
val profile = Profile(
    name = "拆分隧道",
    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

手动SOCKS5代理

Use without VpnService (manual app configuration):
kotlin
// 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
}
不使用VpnService(需手动配置应用):
kotlin
// 仅启动SOCKS5服务器,不启用VPN
fun startSocksOnly(profile: Profile) {
    GooseRelay.start(profile.toJson())
    // 应用需手动配置使用127.0.0.1:1080作为SOCKS5代理
}

Logging and Telemetry

日志与遥测

Real-time Logs

实时日志

The app provides a Logs tab showing Android and Go core logs:
kotlin
// 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
应用提供日志标签页,展示Android和Go核心模块的日志:
kotlin
// Android端日志收集器,转发至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
        ))
        // 通知UI观察者
        notifyLogListeners()
    }
}

// Go端:通过自定义写入器捕获日志

Telemetry Stats

遥测统计

Real-time connection statistics:
kotlin
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"
}
实时连接统计数据:
kotlin
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)
    
    // 更新UI卡片
    uploadCard.text = formatBytes(stats.bytesUploaded)
    downloadCard.text = formatBytes(stats.bytesDownloaded)
    latencyCard.text = "${stats.avgLatency}ms"
}

CI/CD and Release

CI/CD与发布

GitHub Actions Workflows

GitHub Actions工作流

Debug CI (.github/workflows/android-ci.yml):
yaml
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):
yaml
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 file
  • ANDROID_KEYSTORE_PASSWORD
    : Keystore password
  • ANDROID_KEY_ALIAS
    : Key alias in keystore
  • ANDROID_KEY_PASSWORD
    : Key password
Debug CI (.github/workflows/android-ci.yml):
yaml
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
发布工作流 (.github/workflows/release.yml):
yaml
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
所需密钥:
  • ANDROID_KEYSTORE_BASE64
    :Base64编码的密钥库文件
  • ANDROID_KEYSTORE_PASSWORD
    :密钥库密码
  • ANDROID_KEY_ALIAS
    :密钥库中的密钥别名
  • ANDROID_KEY_PASSWORD
    :密钥密码

Troubleshooting

故障排除

Connection Stuck in "Preparing" State

连接卡在“准备中”状态

Symptoms: VPN shows "Preparing" but never connects.
Causes:
  1. Invalid
    script_keys
    (deployment IDs)
  2. Mismatched
    tunnel_key
    between client and server
  3. Apps Script deployment not accessible
Solution:
bash
undefined
症状: VPN显示“准备中”但始终无法连接。
原因:
  1. script_keys
    无效(部署ID错误)
  2. 客户端与服务器的
    tunnel_key
    不匹配
  3. Apps Script部署无法访问
解决方案:
bash
undefined

Check logs tab in app for specific errors

查看应用内日志标签页获取具体错误

Common log patterns:

常见日志模式:

"Failed to connect to script" → Check deployment ID

"Failed to connect to script" → 检查部署ID

"Decryption failed" → Check tunnel_key matches server

"Decryption failed" → 确认tunnel_key与服务器一致

"Connection timeout" → Verify VPS server is running

"Connection timeout" → 验证VPS服务器是否运行

Verify server-side key matches:

验证服务器端密钥是否匹配:

On VPS: cat /etc/goose-server/config.json | jq .tunnel_key

在VPS上执行: cat /etc/goose-server/config.json | jq .tunnel_key

In Android profile: check tunnel_key field

在Android配置文件中检查tunnel_key字段

undefined
undefined

SOCKS Port Busy Error

SOCKS端口占用错误

Symptoms: "bind: address already in use" in logs.
Solution:
kotlin
// 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()
}
症状: 日志中出现“bind: address already in use”。
解决方案:
kotlin
undefined

No Traffic Flowing Through VPN

确保重新连接前先干净断开

Symptoms: VPN connected but no internet access.
Checklist:
  1. VPN permission granted
  2. Split tunnel app selection correct
  3. DNS servers configured
  4. Local network excluded if needed
Solution:
kotlin
// 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()
fun reconnect() { // 停止VPN服务 vpnService.stop()
// 等待端口释放
Thread.sleep(2000)

// 检查是否有其他应用占用1080端口
// 若需要则修改配置文件的socks_port
profile.socksPort = 1081

vpnService.start()
}
undefined

Apps Script Deployment Issues

VPN连接后无流量

Symptoms: "Script execution failed" in logs.
Solution:
bash
undefined
症状: VPN已连接但无法访问互联网。
检查清单:
  1. 是否已授予VPN权限
  2. 拆分隧道的应用选择是否正确
  3. 是否已配置DNS服务器
  4. 是否需要排除本地网络
解决方案:
kotlin
// 验证VPN构建器配置
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)
    // ... 排除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)

Apps Script部署问题

Example: AKfycbzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Test deployment manually:

症状: 日志中出现“Script execution failed”。
解决方案:
bash
undefined

Ensure Apps Script has correct VPS endpoint in Code.gs:

验证部署ID格式(应为长字符串)

const VPS_ENDPOINT = "https://your-vps-ip:8443";

示例: AKfycbzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

手动测试部署:

High Latency or Slow Speeds

确保Apps Script的Code.gs中配置了正确的VPS端点:

const VPS_ENDPOINT = "https://your-vps-ip:8443";

Symptoms: Connected but slow performance.
Optimization:
json
{
  "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
undefined

AAR Build Failures

高延迟或速度缓慢

Symptoms:
build_go_mobile.sh
fails.
Solution:
bash
undefined
症状: 已连接但性能低下。
优化配置:
json
{
  "debug_timing": false,
  "sni": [
    "www.google.com"
  ],
  "script_keys": [
    "PRIMARY_DEPLOYMENT_ID",
    "BACKUP_DEPLOYMENT_ID"
  ]
}
提示:
  • 使用多个
    script_keys
    实现负载均衡
  • google_host
    中选择更近的Google IP
  • 优化VPS服务器配置
  • 临时启用
    debug_timing
    以识别瓶颈

Ensure gomobile is installed

AAR构建失败

go install golang.org/x/mobile/cmd/gomobile@latest gomobile init
症状:
build_go_mobile.sh
执行失败。
解决方案:
bash
undefined

Check Android NDK is installed

确保已安装gomobile

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
go install golang.org/x/mobile/cmd/gomobile@latest gomobile init

Retry build

检查Android NDK是否已安装

在Android Studio中: Tools → SDK Manager → SDK Tools → NDK

设置环境变量

bash android/build_go_mobile.sh
undefined
export ANDROID_HOME=$HOME/Android/Sdk export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/25.1.8937393

Certificate/TLS Errors

重新尝试构建

Symptoms: "x509: certificate signed by unknown authority"
Solution:
go
// 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
bash android/build_go_mobile.sh
undefined

Best Practices

证书/TLS错误

  1. Key Management: Store
    tunnel_key
    in Android Keystore, not plaintext
  2. Multiple Deployments: Use 2-3
    script_keys
    for redundancy
  3. Battery Optimization: Exclude VPN service from battery optimization
  4. Testing: Test with
    debug_timing: true
    during setup
  5. Backup Profiles: Export profiles before app updates
  6. Server Monitoring: Monitor VPS
    goose-server
    logs alongside Android logs
症状: 出现"x509: certificate signed by unknown authority"
解决方案:
go
// 如果VPS使用自签名证书,Apps Script必须信任该证书
// 更优方案:在VPS上使用Let's Encrypt证书

// 在Code.gs中配置:
const ALLOW_SELF_SIGNED = false; // 仅测试环境设置为true

Additional Resources

最佳实践

  1. 密钥管理:
    tunnel_key
    存储在Android密钥库中,避免明文存储
  2. 多部署冗余: 使用2-3个
    script_keys
    实现故障转移
  3. 电池优化: 将VPN服务排除在电池优化之外
  4. 测试建议: 搭建阶段启用
    debug_timing: true
  5. 配置备份: 应用更新前导出配置文件
  6. 服务器监控: 同时监控VPS
    goose-server
    日志和Android日志

额外资源