-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: cancel all outgoing requests when disconnect to prevent ANR (#501)
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
Showing
34 changed files
with
755 additions
and
471 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
Oops, something went wrong.