From f97d62e7940a699904b17e69e59f573732170cfc Mon Sep 17 00:00:00 2001 From: Myth Date: Fri, 3 Nov 2023 04:27:28 +0800 Subject: [PATCH] feat: add android --- .github/workflows/build.yml | 33 ++++++++ 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, 273 insertions(+), 20 deletions(-) create mode 100644 mobile/mobile.go create mode 100644 stack/tun/stack_android.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f86efe1..fab07d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,8 @@ jobs: goarch: riscv64 - goos: windows goarch: arm64 + - goos: android + goarch: arm64 # BEGIN MIPS - goos: linux goarch: mips64 @@ -97,10 +99,41 @@ jobs: run: go mod download - name: Build + if: matrix.goos != 'android' run: | mkdir -p build_assets go build -v -o build_assets/zju-connect -trimpath -ldflags "-s -w -buildid=" . + - name: Build Android AAR + if: matrix.goos == 'android' + run: | + mkdir -p build_assets + sudo apt update && sudo apt install openjdk-17-jdk + 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 + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + gomobile bind -target=android -o build_assets/zju-connect.aar ./mobile + - name: Rename Windows zju-connect if: matrix.goos == 'windows' run: | 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 (