Skip to content

Commit

Permalink
fix: cancel all outgoing requests when disconnect to prevent ANR (#501)
Browse files Browse the repository at this point in the history
In this PR, I fixed an ANR issue by adding a `context.Context` object to the `doh.Transport.Query` method. This `ctx` will be passed to all network related calls, and it will be cancelled when user `Disconnect`s.

But if we just simply add `ctx` to the method, `gomobile` requires we export all related packages (e.g., `time`) that are used by `context.Context`, which is not applicable. Therefore we refactored the go code structure and introduced a new `backend` package that will be the only interface that Java code can use.

In addition, I retired `github.com/eycorsican/go-tun2socks/common/log` and introduced our own `logging` package.
  • Loading branch information
jyyi1 authored Feb 22, 2024
1 parent a6e4eca commit 4240f84
Show file tree
Hide file tree
Showing 34 changed files with 755 additions and 471 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.iml
*.p12
.gradle
.vscode/
/Android/local.properties
/Android/keystore.properties
/Android/.idea/
Expand Down
5 changes: 2 additions & 3 deletions Android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ try {

// Go backend build constants
def goSourceDir = "${projectDir}/src/go"
def goSourcePackages = ["${goSourceDir}/intra",
"${goSourceDir}/intra/android",
"${goSourceDir}/intra/doh",
def goSourcePackages = ["${goSourceDir}/backend",
"${goSourceDir}/intra",
"${goSourceDir}/intra/split",
"${goSourceDir}/intra/protect"]
def goBuildDir = file("${buildDir}/go")
Expand Down
34 changes: 34 additions & 0 deletions Android/app/src/go/backend/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2024 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 backend exposes objects and functions for Intra's application code (i.e., Java code).
It is the only packages that should be used by the application code, and is not intended for
use by other Go code.
This package provides the following features:
# DoHServer
[DoHServer] connects to a DNS-over-HTTPS (DoH) server that handles DNS requests.
# Session
Intra [Session] reads from a local tun device and:
- redirects DNS requests to a specific [DoHServer]
- splits TLS packets into two randomly sized packets
- Forwards all other traffic untouched
*/
package backend
152 changes: 152 additions & 0 deletions Android/app/src/go/backend/doh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2024 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 backend

import (
"context"
"errors"
"fmt"
"strings"

"localhost/Intra/Android/app/src/go/doh"
"localhost/Intra/Android/app/src/go/intra/protect"
)

// DoHServer represents a DNS-over-HTTPS server.
type DoHServer struct {
r doh.Resolver
}

// NewDoHServer creates a DoHServer that connects to the specified DoH server.
//
// url is the URL of a DoH server (no template, POST-only).
//
// ipsStr is an optional comma-separated list of IP addresses for the server. It will be used when the url
// cannot be resolved to working IP addresses. (string is required cuz gomobile doesn't support []string)
//
// protector is Android's socket protector to use for all external network activity.
//
// listener will be notified after each DNS query succeeds or fails.
func NewDoHServer(
url string, ipsStr string, protector protect.Protector, listener DoHListener,
) (*DoHServer, error) {
ips := []string{}
if len(ipsStr) > 0 {
ips = strings.Split(ipsStr, ",")
}
dialer := protect.MakeDialer(protector)
t, err := doh.NewResolver(url, ips, dialer, nil, makeInternalDoHListener(listener))
if err != nil {
return nil, err
}
return &DoHServer{t}, nil
}

// dohQuery is used by [DoHServer].Probe.
var dohQuery = []byte{
0, 0, // [0-1] query ID
1, 0, // [2-3] flags, RD=1
0, 1, // [4-5] QDCOUNT (number of queries) = 1
0, 0, // [6-7] ANCOUNT (number of answers) = 0
0, 0, // [8-9] NSCOUNT (number of authoritative answers) = 0
0, 0, // [10-11] ARCOUNT (number of additional records) = 0

// Start of first query
7, 'y', 'o', 'u', 't', 'u', 'b', 'e',
3, 'c', 'o', 'm',
0, // null terminator of FQDN (DNS root)
0, 1, // QTYPE = A
0, 1, // QCLASS = IN (Internet)
}

// Probe checks whether the [DoHServer] server can handle DNS-over-HTTPS (DoH) requests.
//
// If the server responds correctly, the function returns nil. Otherwise, the function returns an error.
func Probe(s *DoHServer) error {
resp, err := s.r.Query(context.Background(), dohQuery)
if err != nil {
return fmt.Errorf("failed to send query: %w", err)
}
if len(resp) == 0 {
return errors.New("invalid DoH response")
}
return nil
}

////////// event listeners

// DoHQueryToken is an opaque object used to match responses to queries.
// The same DoHQueryToken that returned by [DoHListener].OnQuery be passed
// to the corresponding [DoHListener].OnResponse.
type DoHQueryToken doh.Token

// DoHListener is an event listener that receives DoH request reports.
// Application code can implement this interface to receive these reports.
type DoHListener interface {
// OnQuery will be called when a DoH request is issued to url.
// Application code return an arbitrary DoHQueryToken object for internal use,
// the same object will be passed to OnResponse.
OnQuery(url string) DoHQueryToken

// OnResponse will be called when a DoH response has been received.
OnResponse(DoHQueryToken, *DoHQuerySumary)
}

// DoHStatus is an integer representing the status of a DoH transaction.
type DoHStatus = int

const (
DoHStatusComplete DoHStatus = doh.Complete // Transaction completed successfully
DoHStatusSendFailed DoHStatus = doh.SendFailed // Failed to send query
DoHStatusHTTPError DoHStatus = doh.HTTPError // Got a non-200 HTTP status
DoHStatusBadQuery DoHStatus = doh.BadQuery // Malformed input
DoHStatusBadResponse DoHStatus = doh.BadResponse // Response was invalid
DoHStatusInternalError DoHStatus = doh.InternalError // This should never happen
)

// DoHQuerySumary is the summary of a DNS transaction.
// It will be reported to [DoHListener].OnResponse when it is complete.
type DoHQuerySumary struct {
summ *doh.Summary
}

func (q DoHQuerySumary) GetQuery() []byte { return q.summ.Query }
func (q DoHQuerySumary) GetResponse() []byte { return q.summ.Response }
func (q DoHQuerySumary) GetServer() string { return q.summ.Server }
func (q DoHQuerySumary) GetStatus() DoHStatus { return q.summ.Status }
func (q DoHQuerySumary) GetHTTPStatus() int { return q.summ.HTTPStatus }
func (q DoHQuerySumary) GetLatency() float64 { return q.summ.Latency }

// dohListenerAdapter is an adapter for the internal [doh.Listener].
type dohListenerAdapter struct {
l DoHListener
}

// makeInternalDoHListener creates a [doh.Listener] from the public [DoHListener]
// interface that will be implemented by the application code.
func makeInternalDoHListener(l DoHListener) doh.Listener {
if l == nil {
return nil
}
return &dohListenerAdapter{l}
}

func (e dohListenerAdapter) OnQuery(url string) doh.Token {
return e.l.OnQuery(url)
}

func (e dohListenerAdapter) OnResponse(t doh.Token, s *doh.Summary) {
e.l.OnResponse(t, &DoHQuerySumary{s})
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Jigsaw Operations LLC
// Copyright 2024 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.
Expand All @@ -12,16 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks
package backend

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)
}
92 changes: 92 additions & 0 deletions Android/app/src/go/backend/tunnel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2024 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 backend

import (
"errors"
"io"
"io/fs"
"localhost/Intra/Android/app/src/go/intra"
"localhost/Intra/Android/app/src/go/intra/protect"
"localhost/Intra/Android/app/src/go/logging"
"localhost/Intra/Android/app/src/go/tuntap"
"os"

"github.com/Jigsaw-Code/outline-sdk/network"
)

// Session represents an Intra communication session.
//
// TODO: copy methods of intra.Tunnel here and do not expose intra package
type Session struct {
// TODO: hide this internal Tunnel when finished moving everything to backend
*intra.Tunnel
}

func (s *Session) SetDoHServer(svr *DoHServer) { s.SetDNS(svr.r) }

// ConnectSession 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 intra [Session] acquires an additional reference to it,
// which is released by [Session].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.
//
// dohdns is the initial DoH transport and 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.
func ConnectSession(
fd int, fakedns string, dohdns *DoHServer, protector protect.Protector, listener intra.Listener,
) (*Session, error) {
// TODO: define Tunnel type in this backend package, and do not export intra package
tun, err := tuntap.MakeTunDeviceFromFD(fd)
if err != nil {
return nil, err
}
if dohdns == nil {
return nil, errors.New("dohdns must not be nil")
}
t, err := intra.NewTunnel(fakedns, dohdns.r, tun, protector, listener)
if err != nil {
return nil, err
}
go copyUntilEOF(t, tun)
go copyUntilEOF(tun, t)
return &Session{t}, nil
}

func copyUntilEOF(dst, src io.ReadWriteCloser) {
logging.Debug("IntraSession(copyUntilEOF) - start relaying traffic", "src", src, "dst", dst)
defer logging.Debug("IntraSession(copyUntilEOF) - stop relaying traffic", "src", src, "dst", dst)

const commonMTU = 1500
buf := make([]byte, commonMTU)
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import (
"crypto/x509"
"errors"
"io"

"github.com/eycorsican/go-tun2socks/common/log"
"localhost/Intra/Android/app/src/go/logging"
)

// ClientAuth interface for providing TLS certificates and signatures.
Expand Down Expand Up @@ -52,12 +51,12 @@ type clientAuthWrapper struct {
func (ca *clientAuthWrapper) GetClientCertificate(
info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
if ca.signer == nil {
log.Warnf("Client certificate requested but not supported")
logging.Warn("Client certificate requested but not supported")
return &tls.Certificate{}, nil
}
cert := ca.signer.GetClientCertificate()
if cert == nil {
log.Warnf("Unable to fetch client certificate")
logging.Warn("Unable to fetch client certificate")
return &tls.Certificate{}, nil
}
chain := [][]byte{cert}
Expand All @@ -67,13 +66,13 @@ func (ca *clientAuthWrapper) GetClientCertificate(
}
leaf, err := x509.ParseCertificate(cert)
if err != nil {
log.Warnf("Unable to parse client certificate: %v", err)
logging.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")
logging.Warn("Only ECDSA client certificates are supported")
return &tls.Certificate{}, nil
}
return &tls.Certificate{
Expand All @@ -91,7 +90,7 @@ func (ca *clientAuthWrapper) Public() crypto.PublicKey {
cert := ca.signer.GetClientCertificate()
leaf, err := x509.ParseCertificate(cert)
if err != nil {
log.Warnf("Unable to parse client certificate: %v", err)
logging.Warnf("Unable to parse client certificate: %v", err)
return nil
}
return leaf.PublicKey
Expand Down
File renamed without changes.
Loading

0 comments on commit 4240f84

Please sign in to comment.