开发自己的 VPN 程序:shadowsocks-android 同步代码及编译踩坑

android Nov 04, 2018

官方仓库: shadowsocks-android
项目非常成熟,很适合拿来打造自己的 VPN app。

工具版本

划重点,要想少踩坑,要先确认

PREREQUISITES
JDK 1.8
Go 1.11+
Android SDK
Android NDK r16+

因为没注意,使用的 Go 的版本不对导致卡在这很久。

代理配置

有些子模块会从 googlesource 同步代码,所以需要配置代理。
用下面的命令给 git 配置代理

git config --global http.proxy "localhost:1080"

必须使用 --global 配置

同步代码

官方提供了两种方式同步代码:

Clone the repo using git clone --recurse-submodules or update submodules using git submodule update --init --recursive

但是使用第二种同步时,发现有很多模块代码被删除了。最终还是要手动到仓库里 stash 删除记录恢复代码。所以推荐使用第一种方式:

git clone --recurse-submodules https://github.com/shadowsocks/shadowsocks-android.git

下载 go 依赖

开始编译时,编译脚本会自动自行core/src/overture/make.bash, 它会去网站下载 go 运行需要的依赖包。在这之前需要确保这三件事:

  1. 确保 ANDROID_HOME 和 ANDROID_NDK_HOME 已配置
export ANDROID_HOME={Your Android Home}
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
  1. 确保 go 已加入 PATH
export PATH=$PATH:/usr/local/go/bin/
  1. 若在墙内,请配置代理
export http_proxy="http://127.0.0.1:1080"
export https_proxy=$http_proxy

注意一定要 export,因为脚本运行是执行 sh -c make.bash

task goBuild(type: Exec) {
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        executable "cmd.exe"
        args "/c", file("src/overture/make.bat").absolutePath , minSdkVersion
    } else {
        executable "sh"
        args "-c", "src/overture/make.bash " + minSdkVersion
    }
}

Android Studio 编译错误

代码同步好准备编译了,但是发现使用 gradle 脚本编译没问题,使用 AS 直接 run 却报错。
原因是 InstantRun 会热同步代码,把这个开关关掉:
-----2018-11-04---1.44.28

默认配置的 VPN 无法使用

好了,现在你的程序在你手机里跑起来了。可是却发现,使用自带的配置连接失败。
这不是你的错,而是官方为了防止第三方开发者获取官方代理信息所设计的安全机制:

你看到的 ip 198.199.101.152 只是个没什么卵用的字符串,它存在的唯一意义是判断是否正在使用官方配置。实际运行时代码如下:

if (profile.host == "198.199.101.152") {
    val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()
    val mdg = MessageDigest.getInstance("SHA-1")
    mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
    val requestBody = FormBody.Builder()
        .add("sig", String(Base64.encode(mdg.digest(), 0)))
        .build()
    val request = Request.Builder()
        .url(Core.remoteConfig.getString("proxy_url"))
        .post(requestBody)
        .build()

    val proxies = client.newCall(request).execute()
        .body()!!.string().split('|').toMutableList()
    proxies.shuffle()
    val proxy = proxies.first().split(':')
    profile.host = proxy[0].trim()
    profile.remotePort = proxy[1].trim().toInt()
    profile.password = proxy[2].trim()
    profile.method = proxy[3].trim()
}

翻译一下:

// 如果用户使用官方配置
if (profile.host == "198.199.101.152") {
    // 从 firebase 获取代理列表地址
    Core.remoteConfig.getString("proxy_url")
    
    // 获取签名信息
    Core.packageInfo.signaturesCompat.first().toByteArray()
    
    // 使用签名信息作请求体,请求服务器获取可使用代理列表
    val request = Request.Builder()
        .url(Core.remoteConfig.getString("proxy_url"))
        .post(requestBody)
        .build()
        
    // 把列表随机一下,取第一个来用
     val proxies = client.newCall(request).execute()
        .body()!!.string().split('|').toMutableList()
    proxies.shuffle()
    val proxy = proxies.first().split(':')
    
    // 应用配置
    profile.host = proxy[0].trim()
    profile.remotePort = proxy[1].trim().toInt()
    profile.password = proxy[2].trim()
    profile.method = proxy[3].trim()
}

服务端做了客户端签名的鉴权,所以客户端签名不对,无法获取到真正可使用的代理配置。

结束

现在开始,你可以真正开始开发你的 VPN 程序了。要做的只是: 定义你自己的界面,然后调用它的服务接口。

/* 看板娘 */