用 Golang 来实现的代理不要太多,像 V2RayBrookClash。一方面得益于 Golang 标准库对于网络编程的支持,另一方面也是由于其便利的交叉编译。如果要在 Android 上用 Golang 实现一套代理方案,还是必须要处理一些问题,或者说是所有的代理方案都要解决的平台特性。

IPC 传递文件描述符

Android 上使用 VpnService 由系统创建一个 TUN 虚拟网卡并接收所有的流量,API 会返回一个 file descriptor,通过这个 FD 可以读取/写入 IP packet。

为了稳定性和性能考虑,我们通常会将代理程序放到一个单独的进程里。但是在 Android 的阉割版 Linux 环境下,子进程是无法访问父进程的 FD。这里就需要用到 Linux 通用的进程间共享 FD 的方案,也就是给 Unix Socket 设置 SCM_RIGHTS 标识,然后用 recvmsgsendmsg 来收发。在 Android 和 Golang 中都有对应的 API。

public void LocalSocket.setFileDescriptorsForSend (FileDescriptor[] fds)
public FileDescriptor[] LocalSocket.getAncillaryFileDescriptors ()
// UnixRights encodes file descriptors into a socket control message
func syscall.UnixRights(fds ...int) []byte
func syscall.Sendmsg(fd int, p, oob []byte, to Sockaddr, flags int) (err error)

func syscall.Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from Sockaddr, err error)
// ParseSocketControlMessage parses b as an array of socket control
// messages.
func syscall.ParseSocketControlMessage(b []byte) ([]SocketControlMessage, error)
// ParseUnixRights decodes a socket control message that contains an
// integer array of open file descriptors from another process.
func syscall.ParseUnixRights(m *SocketControlMessage) ([]int, error)

Socket 建连之前 protect 文件描述符

如果我们在创建 TUN 的时候(启动 VpnService )设置的路由是 0.0.0.0/0 的情况下,所有的本地流量都会经过 TUN 设备,包括代理应用的数据包,这样就会进入一个死循环。这里需要用到 VpnService 的 protect 函数,这个函数接收一个 TCP/UDP 的 Socket 或者一个 FD。经过 protected 的 Socket 收发的数据包就不在经过 TUN 设备了。

Golang 的 net 库对网络做了很好的封装,比如创建一个TCP连接,使用 net.Dial 就可以拿到一个成功连接 net.Conn。但是在 Android 上,我们必须先拿到 Socket 的 FD 并调用 protect,否则 net.Dial 只会返回 error。

基于 V2Ray 实现的代理(AndroidLibV2rayv2rayNG)通常是直接创建 unix.Socket 然后调用 unix.Connect 建立连接,再对外封装成 net.Dial 方法。

在 Go 1.11 里标准库给 net.Dial 添加了 Dialer.Control 这个字段,相当于给 net.Dialer 注册了一个Socket创建之后、连接建立之前的回调,可以很方便的拿到 net.Dialer 的 FD。

d := &net.Dialer{}
d.Control = func(network, address string, c syscall.RawConn) error {
    return c.Control(func(fd uintptr) {
        // Access socket fd
    })
}

P.S. 这里也可以通过 VpnService.Builder.addDisallowedApplication 把代理应用排除在外,这样就不需要使用 protect 了🤣。可以参考最近刚刚开源的 ClashForAndroid

选择 Gomobile 还是编译成 Exectuable Binary

Golang 官方实现了 Gomobile 用于编译移动端可用的 Golang 程序。在 Android 平台上编译的是包含 JNI interface 的 AAR 文件,可以直接从 Java 调用 Golang 代码。所以这种情况下通常将 Gomobile 和 VpnService 运行在同一个进程中,这样的话就不需要上面提到的 IPC 的工作了。这样实现的项目有 V2RayNGkitsunebi-android

App->VpnService: AIDL
Note over App: Main Process
VpnService-->Gomobile: JNI
Note over VpnService: VPN Process
Gomobile-->VpnService: JNI
Note over Gomobile: VPN Process
VpnService->App: AIDL

另一种做法是将 Golang 编译成可执行的二进制文件,然后通过 ProcessBuilder 或者 Runtime.exec 运行在独立进程里,需要 Unix Socket 进行进程间通讯。因为 ShadowSocks-Android 就是这样的实现的,所以也算是一个比较稳定主流的方案,这样实现的项目有 ClashForAndroid

App->VpnService: AIDL
Note over App: Main Process
VpnService->Binary: Unix Socket
Note over VpnService: VPN Process
Binary->VpnService: Unix Socket
Note over Binary: Isolate Process
VpnService->App: AIDL

目前来看两种方案都可以很好的工作在 Android 上,Gomobile 的优点就是 Java->Golang 调用比较方便,编译简单,但是相对的 Golang->Java 只能通过实现接口,另外包体积较大。具体选哪种就见仁见智了。

参考

socks代理转VPN