From 4240f8424d113e19cb0c7e0d5e1345a2ef4f65ef Mon Sep 17 00:00:00 2001 From: "J. Yi" <93548144+jyyi1@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:50:47 -0500 Subject: [PATCH] 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. --- .gitignore | 1 + Android/app/build.gradle | 5 +- Android/app/src/go/backend/doc.go | 34 +++ Android/app/src/go/backend/doh.go | 152 +++++++++++++ .../src/go/{intra/android => backend}/init.go | 7 +- Android/app/src/go/backend/tunnel.go | 92 ++++++++ .../app/src/go/{intra => }/doh/client_auth.go | 13 +- .../go/{intra => }/doh/client_auth_test.go | 0 Android/app/src/go/{intra => }/doh/doh.go | 205 +++++++++-------- .../app/src/go/{intra => }/doh/doh_test.go | 209 +++++++++++------- .../app/src/go/{intra => }/doh/ipmap/ipmap.go | 5 +- .../go/{intra => }/doh/ipmap/ipmap_test.go | 0 Android/app/src/go/{intra => }/doh/padding.go | 0 Android/app/src/go/intra/android/tun2socks.go | 110 --------- Android/app/src/go/intra/doh/atomic.go | 38 ---- Android/app/src/go/intra/packet_proxy.go | 16 +- Android/app/src/go/intra/protect/protect.go | 5 +- Android/app/src/go/intra/sni_reporter.go | 22 +- Android/app/src/go/intra/sni_reporter_test.go | 17 +- .../app/src/go/intra/split/example/main.go | 2 +- Android/app/src/go/intra/split/retrier.go | 12 +- .../app/src/go/intra/split/retrier_test.go | 3 +- Android/app/src/go/intra/stream_dialer.go | 15 +- Android/app/src/go/intra/tcp.go | 3 +- Android/app/src/go/intra/tunnel.go | 32 +-- Android/app/src/go/logging/logging.go | 75 +++++++ .../src/go/{intra/android => tuntap}/tun.go | 6 +- .../main/java/app/intra/net/doh/Prober.java | 16 -- .../src/main/java/app/intra/net/doh/Race.java | 5 + .../app/intra/net/go/GoIntraListener.java | 38 ++-- .../main/java/app/intra/net/go/GoProber.java | 17 +- .../java/app/intra/net/go/GoVpnAdapter.java | 47 ++-- go.mod | 9 +- go.sum | 15 ++ 34 files changed, 755 insertions(+), 471 deletions(-) create mode 100644 Android/app/src/go/backend/doc.go create mode 100644 Android/app/src/go/backend/doh.go rename Android/app/src/go/{intra/android => backend}/init.go (84%) create mode 100644 Android/app/src/go/backend/tunnel.go rename Android/app/src/go/{intra => }/doh/client_auth.go (90%) rename Android/app/src/go/{intra => }/doh/client_auth_test.go (100%) rename Android/app/src/go/{intra => }/doh/doh.go (64%) rename Android/app/src/go/{intra => }/doh/doh_test.go (82%) rename Android/app/src/go/{intra => }/doh/ipmap/ipmap.go (97%) rename Android/app/src/go/{intra => }/doh/ipmap/ipmap_test.go (100%) rename Android/app/src/go/{intra => }/doh/padding.go (100%) delete mode 100644 Android/app/src/go/intra/android/tun2socks.go delete mode 100644 Android/app/src/go/intra/doh/atomic.go create mode 100644 Android/app/src/go/logging/logging.go rename Android/app/src/go/{intra/android => tuntap}/tun.go (90%) diff --git a/.gitignore b/.gitignore index 418b3b88..40c8400d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iml *.p12 .gradle +.vscode/ /Android/local.properties /Android/keystore.properties /Android/.idea/ diff --git a/Android/app/build.gradle b/Android/app/build.gradle index 0074f7f8..fccb012e 100644 --- a/Android/app/build.gradle +++ b/Android/app/build.gradle @@ -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") diff --git a/Android/app/src/go/backend/doc.go b/Android/app/src/go/backend/doc.go new file mode 100644 index 00000000..93d17d7f --- /dev/null +++ b/Android/app/src/go/backend/doc.go @@ -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 diff --git a/Android/app/src/go/backend/doh.go b/Android/app/src/go/backend/doh.go new file mode 100644 index 00000000..67366898 --- /dev/null +++ b/Android/app/src/go/backend/doh.go @@ -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}) +} diff --git a/Android/app/src/go/intra/android/init.go b/Android/app/src/go/backend/init.go similarity index 84% rename from Android/app/src/go/intra/android/init.go rename to Android/app/src/go/backend/init.go index a91226f8..fe9a89c8 100644 --- a/Android/app/src/go/intra/android/init.go +++ b/Android/app/src/go/backend/init.go @@ -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. @@ -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) } diff --git a/Android/app/src/go/backend/tunnel.go b/Android/app/src/go/backend/tunnel.go new file mode 100644 index 00000000..63d3dd18 --- /dev/null +++ b/Android/app/src/go/backend/tunnel.go @@ -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) +} diff --git a/Android/app/src/go/intra/doh/client_auth.go b/Android/app/src/go/doh/client_auth.go similarity index 90% rename from Android/app/src/go/intra/doh/client_auth.go rename to Android/app/src/go/doh/client_auth.go index e7e5aa97..10c5cf49 100644 --- a/Android/app/src/go/intra/doh/client_auth.go +++ b/Android/app/src/go/doh/client_auth.go @@ -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. @@ -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} @@ -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{ @@ -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 diff --git a/Android/app/src/go/intra/doh/client_auth_test.go b/Android/app/src/go/doh/client_auth_test.go similarity index 100% rename from Android/app/src/go/intra/doh/client_auth_test.go rename to Android/app/src/go/doh/client_auth_test.go diff --git a/Android/app/src/go/intra/doh/doh.go b/Android/app/src/go/doh/doh.go similarity index 64% rename from Android/app/src/go/intra/doh/doh.go rename to Android/app/src/go/doh/doh.go index 6242ef51..82e2f69a 100644 --- a/Android/app/src/go/intra/doh/doh.go +++ b/Android/app/src/go/doh/doh.go @@ -16,12 +16,12 @@ package doh import ( "bytes" + "context" "crypto/tls" "encoding/binary" "errors" "fmt" "io" - "io/ioutil" "math" "net" "net/http" @@ -32,9 +32,10 @@ import ( "sync" "time" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh/ipmap" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/split" - "github.com/eycorsican/go-tun2socks/common/log" + "localhost/Intra/Android/app/src/go/doh/ipmap" + "localhost/Intra/Android/app/src/go/intra/split" + "localhost/Intra/Android/app/src/go/logging" + "golang.org/x/net/dns/dnsmessage" ) @@ -77,20 +78,22 @@ type Listener interface { OnResponse(Token, *Summary) } -// Transport represents a DNS query transport. This interface is exported by gobind, -// so it has to be very simple. -type Transport interface { - // Given a DNS query (including ID), returns a DNS response with matching - // ID, or an error if no response was received. The error may be accompanied - // by a SERVFAIL response if appropriate. - Query(q []byte) ([]byte, error) - // Return the server URL used to initialize this transport. +// Resolver represents a DNS-over-HTTPS (DoH) resolver. +type Resolver interface { + // Query sends a DNS query represented by q (including ID) to this DoH resolver + // (located at GetURL) using the provided context, and returns the correponding + // + // A non-nil error will be returned if no response was received from the DoH resolver, + // the error may also be accompanied by a SERVFAIL response if appropriate. + Query(ctx context.Context, q []byte) ([]byte, error) + + // Return the server URL used to initialize this DoH resolver. GetURL() string } // TODO: Keep a context here so that queries can be canceled. -type transport struct { - Transport +type resolver struct { + Resolver url string hostname string port int @@ -105,8 +108,8 @@ type transport struct { // Wait up to three seconds for the TCP handshake to complete. const tcpTimeout time.Duration = 3 * time.Second -func (t *transport) dial(network, addr string) (net.Conn, error) { - log.Debugf("Dialing %s", addr) +func (r *resolver) dial(ctx context.Context, network, addr string) (net.Conn, error) { + logging.Debug("DoH(resolver.dial) - dialing", "addr", addr) domain, portStr, err := net.SplitHostPort(addr) if err != nil { return nil, err @@ -122,46 +125,47 @@ func (t *transport) dial(network, addr string) (net.Conn, error) { // TODO: Improve IP fallback strategy with parallelism and Happy Eyeballs. var conn net.Conn - ips := t.ips.Get(domain) + ips := r.ips.Get(domain) confirmed := ips.Confirmed() if confirmed != nil { - log.Debugf("Trying confirmed IP %s for addr %s", confirmed.String(), addr) - if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(confirmed), nil); err == nil { - log.Infof("Confirmed IP %s worked", confirmed.String()) + logging.Debug("DoH(resolver.dial) - trying confirmed IP", "confirmedIP", confirmed, "addr", addr) + if conn, err = split.DialWithSplitRetry(ctx, r.dialer, tcpaddr(confirmed), nil); err == nil { + logging.Info("DoH(resolver.dial) - confirmed IP worked", "confirmedIP", confirmed) return conn, nil } - log.Debugf("Confirmed IP %s failed with err %v", confirmed.String(), err) + logging.Debug("DoH(resolver.dial) - confirmed IP failed", "confirmedIP", confirmed, "err", err) ips.Disconfirm(confirmed) } - log.Debugf("Trying all IPs") + logging.Debug("DoH(resolver.dial) - trying all IPs") for _, ip := range ips.GetAll() { if ip.Equal(confirmed) { // Don't try this IP twice. continue } - if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(ip), nil); err == nil { - log.Infof("Found working IP: %s", ip.String()) + if conn, err = split.DialWithSplitRetry(ctx, r.dialer, tcpaddr(ip), nil); err == nil { + logging.Info("DoH(resolver.dial) - found working IP", "ip", ip) return conn, nil } } return nil, err } -// NewTransport returns a DoH DNSTransport, ready for use. +// NewResolver returns a DoH [Resolver], ready for use. // This is a POST-only DoH implementation, so the DoH template should be a URL. -// `rawurl` is the DoH template in string form. -// `addrs` is a list of domains or IP addresses to use as fallback, if the hostname // -// lookup fails or returns non-working addresses. +// `rawurl` is the DoH template in string form. // -// `dialer` is the dialer that the transport will use. The transport will modify the dialer's +// `addrs` is a list of domains or IP addresses to use as fallback, if the hostname lookup fails or +// returns non-working addresses. // -// timeout but will not mutate it otherwise. +// `dialer` is the dialer that the [Resolver] will use. The [Resolver] will modify the dialer's +// timeout but will not mutate it otherwise. // // `auth` will provide a client certificate if required by the TLS server. +// // `listener` will receive the status of each DNS query when it is complete. -func NewTransport(rawurl string, addrs []string, dialer *net.Dialer, auth ClientAuth, listener Listener) (Transport, error) { +func NewResolver(rawurl string, addrs []string, dialer *net.Dialer, auth ClientAuth, listener Listener) (Resolver, error) { if dialer == nil { dialer = &net.Dialer{} } @@ -184,7 +188,7 @@ func NewTransport(rawurl string, addrs []string, dialer *net.Dialer, auth Client port = 443 } - t := &transport{ + t := &resolver{ url: rawurl, hostname: parsedurl.Hostname(), port: port, @@ -211,7 +215,7 @@ func NewTransport(rawurl string, addrs []string, dialer *net.Dialer, auth Client // Override the dial function. t.client.Transport = &http.Transport{ - Dial: t.dial, + DialContext: t.dial, ForceAttemptHTTP2: true, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 20 * time.Second, // Same value as Android DNS-over-TLS @@ -247,15 +251,15 @@ func (e *httpError) Error() string { // Independent of the query's success or failure, this function also returns the // address of the server on a best-effort basis, or nil if the address could not // be determined. -func (t *transport) doQuery(q []byte) (response []byte, server *net.TCPAddr, qerr *queryError) { +func (r *resolver) doQuery(ctx context.Context, q []byte) (response []byte, server *net.TCPAddr, qerr *queryError) { if len(q) < 2 { qerr = &queryError{BadQuery, fmt.Errorf("Query length is %d", len(q))} return } - t.hangoverLock.RLock() - inHangover := time.Now().Before(t.hangoverExpiration) - t.hangoverLock.RUnlock() + r.hangoverLock.RLock() + inHangover := time.Now().Before(r.hangoverExpiration) + r.hangoverLock.RUnlock() if inHangover { response = tryServfail(q) qerr = &queryError{HTTPError, errors.New("Forwarder is in servfail hangover")} @@ -272,14 +276,14 @@ func (t *transport) doQuery(q []byte) (response []byte, server *net.TCPAddr, qer // Zero out the query ID. id := binary.BigEndian.Uint16(q) binary.BigEndian.PutUint16(q, 0) - req, err := http.NewRequest(http.MethodPost, t.url, bytes.NewBuffer(q)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.url, bytes.NewBuffer(q)) if err != nil { qerr = &queryError{InternalError, err} return } var hostname string - response, hostname, server, qerr = t.sendRequest(id, req) + response, hostname, server, qerr = r.sendRequest(id, req) // Restore the query ID. binary.BigEndian.PutUint16(q, id) @@ -297,21 +301,21 @@ func (t *transport) doQuery(q []byte) (response []byte, server *net.TCPAddr, qer if qerr != nil { if qerr.status != SendFailed { - t.hangoverLock.Lock() - t.hangoverExpiration = time.Now().Add(hangoverDuration) - t.hangoverLock.Unlock() + r.hangoverLock.Lock() + r.hangoverExpiration = time.Now().Add(hangoverDuration) + r.hangoverLock.Unlock() } response = tryServfail(q) } else if server != nil { // Record a working IP address for this server iff qerr is nil - t.ips.Get(hostname).Confirm(server.IP) + r.ips.Get(hostname).Confirm(server.IP) } return } -func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, hostname string, server *net.TCPAddr, qerr *queryError) { - hostname = t.hostname +func (r *resolver) sendRequest(id uint16, req *http.Request) (response []byte, hostname string, server *net.TCPAddr, qerr *queryError) { + hostname = r.hostname // The connection used for this request. If the request fails, we will close // this socket, in case it is no longer functioning. @@ -324,13 +328,13 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, if qerr == nil { return } - log.Infof("%d Query failed: %v", id, qerr) + logging.Info("DoH(resolver.sendRequest) - done", "id", id, "queryError", qerr) if server != nil { - log.Debugf("%d Disconfirming %s", id, server.IP.String()) - t.ips.Get(hostname).Disconfirm(server.IP) + logging.Debug("DoH(resolver.sendRequest) - disconfirming IP", "id", id, "ip", server.IP) + r.ips.Get(hostname).Disconfirm(server.IP) } if conn != nil { - log.Infof("%d Closing failing DoH socket", id) + logging.Info("DoH(resolver.sendRequest) - closing failing DoH socket", "id", id) conn.Close() } }() @@ -341,10 +345,10 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, // reading the variables it has set. trace := httptrace.ClientTrace{ GetConn: func(hostPort string) { - log.Debugf("%d GetConn(%s)", id, hostPort) + logging.Debugf("%d GetConn(%s)", id, hostPort) }, GotConn: func(info httptrace.GotConnInfo) { - log.Debugf("%d GotConn(%v)", id, info) + logging.Debugf("%d GotConn(%v)", id, info) if info.Conn == nil { return } @@ -353,41 +357,41 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, server = conn.RemoteAddr().(*net.TCPAddr) }, PutIdleConn: func(err error) { - log.Debugf("%d PutIdleConn(%v)", id, err) + logging.Debugf("%d PutIdleConn(%v)", id, err) }, GotFirstResponseByte: func() { - log.Debugf("%d GotFirstResponseByte()", id) + logging.Debugf("%d GotFirstResponseByte()", id) }, Got100Continue: func() { - log.Debugf("%d Got100Continue()", id) + logging.Debugf("%d Got100Continue()", id) }, Got1xxResponse: func(code int, header textproto.MIMEHeader) error { - log.Debugf("%d Got1xxResponse(%d, %v)", id, code, header) + logging.Debugf("%d Got1xxResponse(%d, %v)", id, code, header) return nil }, DNSStart: func(info httptrace.DNSStartInfo) { - log.Debugf("%d DNSStart(%v)", id, info) + logging.Debugf("%d DNSStart(%v)", id, info) }, DNSDone: func(info httptrace.DNSDoneInfo) { - log.Debugf("%d, DNSDone(%v)", id, info) + logging.Debugf("%d, DNSDone(%v)", id, info) }, ConnectStart: func(network, addr string) { - log.Debugf("%d ConnectStart(%s, %s)", id, network, addr) + logging.Debugf("%d ConnectStart(%s, %s)", id, network, addr) }, ConnectDone: func(network, addr string, err error) { - log.Debugf("%d ConnectDone(%s, %s, %v)", id, network, addr, err) + logging.Debugf("%d ConnectDone(%s, %s, %v)", id, network, addr, err) }, TLSHandshakeStart: func() { - log.Debugf("%d TLSHandshakeStart()", id) + logging.Debugf("%d TLSHandshakeStart()", id) }, TLSHandshakeDone: func(state tls.ConnectionState, err error) { - log.Debugf("%d TLSHandshakeDone(%v, %v)", id, state, err) + logging.Debugf("%d TLSHandshakeDone(%v, %v)", id, state, err) }, WroteHeaders: func() { - log.Debugf("%d WroteHeaders()", id) + logging.Debugf("%d WroteHeaders()", id) }, WroteRequest: func(info httptrace.WroteRequestInfo) { - log.Debugf("%d WroteRequest(%v)", id, info) + logging.Debugf("%d WroteRequest(%v)", id, info) }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), &trace)) @@ -396,20 +400,20 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, req.Header.Set("Content-Type", mimetype) req.Header.Set("Accept", mimetype) req.Header.Set("User-Agent", "Intra") - log.Debugf("%d Sending query", id) - httpResponse, err := t.client.Do(req) + logging.Debug("DoH(resolver.sendRequest) - sending query", "id", id) + httpResponse, err := r.client.Do(req) if err != nil { qerr = &queryError{SendFailed, err} return } - log.Debugf("%d Got response", id) - response, err = ioutil.ReadAll(httpResponse.Body) + logging.Debug("DoH(resolver.sendRequest) - got response", "id", id) + response, err = io.ReadAll(httpResponse.Body) if err != nil { qerr = &queryError{BadResponse, err} return } httpResponse.Body.Close() - log.Debugf("%d Closed response", id) + logging.Debug("DoH(resolver.sendRequest) - response closed", "id", id) // Update the hostname, which could have changed due to a redirect. hostname = httpResponse.Request.URL.Hostname() @@ -419,7 +423,7 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, req.Write(reqBuf) respBuf := new(bytes.Buffer) httpResponse.Write(respBuf) - log.Debugf("%d request: %s\nresponse: %s", id, reqBuf.String(), respBuf.String()) + logging.Debug("DoH(resolver.sendRequest) - response invalid", "id", id, "req", reqBuf, "resp", respBuf) qerr = &queryError{HTTPError, &httpError{httpResponse.StatusCode}} return @@ -428,16 +432,17 @@ func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, return } -func (t *transport) Query(q []byte) ([]byte, error) { +func (r *resolver) Query(ctx context.Context, q []byte) ([]byte, error) { var token Token - if t.listener != nil { - token = t.listener.OnQuery(t.url) + if r.listener != nil { + token = r.listener.OnQuery(r.url) } before := time.Now() - response, server, qerr := t.doQuery(q) + response, server, qerr := r.doQuery(ctx, q) after := time.Now() + errIsCancel := false var err error status := Complete httpStatus := http.StatusOK @@ -445,6 +450,7 @@ func (t *transport) Query(q []byte) ([]byte, error) { err = qerr status = qerr.status httpStatus = 0 + errIsCancel = errors.Is(qerr, context.Canceled) var herr *httpError if errors.As(qerr.err, &herr) { @@ -452,14 +458,25 @@ func (t *transport) Query(q []byte) ([]byte, error) { } } - if t.listener != nil { + // Stop sending OnResponse when the error is cancelled because the cancelled + // error is typically triggered by a Disconnect() operation, which will cause + // the following deadlock: + // 1. Java - synchronized VpnController.stop() + // 2. Go - context.Cancel() + // 3. Go - (if we don't stop sending OnResponse) + // 4. Java - GoIntraListener.onResponse + // 5. Java - synchronized VpnController.onConnectionStateChanged() + // Deadlock happens (both Step 1 and Step 5 are marked as synchronized)! + // + // TODO: make stop() an asynchronized function + if r.listener != nil && !errIsCancel { latency := after.Sub(before) var ip string if server != nil { ip = server.IP.String() } - t.listener.OnResponse(token, &Summary{ + r.listener.OnResponse(token, &Summary{ Latency: latency.Seconds(), Query: q, Response: response, @@ -471,13 +488,13 @@ func (t *transport) Query(q []byte) ([]byte, error) { return response, err } -func (t *transport) GetURL() string { - return t.url +func (r *resolver) GetURL() string { + return r.url } -// Perform a query using the transport, and send the response to the writer. -func forwardQuery(t Transport, q []byte, c io.Writer) error { - resp, qerr := t.Query(q) +// Perform a query using the Resolver, and send the response to the writer. +func forwardQuery(r Resolver, q []byte, c io.Writer) error { + resp, qerr := r.Query(context.Background(), q) if resp == nil && qerr != nil { return qerr } @@ -500,45 +517,44 @@ func forwardQuery(t Transport, q []byte, c io.Writer) error { return qerr } -// Perform a query using the transport, send the response to the writer, +// Perform a query using the Resolver, send the response to the writer, // and close the writer if there was an error. -func forwardQueryAndCheck(t Transport, q []byte, c io.WriteCloser) { - if err := forwardQuery(t, q, c); err != nil { - log.Warnf("Query forwarding failed: %v", err) +func forwardQueryAndCheck(r Resolver, q []byte, c io.WriteCloser) { + if err := forwardQuery(r, q, c); err != nil { + logging.Warn("DoH(forwardQueryAndCheck) - forwarding failed", "err", err) c.Close() } } -// Accept a DNS-over-TCP socket from a stub resolver, and connect the socket -// to this DNSTransport. -func Accept(t Transport, c io.ReadWriteCloser) { +// Accept a DNS-over-TCP socket, and connect the socket to a DoH Resolver. +func Accept(r Resolver, c io.ReadWriteCloser) { qlbuf := make([]byte, 2) for { n, err := c.Read(qlbuf) if n == 0 { - log.Debugf("TCP query socket clean shutdown") + logging.Debug("DoH(Accept) - TCP query socket clean shutdown") break } if err != nil { - log.Warnf("Error reading from TCP query socket: %v", err) + logging.Warn("DoH(Accept) - failed to read from TCP query socket", "err", err) break } if n < 2 { - log.Warnf("Incomplete query length") + logging.Warn("DoH(Accept) - incomplete query length") break } qlen := binary.BigEndian.Uint16(qlbuf) q := make([]byte, qlen) n, err = c.Read(q) if err != nil { - log.Warnf("Error reading query: %v", err) + logging.Warn("DoH(Accept) - failed to read query", "err", err) break } if n != int(qlen) { - log.Warnf("Incomplete query: %d < %d", n, qlen) + logging.Warn("DoH(Accept) - incomplete query (n < qlen)", "n", n, "qlen", qlen) break } - go forwardQueryAndCheck(t, q, c) + go forwardQueryAndCheck(r, q, c) } // TODO: Cancel outstanding queries at this point. c.Close() @@ -546,6 +562,7 @@ func Accept(t Transport, c io.ReadWriteCloser) { // Servfail returns a SERVFAIL response to the query q. func Servfail(q []byte) ([]byte, error) { + defer logging.Debug("DoH(SERVFAIL) - response generated") var msg dnsmessage.Message if err := msg.Unpack(q); err != nil { return nil, err @@ -560,7 +577,7 @@ func Servfail(q []byte) ([]byte, error) { func tryServfail(q []byte) []byte { response, err := Servfail(q) if err != nil { - log.Warnf("Error constructing servfail: %v", err) + logging.Warn("DoH(SERVFAIL) - failed to construct response", "err", err) } return response } diff --git a/Android/app/src/go/intra/doh/doh_test.go b/Android/app/src/go/doh/doh_test.go similarity index 82% rename from Android/app/src/go/intra/doh/doh_test.go rename to Android/app/src/go/doh/doh_test.go index 7ad0797a..5d5164c5 100644 --- a/Android/app/src/go/intra/doh/doh_test.go +++ b/Android/app/src/go/doh/doh_test.go @@ -16,27 +16,22 @@ package doh import ( "bytes" + "context" "encoding/binary" "errors" "io" - "io/ioutil" "net" "net/http" "net/http/httptrace" "net/url" "reflect" "testing" + "time" + "github.com/stretchr/testify/require" "golang.org/x/net/dns/dnsmessage" ) -var testURL = "https://dns.google/dns-query" -var ips = []string{ - "8.8.8.8", - "8.8.4.4", - "2001:4860:4860::8888", - "2001:4860:4860::8844", -} var parsedURL *url.URL var simpleQuery dnsmessage.Message = dnsmessage.Message{ @@ -123,24 +118,21 @@ var uncompressedQueryBytes []byte = []byte{ } func init() { - parsedURL, _ = url.Parse(testURL) + parsedURL, _ = url.Parse(googleDoH.url) } // Check that the constructor works. -func TestNewTransport(t *testing.T) { - _, err := NewTransport(testURL, ips, nil, nil, nil) - if err != nil { - t.Fatal(err) - } +func TestNewResolver(t *testing.T) { + newTestDoHResolver(t, googleDoH) } // Check that the constructor rejects unsupported URLs. func TestBadUrl(t *testing.T) { - _, err := NewTransport("ftp://www.example.com", nil, nil, nil, nil) + _, err := NewResolver("ftp://www.example.com", nil, nil, nil, nil) if err == nil { t.Error("Expected error") } - _, err = NewTransport("https://www.example", nil, nil, nil, nil) + _, err = NewResolver("https://www.example", nil, nil, nil, nil) if err == nil { t.Error("Expected error") } @@ -149,8 +141,8 @@ func TestBadUrl(t *testing.T) { // Check for failure when the query is too short to be valid. func TestShortQuery(t *testing.T) { var qerr *queryError - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - _, err := doh.Query([]byte{}) + doh := newTestDoHResolver(t, googleDoH) + _, err := doh.Query(context.Background(), []byte{}) if err == nil { t.Error("Empty query should fail") } else if !errors.As(err, &qerr) { @@ -159,7 +151,7 @@ func TestShortQuery(t *testing.T) { t.Errorf("Wrong error status: %d", qerr.status) } - _, err = doh.Query([]byte{1}) + _, err = doh.Query(context.Background(), []byte{1}) if err == nil { t.Error("One byte query should fail") } else if !errors.As(err, &qerr) { @@ -187,12 +179,8 @@ func TestQueryIntegration(t *testing.T) { } testQuery := func(queryData []byte) { - - doh, err := NewTransport(testURL, ips, nil, nil, nil) - if err != nil { - t.Fatal(err) - } - resp, err2 := doh.Query(queryData) + doh := newTestDoHResolver(t, googleDoH) + resp, err2 := doh.Query(context.Background(), queryData) if err2 != nil { t.Fatal(err2) } @@ -238,16 +226,15 @@ func (r *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) // Check that a DNS query is converted correctly into an HTTP query. func TestRequest(t *testing.T) { - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + resolver := newTestDoHResolver(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt - go doh.Query(simpleQueryBytes) + resolver.client.Transport = rt + go resolver.Query(context.Background(), simpleQueryBytes) req := <-rt.req - if req.URL.String() != testURL { - t.Errorf("URL mismatch: %s != %s", req.URL.String(), testURL) + if req.URL.String() != googleDoH.url { + t.Errorf("URL mismatch: %s != %s", req.URL.String(), googleDoH.url) } - reqBody, err := ioutil.ReadAll(req.Body) + reqBody, err := io.ReadAll(req.Body) if err != nil { t.Error(err) } @@ -287,10 +274,9 @@ func queriesMostlyEqual(m1 dnsmessage.Message, m2 dnsmessage.Message) bool { // Check that a DOH response is returned correctly. func TestResponse(t *testing.T) { - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + resolver := newTestDoHResolver(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt // Fake server. go func() { @@ -308,7 +294,7 @@ func TestResponse(t *testing.T) { w.Close() }() - resp, err := doh.Query(simpleQueryBytes) + resp, err := resolver.Query(context.Background(), simpleQueryBytes) if err != nil { t.Error(err) } @@ -326,10 +312,9 @@ func TestResponse(t *testing.T) { // Simulate an empty response. (This is not a compliant server // behavior.) func TestEmptyResponse(t *testing.T) { - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + resolver := newTestDoHResolver(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt // Fake server. go func() { @@ -344,7 +329,7 @@ func TestEmptyResponse(t *testing.T) { } }() - _, err := doh.Query(simpleQueryBytes) + _, err := resolver.Query(context.Background(), simpleQueryBytes) var qerr *queryError if err == nil { t.Error("Empty body should cause an error") @@ -357,10 +342,9 @@ func TestEmptyResponse(t *testing.T) { // Simulate a non-200 HTTP response code. func TestHTTPError(t *testing.T) { - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + resolver := newTestDoHResolver(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt go func() { <-rt.req @@ -374,7 +358,7 @@ func TestHTTPError(t *testing.T) { w.Close() }() - _, err := doh.Query(simpleQueryBytes) + _, err := resolver.Query(context.Background(), simpleQueryBytes) var qerr *queryError if err == nil { t.Error("Empty body should cause an error") @@ -387,13 +371,12 @@ func TestHTTPError(t *testing.T) { // Simulate an HTTP query error. func TestSendFailed(t *testing.T) { - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + resolver := newTestDoHResolver(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt rt.err = errors.New("test") - _, err := doh.Query(simpleQueryBytes) + _, err := resolver.Query(context.Background(), simpleQueryBytes) var qerr *queryError if err == nil { t.Error("Send failure should be reported") @@ -409,14 +392,13 @@ func TestSendFailed(t *testing.T) { // Test if DoH resolver IPs are confirmed and disconfirmed // when queries suceeded and fail, respectively. func TestDohIPConfirmDisconfirm(t *testing.T) { - u, _ := url.Parse(testURL) - doh, _ := NewTransport(testURL, ips, nil, nil, nil) - transport := doh.(*transport) + u, _ := url.Parse(googleDoH.url) + resolver := newTestDoHResolver(t, googleDoH) hostname := u.Hostname() - ipmap := transport.ips.Get(hostname) + ipmap := resolver.ips.Get(hostname) // send a valid request to first have confirmed-ip set - res, _ := doh.Query(simpleQueryBytes) + res, _ := resolver.Query(context.Background(), simpleQueryBytes) mustUnpack(res) ip1 := ipmap.Confirmed() @@ -426,7 +408,7 @@ func TestDohIPConfirmDisconfirm(t *testing.T) { // simulate http-fail with doh server-ip set to previously confirmed-ip rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt go func() { req := <-rt.req trace := httptrace.ContextClientTrace(req.Context()) @@ -442,7 +424,7 @@ func TestDohIPConfirmDisconfirm(t *testing.T) { Request: &http.Request{URL: u}, } }() - doh.Query(simpleQueryBytes) + resolver.Query(context.Background(), simpleQueryBytes) ip2 := ipmap.Confirmed() if ip2 != nil { @@ -450,19 +432,6 @@ func TestDohIPConfirmDisconfirm(t *testing.T) { } } -type fakeListener struct { - Listener - summary *Summary -} - -func (l *fakeListener) OnQuery(url string) Token { - return nil -} - -func (l *fakeListener) OnResponse(tok Token, summ *Summary) { - l.summary = summ -} - type fakeConn struct { net.TCPConn remoteAddr *net.TCPAddr @@ -474,11 +443,9 @@ func (c *fakeConn) RemoteAddr() net.Addr { // Check that the DNSListener is called with a correct summary. func TestListener(t *testing.T) { - listener := &fakeListener{} - doh, _ := NewTransport(testURL, ips, nil, nil, listener) - transport := doh.(*transport) + resolver, listener := newTestDoHResolverWithListener(t, googleDoH) rt := makeTestRoundTripper() - transport.client.Transport = rt + resolver.client.Transport = rt go func() { req := <-rt.req @@ -500,7 +467,7 @@ func TestListener(t *testing.T) { w.Close() }() - doh.Query(simpleQueryBytes) + resolver.Query(context.Background(), simpleQueryBytes) s := listener.summary if s.Latency < 0 { t.Errorf("Negative latency: %f", s.Latency) @@ -547,14 +514,14 @@ func makePair() (io.ReadWriteCloser, io.ReadWriteCloser) { return &socket{r1, w2}, &socket{r2, w1} } -type fakeTransport struct { - Transport +type fakeResolver struct { + Resolver query chan []byte response chan []byte err error } -func (t *fakeTransport) Query(q []byte) ([]byte, error) { +func (t *fakeResolver) Query(ctx context.Context, q []byte) ([]byte, error) { t.query <- q if t.err != nil { return nil, t.err @@ -562,18 +529,18 @@ func (t *fakeTransport) Query(q []byte) ([]byte, error) { return <-t.response, nil } -func (t *fakeTransport) GetURL() string { +func (t *fakeResolver) GetURL() string { return "fake" } -func (t *fakeTransport) Close() { +func (t *fakeResolver) Close() { t.err = errors.New("closed") close(t.query) close(t.response) } -func newFakeTransport() *fakeTransport { - return &fakeTransport{ +func newFakeResolver() *fakeResolver { + return &fakeResolver{ query: make(chan []byte), response: make(chan []byte), } @@ -581,7 +548,7 @@ func newFakeTransport() *fakeTransport { // Test a successful query over TCP func TestAccept(t *testing.T) { - doh := newFakeTransport() + doh := newFakeResolver() client, server := makePair() // Start the forwarder running. @@ -640,7 +607,7 @@ func TestAccept(t *testing.T) { // Sends a TCP query that results in failure. When a query fails, // Accept should close the TCP socket. func TestAcceptFail(t *testing.T) { - doh := newFakeTransport() + doh := newFakeResolver() client, server := makePair() // Start the forwarder running. @@ -672,7 +639,7 @@ func TestAcceptFail(t *testing.T) { // Sends a TCP query, and closes the socket before the response is sent. // This tests for crashes when a response cannot be delivered. func TestAcceptClose(t *testing.T) { - doh := newFakeTransport() + doh := newFakeResolver() client, server := makePair() // Start the forwarder running. @@ -702,7 +669,7 @@ func TestAcceptClose(t *testing.T) { // Test failure due to a response that is larger than the // maximum message size for DNS over TCP (65535). func TestAcceptOversize(t *testing.T) { - doh := newFakeTransport() + doh := newFakeResolver() client, server := makePair() // Start the forwarder running. @@ -889,3 +856,77 @@ func TestServfail(t *testing.T) { t.Errorf("Wrong question: %v", servfail.Questions[0]) } } + +func TestQueryCanBeCancelled(t *testing.T) { + expectDoHTimeout := func(config testingDoHConfig, msg string) { + doh := newTestDoHResolver(t, config) + st := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + doh.Query(ctx, simpleQueryBytes) + require.WithinRange(t, time.Now(), st.Add(1500*time.Millisecond), st.Add(2500*time.Millisecond), msg) + } + + expectDoHTimeout(unreachableDoH, "unreachable resolver should timeout within deadline of ctx") + + // Intentionally create a local DoH server that does not accept any requests + addr, err := url.Parse(localDoH.url) + require.NoError(t, err) + dohAddr, err := net.ResolveTCPAddr("tcp", addr.Host) + require.NoError(t, err) + svr, err := net.ListenTCP("tcp", dohAddr) + require.NoError(t, err) + defer svr.Close() + + expectDoHTimeout(localDoH, "unresponsive resolver should timeout within deadline of ctx") +} + +/******** Test DoH servers ********/ +type testingDoHConfig struct { + url string + ips []string +} + +var unreachableDoH = testingDoHConfig{ + url: "https://1.2.3.4:443", + ips: []string{"1.2.3.4"}, +} + +var googleDoH = testingDoHConfig{ + url: "https://dns.google/dns-query", + ips: []string{ + "8.8.8.8", + "8.8.4.4", + "2001:4860:4860::8888", + "2001:4860:4860::8844", + }, +} + +var localDoH = testingDoHConfig{ + url: "https://localhost:34443", + ips: []string{"127.0.0.1"}, +} + +/********** DoH Resolver Test Helpers **********/ +type testingDoHListener struct { + Listener + summary *Summary +} + +func (l *testingDoHListener) OnQuery(url string) Token { return nil } +func (l *testingDoHListener) OnResponse(tok Token, s *Summary) { l.summary = s } + +func newTestDoHResolver(t *testing.T, config testingDoHConfig) *resolver { + doh, err := NewResolver(config.url, config.ips, nil, nil, nil) + require.NoError(t, err) + require.NotNil(t, doh) + return doh.(*resolver) +} + +func newTestDoHResolverWithListener(t *testing.T, config testingDoHConfig) (*resolver, *testingDoHListener) { + listener := &testingDoHListener{} + doh, err := NewResolver(config.url, config.ips, nil, nil, listener) + require.NoError(t, err) + require.NotNil(t, doh) + return doh.(*resolver), listener +} diff --git a/Android/app/src/go/intra/doh/ipmap/ipmap.go b/Android/app/src/go/doh/ipmap/ipmap.go similarity index 97% rename from Android/app/src/go/intra/doh/ipmap/ipmap.go rename to Android/app/src/go/doh/ipmap/ipmap.go index 9c94a5db..63444ece 100644 --- a/Android/app/src/go/intra/doh/ipmap/ipmap.go +++ b/Android/app/src/go/doh/ipmap/ipmap.go @@ -16,11 +16,10 @@ package ipmap import ( "context" + "localhost/Intra/Android/app/src/go/logging" "math/rand" "net" "sync" - - "github.com/eycorsican/go-tun2socks/common/log" ) // IPMap maps hostnames to IPSets. @@ -104,7 +103,7 @@ func (s *IPSet) Add(hostname string) { // Don't hold the ipMap lock during blocking I/O. resolved, err := s.r.LookupIPAddr(context.TODO(), hostname) if err != nil { - log.Warnf("Failed to resolve %s: %v", hostname, err) + logging.Warnf("Failed to resolve %s: %v", hostname, err) } s.Lock() for _, addr := range resolved { diff --git a/Android/app/src/go/intra/doh/ipmap/ipmap_test.go b/Android/app/src/go/doh/ipmap/ipmap_test.go similarity index 100% rename from Android/app/src/go/intra/doh/ipmap/ipmap_test.go rename to Android/app/src/go/doh/ipmap/ipmap_test.go diff --git a/Android/app/src/go/intra/doh/padding.go b/Android/app/src/go/doh/padding.go similarity index 100% rename from Android/app/src/go/intra/doh/padding.go rename to Android/app/src/go/doh/padding.go diff --git a/Android/app/src/go/intra/android/tun2socks.go b/Android/app/src/go/intra/android/tun2socks.go deleted file mode 100644 index df483d47..00000000 --- a/Android/app/src/go/intra/android/tun2socks.go +++ /dev/null @@ -1,110 +0,0 @@ -// 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/app/src/go/intra" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/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) -} diff --git a/Android/app/src/go/intra/doh/atomic.go b/Android/app/src/go/intra/doh/atomic.go deleted file mode 100644 index c0a179c9..00000000 --- a/Android/app/src/go/intra/doh/atomic.go +++ /dev/null @@ -1,38 +0,0 @@ -// 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) -} diff --git a/Android/app/src/go/intra/packet_proxy.go b/Android/app/src/go/intra/packet_proxy.go index 5232857a..fecb8f48 100644 --- a/Android/app/src/go/intra/packet_proxy.go +++ b/Android/app/src/go/intra/packet_proxy.go @@ -15,6 +15,7 @@ package intra import ( + "context" "errors" "fmt" "net" @@ -22,23 +23,25 @@ import ( "sync/atomic" "time" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/protect" + "localhost/Intra/Android/app/src/go/doh" + "localhost/Intra/Android/app/src/go/intra/protect" + "github.com/Jigsaw-Code/outline-sdk/network" "github.com/Jigsaw-Code/outline-sdk/transport" ) type intraPacketProxy struct { fakeDNSAddr netip.AddrPort - dns atomic.Pointer[doh.Transport] + dns atomic.Pointer[doh.Resolver] proxy network.PacketProxy listener UDPListener + ctx context.Context } var _ network.PacketProxy = (*intraPacketProxy)(nil) func newIntraPacketProxy( - fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener UDPListener, + ctx context.Context, fakeDNS netip.AddrPort, dns doh.Resolver, protector protect.Protector, listener UDPListener, ) (*intraPacketProxy, error) { if dns == nil { return nil, errors.New("dns is required") @@ -58,6 +61,7 @@ func newIntraPacketProxy( fakeDNSAddr: fakeDNS, proxy: pp, listener: listener, + ctx: ctx, } dohpp.dns.Store(&dns) @@ -84,7 +88,7 @@ func (p *intraPacketProxy) NewSession(resp network.PacketResponseReceiver) (netw }, nil } -func (p *intraPacketProxy) SetDNS(dns doh.Transport) error { +func (p *intraPacketProxy) SetDNS(dns doh.Resolver) error { if dns == nil { return errors.New("dns is required") } @@ -122,7 +126,7 @@ func (req *dohPacketReqSender) WriteTo(p []byte, destination netip.AddrPort) (in } }() - resp, err := (*req.proxy.dns.Load()).Query(p) + resp, err := (*req.proxy.dns.Load()).Query(req.proxy.ctx, p) if err != nil { return 0, fmt.Errorf("DoH request error: %w", err) } diff --git a/Android/app/src/go/intra/protect/protect.go b/Android/app/src/go/intra/protect/protect.go index f3d29acc..87e5fcb7 100644 --- a/Android/app/src/go/intra/protect/protect.go +++ b/Android/app/src/go/intra/protect/protect.go @@ -18,11 +18,10 @@ import ( "context" "errors" "fmt" + "localhost/Intra/Android/app/src/go/logging" "net" "strings" "syscall" - - "github.com/eycorsican/go-tun2socks/common/log" ) // Protector provides the ability to bypass a VPN on Android, pre-Lollipop. @@ -45,7 +44,7 @@ func makeControl(p Protector) func(string, string, syscall.RawConn) error { return c.Control(func(fd uintptr) { if !p.Protect(int32(fd)) { // TODO: Record and report these errors. - log.Errorf("Failed to protect a %s socket", network) + logging.Errorf("Failed to protect a %s socket", network) } }) } diff --git a/Android/app/src/go/intra/sni_reporter.go b/Android/app/src/go/intra/sni_reporter.go index 4346721a..f1a3dd7f 100644 --- a/Android/app/src/go/intra/sni_reporter.go +++ b/Android/app/src/go/intra/sni_reporter.go @@ -15,13 +15,15 @@ package intra import ( + "context" "io" "sync" "time" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" + "localhost/Intra/Android/app/src/go/doh" + "localhost/Intra/Android/app/src/go/logging" + "github.com/Jigsaw-Code/choir" - "github.com/eycorsican/go-tun2socks/common/log" ) // Number of bins to assign reports to. Should be large enough for @@ -40,13 +42,13 @@ const burst = 10 * time.Second // tcpSNIReporter is a thread-safe wrapper around choir.Reporter type tcpSNIReporter struct { mu sync.RWMutex // Protects dns, suffix, and r. - dns doh.Transport + dns doh.Resolver suffix string r choir.Reporter } // SetDNS changes the DNS transport used for uploading reports. -func (r *tcpSNIReporter) SetDNS(dns doh.Transport) { +func (r *tcpSNIReporter) SetDNS(dns doh.Resolver) { r.mu.Lock() r.dns = dns r.mu.Unlock() @@ -60,11 +62,11 @@ func (r *tcpSNIReporter) Send(report choir.Report) error { r.mu.RUnlock() q, err := choir.FormatQuery(report, suffix) if err != nil { - log.Warnf("Failed to construct query for Choir: %v", err) + logging.Warnf("Failed to construct query for Choir: %v", err) return nil } - if _, err = dns.Query(q); err != nil { - log.Infof("Failed to deliver query for Choir: %v", err) + if _, err = dns.Query(context.Background(), q); err != nil { + logging.Infof("Failed to deliver query for Choir: %v", err) } return nil } @@ -104,13 +106,13 @@ func (r *tcpSNIReporter) Report(summary TCPSocketSummary) { } resultValue, err := choir.NewValue(result) if err != nil { - log.Fatalf("Bad result %s: %v", result, err) + logging.Errorf("Bad result %s: %v", result, err) } responseValue, err := choir.NewValue(response) if err != nil { - log.Fatalf("Bad response %s: %v", response, err) + logging.Errorf("Bad response %s: %v", response, err) } if err := reporter.Report(summary.Retry.SNI, resultValue, responseValue); err != nil { - log.Warnf("Choir report failed: %v", err) + logging.Warnf("Choir report failed: %v", err) } } diff --git a/Android/app/src/go/intra/sni_reporter_test.go b/Android/app/src/go/intra/sni_reporter_test.go index 9826dd1c..c89122f1 100644 --- a/Android/app/src/go/intra/sni_reporter_test.go +++ b/Android/app/src/go/intra/sni_reporter_test.go @@ -16,29 +16,30 @@ package intra import ( "bytes" + "context" "errors" "strings" "testing" "golang.org/x/net/dns/dnsmessage" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/split" + "localhost/Intra/Android/app/src/go/doh" + "localhost/Intra/Android/app/src/go/intra/split" ) type qfunc func(q []byte) ([]byte, error) -type fakeTransport struct { - doh.Transport +type fakeResolver struct { + doh.Resolver query qfunc } -func (t *fakeTransport) Query(q []byte) ([]byte, error) { - return t.query(q) +func (r *fakeResolver) Query(ctx context.Context, q []byte) ([]byte, error) { + return r.query(q) } -func newFakeTransport(query qfunc) *fakeTransport { - return &fakeTransport{query: query} +func newFakeTransport(query qfunc) *fakeResolver { + return &fakeResolver{query: query} } func sendReport(t *testing.T, r *tcpSNIReporter, summary TCPSocketSummary, response []byte, responseErr error) string { diff --git a/Android/app/src/go/intra/split/example/main.go b/Android/app/src/go/intra/split/example/main.go index 9644e5f1..445cfc9d 100644 --- a/Android/app/src/go/intra/split/example/main.go +++ b/Android/app/src/go/intra/split/example/main.go @@ -22,7 +22,7 @@ import ( "net" "os" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/split" + "localhost/Intra/Android/app/src/go/intra/split" ) func main() { diff --git a/Android/app/src/go/intra/split/retrier.go b/Android/app/src/go/intra/split/retrier.go index da24065a..7367ec9e 100644 --- a/Android/app/src/go/intra/split/retrier.go +++ b/Android/app/src/go/intra/split/retrier.go @@ -15,8 +15,10 @@ package split import ( + "context" "errors" "io" + "localhost/Intra/Android/app/src/go/logging" "math/rand" "net" "sync" @@ -117,9 +119,11 @@ const DefaultTimeout time.Duration = 0 // `dialer` will be used to establish the connection. // `addr` is the destination. // If `stats` is non-nil, it will be populated with retry-related information. -func DialWithSplitRetry(dialer *net.Dialer, addr *net.TCPAddr, stats *RetryStats) (DuplexConn, error) { +func DialWithSplitRetry(ctx context.Context, dialer *net.Dialer, addr *net.TCPAddr, stats *RetryStats) (DuplexConn, error) { + logging.Debug("SplitRetry(DialWithSplitRetry) - dialing", "addr", addr) before := time.Now() - conn, err := dialer.Dial(addr.Network(), addr.String()) + conn, err := dialer.DialContext(ctx, addr.Network(), addr.String()) + logging.Debug("SplitRetry(DialWithSplitRetry) - dialed", "err", err) if err != nil { return nil, err } @@ -162,6 +166,7 @@ func (r *retrier) Read(buf []byte) (n int, err error) { // Read failed. Retry. n, err = r.retry(buf) } + logging.Debug("SplitRetry(retrier.Read) - direct conn succeeded, no need to split") close(r.retryCompleteFlag) // Unset read deadline. r.conn.SetReadDeadline(time.Time{}) @@ -172,6 +177,9 @@ func (r *retrier) Read(buf []byte) (n int, err error) { } func (r *retrier) retry(buf []byte) (n int, err error) { + logging.Debug("SplitRetry(retrier.retry) - retrying...") + defer func() { logging.Debug("SplitRetry(retrier.retry) - retried", "n", n, "err", err) }() + r.conn.Close() var newConn net.Conn if newConn, err = r.dialer.Dial(r.addr.Network(), r.addr.String()); err != nil { diff --git a/Android/app/src/go/intra/split/retrier_test.go b/Android/app/src/go/intra/split/retrier_test.go index d539c2e9..c68c542a 100644 --- a/Android/app/src/go/intra/split/retrier_test.go +++ b/Android/app/src/go/intra/split/retrier_test.go @@ -16,6 +16,7 @@ package split import ( "bytes" + "context" "io" "net" "testing" @@ -46,7 +47,7 @@ func makeSetup(t *testing.T) *setup { t.Error("Server isn't TCP?") } var stats RetryStats - clientSide, err := DialWithSplitRetry(&net.Dialer{}, serverAddr, &stats) + clientSide, err := DialWithSplitRetry(context.Background(), &net.Dialer{}, serverAddr, &stats) if err != nil { t.Error(err) } diff --git a/Android/app/src/go/intra/stream_dialer.go b/Android/app/src/go/intra/stream_dialer.go index 2d27466d..0af65b8a 100644 --- a/Android/app/src/go/intra/stream_dialer.go +++ b/Android/app/src/go/intra/stream_dialer.go @@ -23,15 +23,16 @@ import ( "sync/atomic" "time" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/protect" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/split" + "localhost/Intra/Android/app/src/go/doh" + "localhost/Intra/Android/app/src/go/intra/protect" + "localhost/Intra/Android/app/src/go/intra/split" + "github.com/Jigsaw-Code/outline-sdk/transport" ) type intraStreamDialer struct { fakeDNSAddr netip.AddrPort - dns atomic.Pointer[doh.Transport] + dns atomic.Pointer[doh.Resolver] dialer *net.Dialer alwaysSplitHTTPS atomic.Bool listener TCPListener @@ -42,7 +43,7 @@ var _ transport.StreamDialer = (*intraStreamDialer)(nil) func newIntraStreamDialer( fakeDNS netip.AddrPort, - dns doh.Transport, + dns doh.Resolver, protector protect.Protector, listener TCPListener, sniReporter *tcpSNIReporter, @@ -85,7 +86,7 @@ func (sd *intraStreamDialer) Dial(ctx context.Context, raddr string) (transport. return makeTCPWrapConn(conn, stats, sd.listener, sd.sniReporter), nil } -func (sd *intraStreamDialer) SetDNS(dns doh.Transport) error { +func (sd *intraStreamDialer) SetDNS(dns doh.Resolver) error { if dns == nil { return errors.New("dns is required") } @@ -99,7 +100,7 @@ func (sd *intraStreamDialer) dial(ctx context.Context, dest netip.AddrPort, stat return split.DialWithSplit(sd.dialer, net.TCPAddrFromAddrPort(dest)) } else { stats.Retry = &split.RetryStats{} - return split.DialWithSplitRetry(sd.dialer, net.TCPAddrFromAddrPort(dest), stats.Retry) + return split.DialWithSplitRetry(ctx, sd.dialer, net.TCPAddrFromAddrPort(dest), stats.Retry) } } else { tcpsd := &transport.TCPStreamDialer{ diff --git a/Android/app/src/go/intra/tcp.go b/Android/app/src/go/intra/tcp.go index f61631d2..f50e3a4d 100644 --- a/Android/app/src/go/intra/tcp.go +++ b/Android/app/src/go/intra/tcp.go @@ -23,7 +23,8 @@ import ( "sync/atomic" "time" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/split" + "localhost/Intra/Android/app/src/go/intra/split" + "github.com/Jigsaw-Code/outline-sdk/transport" ) diff --git a/Android/app/src/go/intra/tunnel.go b/Android/app/src/go/intra/tunnel.go index 0d79e022..a8098dec 100644 --- a/Android/app/src/go/intra/tunnel.go +++ b/Android/app/src/go/intra/tunnel.go @@ -15,6 +15,7 @@ package intra import ( + "context" "errors" "fmt" "io" @@ -22,8 +23,9 @@ import ( "os" "strings" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/doh" - "github.com/Jigsaw-Code/Intra/Android/app/src/go/intra/protect" + "localhost/Intra/Android/app/src/go/doh" + "localhost/Intra/Android/app/src/go/intra/protect" + "github.com/Jigsaw-Code/outline-sdk/network" "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" ) @@ -33,17 +35,18 @@ import ( type Listener interface { UDPListener TCPListener - doh.Listener } // Tunnel represents an Intra session. type Tunnel struct { network.IPDevice - sd *intraStreamDialer - pp *intraPacketProxy - sni *tcpSNIReporter - tun io.Closer + ctx context.Context + cancel context.CancelFunc + sd *intraStreamDialer + pp *intraPacketProxy + sni *tcpSNIReporter + tun io.Closer } // NewTunnel creates a connected Intra session. @@ -56,10 +59,11 @@ type Tunnel struct { // // These will normally be localhost with a high-numbered port. // -// `dohdns` is the initial DOH transport. +// `dohdns` is the initial [Resolver]. +// // `eventListener` will be notified at the completion of every tunneled socket. func NewTunnel( - fakedns string, dohdns doh.Transport, tun io.Closer, protector protect.Protector, eventListener Listener, + fakedns string, dohdns doh.Resolver, tun io.Closer, protector protect.Protector, eventListener Listener, ) (t *Tunnel, err error) { if eventListener == nil { return nil, errors.New("eventListener is required") @@ -76,13 +80,14 @@ func NewTunnel( }, tun: tun, } + t.ctx, t.cancel = context.WithCancel(context.Background()) t.sd, err = newIntraStreamDialer(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener, t.sni) if err != nil { return nil, fmt.Errorf("failed to create stream dialer: %w", err) } - t.pp, err = newIntraPacketProxy(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener) + t.pp, err = newIntraPacketProxy(t.ctx, fakeDNSAddr.AddrPort(), dohdns, protector, eventListener) if err != nil { return nil, fmt.Errorf("failed to create packet proxy: %w", err) } @@ -95,10 +100,10 @@ func NewTunnel( return } -// Set the DNSTransport. This method must be called before connecting the transport -// to the TUN device. The transport can be changed at any time during operation, but +// Set the DNS Resolver. This method must be called before connecting the transport +// to the TUN device. The transport can be changed at any time during operation, but // must not be nil. -func (t *Tunnel) SetDNS(dns doh.Transport) { +func (t *Tunnel) SetDNS(dns doh.Resolver) { t.sd.SetDNS(dns) t.pp.SetDNS(dns) t.sni.SetDNS(dns) @@ -118,6 +123,7 @@ func (t *Tunnel) EnableSNIReporter(filename, suffix, country string) error { } func (t *Tunnel) Disconnect() { + t.cancel() t.Close() t.tun.Close() } diff --git a/Android/app/src/go/logging/logging.go b/Android/app/src/go/logging/logging.go new file mode 100644 index 00000000..510e6010 --- /dev/null +++ b/Android/app/src/go/logging/logging.go @@ -0,0 +1,75 @@ +// 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 logging is a centralized logging system for Intra's Go backend. +It offers efficient logging methods that save CPU power by only formatting +messages that need to be logged. +*/ +package logging + +import ( + "context" + "fmt" + "log/slog" + "os" +) + +var logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelWarn, +})) + +func Debug(msg string, args ...any) { + logger.Debug(msg, args...) +} + +func Debugf(format string, args ...any) { + if !logger.Enabled(context.Background(), slog.LevelDebug) { + return + } + logger.Debug(fmt.Sprintf(format, args...)) +} + +func Info(msg string, args ...any) { + logger.Info(msg, args...) +} + +func Infof(format string, args ...any) { + if !logger.Enabled(context.Background(), slog.LevelInfo) { + return + } + logger.Info(fmt.Sprintf(format, args...)) +} + +func Warn(msg string, args ...any) { + logger.Warn(msg, args...) +} + +func Warnf(format string, args ...any) { + if !logger.Enabled(context.Background(), slog.LevelWarn) { + return + } + logger.Warn(fmt.Sprintf(format, args...)) +} + +func Error(msg string, args ...any) { + logger.Error(msg, args...) +} + +func Errorf(format string, args ...any) { + if !logger.Enabled(context.Background(), slog.LevelError) { + return + } + logger.Error(fmt.Sprintf(format, args...)) +} diff --git a/Android/app/src/go/intra/android/tun.go b/Android/app/src/go/tuntap/tun.go similarity index 90% rename from Android/app/src/go/intra/android/tun.go rename to Android/app/src/go/tuntap/tun.go index e3639dc9..c47b5aac 100644 --- a/Android/app/src/go/intra/android/tun.go +++ b/Android/app/src/go/tuntap/tun.go @@ -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. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tun2socks +package tuntap import ( "errors" @@ -21,7 +21,7 @@ import ( "golang.org/x/sys/unix" ) -func makeTunFile(fd int) (*os.File, error) { +func MakeTunDeviceFromFD(fd int) (*os.File, error) { if fd < 0 { return nil, errors.New("must provide a valid TUN file descriptor") } diff --git a/Android/app/src/main/java/app/intra/net/doh/Prober.java b/Android/app/src/main/java/app/intra/net/doh/Prober.java index 411c7d80..5114dff5 100644 --- a/Android/app/src/main/java/app/intra/net/doh/Prober.java +++ b/Android/app/src/main/java/app/intra/net/doh/Prober.java @@ -19,22 +19,6 @@ * A prober can perform asynchronous checks to determine whether a DOH server is working. */ public abstract class Prober { - - protected static final byte[] QUERY_DATA = { - 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) - }; - public interface Callback { void onCompleted(boolean succeeded); } diff --git a/Android/app/src/main/java/app/intra/net/doh/Race.java b/Android/app/src/main/java/app/intra/net/doh/Race.java index 987da428..fd13bf39 100644 --- a/Android/app/src/main/java/app/intra/net/doh/Race.java +++ b/Android/app/src/main/java/app/intra/net/doh/Race.java @@ -16,6 +16,8 @@ package app.intra.net.doh; import android.content.Context; +import android.util.Log; + import app.intra.net.go.GoProber; /** @@ -23,6 +25,7 @@ * the fastest probe succeeds or all probes have failed. Each instance can only be used once. */ public class Race { + private static final String TAG = "DoHProbe"; // tag for logging public interface Listener { /** @@ -64,11 +67,13 @@ private static class Collector { synchronized void onCompleted(int index, boolean succeeded) { if (succeeded) { + Log.i(TAG, "DoH Server No. " + index + ": succeeded"); if (!reportedSuccess) { listener.onResult(index); reportedSuccess = true; } } else { + Log.w(TAG, "DoH Server No. " + index + ": failed"); ++numFailed; if (numFailed == numCallbacks) { // All probes failed diff --git a/Android/app/src/main/java/app/intra/net/go/GoIntraListener.java b/Android/app/src/main/java/app/intra/net/go/GoIntraListener.java index a84bbae8..d1af2e89 100644 --- a/Android/app/src/main/java/app/intra/net/go/GoIntraListener.java +++ b/Android/app/src/main/java/app/intra/net/go/GoIntraListener.java @@ -17,19 +17,21 @@ import android.os.SystemClock; import androidx.collection.LongSparseArray; +import com.google.firebase.perf.FirebasePerformance; +import com.google.firebase.perf.metrics.HttpMetric; +import java.net.ProtocolException; +import java.util.Calendar; import app.intra.net.dns.DnsPacket; import app.intra.net.doh.Transaction; import app.intra.net.doh.Transaction.Status; -import app.intra.sys.firebase.AnalyticsWrapper; import app.intra.sys.IntraVpnService; -import com.google.firebase.perf.FirebasePerformance; -import com.google.firebase.perf.metrics.HttpMetric; -import doh.Doh; -import doh.Token; +import app.intra.sys.firebase.AnalyticsWrapper; +import backend.Backend; +import backend.DoHListener; +import backend.DoHQuerySumary; +import backend.DoHQueryToken; import intra.TCPSocketSummary; import intra.UDPSocketSummary; -import java.net.ProtocolException; -import java.util.Calendar; import split.RetryStats; /** @@ -37,7 +39,7 @@ * when a socket has concluded, with performance metrics for that socket, and this class forwards * those metrics to Firebase. */ -public class GoIntraListener implements intra.Listener { +public class GoIntraListener implements intra.Listener, DoHListener { // UDP is often used for one-off messages and pings. The relative overhead of reporting metrics // on these short messages would be large, so we only report metrics on sockets that transfer at @@ -91,20 +93,20 @@ public void onUDPSocketClosed(UDPSocketSummary summary) { private static final LongSparseArray goStatusMap = new LongSparseArray<>(); static { - goStatusMap.put(Doh.Complete, Status.COMPLETE); - goStatusMap.put(Doh.SendFailed, Status.SEND_FAIL); - goStatusMap.put(Doh.HTTPError, Status.HTTP_ERROR); - goStatusMap.put(Doh.BadQuery, Status.INTERNAL_ERROR); // TODO: Add a BAD_QUERY Status - goStatusMap.put(Doh.BadResponse, Status.BAD_RESPONSE); - goStatusMap.put(Doh.InternalError, Status.INTERNAL_ERROR); + goStatusMap.put(Backend.DoHStatusComplete, Status.COMPLETE); + goStatusMap.put(Backend.DoHStatusSendFailed, Status.SEND_FAIL); + goStatusMap.put(Backend.DoHStatusHTTPError, Status.HTTP_ERROR); + goStatusMap.put(Backend.DoHStatusBadQuery, Status.INTERNAL_ERROR); // TODO: Add a BAD_QUERY Status + goStatusMap.put(Backend.DoHStatusBadResponse, Status.BAD_RESPONSE); + goStatusMap.put(Backend.DoHStatusInternalError, Status.INTERNAL_ERROR); } - // Wrapping HttpMetric into a doh.Token allows us to get paired query and response notifications + // Wrapping HttpMetric into a DoHQueryToken allows us to get paired query and response notifications // from Go without reverse-binding any Java APIs into Go. Pairing these notifications is // required by the structure of the HttpMetric API (which does not have any other way to record // latency), and reverse binding is worth avoiding, especially because it's not compatible with // the Go module system (https://github.com/golang/go/issues/27234). - private class Metric implements doh.Token { + private static class Metric implements DoHQueryToken { final HttpMetric metric; Metric(String url) { metric = FirebasePerformance.getInstance().newHttpMetric(url, "POST"); @@ -112,7 +114,7 @@ private class Metric implements doh.Token { } @Override - public Token onQuery(String url) { + public DoHQueryToken onQuery(String url) { Metric m = new Metric(url); m.metric.start(); return m; @@ -123,7 +125,7 @@ private static int len(byte[] a) { } @Override - public void onResponse(Token token, doh.Summary summary) { + public void onResponse(DoHQueryToken token, DoHQuerySumary summary) { if (summary.getHTTPStatus() != 0 && token != null) { // HTTP transaction completed. Report performance metrics. Metric m = (Metric)token; diff --git a/Android/app/src/main/java/app/intra/net/go/GoProber.java b/Android/app/src/main/java/app/intra/net/go/GoProber.java index 4474f13a..261dcba1 100644 --- a/Android/app/src/main/java/app/intra/net/go/GoProber.java +++ b/Android/app/src/main/java/app/intra/net/go/GoProber.java @@ -20,9 +20,9 @@ import android.os.Build.VERSION_CODES; import app.intra.net.doh.Prober; import app.intra.sys.VpnController; -import doh.Transport; +import backend.Backend; +import backend.DoHServer; import protect.Protector; -import tun2socks.Tun2socks; /** * Implements a Probe using the Go-based DoH client. @@ -43,17 +43,8 @@ public void probe(String url, Callback callback) { // Protection isn't needed for Lollipop+, or if the VPN is not active. Protector protector = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP ? null : VpnController.getInstance().getIntraVpnService(); - Transport transport = Tun2socks.newDoHTransport(url, dohIPs, protector, null, null); - if (transport == null) { - callback.onCompleted(false); - return; - } - byte[] response = transport.query(QUERY_DATA); - if (response != null && response.length > 0) { - callback.onCompleted(true); - return; - } - callback.onCompleted(false); + Backend.probe(new DoHServer(url, dohIPs, protector, null)); + callback.onCompleted(true); } catch (Exception e) { callback.onCompleted(false); } diff --git a/Android/app/src/main/java/app/intra/net/go/GoVpnAdapter.java b/Android/app/src/main/java/app/intra/net/go/GoVpnAdapter.java index 9abc7f46..6b0d379e 100644 --- a/Android/app/src/main/java/app/intra/net/go/GoVpnAdapter.java +++ b/Android/app/src/main/java/app/intra/net/go/GoVpnAdapter.java @@ -25,6 +25,11 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; import app.intra.R; import app.intra.sys.CountryCode; import app.intra.sys.IntraVpnService; @@ -33,14 +38,10 @@ import app.intra.sys.firebase.AnalyticsWrapper; import app.intra.sys.firebase.LogWrapper; import app.intra.sys.firebase.RemoteConfig; -import doh.Transport; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Locale; +import backend.Backend; +import backend.DoHServer; +import backend.Session; import protect.Protector; -import tun2socks.Tun2socks; /** * This is a VpnAdapter that captures all traffic and routes it through a go-tun2socks instance with @@ -87,7 +88,7 @@ String make(String template) { private ParcelFileDescriptor tunFd; // The Intra session object from go-tun2socks. Initially null. - private intra.Tunnel tunnel; + private Session session; private GoIntraListener listener; public static GoVpnAdapter establish(@NonNull IntraVpnService vpnService) { @@ -108,7 +109,7 @@ public synchronized void start() { } private void connectTunnel() { - if (tunnel != null) { + if (session != null) { return; } // VPN parameters @@ -120,10 +121,10 @@ private void connectTunnel() { try { LogWrapper.log(Log.INFO, LOG_TAG, "Starting go-tun2socks"); - Transport transport = makeDohTransport(dohURL); + DoHServer server = makeDoHServer(dohURL); // connectIntraTunnel makes a copy of the file descriptor. - tunnel = Tun2socks.connectIntraTunnel(tunFd.getFd(), fakeDns, - transport, getProtector(), listener); + session = Backend.connectSession(tunFd.getFd(), fakeDns, + server, getProtector(), listener); } catch (Exception e) { LogWrapper.logException(e); VpnController.getInstance().onConnectionStateChanged(vpnService, IntraVpnService.State.FAILING); @@ -149,7 +150,7 @@ private void enableChoir() { } String file = vpnService.getFilesDir() + File.separator + CHOIR_FILENAME; try { - tunnel.enableSNIReporter(file, "intra.metrics.gstatic.com", country); + session.enableSNIReporter(file, "intra.metrics.gstatic.com", country); } catch (Exception e) { // Choir setup failure is logged but otherwise ignored, because it does not prevent Intra // from functioning correctly. @@ -188,8 +189,8 @@ private static ParcelFileDescriptor establishVpn(IntraVpnService vpnService) { } public synchronized void close() { - if (tunnel != null) { - tunnel.disconnect(); + if (session != null) { + session.disconnect(); } if (tunFd != null) { try { @@ -201,21 +202,21 @@ public synchronized void close() { tunFd = null; } - private doh.Transport makeDohTransport(@Nullable String url) throws Exception { + private DoHServer makeDoHServer(@Nullable String url) throws Exception { @NonNull String realUrl = PersistentState.expandUrl(vpnService, url); String dohIPs = getIpString(vpnService, realUrl); String host = new URL(realUrl).getHost(); long startTime = SystemClock.elapsedRealtime(); - final doh.Transport transport; + final DoHServer server; try { - transport = Tun2socks.newDoHTransport(realUrl, dohIPs, getProtector(), null, listener); + server = new DoHServer(realUrl, dohIPs, getProtector(), listener); } catch (Exception e) { AnalyticsWrapper.get(vpnService).logBootstrapFailed(host); throw e; } int delta = (int) (SystemClock.elapsedRealtime() - startTime); AnalyticsWrapper.get(vpnService).logBootstrap(host, delta); - return transport; + return server; } /** @@ -228,7 +229,7 @@ public synchronized void updateDohUrl() { // Adapter is closed. return; } - if (tunnel == null) { + if (session == null) { // Attempt to re-create the tunnel. Creation may have failed originally because the DoH // server could not be reached. This will update the DoH URL as well. connectTunnel(); @@ -240,11 +241,11 @@ public synchronized void updateDohUrl() { // out. String url = PersistentState.getServerUrl(vpnService); try { - tunnel.setDNS(makeDohTransport(url)); + session.setDoHServer(makeDoHServer(url)); } catch (Exception e) { LogWrapper.logException(e); - tunnel.disconnect(); - tunnel = null; + session.disconnect(); + session = null; VpnController.getInstance().onConnectionStateChanged(vpnService, IntraVpnService.State.FAILING); } } diff --git a/go.mod b/go.mod index d9b8bd37..9f6368f2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Jigsaw-Code/Intra +module localhost/Intra go 1.21.1 @@ -6,15 +6,20 @@ require ( github.com/Jigsaw-Code/choir v1.0.1 github.com/Jigsaw-Code/getsni v1.0.0 github.com/Jigsaw-Code/outline-sdk v0.0.7 - github.com/eycorsican/go-tun2socks v1.16.11 + github.com/stretchr/testify v1.8.2 golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe golang.org/x/net v0.16.0 golang.org/x/sys v0.13.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/eycorsican/go-tun2socks v1.16.11 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/tools v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 166b2e17..9af645de 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,24 @@ github.com/Jigsaw-Code/getsni v1.0.0 h1:OUTIu7wTBi/7DMX+RkZrN7XhU3UDevTEsAWK4gsq github.com/Jigsaw-Code/getsni v1.0.0/go.mod h1:Ps0Ec3fVMKLyAItVbMKoQFq1lDjtFQXZ+G5nRNNh/QE= github.com/Jigsaw-Code/outline-sdk v0.0.7 h1:WlFaV1tFpIQ/pflrKwrQuNIP3kJpgh7yJuqiTb54sGA= github.com/Jigsaw-Code/outline-sdk v0.0.7/go.mod h1:hhlKz0+r9wSDFT8usvN8Zv/BFToCIFAUn1P2Qk8G2CM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8= github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -35,5 +46,9 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=