From 594a3b46b43555b0d9a2b91eb15a1aa72f7feab9 Mon Sep 17 00:00:00 2001 From: Myth Date: Fri, 3 Nov 2023 07:05:56 +0800 Subject: [PATCH] feat: add android --- .github/workflows/build-android.yml | 89 ++++++++++++++++ client/rvpn_conn.go | 19 ++-- mobile/mobile.go | 45 ++++++++ stack/gvisor/stack.go | 18 +++- stack/tun/stack.go | 21 +++- stack/tun/stack_android.go | 155 ++++++++++++++++++++++++++++ stack/tun/stack_linux.go | 2 + 7 files changed, 329 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/build-android.yml create mode 100644 mobile/mobile.go create mode 100644 stack/tun/stack_android.go diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 0000000..5516995 --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,89 @@ +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 install golang.org/x/mobile/cmd/gomobile@latest + export PATH="/home/runner/go/bin:${PATH}" + gomobile init + 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 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/client/rvpn_conn.go b/client/rvpn_conn.go index 57346b1..67fa26b 100644 --- a/client/rvpn_conn.go +++ b/client/rvpn_conn.go @@ -1,6 +1,7 @@ package client import ( + "errors" "github.com/mythologyli/zju-connect/log" "io" ) @@ -15,7 +16,6 @@ type RvpnConn struct { recvErrCount int } -// always success or panic func (r *RvpnConn) Read(p []byte) (n int, err error) { for n, err = r.recvConn.Read(p); err != nil && r.recvErrCount < 5; { log.Printf("Error occurred while receiving, retrying: %v", err) @@ -24,18 +24,16 @@ func (r *RvpnConn) Read(p []byte) (n int, err error) { _ = r.recvConn.Close() r.recvConn, err = r.easyConnectClient.RecvConn() if err != nil { - // TODO graceful shutdown - panic(err) + return 0, err } r.recvErrCount++ if r.recvErrCount >= 5 { - panic("recv retry limit exceeded.") + return 0, errors.New("recv error count exceeded") } } return } -// always success or panic func (r *RvpnConn) Write(p []byte) (n int, err error) { for n, err = r.sendConn.Write(p); err != nil && r.sendErrCount < 5; { log.Printf("Error occurred while sending, retrying: %v", err) @@ -44,12 +42,11 @@ func (r *RvpnConn) Write(p []byte) (n int, err error) { _ = r.sendConn.Close() r.sendConn, err = r.easyConnectClient.SendConn() if err != nil { - // TODO graceful shutdown - panic(err) + return 0, err } r.sendErrCount++ if r.sendErrCount >= 5 { - panic("send retry limit exceeded.") + return 0, errors.New("send error count exceeded") } } return @@ -75,14 +72,12 @@ func NewRvpnConn(ec *EasyConnectClient) (*RvpnConn, error) { var err error c.sendConn, err = ec.SendConn() if err != nil { - log.Printf("Error occurred while creating sendConn: %v", err) - panic(err) + return nil, err } c.recvConn, err = ec.RecvConn() if err != nil { - log.Printf("Error occurred while creating recvConn: %v", err) - panic(err) + return nil, err } return c, nil } diff --git a/mobile/mobile.go b/mobile/mobile.go new file mode 100644 index 0000000..1f13f93 --- /dev/null +++ b/mobile/mobile.go @@ -0,0 +1,45 @@ +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 StartStack(fd int) { + vpnTUNStack, err := tun.NewStack(vpnClient, "") + if err != nil { + return + } + + vpnTUNStack.SetupTun(fd) + vpnTUNStack.Run() +} diff --git a/stack/gvisor/stack.go b/stack/gvisor/stack.go index 077e553..0857689 100644 --- a/stack/gvisor/stack.go +++ b/stack/gvisor/stack.go @@ -76,7 +76,10 @@ func (ep *Endpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) } if ep.rvpnConn != nil { - n, _ := ep.rvpnConn.Write(buf) + n, err := ep.rvpnConn.Write(buf) + if err != nil { + panic(err) + } log.DebugPrintf("Send: wrote %d bytes", n) log.DebugDumpHex(buf[:n]) @@ -133,13 +136,20 @@ func NewStack(easyConnectClient *client.EasyConnectClient) (*Stack, error) { } func (s *Stack) Run() { - - s.endpoint.rvpnConn, _ = client.NewRvpnConn(s.endpoint.easyConnectClient) + var err error + s.endpoint.rvpnConn, err = client.NewRvpnConn(s.endpoint.easyConnectClient) + if err != nil { + log.Printf("Error occurred while creating sendConn: %v", err) + panic(err) + } // Read from VPN server and send to gVisor stack for { buf := make([]byte, 1500) - n, _ := s.endpoint.rvpnConn.Read(buf) + n, err := s.endpoint.rvpnConn.Read(buf) + if err != nil { + panic(err) + } log.DebugPrintf("Recv: read %d bytes", n) log.DebugDumpHex(buf[:n]) diff --git a/stack/tun/stack.go b/stack/tun/stack.go index 9f6ba8c..7088c91 100644 --- a/stack/tun/stack.go +++ b/stack/tun/stack.go @@ -1,3 +1,5 @@ +//go:build !android + package tun import ( @@ -14,18 +16,26 @@ type Stack struct { } func (s *Stack) Run() { - s.rvpnConn, _ = client.NewRvpnConn(s.endpoint.easyConnectClient) + var err error + s.rvpnConn, err = client.NewRvpnConn(s.endpoint.easyConnectClient) + if err != nil { + log.Printf("Error occurred while creating sendConn: %v", err) + panic(err) + } // Read from VPN server and send to TUN stack go func() { for { buf := make([]byte, 1500) - n, _ := s.rvpnConn.Read(buf) + n, err := s.rvpnConn.Read(buf) + if err != nil { + panic(err) + } log.DebugPrintf("Recv: read %d bytes", n) log.DebugDumpHex(buf[:n]) - err := s.endpoint.Write(buf[:n]) + err = s.endpoint.Write(buf[:n]) if err != nil { log.Printf("Error occurred while writing to TUN stack: %v", err) panic(err) @@ -57,7 +67,10 @@ func (s *Stack) Run() { continue } - _, _ = s.rvpnConn.Write(buf[:n]) + _, err = s.rvpnConn.Write(buf[:n]) + if err != nil { + panic(err) + } log.DebugPrintf("Send: wrote %d bytes", n) log.DebugDumpHex(buf[:n]) diff --git a/stack/tun/stack_android.go b/stack/tun/stack_android.go new file mode 100644 index 0000000..c0d5869 --- /dev/null +++ b/stack/tun/stack_android.go @@ -0,0 +1,155 @@ +package tun + +import ( + "context" + "github.com/mythologyli/zju-connect/client" + "github.com/mythologyli/zju-connect/log" + "golang.org/x/net/ipv4" + "io" + "net" + "os" + "syscall" +) + +type Stack struct { + endpoint *Endpoint + rvpnConn io.ReadWriteCloser +} + +func (s *Stack) Run() { + var err error + s.rvpnConn, err = client.NewRvpnConn(s.endpoint.easyConnectClient) + if err != nil { + log.Printf("Error occurred while creating sendConn: %v", err) + return + } + + ctxRead, cancelRead := context.WithCancel(context.Background()) + ctxWrite, cancelWrite := context.WithCancel(context.Background()) + + // Read from VPN server and send to TUN stack + go func() { + for { + select { + case <-ctxRead.Done(): + cancelWrite() + return + default: + buf := make([]byte, 1500) + n, err := s.rvpnConn.Read(buf) + if err != nil { + cancelWrite() + 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) + cancelWrite() + return + } + } + } + }() + + // Read from TUN stack and send to VPN server + for { + select { + case <-ctxWrite.Done(): + cancelRead() + return + default: + buf := make([]byte, 1500) + n, err := s.endpoint.Read(buf) + if err != nil { + log.Printf("Error occurred while reading from TUN stack: %v", err) + cancelRead() + return + } + + if n < 20 { + continue + } + + 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 + } + + _, err = s.rvpnConn.Write(buf[:n]) + if err != nil { + cancelRead() + 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, dnsServer string) (*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 b24d2b6..fca61c9 100644 --- a/stack/tun/stack_linux.go +++ b/stack/tun/stack_linux.go @@ -1,3 +1,5 @@ +//go:build !android + package tun import (