diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 0000000..9d8b19a --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,91 @@ +name: Build Android + +on: + release: + types: [ published ] + push: + paths: + - "**/*.go" + - "go.mod" + - "go.sum" + - ".github/workflows/*.yml" + pull_request: + types: + - opened + paths: + - "**/*.go" + - "go.mod" + - "go.sum" + +permissions: write-all + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout codebase + uses: actions/checkout@v3 + + - name: Set up Go + run: | + wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf go1.21.3.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + + - name: Get project dependencies + run: go mod download + + - name: Build Android AAR + run: | + go env + go install golang.org/x/mobile/cmd/gomobile@latest + go get golang.org/x/mobile/bind@latest + export PATH="/home/runner/go/bin:${PATH}" + mkdir -p build_assets + sudo apt update && sudo apt install openjdk-17-jdk + export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 + export NDK_LTS_VERSION=23.2.8568313 + export SDK_TOOLS_VERSION=10406996 + export ANDROID_PLATFORM_VERSION=24 + export ANDROID_HOME="/home/runner/android-sdk" + export ANDROID_SDK_ROOT=$ANDROID_HOME + export CMDLINE_TOOLS_ROOT="${ANDROID_HOME}/cmdline-tools/latest/bin" + export ADB_INSTALL_TIMEOUT=120 + export PATH="${ANDROID_HOME}/emulator:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/platform-tools/bin:${PATH}" + export ANDROID_NDK_HOME="/home/runner/android-sdk/ndk/${NDK_LTS_VERSION}" + export ANDROID_NDK_ROOT="${ANDROID_NDK_HOME}" + mkdir -p ${ANDROID_HOME}/cmdline-tools + mkdir ${ANDROID_HOME}/platforms + mkdir ${ANDROID_HOME}/ndk + wget -O /tmp/cmdline-tools.zip -t 5 --no-verbose "https://dl.google.com/android/repository/commandlinetools-linux-${SDK_TOOLS_VERSION}_latest.zip" + unzip -q /tmp/cmdline-tools.zip -d ${ANDROID_HOME}/cmdline-tools + rm /tmp/cmdline-tools.zip + mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest + echo y | ${CMDLINE_TOOLS_ROOT}/sdkmanager "build-tools;${ANDROID_PLATFORM_VERSION}.0.0" + echo y | ${CMDLINE_TOOLS_ROOT}/sdkmanager "platforms;android-${ANDROID_PLATFORM_VERSION}" + echo y | ${CMDLINE_TOOLS_ROOT}/sdkmanager "ndk;${NDK_LTS_VERSION}" + sudo apt install -y --no-install-recommends g++ libc6-dev + gomobile init + gomobile bind -target=android -o build_assets/zju-connect.aar ./mobile + + - name: Upload artifact + if: github.event_name != 'release' + uses: actions/upload-artifact@v3 + with: + name: zju-connect-android-aar + path: build_assets/* + + - name: Create ZIP archive + if: github.event_name == 'release' + run: | + pushd build_assets || exit 1 + zip -9vr ../zju-connect-android-aar.zip . + popd || exit 1 + + - name: Upload release binary + if: github.event_name == 'release' + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release upload ${{ github.event.release.tag_name }} zju-connect-android-aar.zip diff --git a/mobile/mobile.go b/mobile/mobile.go new file mode 100644 index 0000000..3cabcdf --- /dev/null +++ b/mobile/mobile.go @@ -0,0 +1,72 @@ +package mobile + +import ( + "github.com/mythologyli/zju-connect/client" + "github.com/mythologyli/zju-connect/log" + "github.com/mythologyli/zju-connect/stack/tun" +) + +var vpnClient *client.EasyConnectClient + +func Login(server string, username string, password string) string { + log.Init() + + vpnClient = client.NewEasyConnectClient( + server, + username, + password, + "", + false, + false, + ) + err := vpnClient.Setup() + if err != nil { + return "" + } + + log.Printf("EasyConnect client started") + + clientIP, err := vpnClient.IP() + if err != nil { + return "" + } + + return clientIP.String() +} + +func DebugLogin(server string, username string, password string) string { + log.Init() + log.EnableDebug() + + vpnClient = client.NewEasyConnectClient( + server, + username, + password, + "", + false, + false, + ) + err := vpnClient.Setup() + if err != nil { + return "" + } + + log.Printf("EasyConnect client started") + + clientIP, err := vpnClient.IP() + if err != nil { + return "" + } + + return clientIP.String() +} + +func StartStack(fd int) { + vpnTUNStack, err := tun.NewStack(vpnClient) + if err != nil { + return + } + + vpnTUNStack.SetupTun(fd) + vpnTUNStack.Run() +} diff --git a/stack/tun/stack.go b/stack/tun/stack.go index 92e3c1f..7d74bea 100644 --- a/stack/tun/stack.go +++ b/stack/tun/stack.go @@ -1,3 +1,5 @@ +//go:build !android + package tun import ( diff --git a/stack/tun/stack_android.go b/stack/tun/stack_android.go new file mode 100644 index 0000000..723ac00 --- /dev/null +++ b/stack/tun/stack_android.go @@ -0,0 +1,131 @@ +package tun + +import ( + "github.com/mythologyli/zju-connect/client" + "github.com/mythologyli/zju-connect/log" + "golang.org/x/net/ipv4" + "io" + "net" + "os" + "syscall" +) + +const MTU uint32 = 1400 + +type Stack struct { + endpoint *Endpoint + rvpnConn io.ReadWriteCloser +} + +func (s *Stack) Run() { + var connErr error + s.rvpnConn, connErr = client.NewRvpnConn(s.endpoint.easyConnectClient) + if connErr != nil { + return + } + // Read from VPN server and send to TUN stack + go func() { + for { + buf := make([]byte, MTU) + n, err := s.rvpnConn.Read(buf) + if err != nil { + log.Printf("Error occurred while reading from VPN server: %v", err) + return + } + log.DebugPrintf("Recv: read %d bytes", n) + log.DebugDumpHex(buf[:n]) + + err = s.endpoint.Write(buf[:n]) + if err != nil { + log.Printf("Error occurred while writing to TUN stack: %v", err) + return + } + } + }() + + // Read from TUN stack and send to VPN server + for { + buf := make([]byte, MTU) + n, err := s.endpoint.Read(buf) + if err != nil { + log.Printf("Error occurred while reading from TUN stack: %v", err) + return + } + + header, err := ipv4.ParseHeader(buf[:n]) + if err != nil { + continue + } + + // Filter out non-TCP/UDP packets otherwise error may occur + if header.Protocol != syscall.IPPROTO_TCP && header.Protocol != syscall.IPPROTO_UDP { + continue + } + + n, err = s.rvpnConn.Write(buf[:n]) + if err != nil { + log.Printf("Error occurred while writing to VPN server: %v", err) + return + } + log.DebugPrintf("Send: wrote %d bytes", n) + log.DebugDumpHex(buf[:n]) + } +} + +type Endpoint struct { + easyConnectClient *client.EasyConnectClient + + readWriteCloser io.ReadWriteCloser + ip net.IP + + tcpDialer *net.Dialer + udpDialer *net.Dialer +} + +func (ep *Endpoint) Write(buf []byte) error { + _, err := ep.readWriteCloser.Write(buf) + return err +} + +func (ep *Endpoint) Read(buf []byte) (int, error) { + return ep.readWriteCloser.Read(buf) +} + +func (s *Stack) AddRoute(target string) error { + return nil +} + +func NewStack(easyConnectClient *client.EasyConnectClient) (*Stack, error) { + s := &Stack{} + + s.endpoint = &Endpoint{ + easyConnectClient: easyConnectClient, + } + + var err error + s.endpoint.ip, err = easyConnectClient.IP() + if err != nil { + return nil, err + } + + // We need this dialer to bind to device otherwise packets will not be sent via TUN + s.endpoint.tcpDialer = &net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: s.endpoint.ip, + Port: 0, + }, + } + + s.endpoint.udpDialer = &net.Dialer{ + LocalAddr: &net.UDPAddr{ + IP: s.endpoint.ip, + Port: 0, + }, + } + + return s, nil +} + +func (s *Stack) SetupTun(fd int) { + s.endpoint.readWriteCloser = os.NewFile(uintptr(fd), "tun") +} diff --git a/stack/tun/stack_linux.go b/stack/tun/stack_linux.go index 5c83f3d..95570b7 100644 --- a/stack/tun/stack_linux.go +++ b/stack/tun/stack_linux.go @@ -1,3 +1,5 @@ +//go:build !android + package tun import (