Skip to content

Commit

Permalink
refactor: build tun2socks from source within Android Studio (#487)
Browse files Browse the repository at this point in the history
This PR moves all Go code from [`go-tun2socks/intra`](https://github.com/Jigsaw-Code/outline-go-tun2socks/tree/master/intra) repository to `./Android/tun2socks/intra`. It also updates the import packages from `github.com/Jigsaw-Code/outline-go-tun2socks/intra` to `github.com/Jigsaw-Code/Intra/Android/tun2socks/intra`, and copyright headers from `Outline Authors` to `Jigsaw LLC`.

Additionally, it removes the need to depend on a binary file, by defining the `gomobile` bind actions in `./Android/tun2socks/build.gradle`. This means that we can now build the entire app completely from source code in Android Studio.
  • Loading branch information
jyyi1 authored Oct 11, 2023
1 parent 874e406 commit d355484
Show file tree
Hide file tree
Showing 35 changed files with 4,897 additions and 10 deletions.
10 changes: 5 additions & 5 deletions Android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ android {
storePassword keystoreProperties['storePassword']
}
}
compileSdkVersion 33
buildToolsVersion '33.0.0'
compileSdkVersion ANDROID_COMPILE_SDK_VERSION as int
buildToolsVersion ANDROID_BUILD_TOOLS_VERSION

defaultConfig {
applicationId "app.intra"
// Firebase Crashlytics requires SDK version 16.
minSdkVersion 16
targetSdkVersion 33
minSdkVersion ANDROID_MIN_SDK_VERSION as int
targetSdkVersion ANDROID_TARGET_SDK_VERSION as int
versionCode 64
versionName "1.3.7"
vectorDrawables.useSupportLibrary = true
Expand Down Expand Up @@ -107,7 +107,7 @@ dependencies {
implementation 'com.google.firebase:firebase-crashlytics-ndk:18.2.6'
implementation 'com.google.firebase:firebase-config:21.0.1'
// For go-tun2socks
implementation project(":tun2socks")
implementation project(path: ':tun2socks', configuration: 'aarBinary')
}

// For Firebase Analytics
Expand Down
8 changes: 7 additions & 1 deletion Android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.enableJetifier=true
android.useAndroidX=true
android.useAndroidX=true

ANDROID_COMPILE_SDK_VERSION=33
ANDROID_BUILD_TOOLS_VERSION=33.0.0

ANDROID_MIN_SDK_VERSION=16
ANDROID_TARGET_SDK_VERSION=33
2 changes: 0 additions & 2 deletions Android/tun2socks/README.md

This file was deleted.

80 changes: 78 additions & 2 deletions Android/tun2socks/build.gradle
Original file line number Diff line number Diff line change
@@ -1,2 +1,78 @@
configurations.maybeCreate("default")
artifacts.add("default", file('tun2socks.aar'))
// We use Android library plugin to get the Android SDK path.
plugins {
id('com.android.library')
}

android {
compileSdkVersion ANDROID_COMPILE_SDK_VERSION as int
defaultConfig {
minSdkVersion ANDROID_MIN_SDK_VERSION as int
}
}

configurations {
aarBinary {
canBeConsumed = true
canBeResolved = false
}
}

def goBuildDir = file("${buildDir}/go")
def outputAAR = file("${buildDir}/tun2socks.aar")

def srcDir = "${rootDir}/tun2socks/intra"
def srcPackages = [srcDir,
"${srcDir}/android",
"${srcDir}/doh",
"${srcDir}/split",
"${srcDir}/protect"]

// Make sure that the go build directory exists.
task ensureBuildDir() {
doLast {
goBuildDir.mkdirs()
}
}

// Install `gomobile` and `gobind` to the build directory, so that the user
// does not need to install them on their system or call `gomobile init`.
task ensureGoMobile(type: Exec, dependsOn: ensureBuildDir) {
// Define outputs so this task will only be executed when they don't exist
outputs.file("${goBuildDir}/gomobile")
outputs.file("${goBuildDir}/gobind")

commandLine('go', 'build',
'-o', goBuildDir,
'golang.org/x/mobile/cmd/gomobile',
'golang.org/x/mobile/cmd/gobind')
}

// Invoke `gomobile bind` to build from `srcPackages` to `outputAAR`.
// `gomobile` needs the `ANDROID_HOME` environment variable to be set, and the
// parent directory of `gobind` must be in the `PATH` as well.
task gobind(type: Exec, dependsOn: ensureGoMobile) {
// Define inputs and outputs so Gradle will enable incremental builds
inputs.dir(srcDir)
outputs.file(outputAAR)

workingDir goBuildDir
environment 'ANDROID_HOME', android.sdkDirectory
environment 'PATH', goBuildDir.getPath() +
System.getProperty('path.separator') +
System.getenv('PATH')

commandLine("${goBuildDir}/gomobile", 'bind',
'-ldflags=-s -w',
'-target=android',
"-androidapi=${android.defaultConfig.minSdk}",
'-o', outputAAR,
*srcPackages)
}

// AAR file that can be consumed by other projects. For example:
// implementation project(path: ':tun2socks', configuration: 'aarBinary')
artifacts {
aarBinary(outputAAR) {
builtBy(gobind)
}
}
27 changes: 27 additions & 0 deletions Android/tun2socks/intra/android/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks

import (
"runtime/debug"

"github.com/eycorsican/go-tun2socks/common/log"
)

func init() {
// Conserve memory by increasing garbage collection frequency.
debug.SetGCPercent(10)
log.SetLevel(log.WARN)
}
38 changes: 38 additions & 0 deletions Android/tun2socks/intra/android/tun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks

import (
"errors"
"os"

"golang.org/x/sys/unix"
)

func makeTunFile(fd int) (*os.File, error) {
if fd < 0 {
return nil, errors.New("must provide a valid TUN file descriptor")
}
// Make a copy of `fd` so that os.File's finalizer doesn't close `fd`.
newfd, err := unix.Dup(fd)
if err != nil {
return nil, err
}
file := os.NewFile(uintptr(newfd), "")
if file == nil {
return nil, errors.New("failed to open TUN file descriptor")
}
return file, nil
}
110 changes: 110 additions & 0 deletions Android/tun2socks/intra/android/tun2socks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks

import (
"errors"
"io"
"io/fs"
"log"
"os"
"strings"

"github.com/Jigsaw-Code/Intra/Android/tun2socks/intra"
"github.com/Jigsaw-Code/Intra/Android/tun2socks/intra/doh"
"github.com/Jigsaw-Code/Intra/Android/tun2socks/intra/protect"
"github.com/Jigsaw-Code/outline-sdk/network"
)

// ConnectIntraTunnel reads packets from a TUN device and applies the Intra routing
// rules. Currently, this only consists of redirecting DNS packets to a specified
// server; all other data flows directly to its destination.
//
// `fd` is the TUN device. The IntraTunnel acquires an additional reference to it, which
//
// is released by IntraTunnel.Disconnect(), so the caller must close `fd` _and_ call
// Disconnect() in order to close the TUN device.
//
// `fakedns` is the DNS server that the system believes it is using, in "host:port" style.
//
// The port is normally 53.
//
// `udpdns` and `tcpdns` are the location of the actual DNS server being used. For DNS
//
// tunneling in Intra, these are typically high-numbered ports on localhost.
//
// `dohdns` is the initial DoH transport. It must not be `nil`.
// `protector` is a wrapper for Android's VpnService.protect() method.
// `eventListener` will be provided with a summary of each TCP and UDP socket when it is closed.
//
// Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to
// connect.
func ConnectIntraTunnel(
fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, eventListener intra.Listener,
) (*intra.Tunnel, error) {
tun, err := makeTunFile(fd)
if err != nil {
return nil, err
}
t, err := intra.NewTunnel(fakedns, dohdns, tun, protector, eventListener)
if err != nil {
return nil, err
}
go copyUntilEOF(t, tun)
go copyUntilEOF(tun, t)
return t, nil
}

// NewDoHTransport returns a DNSTransport that connects to the specified DoH server.
// `url` is the URL of a DoH server (no template, POST-only). If it is nonempty, it
//
// overrides `udpdns` and `tcpdns`.
//
// `ips` is an optional comma-separated list of IP addresses for the server. (This
//
// wrapper is required because gomobile can't make bindings for []string.)
//
// `protector` is the socket protector to use for all external network activity.
// `auth` will provide a client certificate if required by the TLS server.
// `eventListener` will be notified after each DNS query succeeds or fails.
func NewDoHTransport(
url string, ips string, protector protect.Protector, auth doh.ClientAuth, eventListener intra.Listener,
) (doh.Transport, error) {
split := []string{}
if len(ips) > 0 {
split = strings.Split(ips, ",")
}
dialer := protect.MakeDialer(protector)
return doh.NewTransport(url, split, dialer, auth, eventListener)
}

func copyUntilEOF(dst, src io.ReadWriteCloser) {
log.Printf("[debug] start relaying traffic [%s] -> [%s]", src, dst)
defer log.Printf("[debug] stop relaying traffic [%s] -> [%s]", src, dst)

const commonMTU = 1500
buf := make([]byte, commonMTU)
defer dst.Close()
for {
_, err := io.CopyBuffer(dst, src, buf)
if err == nil || isErrClosed(err) {
return
}
}
}

func isErrClosed(err error) bool {
return errors.Is(err, os.ErrClosed) || errors.Is(err, fs.ErrClosed) || errors.Is(err, network.ErrClosed)
}
38 changes: 38 additions & 0 deletions Android/tun2socks/intra/doh/atomic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package doh

import (
"sync/atomic"
)

// Atomic is atomic.Value, specialized for doh.Transport.
type Atomic struct {
v atomic.Value
}

// Store a DNSTransport. d must not be nil.
func (a *Atomic) Store(t Transport) {
a.v.Store(t)
}

// Load the DNSTransport, or nil if it has not been stored.
func (a *Atomic) Load() Transport {
v := a.v.Load()
if v == nil {
return nil
}
return v.(Transport)
}
Loading

0 comments on commit d355484

Please sign in to comment.