Skip to content

Commit

Permalink
refactor: build tun2socks from source within Android Studio
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 committed Oct 5, 2023
1 parent 874e406 commit 7dc42c4
Show file tree
Hide file tree
Showing 33 changed files with 4,848 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Android/app/build.gradle
Original file line number Diff line number Diff line change
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
2 changes: 0 additions & 2 deletions Android/tun2socks/README.md

This file was deleted.

47 changes: 45 additions & 2 deletions Android/tun2socks/build.gradle
Original file line number Diff line number Diff line change
@@ -1,2 +1,45 @@
configurations.maybeCreate("default")
artifacts.add("default", file('tun2socks.aar'))
plugins {
id('com.android.library')
}

android {
compileSdkVersion 33
}

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


def goBuildDir = file("${buildDir}/go")
def outputAAR = file("${buildDir}/tun2socks.aar")
def srcPkgs = ["${rootDir}/tun2socks/intra", "${rootDir}/tun2socks/intra/android", "${rootDir}/tun2socks/intra/doh", "${rootDir}/tun2socks/intra/split", "${rootDir}/tun2socks/intra/protect"]

task ensureBuildDir() {
doLast {
goBuildDir.mkdirs()
}
}

task ensureGoMobile(type: Exec, dependsOn: ensureBuildDir) {
environment 'GOBIN', goBuildDir.getPath()
commandLine 'go', 'install', 'golang.org/x/mobile/cmd/gomobile@latest', 'golang.org/x/mobile/cmd/gobind@latest'
}

task gobind(type: Exec, dependsOn: ensureGoMobile) {
workingDir goBuildDir
environment 'ANDROID_HOME', android.sdkDirectory
environment 'PATH', goBuildDir.getPath() + System.getProperty('path.separator') + System.getenv('PATH')
commandLine("${goBuildDir}/gomobile", 'bind', '-target=android', '-androidapi=16', '-o', outputAAR, *srcPkgs)
}


// AAR file that can be consumed by other projects
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)
}
116 changes: 116 additions & 0 deletions Android/tun2socks/intra/doh/client_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 (
"crypto"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"errors"
"io"

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

// ClientAuth interface for providing TLS certificates and signatures.
type ClientAuth interface {
// GetClientCertificate returns the client certificate (if any).
// May block as the first call may cause certificates to load.
// Returns a DER encoded X.509 client certificate.
GetClientCertificate() []byte
// GetIntermediateCertificate returns the chaining certificate (if any).
// It does not block or cause certificates to load.
// Returns a DER encoded X.509 certificate.
GetIntermediateCertificate() []byte
// Request a signature on a digest.
Sign(digest []byte) []byte
}

// clientAuthWrapper manages certificate loading and usage during TLS handshakes.
// Implements crypto.Signer.
type clientAuthWrapper struct {
signer ClientAuth
}

// GetClientCertificate returns the client certificate chain as a tls.Certificate.
// Returns an empty Certificate on failure, permitting the handshake to
// continue without authentication.
// Implements tls.Config GetClientCertificate().
func (ca *clientAuthWrapper) GetClientCertificate(
info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
if ca.signer == nil {
log.Warnf("Client certificate requested but not supported")
return &tls.Certificate{}, nil
}
cert := ca.signer.GetClientCertificate()
if cert == nil {
log.Warnf("Unable to fetch client certificate")
return &tls.Certificate{}, nil
}
chain := [][]byte{cert}
intermediate := ca.signer.GetIntermediateCertificate()
if intermediate != nil {
chain = append(chain, intermediate)
}
leaf, err := x509.ParseCertificate(cert)
if err != nil {
log.Warnf("Unable to parse client certificate: %v", err)
return &tls.Certificate{}, nil
}
_, isECDSA := leaf.PublicKey.(*ecdsa.PublicKey)
if !isECDSA {
// RSA-PSS and RSA-SSA both need explicit signature generation support.
log.Warnf("Only ECDSA client certificates are supported")
return &tls.Certificate{}, nil
}
return &tls.Certificate{
Certificate: chain,
PrivateKey: ca,
Leaf: leaf,
}, nil
}

// Public returns the public key for the client certificate.
func (ca *clientAuthWrapper) Public() crypto.PublicKey {
if ca.signer == nil {
return nil
}
cert := ca.signer.GetClientCertificate()
leaf, err := x509.ParseCertificate(cert)
if err != nil {
log.Warnf("Unable to parse client certificate: %v", err)
return nil
}
return leaf.PublicKey
}

// Sign a digest.
func (ca *clientAuthWrapper) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
if ca.signer == nil {
return nil, errors.New("no client certificate")
}
signature := ca.signer.Sign(digest)
if signature == nil {
return nil, errors.New("failed to create signature")
}
return signature, nil
}

func newClientAuthWrapper(signer ClientAuth) clientAuthWrapper {
return clientAuthWrapper{
signer: signer,
}
}
Loading

0 comments on commit 7dc42c4

Please sign in to comment.