From 0b02d88fae0501ee48afb0b283993a5cecfa36ae Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 19 Jan 2024 11:17:54 -0500 Subject: [PATCH 01/15] Create Smart Proxy --- x/examples/smart-proxy/config.json | 115 ++++ x/examples/smart-proxy/config_broken.json | 18 + x/examples/smart-proxy/main.go | 131 +++++ x/internal/dnsextra/cache.go | 117 ++++ x/internal/dnsextra/dialer.go | 152 +++++ x/mobileproxy/README.md | 116 +++- x/mobileproxy/mobileproxy.go | 66 +++ x/smart/stream_dialer.go | 664 ++++++++++++++++++++++ 8 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 x/examples/smart-proxy/config.json create mode 100644 x/examples/smart-proxy/config_broken.json create mode 100644 x/examples/smart-proxy/main.go create mode 100644 x/internal/dnsextra/cache.go create mode 100644 x/internal/dnsextra/dialer.go create mode 100644 x/smart/stream_dialer.go diff --git a/x/examples/smart-proxy/config.json b/x/examples/smart-proxy/config.json new file mode 100644 index 00000000..31c5ee5a --- /dev/null +++ b/x/examples/smart-proxy/config.json @@ -0,0 +1,115 @@ +{ + "dns": [ + {"system": {}}, + + {"https": {"name": "2620:fe::fe"}, "//": "Quad9"}, + {"https": {"name": "9.9.9.9"}}, + {"https": {"name": "149.112.112.112"}}, + + {"https": {"name": "2001:4860:4860::8888"}, "//": "Google"}, + {"https": {"name": "8.8.8.8"}}, + {"https": {"name": "2001:4860:4860::8844"}}, + {"https": {"name": "8.8.4.4"}}, + + {"https": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"}, + {"https": {"name": "1.1.1.1"}}, + {"https": {"name": "2606:4700:4700::1001"}}, + {"https": {"name": "1.0.0.1"}}, + {"https": {"name": "cloudflare-dns.com.", "address": "cloudflare.net."}}, + + {"https": {"name": "2620:119:35::35"}, "//": "OpenDNS"}, + {"https": {"name": "208.67.220.220"}}, + {"https": {"name": "2620:119:53::53"}}, + {"https": {"name": "208.67.222.222"}}, + + {"https": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"}, + {"https": {"name": "185.71.138.138"}}, + + {"https": {"name": "doh.dns.sb", "address": "cloudflare.net:443"}, "//": "DNS.SB"}, + + + {"tls": {"name": "2620:fe::fe"}, "//": "Quad9"}, + {"tls": {"name": "9.9.9.9"}}, + {"tls": {"name": "149.112.112.112"}}, + + {"tls": {"name": "2001:4860:4860::8888"}, "//": "Google"}, + {"tls": {"name": "8.8.8.8"}}, + {"tls": {"name": "2001:4860:4860::8844"}}, + {"tls": {"name": "8.8.4.4"}}, + + {"tls": {"name": "2606:4700:4700::1111"}, "//": "Cloudflare"}, + {"tls": {"name": "1.1.1.1"}}, + {"tls": {"name": "2606:4700:4700::1001"}}, + {"tls": {"name": "1.0.0.1"}}, + + {"tls": {"name": "2620:119:35::35"}, "//": "OpenDNS"}, + {"tls": {"name": "208.67.220.220"}}, + {"tls": {"name": "2620:119:53::53"}}, + {"tls": {"name": "208.67.222.222"}}, + + {"tls": {"name": "2001:67c:930::1"}, "//": "Wikimedia DNS"}, + {"tls": {"name": "185.71.138.138"}}, + + + {"tcp": {"address": "2620:fe::fe"}, "//": "Quad9"}, + {"tcp": {"address": "9.9.9.9"}}, + {"tcp": {"address": "149.112.112.112"}}, + {"tcp": {"address": "[2620:fe::fe]:9953"}}, + {"tcp": {"address": "9.9.9.9:9953"}}, + {"tcp": {"address": "149.112.112.112:9953"}}, + + {"tcp": {"address": "2001:4860:4860::8888"}, "//": "Google"}, + {"tcp": {"address": "8.8.8.8"}}, + {"tcp": {"address": "2001:4860:4860::8844"}}, + {"tcp": {"address": "8.8.4.4"}}, + + {"tcp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"}, + {"tcp": {"address": "1.1.1.1"}}, + {"tcp": {"address": "2606:4700:4700::1001"}}, + {"tcp": {"address": "1.0.0.1"}}, + + {"tcp": {"address": "2620:119:35::35"}, "//": "OpenDNS"}, + {"tcp": {"address": "208.67.220.220"}}, + {"tcp": {"address": "2620:119:53::53"}}, + {"tcp": {"address": "208.67.222.222"}}, + {"tcp": {"address": "[2620:119:35::35]:443"}}, + {"tcp": {"address": "208.67.220.220:443"}}, + {"tcp": {"address": "[2620:119:35::35]:5353"}}, + {"tcp": {"address": "208.67.220.220:5353"}}, + + + {"udp": {"address": "2620:fe::fe"}, "//": "Quad9"}, + {"udp": {"address": "9.9.9.9"}}, + {"udp": {"address": "149.112.112.112"}}, + {"udp": {"address": "[2620:fe::fe]:9953"}}, + {"udp": {"address": "9.9.9.9:9953"}}, + {"udp": {"address": "149.112.112.112:9953"}}, + + {"udp": {"address": "2001:4860:4860::8888"}, "//": "Google"}, + {"udp": {"address": "8.8.8.8"}}, + {"udp": {"address": "2001:4860:4860::8844"}}, + {"udp": {"address": "8.8.4.4"}}, + + {"udp": {"address": "2606:4700:4700::1111"}, "//": "Cloudflare"}, + {"udp": {"address": "1.1.1.1"}}, + {"udp": {"address": "2606:4700:4700::1001"}}, + {"udp": {"address": "1.0.0.1"}}, + + {"udp": {"address": "2620:119:35::35"}, "//": "OpenDNS"}, + {"udp": {"address": "208.67.220.220"}}, + {"udp": {"address": "2620:119:53::53"}}, + {"udp": {"address": "208.67.222.222"}}, + {"udp": {"address": "[2620:119:35::35]:443"}}, + {"udp": {"address": "208.67.220.220:443"}}, + {"udp": {"address": "[2620:119:35::35]:5353"}}, + {"udp": {"address": "208.67.220.220:5353"}} + ], + + "tls": [ + "", + "split:1", + "split:2", + "split:5", + "tlsfrag:1" + ] +} diff --git a/x/examples/smart-proxy/config_broken.json b/x/examples/smart-proxy/config_broken.json new file mode 100644 index 00000000..55a7c7ec --- /dev/null +++ b/x/examples/smart-proxy/config_broken.json @@ -0,0 +1,18 @@ +{ + "dns": [ + {"udp": {"address": "china.cn"}}, + {"udp": {"address": "ns1.tic.ir"}}, + {"tcp": {"address": "ns1.tic.ir"}}, + {"udp": {"address": "tmcell.tm"}}, + {"tls": {"name": "captive-portal.badssl.com", "address": "captive-portal.badssl.com:443"}}, + {"https": {"name": "mitm-software.badssl.com"}} + ], + + "tls": [ + "", + "split:1", + "split:2", + "split:5", + "tlsfrag:1" + ] +} diff --git a/x/examples/smart-proxy/main.go b/x/examples/smart-proxy/main.go new file mode 100644 index 00000000..cc078303 --- /dev/null +++ b/x/examples/smart-proxy/main.go @@ -0,0 +1,131 @@ +// 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 +// +// https://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 main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/signal" + "time" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/config" + "github.com/Jigsaw-Code/outline-sdk/x/httpproxy" + "github.com/Jigsaw-Code/outline-sdk/x/smart" +) + +var debugLog log.Logger = *log.New(io.Discard, "", 0) + +type stringArrayFlagValue []string + +func (v *stringArrayFlagValue) String() string { + return fmt.Sprint(*v) +} + +func (v *stringArrayFlagValue) Set(value string) error { + *v = append(*v, value) + return nil +} + +func main() { + verboseFlag := flag.Bool("v", false, "Enable debug output") + addrFlag := flag.String("localAddr", "localhost:1080", "Local proxy address") + configFlag := flag.String("config", "config.json", "Address of the config file") + transportFlag := flag.String("transport", "", "The base transport for the connections") + var domainsFlag stringArrayFlagValue + flag.Var(&domainsFlag, "domain", "The test domains to find strategies.") + + flag.Parse() + if *verboseFlag { + debugLog = *log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) + } + + if len(domainsFlag) == 0 { + log.Fatal("Must specify flag --domain") + } + + if *configFlag == "" { + log.Fatal("Must specify flag --config") + } + + finderConfig, err := os.ReadFile(*configFlag) + if err != nil { + log.Fatalf("Could not read config: %v", err) + } + + packetDialer, err := config.NewPacketDialer(*transportFlag) + if err != nil { + log.Fatalf("Could not create packet dialer: %v", err) + } + streamDialer, err := config.NewStreamDialer(*transportFlag) + if err != nil { + log.Fatalf("Could not create stream dialer: %v", err) + } + + finder := smart.StrategyFinder{ + LogWriter: debugLog.Writer(), + TestTimeout: 5 * time.Second, + StreamDialer: streamDialer, + PacketDialer: packetDialer, + } + + fmt.Println("Finding strategy") + dialer, err := finder.NewDialer(context.Background(), domainsFlag, finderConfig) + if err != nil { + log.Fatalf("Failed to find dialer: %v", err) + } + logDialer := transport.FuncStreamDialer(func(ctx context.Context, address string) (transport.StreamConn, error) { + conn, err := dialer.DialStream(ctx, address) + if err != nil { + debugLog.Printf("Failed to dial %v: %v\n", address, err) + } + return conn, err + }) + + listener, err := net.Listen("tcp", *addrFlag) + if err != nil { + log.Fatalf("Could not listen on address %v: %v", *addrFlag, err) + } + defer listener.Close() + fmt.Printf("Proxy listening on %v\n", listener.Addr().String()) + + server := http.Server{ + Handler: httpproxy.NewProxyHandler(logDialer), + ErrorLog: &debugLog, + } + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Fatalf("Error running web server: %v", err) + } + }() + + // Wait for interrupt signal to stop the proxy. + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + <-sig + fmt.Print("Shutting down") + // Gracefully shut down the server, with a 5s timeout. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Failed to shutdown gracefully: %v", err) + } +} diff --git a/x/internal/dnsextra/cache.go b/x/internal/dnsextra/cache.go new file mode 100644 index 00000000..47624010 --- /dev/null +++ b/x/internal/dnsextra/cache.go @@ -0,0 +1,117 @@ +// 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 +// +// https://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 dnsextra + +import ( + "context" + "strings" + "time" + + "github.com/Jigsaw-Code/outline-sdk/dns" + "golang.org/x/net/dns/dnsmessage" +) + +// canonicalName returns the domain name in canonical form. A name in canonical +// form is lowercase and fully qualified. Only US-ASCII letters are affected. See +// Section 6.2 in RFC 4034. +func canonicalName(s string) string { + return strings.Map(func(r rune) rune { + if r >= 'A' && r <= 'Z' { + r += 'a' - 'A' + } + return r + }, s) +} + +type cacheEntry struct { + key string + msg *dnsmessage.Message + expire time.Time +} + +// cacheResolver is a very simple caching [RoundTripper]. +// It doesn't use the response TTL and doesn't cache empty answers. +// It also doesn't dedup duplicate in-flight requests. +type cacheResolver struct { + resolver dns.Resolver + cache []cacheEntry +} + +var _ dns.Resolver = (*cacheResolver)(nil) + +func NewCacheResolver(resolver dns.Resolver, numEntries int) dns.Resolver { + return &cacheResolver{resolver: resolver, cache: make([]cacheEntry, numEntries)} +} + +func (r *cacheResolver) removeExpired() { + now := time.Now() + last := 0 + for _, entry := range r.cache { + if entry.expire.After(now) { + r.cache[last] = entry + last++ + } + } + r.cache = r.cache[:last] +} + +func (r *cacheResolver) moveToFront(index int) { + entry := r.cache[index] + copy(r.cache[1:], r.cache[:index]) + r.cache[0] = entry +} + +func makeCacheKey(q dnsmessage.Question) string { + domainKey := canonicalName(q.Name.String()) + return strings.Join([]string{domainKey, q.Type.String(), q.Class.String()}, "|") +} + +func (r *cacheResolver) searchCache(key string) *dnsmessage.Message { + for ei, entry := range r.cache { + if entry.key == key { + r.moveToFront(ei) + // TODO: update TTLs + // TODO: make names match + return entry.msg + } + } + return nil +} + +func (r *cacheResolver) addToCache(key string, msg *dnsmessage.Message) { + newSize := len(r.cache) + 1 + if newSize > cap(r.cache) { + newSize = cap(r.cache) + } + r.cache = r.cache[:newSize] + copy(r.cache[1:], r.cache[:newSize-1]) + // TODO: copy and normalize names + r.cache[0] = cacheEntry{key: key, msg: msg, expire: time.Now().Add(60 * time.Second)} +} + +func (r *cacheResolver) Query(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) { + r.removeExpired() + cacheKey := makeCacheKey(q) + if msg := r.searchCache(cacheKey); msg != nil { + return msg, nil + } + msg, err := r.resolver.Query(ctx, q) + if err != nil { + // TODO: cache NXDOMAIN. See https://datatracker.ietf.org/doc/html/rfc2308. + return nil, err + } + r.addToCache(cacheKey, msg) + return msg, nil +} diff --git a/x/internal/dnsextra/dialer.go b/x/internal/dnsextra/dialer.go new file mode 100644 index 00000000..cd216211 --- /dev/null +++ b/x/internal/dnsextra/dialer.go @@ -0,0 +1,152 @@ +// 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 +// +// https://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 dnsextra + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "github.com/Jigsaw-Code/outline-sdk/dns" + "github.com/Jigsaw-Code/outline-sdk/transport" + "golang.org/x/net/dns/dnsmessage" +) + +type resolverStreamDialer struct { + resolver dns.Resolver + dialer transport.StreamDialer +} + +var _ transport.StreamDialer = (*resolverStreamDialer)(nil) + +// Returns a [context.Context] that is already done. +func newDoneContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx +} + +func (d *resolverStreamDialer) lookupIPv4(ctx context.Context, domain string) ([]net.IP, error) { + ips := []net.IP{} + q, err := dns.NewQuestion(domain, dnsmessage.TypeA) + if err != nil { + return nil, err + } + response, err := d.resolver.Query(ctx, *q) + if err != nil { + return nil, err + } + if response.RCode != dnsmessage.RCodeSuccess { + return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) + } + for _, answer := range response.Answers { + if answer.Header.Type != dnsmessage.TypeA { + continue + } + if rr, ok := answer.Body.(*dnsmessage.AResource); ok { + ips = append(ips, net.IP(rr.A[:])) + } + } + if len(ips) == 0 { + return nil, errors.New("no ips found") + } + return ips, nil +} + +// MakeFullyQualified makes the domain fully-qualified, ending on a dot ("."). +// This is useful in domain resolution to avoid ambiguity with local domains +// and domain search. +func MakeFullyQualified(domain string) string { + if len(domain) > 0 && domain[len(domain)-1] == '.' { + return domain + } + return domain + "." +} + +func (d *resolverStreamDialer) DialStream(ctx context.Context, addr string) (transport.StreamConn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("failed to parse address: %w", err) + } + var ips []net.IP + ip := net.ParseIP(host) + if ip != nil { + ips = []net.IP{ip} + } else { + // TODO: Implement standard Happy Eyeballs v2. + // Need to properly sort addresses. + // We don't do domain search. + fqdn := MakeFullyQualified(host) + ips, err = d.lookupIPv4(ctx, fqdn) + if err != nil { + return nil, fmt.Errorf("failed to lookup IPv4 ips: %w", err) + } + } + type dialResult struct { + Conn transport.StreamConn + Err error + } + // Communicates the result of each dial. + resultChan := make(chan dialResult) + // Indicates to attempts that the search is done, so they don't get stuck. + searchCtx, searchDone := context.WithCancel(context.Background()) + defer searchDone() + // Used to space out each attempt. The initial value is done because there's no wait needed. + waitCtx := newDoneContext() + // Next entry to start dialing. + next := 0 + // How many connection attempts are not done. + toTry := len(ips) + var dialErr error + for toTry > 0 { + if next == len(ips) { + waitCtx = searchCtx + } + select { + case <-waitCtx.Done(): + // Start a new attempt. + ip := ips[next] + next++ + var waitDone context.CancelFunc + waitCtx, waitDone = context.WithTimeout(searchCtx, 250*time.Millisecond) + go func(ip net.IP, waitDone context.CancelFunc) { + defer waitDone() + conn, err := d.dialer.DialStream(ctx, net.JoinHostPort(ip.String(), port)) + select { + case <-searchCtx.Done(): + if conn != nil { + conn.Close() + } + case resultChan <- dialResult{Conn: conn, Err: err}: + } + }(ip, waitDone) + + case result := <-resultChan: + toTry-- + if result.Err != nil { + dialErr = errors.Join(dialErr, result.Err) + continue + } + return result.Conn, nil + } + } + return nil, dialErr +} + +func NewStreamDialer(resolver dns.Resolver, dialer transport.StreamDialer) transport.StreamDialer { + return &resolverStreamDialer{resolver, dialer} +} diff --git a/x/mobileproxy/README.md b/x/mobileproxy/README.md index 2065f38a..31cb82e2 100644 --- a/x/mobileproxy/README.md +++ b/x/mobileproxy/README.md @@ -47,6 +47,7 @@ The header file below is an example of the Objective-C interface that Go Mobile @class MobileproxyProxy; +@class MobileproxyStringList; /** * Proxy enables you to get the actual address bound by the server and stop the service when no longer needed. @@ -76,12 +77,40 @@ The function takes a timeoutSeconds number instead of a [time.Duration] so it's - (void)stop:(long)timeoutSeconds; @end +/** + * StringList allows us to pass a list of strings to the Go Mobile functions, since Go Mobiule doesn't +support slices as parameters. + */ +@interface MobileproxyStringList : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * Append adds the string value to the end of the list. + */ +- (void)append:(NSString* _Nullable)value; +@end + +/** + * NewListFromLines creates a StringList by splitting the input string on new lines. + */ +FOUNDATION_EXPORT MobileproxyStringList* _Nullable MobileproxyNewListFromLines(NSString* _Nullable lines); + /** * RunProxy runs a local web proxy that listens on localAddress, and uses the transportConfig to create a [transport.StreamDialer] that is used to connect to the requested destination. */ FOUNDATION_EXPORT MobileproxyProxy* _Nullable MobileproxyRunProxy(NSString* _Nullable localAddress, NSString* _Nullable transportConfig, NSError* _Nullable* _Nullable error); +/** + * RunSmartProxy will run a local proxy that automatically selects a DNS and TLS strategy to use. +The local proxy will listen on localAddress. It will use testDomain to find a strategy that works. +The strategies to search are given in the searchConfig. + */ +FOUNDATION_EXPORT MobileproxyProxy* _Nullable MobileproxyRunSmartProxy(NSString* _Nullable localAddress, MobileproxyStringList* _Nullable testDomains, NSString* _Nullable searchConfig, NSError* _Nullable* _Nullable error); + #endif ``` @@ -121,11 +150,21 @@ public abstract class Mobileproxy { + /** + * NewListFromLines creates a StringList by splitting the input string on new lines. + */ + public static native StringList newListFromLines(String lines); /** * RunProxy runs a local web proxy that listens on localAddress, and uses the transportConfig to create a [transport.StreamDialer] that is used to connect to the requested destination. */ public static native Proxy runProxy(String localAddress, String transportConfig) throws Exception; + /** + * RunSmartProxy will run a local proxy that automatically selects a DNS and TLS strategy to use. + The local proxy will listen on localAddress. It will use testDomain to find a strategy that works. + The strategies to search are given in the searchConfig. + */ + public static native Proxy runSmartProxy(String localAddress, StringList testDomains, String searchConfig) throws Exception; } ``` @@ -197,13 +236,69 @@ public final class Proxy implements Seq.Proxy { } ``` +`StringList.java`: + +```java +// Code generated by gobind. DO NOT EDIT. + +// Java class mobileproxy.StringList is a proxy for talking to a Go program. +// +// autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy +package mobileproxy; + +import go.Seq; + +/** + * StringList allows us to pass a list of strings to the Go Mobile functions, since Go Mobiule doesn't +support slices as parameters. + */ +public final class StringList implements Seq.Proxy { + static { Mobileproxy.touch(); } + + private final int refnum; + + @Override public final int incRefnum() { + Seq.incGoRef(refnum, this); + return refnum; + } + + StringList(int refnum) { this.refnum = refnum; Seq.trackGoRef(refnum, this); } + + public StringList() { this.refnum = __New(); Seq.trackGoRef(refnum, this); } + + private static native int __New(); + + /** + * Append adds the string value to the end of the list. + */ + public native void append(String value); + @Override public boolean equals(Object o) { + if (o == null || !(o instanceof StringList)) { + return false; + } + StringList that = (StringList)o; + return true; + } + + @Override public int hashCode() { + return java.util.Arrays.hashCode(new Object[] {}); + } + + @Override public String toString() { + StringBuilder b = new StringBuilder(); + b.append("StringList").append("{"); + return b.append("}").toString(); + } +} +``` + ## Add the library to your mobile project To add the library to your mobile project, see Go Mobile's [Building and deploying to iOS](https://github.com/golang/go/wiki/Mobile#building-and-deploying-to-ios-1) and [Building and deploying to Android](https://github.com/golang/go/wiki/Mobile#building-and-deploying-to-android-1). -## Use the library +## Using the basic local proxy forwarder You need to call the `RunProxy` function passing the local address to use, and the transport configuration. @@ -217,6 +312,23 @@ val proxy = mobileproxy.runProxy("localhost:0", "split:3") proxy.stop() ``` +## Using the smart local proxy forwarder ("Smart Proxy") + +The Smart Proxy can automatically try multiple strategies to unblock access to the test domains you specify. +You need to specify a strategy config in JSON format ([example](../examples/smart-proxy/config.json)). + +On Android, the Kotlin code would look like this: +```kotlin +// Use port zero to let the system pick an open port for you. +val testDomains = mobileproxy.newListFromLines("www.youtube.com\ni.ytimg.com") +val strategiesConfig = "..." // Config JSON. +val proxy = mobileproxy.runSmartProxy("localhost:0", testDomains, strategies) +// Configure your networking library using proxy.host() and proxy.port() or proxy.address(). +// ... +// Stop running the proxy. +proxy.stop() +``` + ## Configure your HTTP client or networking library You need to configure your networking library to use the local proxy. How you do it depends on the networking library you are using. @@ -265,7 +377,7 @@ We are working on instructions on how use the local proxy in a Webview. On Android, you will likely have to implement [WebViewClient.shouldInterceptRequest](https://developer.android.com/reference/android/webkit/WebViewClient#shouldInterceptRequest(android.webkit.WebView,%20android.webkit.WebResourceRequest)) to fulfill requests using an HTTP client that uses the local proxy. -On iOS, you will have to use [NWParameters.PrivacyContext.proxyConfigurations](https://developer.apple.com/documentation/network/nwparameters/privacycontext/4156642-proxyconfigurations). It is iOS 17.0+ and MacOS 14.0+ only. As a fallback you can force encrypted DNS in iOS 14+ via [NWParameters.PrivacyContext.requireEncryptedNameResolution(_:fallbackResolver:)](https://developer.apple.com/documentation/network/nwparameters/privacycontext/3548851-requireencryptednameresolution). +On iOS, we are still looking for ideas. There's [WKWebViewConfiguration.setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler), but the documentation says it can't be used to intercept HTTPS. If you know how to use a proxy with the WKWebView, please let us know! ## Clean up diff --git a/x/mobileproxy/mobileproxy.go b/x/mobileproxy/mobileproxy.go index 3f89648a..c396d96e 100644 --- a/x/mobileproxy/mobileproxy.go +++ b/x/mobileproxy/mobileproxy.go @@ -24,11 +24,15 @@ import ( "log" "net" "net/http" + "os" "strconv" + "strings" "time" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/x/config" "github.com/Jigsaw-Code/outline-sdk/x/httpproxy" + "github.com/Jigsaw-Code/outline-sdk/x/smart" ) // Proxy enables you to get the actual address bound by the server and stop the service when no longer needed. @@ -90,3 +94,65 @@ func RunProxy(localAddress string, transportConfig string) (*Proxy, error) { } return &Proxy{host: host, port: port, server: server}, nil } + +// NewListFromLines creates a StringList by splitting the input string on new lines. +func NewListFromLines(lines string) *StringList { + return &StringList{list: strings.Split(lines, "\n")} +} + +// StringList allows us to pass a list of strings to the Go Mobile functions, since Go Mobiule doesn't +// support slices as parameters. +type StringList struct { + list []string +} + +// Append adds the string value to the end of the list. +func (l *StringList) Append(value string) { + l.list = append(l.list, value) +} + +// RunSmartProxy will run a local proxy that automatically selects a DNS and TLS strategy to use. +// The local proxy will listen on localAddress. It will use testDomain to find a strategy that works. +// The strategies to search are given in the searchConfig. +func RunSmartProxy(localAddress string, testDomains *StringList, searchConfig string) (*Proxy, error) { + // TODO: inject the base dialer for tests. + logWriter := os.Stderr + finder := smart.StrategyFinder{ + LogWriter: logWriter, + TestTimeout: 5 * time.Second, + StreamDialer: &transport.TCPDialer{}, + PacketDialer: &transport.UDPDialer{}, + } + + fmt.Println("Finding strategy") + testDomainsSlice := append(make([]string, 0, len(testDomains.list)), testDomains.list...) + dialer, err := finder.NewDialer(context.Background(), testDomainsSlice, []byte(searchConfig)) + if err != nil { + return nil, fmt.Errorf("failed to find dialer: %v", err) + } + logDialer := transport.FuncStreamDialer(func(ctx context.Context, address string) (transport.StreamConn, error) { + conn, err := dialer.DialStream(ctx, address) + if err != nil { + fmt.Fprintf(logWriter, "Failed to dial %v: %v\n", address, err) + } + return conn, err + }) + + listener, err := net.Listen("tcp", localAddress) + if err != nil { + return nil, fmt.Errorf("could not listen on address %v: %v", localAddress, err) + } + + server := &http.Server{Handler: httpproxy.NewProxyHandler(logDialer)} + go server.Serve(listener) + + host, portStr, err := net.SplitHostPort(listener.Addr().String()) + if err != nil { + return nil, fmt.Errorf("could not parse proxy address '%v': %v", listener.Addr().String(), err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, fmt.Errorf("could not parse proxy port '%v': %v", portStr, err) + } + return &Proxy{host: host, port: port, server: server}, nil +} diff --git a/x/smart/stream_dialer.go b/x/smart/stream_dialer.go new file mode 100644 index 00000000..c1f35fee --- /dev/null +++ b/x/smart/stream_dialer.go @@ -0,0 +1,664 @@ +// 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 +// +// https://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 smart + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/url" + "sync" + "time" + "unicode" + + "github.com/Jigsaw-Code/outline-sdk/dns" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/config" + "github.com/Jigsaw-Code/outline-sdk/x/internal/dnsextra" + "golang.org/x/net/dns/dnsmessage" +) + +// TODO: +// - Add DNS caching +// - Parallelize TLS +// - Add debug logging to proxy handler +// - Figure out what to do for IPv6. +// - We should auto detect if underlying dialer supports it. +// - We need to make the ResolverDialer smarter +// - Improve plaintext DNS search +// - Use SOA or NS query. Need to account for CNAMEs. +// - Use injection fingerprint +// - Use TLS validator. Successful TLS, but cert not for domain is clear sign. (if SNI specified) +// - Downgrade, not drop, case not matched +// - Investigate why TLS is succeeding for www.rferl.org @ 188.43.20.67 +// - Also, is fake SNI working? + +// IP validation +// - Check against hardcoded ground truth (IPs, PTR record) +// - Check against encrypted answers +// - Try TLS connection. May need fragmentation +// +// Dialer: +// - check cache +// - resolve A and AAAA, save to cache +// - Go resolution: https://cs.opensource.google/go/go/+/master:src/net/dnsclient_unix.go;l=612;drc=6146a73d279d73b6138191929d2f1fad22188f51 +// - Go Happy Eyeballs (V1): https://cs.opensource.google/go/go/+/master:src/net/dial.go;l=455;drc=1fde99cd6eff725f5cc13748a43b4aef3de557c8 +// - Do basic fallback on dial: https://cs.opensource.google/go/go/+/master:src/net/addrselect.go + +// To test one strategy: +// go run ./x/examples/smart-proxy -v -localAddr=localhost:1080 --transport="" --domain www.rferl.org --config=<(echo '{"dns": [{"https": {"name": "doh.sb"}}]}') + +// mixCase randomizes the case of the domain letters. +func mixCase(domain string) string { + var mixed []rune + for _, r := range domain { + if rand.Intn(2) == 0 { + mixed = append(mixed, unicode.ToLower(r)) + } else { + mixed = append(mixed, unicode.ToUpper(r)) + } + } + return string(mixed) +} + +func getARootNameserver() (string, error) { + nsList, err := net.LookupNS(".") + if err != nil { + return "", fmt.Errorf("could not get list of root nameservers: %v", err) + } + if len(nsList) == 0 { + return "", fmt.Errorf("empty list of root nameservers") + } + return nsList[0].Host, nil +} + +func fingerprint(pd transport.PacketDialer, sd transport.StreamDialer, testDomain string) { + rootNS, err := getARootNameserver() + if err != nil { + log.Fatalf("Failed to find root nameserver: %v", err) + } + + allNSIPs, err := net.LookupIP(rootNS) + if err != nil { + log.Fatalf("Failed to resolve root nameserver: %v", err) + } + ips := []net.IP{} + for _, ip := range allNSIPs { + if ip.To4() != nil { + ips = append(ips, ip) + break + } + } + for _, ip := range allNSIPs { + if ip.To16() != nil { + ips = append(ips, ip) + break + } + } + + q, err := dns.NewQuestion(testDomain, dnsmessage.TypeA) + if err != nil { + log.Fatalf("failed to parse domain name: %v", err) + } + for _, rootNSIP := range ips { + resolvedNS := net.JoinHostPort(rootNSIP.String(), "53") + for _, proto := range []string{"udp", "tcp"} { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + var resolver dns.Resolver + switch proto { + case "tcp": + resolver = dns.NewTCPResolver(sd, resolvedNS) + default: + resolver = dns.NewUDPResolver(pd, resolvedNS) + } + + response, err := resolver.Query(ctx, *q) + fmt.Printf("%v:%v", proto, resolvedNS) + if err != nil { + fmt.Printf("; status=error: %v\n", err) + continue + } + if len(response.Answers) > 0 { + fmt.Printf("; status=unexpected answer (injected): %v ⚠️\n", response.Answers) + // TODO: use RCODE, CNAME and IPs as blocking fingerprint. + continue + } + if response.RCode != dnsmessage.RCodeSuccess { + fmt.Printf("; status=unexpected rcode (injected): %v ⚠️\n", response.Answers) + // TODO: use RCODE, CNAME and IPs as blocking fingerprint. + continue + } + fmt.Print("; status=ok (no injection) ✓\n") + } + } +} + +func evaluateNetResolver(ctx context.Context, resolver *net.Resolver, testDomain string) ([]net.IP, error) { + requestDomain := mixCase(testDomain) + _, err := resolver.LookupCNAME(ctx, requestDomain) + if err != nil { + return nil, fmt.Errorf("could not get cname: %w", err) + } + ips, err := resolver.LookupIP(ctx, "ip", requestDomain) + if err != nil { + return nil, fmt.Errorf("failed to lookup IPs: %w", err) + } + if len(ips) == 0 { + return nil, fmt.Errorf("no ip answer") + } + for _, ip := range ips { + if ip.IsLoopback() { + return nil, fmt.Errorf("localhost ip: %v", ip) // -1 + } + if ip.IsPrivate() { + return nil, fmt.Errorf("private ip: %v", ip) // -1 + } + if ip.IsUnspecified() { + return nil, fmt.Errorf("zero ip: %v", ip) // -1 + } + // TODO: consider validating the IPs: fingerprint, hardcoded ground truth, trusted response, TLS connection. + } + return ips, nil +} + +func evaluateAddressResponse(response dnsmessage.Message, requestDomain string) ([]net.IP, error) { + if response.RCode != dnsmessage.RCodeSuccess { + return nil, fmt.Errorf("rcode is not success: %v", response.RCode) + } + var ips []net.IP + if len(response.Answers) == 0 { + return ips, errors.New("no answers") // -1 + } + for _, answer := range response.Answers { + if answer.Header.Type != dnsmessage.TypeA && answer.Header.Type != dnsmessage.TypeAAAA { + continue + } + var ip net.IP + switch rr := answer.Body.(type) { + case *dnsmessage.AResource: + ip = net.IP(rr.A[:]) + case *dnsmessage.AAAAResource: + ip = net.IP(rr.AAAA[:]) + default: + continue + } + if ip.IsLoopback() { + return nil, fmt.Errorf("localhost ip: %v", ip) // -1 + } + if ip.IsPrivate() { + return nil, fmt.Errorf("private ip: %v", ip) // -1 + } + if ip.IsUnspecified() { + return nil, fmt.Errorf("zero ip: %v", ip) // -1 + } + ips = append(ips, ip) + } + if len(ips) == 0 { + return ips, fmt.Errorf("no ip answer: %v", response.Answers) // -1 + } + // All popular recursive resolvers we tested maintain the domain case of the request. + // Note that this is not the case of authoritative resolvers. Some of them will return + // a fully normalized domain name, or normalize part of it. + if response.Answers[0].Header.Name.String() != requestDomain { + return ips, fmt.Errorf("domain mismatch: got %v, expected %v", response.Answers[0].Header.Name, requestDomain) // -0.5 or +0.5 if match + } + return ips, nil +} + +func evaluateCNAMEResponse(response dnsmessage.Message, requestDomain string) error { + if response.RCode != dnsmessage.RCodeSuccess { + return fmt.Errorf("rcode is not success: %v", response.RCode) + } + if len(response.Answers) == 0 { + var numSOA int + for _, answer := range response.Authorities { + if _, ok := answer.Body.(*dnsmessage.SOAResource); ok { + numSOA++ + } + } + if numSOA != 1 { + return fmt.Errorf("SOA records is %v, expected 1", numSOA) + } + return nil + } + var cname string + for _, answer := range response.Answers { + if answer.Header.Type != dnsmessage.TypeCNAME { + return fmt.Errorf("bad answer type: %v", answer.Header.Type) + } + if rr, ok := answer.Body.(*dnsmessage.CNAMEResource); ok { + if cname != "" { + return fmt.Errorf("found too many CNAMEs: %v %v", cname, rr.CNAME) + } + cname = rr.CNAME.String() + } + } + if cname == "" { + return fmt.Errorf("no CNAME in answers") + } + return nil +} + +type StrategyFinder struct { + TestTimeout time.Duration + LogWriter io.Writer + StreamDialer transport.StreamDialer + PacketDialer transport.PacketDialer + logMu sync.Mutex +} + +func (f *StrategyFinder) log(format string, a ...any) { + if f.LogWriter != nil { + f.logMu.Lock() + defer f.logMu.Unlock() + fmt.Fprintf(f.LogWriter, format, a...) + } +} + +func (f *StrategyFinder) testDNSClient(baseCtx context.Context, resolver dns.Resolver, testDomain string) ([]net.IP, error) { + // We special case the system resolver, since we can't get a dns.RoundTripper. + if resolver == nil { + ctx, cancel := context.WithTimeout(baseCtx, f.TestTimeout) + defer cancel() + return evaluateNetResolver(ctx, new(net.Resolver), testDomain) + } + + requestDomain := mixCase(testDomain) + + q, err := dns.NewQuestion(requestDomain, dnsmessage.TypeA) + if err != nil { + return nil, fmt.Errorf("failed to create question: %v", err) + } + ctxA, cancelA := context.WithTimeout(baseCtx, f.TestTimeout) + defer cancelA() + response, err := resolver.Query(ctxA, *q) + if err != nil { + return nil, fmt.Errorf("request for A query failed: %w", err) + } + ips, err := evaluateAddressResponse(*response, requestDomain) + if err != nil { + return ips, fmt.Errorf("failed A test: %w", err) + } + // TODO(fortuna): Consider testing whether we can establish a TCP connection to ip:443. + + q, err = dns.NewQuestion(requestDomain, dnsmessage.TypeCNAME) + if err != nil { + return nil, fmt.Errorf("failed to create question: %v", err) + } + ctxCNAME, cancelCNAME := context.WithTimeout(baseCtx, f.TestTimeout) + defer cancelCNAME() + response, err = resolver.Query(ctxCNAME, *q) + if err != nil { + return nil, fmt.Errorf("request for CNAME query failed: %w", err) + } + err = evaluateCNAMEResponse(*response, requestDomain) + if err != nil { + return nil, fmt.Errorf("failed CNAME test: %w", err) + } + return ips, nil +} + +type httpsEntryJSON struct { + Name string `json:"name,omitempty"` + Address string `json:"address,omitempty"` +} + +type tlsEntryJSON struct { + Name string `json:"name,omitempty"` + Address string `json:"address,omitempty"` +} + +type udpEntryJSON struct { + Address string `json:"address,omitempty"` +} + +type tcpEntryJSON struct { + Address string `json:"address,omitempty"` +} + +type dnsEntryJSON struct { + System *struct{} `json:"system,omitempty"` + HTTPS *httpsEntryJSON `json:"https,omitempty"` + TLS *tlsEntryJSON `json:"tls,omitempty"` + UDP *udpEntryJSON `json:"udp,omitempty"` + TCP *tcpEntryJSON `json:"tcp,omitempty"` +} + +type configJSON struct { + DNS []dnsEntryJSON `json:"dns,omitempty"` + TLS []string `json:"tls,omitempty"` +} + +func (f *StrategyFinder) newDNSResolverFromEntry(entry dnsEntryJSON) (dns.Resolver, error) { + if entry.System != nil { + return nil, nil + } else if cfg := entry.HTTPS; cfg != nil { + if cfg.Name == "" { + return nil, fmt.Errorf("https entry has empty server name") + } + serverAddr := cfg.Address + if serverAddr == "" { + serverAddr = cfg.Name + } + _, port, err := net.SplitHostPort(serverAddr) + if err != nil { + serverAddr = net.JoinHostPort(serverAddr, "443") + port = "443" + } + dohURL := url.URL{Scheme: "https", Host: net.JoinHostPort(cfg.Name, port), Path: "/dns-query"} + return dns.NewHTTPSResolver(f.StreamDialer, serverAddr, dohURL.String()), nil + } else if cfg := entry.TLS; cfg != nil { + if cfg.Name == "" { + return nil, fmt.Errorf("tls entry has empty server name") + } + serverAddr := cfg.Address + if serverAddr == "" { + serverAddr = cfg.Name + } + _, _, err := net.SplitHostPort(serverAddr) + if err != nil { + serverAddr = net.JoinHostPort(serverAddr, "853") + } + return dns.NewTLSResolver(f.StreamDialer, serverAddr, cfg.Name), nil + } else if cfg := entry.TCP; cfg != nil { + if cfg.Address == "" { + return nil, fmt.Errorf("tcp entry has empty server address") + } + host, port, err := net.SplitHostPort(cfg.Address) + if err != nil { + host = cfg.Address + port = "53" + } + serverAddr := net.JoinHostPort(host, port) + return dns.NewTCPResolver(f.StreamDialer, serverAddr), nil + } else if cfg := entry.UDP; cfg != nil { + if cfg.Address == "" { + return nil, fmt.Errorf("udp entry has empty server address") + } + host, port, err := net.SplitHostPort(cfg.Address) + if err != nil { + host = cfg.Address + port = "53" + } + serverAddr := net.JoinHostPort(host, port) + return dns.NewUDPResolver(f.PacketDialer, serverAddr), nil + } else { + return nil, errors.New("invalid DNS entry") + } +} + +type resolverEntry struct { + ID string + Resolver dns.Resolver +} + +func (f *StrategyFinder) dnsConfigToRoundTrippers(dnsConfig []dnsEntryJSON) ([]resolverEntry, error) { + if len(dnsConfig) == 0 { + return nil, errors.New("no DNS config entry") + } + rts := make([]resolverEntry, 0, len(dnsConfig)) + for ei, entry := range dnsConfig { + idBytes, err := json.Marshal(entry) + if err != nil { + return nil, fmt.Errorf("cannot serialize entry %v: %w", ei, err) + } + id := string(idBytes) + resolver, err := f.newDNSResolverFromEntry(entry) + if err != nil { + return nil, fmt.Errorf("failed to process entry %v: %w", ei, err) + } + rts = append(rts, resolverEntry{ID: id, Resolver: resolver}) + } + return rts, nil +} + +// Returns a [context.Context] that is already done. +func newDoneContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx +} + +func (f *StrategyFinder) findDNS(testDomains []string, dnsConfig []dnsEntryJSON) (dns.Resolver, error) { + resolvers, err := f.dnsConfigToRoundTrippers(dnsConfig) + if err != nil { + return nil, err + } + type testResult struct { + ID string + Resolver dns.Resolver + Err error + } + // Communicates the result of each test. + resultChan := make(chan testResult) + // Indicates to tests that the search is done, so they don't get stuck writing to the results channel that will no longer be read. + searchCtx, searchDone := context.WithCancel(context.Background()) + defer searchDone() + // Used to space out each test. The initial value is done because there's no wait needed. + waitCtx := newDoneContext() + // Next entry to start testing. + nextResolver := 0 + // How many test entries are not done. + resolversToTest := len(resolvers) + for resolversToTest > 0 { + if nextResolver == len(resolvers) { + // No more tests to start. Make sure the select doesn't trigger on waitCtx. + waitCtx = searchCtx + } + select { + case <-waitCtx.Done(): + // Start a new test. + entry := resolvers[nextResolver] + nextResolver++ + var waitDone context.CancelFunc + waitCtx, waitDone = context.WithTimeout(searchCtx, 250*time.Millisecond) + go func(entry resolverEntry, testDone context.CancelFunc) { + defer testDone() + for _, testDomain := range testDomains { + select { + case <-searchCtx.Done(): + return + default: + } + f.log("🏃 run dns: %v (domain: %v)\n", entry.ID, testDomain) + startTime := time.Now() + ips, err := f.testDNSClient(searchCtx, entry.Resolver, testDomain) + duration := time.Since(startTime) + status := "ok ✅" + if err != nil { + status = fmt.Sprintf("%v ❌", err) + } + f.log("🏁 got dns: %v (domain: %v), duration=%v, ips=%v, status=%v\n", entry.ID, testDomain, duration, ips, status) + if err != nil { + select { + case <-searchCtx.Done(): + return + case resultChan <- testResult{ID: entry.ID, Resolver: entry.Resolver, Err: err}: + return + } + } + } + select { + case <-searchCtx.Done(): + case resultChan <- testResult{ID: entry.ID, Resolver: entry.Resolver, Err: nil}: + } + }(entry, waitDone) + + case result := <-resultChan: + resolversToTest-- + // Process the result of a test. + if result.Err != nil { + continue + } + f.log("✅ selected resolver %v\n", result.ID) + // Tested all domains on this resolver. Return + if result.Resolver != nil { + return dnsextra.NewCacheResolver(result.Resolver, 100), nil + } else { + return nil, nil + } + } + } + return nil, errors.New("could not find working resolver") +} + +func (f *StrategyFinder) findTLS(testDomains []string, baseDialer transport.StreamDialer, tlsConfig []string) (transport.StreamDialer, error) { + if len(tlsConfig) == 0 { + return nil, errors.New("config for TLS is empty. Please specify at least one transport") + } + for _, transportCfg := range tlsConfig { + for di, testDomain := range testDomains { + testAddr := net.JoinHostPort(testDomain, "443") + f.log(" tls=%v (domain: %v)", transportCfg, testDomain) + + tlsDialer, err := config.WrapStreamDialer(baseDialer, transportCfg) + if err != nil { + f.log("; wrap_error=%v ❌\n", err) + break + } + ctx, cancel := context.WithTimeout(context.Background(), f.TestTimeout) + defer cancel() + testConn, err := tlsDialer.DialStream(ctx, testAddr) + if err != nil { + f.log("; dial_error=%v ❌\n", err) + break + } + tlsConn := tls.Client(testConn, &tls.Config{ServerName: testDomain}) + err = tlsConn.HandshakeContext(ctx) + tlsConn.Close() + if err != nil { + f.log("; handshake=%v ❌\n", err) + break + } + f.log("; status=ok ✅\n") + if di+1 < len(testDomains) { + // More domains to test + continue + } + return transport.FuncStreamDialer(func(ctx context.Context, raddr string) (transport.StreamConn, error) { + _, portStr, err := net.SplitHostPort(raddr) + if err != nil { + return nil, fmt.Errorf("failed to parse address: %w", err) + } + portNum, err := net.DefaultResolver.LookupPort(ctx, "tcp", portStr) + if err != nil { + return nil, fmt.Errorf("could not resolve port: %w", err) + } + selectedDialer := baseDialer + if portNum == 443 || portNum == 853 { + selectedDialer = tlsDialer + } + return selectedDialer.DialStream(ctx, raddr) + }), nil + } + } + return nil, errors.New("could not find TLS strategy") +} + +// NewDialer uses the config in configBytes to search for a strategy that unblocks all of the testDomains, returning a dialer with the found strategy. +// It returns an error if no strategy was found that unblocks the testDomains. +// The testDomains must be domains with a TLS service running on port 443. +func (f *StrategyFinder) NewDialer(ctx context.Context, testDomains []string, configBytes []byte) (transport.StreamDialer, error) { + var parsedConfig configJSON + err := json.Unmarshal(configBytes, &parsedConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse config: %v", err) + } + + // Make domain fully-qualified to prevent confusing domain search. + testDomains = append(make([]string, 0, len(testDomains)), testDomains...) + for di, domain := range testDomains { + testDomains[di] = dnsextra.MakeFullyQualified(domain) + } + + dnsRT, err := f.findDNS(testDomains, parsedConfig.DNS) + if err != nil { + return nil, err + } + var dnsDialer transport.StreamDialer + if dnsRT == nil { + if _, ok := f.StreamDialer.(*transport.TCPDialer); !ok { + return nil, fmt.Errorf("cannot use system resolver with base dialer of type %T", f.StreamDialer) + } + dnsDialer = f.StreamDialer + } else { + dnsDialer = dnsextra.NewStreamDialer(dnsRT, f.StreamDialer) + } + + if len(parsedConfig.TLS) == 0 { + return dnsDialer, nil + } + return f.findTLS(testDomains, dnsDialer, parsedConfig.TLS) +} + +/* + Scoring: + - Priority ordering: system, HTTPS, TLS, unencrypted + - Retriable error: -5, hard evidence: -10 + - IsPrivate: -5 + - Validated: +10 + + - For system resolver: base score 2 + - Test [testDomain NS]. If error: -5 + - For each UDP resolver: base score 0 + - Test [testDomain NS]. If error -5 or non-NS answer: -10 + - Test techniques: mix case + - For each TCP resolver: base score 0 + - Test TCP connection. If it fails, likely blocked by IP or port (-5) + - Test [testDomain NS]. If error -5 or non-NS answer: -10 + - Test techniques: mix case, split + - For each TLS resolvers: base score 1 + - Test TCP connection. If it fails, likely blocked by IP (-5) + - Test TLS connection. If it fails, likely blocked by SNI (-10) + - Test techniques: domain fronting, tcp split, tlsrecordfrag + - Try changing case + + Hostmap should not go through scoring at first. We shouldn't use it if not needed. Also, it only helps A/AAAA, it doesn't work with NS, SOA, etc. + + Lookup (domain A/AAAA) at root resolver (doesn’t apply to DoH and system resolver) + no error: 0 + error: -1 + has answer: -1 + + Lookup (domain, NS) (breaks with hostmap) + expected answer: +1 + unknown answer: 0 + bad answer or no answer: -1 + Lookup (domain, CNAME) (breaks with hostmap) + expected answer: +1 + unknown answer: 0 + Lookup IP, then reverse IP. A and AAAA + expected domain: +1 + unknown domain or no answer: 0 + Is Private? + public: 0 + private: -1 +*/ + +// TODO: +// Add recursive resolver. +// Save RTT for sorting. +// What to do about clustering IPs for a resolver? +// Go over list of public resolvers, restricted to working categories. +// Perhaps make a score function? +// If no working category, try alternative ports. +// Define DNS strategy object. Or perhaps Client with debug info. From d31f8bd328ac30307ef5bc001f77dc6980fe9e2b Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 19 Jan 2024 19:05:43 -0500 Subject: [PATCH 02/15] Fix CNAME --- x/smart/cname.go | 71 ++++++++++++++++++++++++++++++++++++++++ x/smart/stream_dialer.go | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 x/smart/cname.go diff --git a/x/smart/cname.go b/x/smart/cname.go new file mode 100644 index 00000000..78607845 --- /dev/null +++ b/x/smart/cname.go @@ -0,0 +1,71 @@ +// 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 +// +// https://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 smart + +/* +#include +#include +#include +#include +*/ +import "C" + +import ( + "context" + "fmt" + "unsafe" +) + +func lookupCNAME(ctx context.Context, domain string) (string, error) { + type result struct { + cname string + err error + } + + results := make(chan result) + go func() { + cname, err := lookupCNAMEBlocking(domain) + results <- result{cname, err} + }() + + select { + case r := <-results: + return r.cname, r.err + case <-ctx.Done(): + return "", ctx.Err() + } +} + +func lookupCNAMEBlocking(host string) (string, error) { + var hints C.struct_addrinfo + var result *C.struct_addrinfo + + chost := C.CString(host) + defer C.free(unsafe.Pointer(chost)) + + hints.ai_family = C.AF_UNSPEC + hints.ai_flags = C.AI_CANONNAME + + // Call getaddrinfo + res := C.getaddrinfo(chost, nil, &hints, &result) + if res != 0 { + return "", fmt.Errorf("getaddrinfo error: %s", C.GoString(C.gai_strerror(res))) + } + defer C.freeaddrinfo(result) + + // Extract canonical name + cname := C.GoString(result.ai_canonname) + return cname, nil +} diff --git a/x/smart/stream_dialer.go b/x/smart/stream_dialer.go index c1f35fee..ae561fc7 100644 --- a/x/smart/stream_dialer.go +++ b/x/smart/stream_dialer.go @@ -154,7 +154,7 @@ func fingerprint(pd transport.PacketDialer, sd transport.StreamDialer, testDomai func evaluateNetResolver(ctx context.Context, resolver *net.Resolver, testDomain string) ([]net.IP, error) { requestDomain := mixCase(testDomain) - _, err := resolver.LookupCNAME(ctx, requestDomain) + _, err := lookupCNAME(ctx, requestDomain) if err != nil { return nil, fmt.Errorf("could not get cname: %w", err) } From d4820339104f67e72b8716a2ccac620e4aca96f5 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 19 Jan 2024 19:10:22 -0500 Subject: [PATCH 03/15] Rename --- x/smart/{cname.go => cname_unix.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename x/smart/{cname.go => cname_unix.go} (100%) diff --git a/x/smart/cname.go b/x/smart/cname_unix.go similarity index 100% rename from x/smart/cname.go rename to x/smart/cname_unix.go From 326ce45a83e4d56baa60043c112fdaa570cccf27 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 19 Jan 2024 19:15:10 -0500 Subject: [PATCH 04/15] Try to fix Windows --- x/smart/cname.go | 30 ++++++++++++++++++++++++++++++ x/smart/cname_unix.go | 2 ++ 2 files changed, 32 insertions(+) create mode 100644 x/smart/cname.go diff --git a/x/smart/cname.go b/x/smart/cname.go new file mode 100644 index 00000000..2df96473 --- /dev/null +++ b/x/smart/cname.go @@ -0,0 +1,30 @@ +// 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 +// +// https://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. + +//go:build !unix + +package smart + +import ( + "context" + "net" +) + +func lookupCNAME(ctx context.Context, domain string) (string, error) { + return net.DefaultResolver.LookupCNAME(ctx, domain) + type result struct { + cname string + err error + } +} diff --git a/x/smart/cname_unix.go b/x/smart/cname_unix.go index 78607845..ed20b3af 100644 --- a/x/smart/cname_unix.go +++ b/x/smart/cname_unix.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build unix + package smart /* From c8036c6f5ba6b0d6073cbffbccab54302f75bd1a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 19 Jan 2024 19:17:46 -0500 Subject: [PATCH 05/15] Fix it --- x/smart/cname.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x/smart/cname.go b/x/smart/cname.go index 2df96473..a1eec5ab 100644 --- a/x/smart/cname.go +++ b/x/smart/cname.go @@ -23,8 +23,4 @@ import ( func lookupCNAME(ctx context.Context, domain string) (string, error) { return net.DefaultResolver.LookupCNAME(ctx, domain) - type result struct { - cname string - err error - } } From 052032b7306e1b8308d11cd882344e60f4bdf412 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Sat, 10 Feb 2024 14:36:03 -0500 Subject: [PATCH 06/15] Use Happy Eyeballs --- x/go.mod | 2 +- x/go.sum | 4 +- x/internal/dnsextra/dialer.go | 152 ---------------------------------- x/smart/resolver_dialer.go | 86 +++++++++++++++++++ x/smart/stream_dialer.go | 17 +++- 5 files changed, 103 insertions(+), 158 deletions(-) delete mode 100644 x/internal/dnsextra/dialer.go create mode 100644 x/smart/resolver_dialer.go diff --git a/x/go.mod b/x/go.mod index e21d972f..85329027 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.20 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e + github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5 github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b github.com/stretchr/testify v1.8.2 github.com/vishvananda/netlink v1.1.0 diff --git a/x/go.sum b/x/go.sum index 0806aa8a..ed5915f8 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,5 +1,5 @@ -github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e h1:56ZI48e68EYYb3m2slu3YJ6C+gWqh8v9bIWk+Bl9dfY= -github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212550-6cd87709dc1e/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5 h1:ylGTGMEJGgQuaZtSB559oZmm5hAMIWEmNq3bUTa71rI= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= 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= diff --git a/x/internal/dnsextra/dialer.go b/x/internal/dnsextra/dialer.go deleted file mode 100644 index cd216211..00000000 --- a/x/internal/dnsextra/dialer.go +++ /dev/null @@ -1,152 +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 -// -// https://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 dnsextra - -import ( - "context" - "errors" - "fmt" - "net" - "time" - - "github.com/Jigsaw-Code/outline-sdk/dns" - "github.com/Jigsaw-Code/outline-sdk/transport" - "golang.org/x/net/dns/dnsmessage" -) - -type resolverStreamDialer struct { - resolver dns.Resolver - dialer transport.StreamDialer -} - -var _ transport.StreamDialer = (*resolverStreamDialer)(nil) - -// Returns a [context.Context] that is already done. -func newDoneContext() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - return ctx -} - -func (d *resolverStreamDialer) lookupIPv4(ctx context.Context, domain string) ([]net.IP, error) { - ips := []net.IP{} - q, err := dns.NewQuestion(domain, dnsmessage.TypeA) - if err != nil { - return nil, err - } - response, err := d.resolver.Query(ctx, *q) - if err != nil { - return nil, err - } - if response.RCode != dnsmessage.RCodeSuccess { - return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) - } - for _, answer := range response.Answers { - if answer.Header.Type != dnsmessage.TypeA { - continue - } - if rr, ok := answer.Body.(*dnsmessage.AResource); ok { - ips = append(ips, net.IP(rr.A[:])) - } - } - if len(ips) == 0 { - return nil, errors.New("no ips found") - } - return ips, nil -} - -// MakeFullyQualified makes the domain fully-qualified, ending on a dot ("."). -// This is useful in domain resolution to avoid ambiguity with local domains -// and domain search. -func MakeFullyQualified(domain string) string { - if len(domain) > 0 && domain[len(domain)-1] == '.' { - return domain - } - return domain + "." -} - -func (d *resolverStreamDialer) DialStream(ctx context.Context, addr string) (transport.StreamConn, error) { - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse address: %w", err) - } - var ips []net.IP - ip := net.ParseIP(host) - if ip != nil { - ips = []net.IP{ip} - } else { - // TODO: Implement standard Happy Eyeballs v2. - // Need to properly sort addresses. - // We don't do domain search. - fqdn := MakeFullyQualified(host) - ips, err = d.lookupIPv4(ctx, fqdn) - if err != nil { - return nil, fmt.Errorf("failed to lookup IPv4 ips: %w", err) - } - } - type dialResult struct { - Conn transport.StreamConn - Err error - } - // Communicates the result of each dial. - resultChan := make(chan dialResult) - // Indicates to attempts that the search is done, so they don't get stuck. - searchCtx, searchDone := context.WithCancel(context.Background()) - defer searchDone() - // Used to space out each attempt. The initial value is done because there's no wait needed. - waitCtx := newDoneContext() - // Next entry to start dialing. - next := 0 - // How many connection attempts are not done. - toTry := len(ips) - var dialErr error - for toTry > 0 { - if next == len(ips) { - waitCtx = searchCtx - } - select { - case <-waitCtx.Done(): - // Start a new attempt. - ip := ips[next] - next++ - var waitDone context.CancelFunc - waitCtx, waitDone = context.WithTimeout(searchCtx, 250*time.Millisecond) - go func(ip net.IP, waitDone context.CancelFunc) { - defer waitDone() - conn, err := d.dialer.DialStream(ctx, net.JoinHostPort(ip.String(), port)) - select { - case <-searchCtx.Done(): - if conn != nil { - conn.Close() - } - case resultChan <- dialResult{Conn: conn, Err: err}: - } - }(ip, waitDone) - - case result := <-resultChan: - toTry-- - if result.Err != nil { - dialErr = errors.Join(dialErr, result.Err) - continue - } - return result.Conn, nil - } - } - return nil, dialErr -} - -func NewStreamDialer(resolver dns.Resolver, dialer transport.StreamDialer) transport.StreamDialer { - return &resolverStreamDialer{resolver, dialer} -} diff --git a/x/smart/resolver_dialer.go b/x/smart/resolver_dialer.go new file mode 100644 index 00000000..d496ce0c --- /dev/null +++ b/x/smart/resolver_dialer.go @@ -0,0 +1,86 @@ +// 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 +// +// https://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 smart + +import ( + "context" + "errors" + "fmt" + "net/netip" + + "github.com/Jigsaw-Code/outline-sdk/dns" + "github.com/Jigsaw-Code/outline-sdk/transport" + "golang.org/x/net/dns/dnsmessage" +) + +func newResolverStreamDialer(resolver dns.Resolver, dialer transport.StreamDialer) transport.StreamDialer { + return &transport.HappyEyeballsStreamDialer{ + Dialer: dialer, + Resolve: transport.NewDualStackHappyEyeballsResolveFunc( + func(ctx context.Context, hostname string) ([]netip.Addr, error) { + ips := []netip.Addr{} + q, err := dns.NewQuestion(hostname, dnsmessage.TypeA) + if err != nil { + return nil, err + } + response, err := resolver.Query(ctx, *q) + if err != nil { + return nil, err + } + if response.RCode != dnsmessage.RCodeSuccess { + return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) + } + for _, answer := range response.Answers { + if answer.Header.Type != dnsmessage.TypeA { + continue + } + if rr, ok := answer.Body.(*dnsmessage.AResource); ok { + ips = append(ips, netip.AddrFrom4(rr.A)) + } + } + if len(ips) == 0 { + return nil, errors.New("no ips found") + } + return ips, nil + }, + func(ctx context.Context, hostname string) ([]netip.Addr, error) { + ips := []netip.Addr{} + q, err := dns.NewQuestion(hostname, dnsmessage.TypeAAAA) + if err != nil { + return nil, err + } + response, err := resolver.Query(ctx, *q) + if err != nil { + return nil, err + } + if response.RCode != dnsmessage.RCodeSuccess { + return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) + } + for _, answer := range response.Answers { + if answer.Header.Type != dnsmessage.TypeAAAA { + continue + } + if rr, ok := answer.Body.(*dnsmessage.AAAAResource); ok { + ips = append(ips, netip.AddrFrom16(rr.AAAA)) + } + } + if len(ips) == 0 { + return nil, errors.New("no ips found") + } + return ips, nil + }, + ), + } +} diff --git a/x/smart/stream_dialer.go b/x/smart/stream_dialer.go index ae561fc7..97eebf47 100644 --- a/x/smart/stream_dialer.go +++ b/x/smart/stream_dialer.go @@ -512,7 +512,7 @@ func (f *StrategyFinder) findDNS(testDomains []string, dnsConfig []dnsEntryJSON) f.log("✅ selected resolver %v\n", result.ID) // Tested all domains on this resolver. Return if result.Resolver != nil { - return dnsextra.NewCacheResolver(result.Resolver, 100), nil + return result.Resolver, nil } else { return nil, nil } @@ -574,6 +574,16 @@ func (f *StrategyFinder) findTLS(testDomains []string, baseDialer transport.Stre return nil, errors.New("could not find TLS strategy") } +// makeFullyQualified makes the domain fully-qualified, ending on a dot ("."). +// This is useful in domain resolution to avoid ambiguity with local domains +// and domain search. +func makeFullyQualified(domain string) string { + if len(domain) > 0 && domain[len(domain)-1] == '.' { + return domain + } + return domain + "." +} + // NewDialer uses the config in configBytes to search for a strategy that unblocks all of the testDomains, returning a dialer with the found strategy. // It returns an error if no strategy was found that unblocks the testDomains. // The testDomains must be domains with a TLS service running on port 443. @@ -587,7 +597,7 @@ func (f *StrategyFinder) NewDialer(ctx context.Context, testDomains []string, co // Make domain fully-qualified to prevent confusing domain search. testDomains = append(make([]string, 0, len(testDomains)), testDomains...) for di, domain := range testDomains { - testDomains[di] = dnsextra.MakeFullyQualified(domain) + testDomains[di] = makeFullyQualified(domain) } dnsRT, err := f.findDNS(testDomains, parsedConfig.DNS) @@ -601,7 +611,8 @@ func (f *StrategyFinder) NewDialer(ctx context.Context, testDomains []string, co } dnsDialer = f.StreamDialer } else { - dnsDialer = dnsextra.NewStreamDialer(dnsRT, f.StreamDialer) + dnsRT = dnsextra.NewCacheResolver(dnsRT, 100) + dnsDialer = newResolverStreamDialer(dnsRT, f.StreamDialer) } if len(parsedConfig.TLS) == 0 { From 0cfa0b0482189a331ea3d65a51cb126f7f4487d0 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 12 Feb 2024 16:31:30 -0500 Subject: [PATCH 07/15] Clean resolver dialer --- x/smart/resolver_dialer.go | 80 +++++++++++++++----------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/x/smart/resolver_dialer.go b/x/smart/resolver_dialer.go index d496ce0c..14e528ed 100644 --- a/x/smart/resolver_dialer.go +++ b/x/smart/resolver_dialer.go @@ -25,61 +25,45 @@ import ( "golang.org/x/net/dns/dnsmessage" ) +func resolveIP(ctx context.Context, resolver dns.Resolver, rrType dnsmessage.Type, hostname string) ([]netip.Addr, error) { + ips := []netip.Addr{} + q, err := dns.NewQuestion(hostname, rrType) + if err != nil { + return nil, err + } + response, err := resolver.Query(ctx, *q) + if err != nil { + return nil, err + } + if response.RCode != dnsmessage.RCodeSuccess { + return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) + } + for _, answer := range response.Answers { + if answer.Header.Type != rrType { + continue + } + if rr, ok := answer.Body.(*dnsmessage.AResource); ok { + ips = append(ips, netip.AddrFrom4(rr.A)) + } + if rr, ok := answer.Body.(*dnsmessage.AAAAResource); ok { + ips = append(ips, netip.AddrFrom16(rr.AAAA)) + } + } + if len(ips) == 0 { + return nil, errors.New("no ips found") + } + return ips, nil +} + func newResolverStreamDialer(resolver dns.Resolver, dialer transport.StreamDialer) transport.StreamDialer { return &transport.HappyEyeballsStreamDialer{ Dialer: dialer, Resolve: transport.NewDualStackHappyEyeballsResolveFunc( func(ctx context.Context, hostname string) ([]netip.Addr, error) { - ips := []netip.Addr{} - q, err := dns.NewQuestion(hostname, dnsmessage.TypeA) - if err != nil { - return nil, err - } - response, err := resolver.Query(ctx, *q) - if err != nil { - return nil, err - } - if response.RCode != dnsmessage.RCodeSuccess { - return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) - } - for _, answer := range response.Answers { - if answer.Header.Type != dnsmessage.TypeA { - continue - } - if rr, ok := answer.Body.(*dnsmessage.AResource); ok { - ips = append(ips, netip.AddrFrom4(rr.A)) - } - } - if len(ips) == 0 { - return nil, errors.New("no ips found") - } - return ips, nil + return resolveIP(ctx, resolver, dnsmessage.TypeAAAA, hostname) }, func(ctx context.Context, hostname string) ([]netip.Addr, error) { - ips := []netip.Addr{} - q, err := dns.NewQuestion(hostname, dnsmessage.TypeAAAA) - if err != nil { - return nil, err - } - response, err := resolver.Query(ctx, *q) - if err != nil { - return nil, err - } - if response.RCode != dnsmessage.RCodeSuccess { - return nil, fmt.Errorf("got %v (%d)", response.RCode.String(), response.RCode) - } - for _, answer := range response.Answers { - if answer.Header.Type != dnsmessage.TypeAAAA { - continue - } - if rr, ok := answer.Body.(*dnsmessage.AAAAResource); ok { - ips = append(ips, netip.AddrFrom16(rr.AAAA)) - } - } - if len(ips) == 0 { - return nil, errors.New("no ips found") - } - return ips, nil + return resolveIP(ctx, resolver, dnsmessage.TypeA, hostname) }, ), } From 79106c9ebe0f045a00323d406a23771c34bc70f1 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 12 Feb 2024 16:47:21 -0500 Subject: [PATCH 08/15] Handle IPv6 properly with ss --- x/examples/smart-proxy/main.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/x/examples/smart-proxy/main.go b/x/examples/smart-proxy/main.go index cc078303..6db27d1c 100644 --- a/x/examples/smart-proxy/main.go +++ b/x/examples/smart-proxy/main.go @@ -24,6 +24,7 @@ import ( "net/http" "os" "os/signal" + "strings" "time" "github.com/Jigsaw-Code/outline-sdk/transport" @@ -79,7 +80,20 @@ func main() { if err != nil { log.Fatalf("Could not create stream dialer: %v", err) } - + if strings.HasPrefix(*transportFlag, "ss:") { + innerDialer := streamDialer + // Hack to disable IPv6 with Shadowsocks, since it doesn't communicate connection success. + streamDialer = transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { + return nil, fmt.Errorf("IPv6 not supported") + } + return innerDialer.DialStream(ctx, addr) + }) + } finder := smart.StrategyFinder{ LogWriter: debugLog.Writer(), TestTimeout: 5 * time.Second, From f7e14d83b598656d9598eb0d06453806e86ec87c Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 15:50:19 -0500 Subject: [PATCH 09/15] Add .gitignore --- x/mobileproxy/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 x/mobileproxy/.gitignore diff --git a/x/mobileproxy/.gitignore b/x/mobileproxy/.gitignore new file mode 100644 index 00000000..e2e7327c --- /dev/null +++ b/x/mobileproxy/.gitignore @@ -0,0 +1 @@ +/out From fb5f1bbaafc18addd95fc719fd35dc6ec8e5dfc3 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 15:50:27 -0500 Subject: [PATCH 10/15] Clean up tools.go --- x/mobileproxy/tools.go | 1 - 1 file changed, 1 deletion(-) diff --git a/x/mobileproxy/tools.go b/x/mobileproxy/tools.go index 4db3a2d0..0ac21c6f 100644 --- a/x/mobileproxy/tools.go +++ b/x/mobileproxy/tools.go @@ -21,7 +21,6 @@ package tools import ( - _ "github.com/Jigsaw-Code/outline-sdk/x/mobileproxy" _ "golang.org/x/mobile/cmd/gobind" _ "golang.org/x/mobile/cmd/gomobile" ) From 6e9d553f3dcd75132395a4419428d3c4e7639f73 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 18:34:43 -0500 Subject: [PATCH 11/15] Build Mobileproxy with Psiphon --- x/go.mod | 25 +- x/go.sum | 68 +++- x/mobileproxy/.gitignore | 1 + x/mobileproxy/README.md | 20 +- x/mobileproxy/Taskfile.yml | 65 ++++ x/mobileproxy/mobileproxy+psiphon/go.mod | 104 ++++++ x/mobileproxy/mobileproxy+psiphon/go.sum | 349 +++++++++++++++++++++ x/mobileproxy/mobileproxy+psiphon/tools.go | 26 ++ x/mobileproxy/tools.go | 1 + 9 files changed, 637 insertions(+), 22 deletions(-) create mode 100644 x/mobileproxy/Taskfile.yml create mode 100644 x/mobileproxy/mobileproxy+psiphon/go.mod create mode 100644 x/mobileproxy/mobileproxy+psiphon/go.sum create mode 100644 x/mobileproxy/mobileproxy+psiphon/tools.go diff --git a/x/go.mod b/x/go.mod index 85329027..f22da790 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,24 +3,41 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.20 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5 + github.com/Jigsaw-Code/outline-sdk v0.0.11 + github.com/go-task/task/v3 v3.34.1 github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/vishvananda/netlink v1.1.0 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a golang.org/x/net v0.19.0 - golang.org/x/sys v0.15.0 + golang.org/x/sys v0.16.0 ) require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/eycorsican/go-tun2socks v1.16.11 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-zglob v0.0.4 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/radovskyb/watcher v1.0.7 // indirect + github.com/sajari/fuzzy v1.0.0 // indirect github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/tools v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + mvdan.cc/sh/v3 v3.7.0 // indirect ) diff --git a/x/go.sum b/x/go.sum index ed5915f8..2f7f5b1c 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,36 +1,67 @@ -github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5 h1:ylGTGMEJGgQuaZtSB559oZmm5hAMIWEmNq3bUTa71rI= -github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240210173357-34209a9ed0f5/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Jigsaw-Code/outline-sdk v0.0.11 h1:dJ2QMQJJmQ1J4/XvJ9lWpdhg40SKNBmmKffV5wyL00I= +github.com/Jigsaw-Code/outline-sdk v0.0.11/go.mod h1:m+KaWzU05VOIdAC0MVnV0HwyzdzH4WIJ8w7eaMrPp70= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-task/task/v3 v3.34.1 h1:yAAxUM54zoaHv+OtDnGgkWSVeiRuaOCn1lPUXPQQA0o= +github.com/go-task/task/v3 v3.34.1/go.mod h1:DqrukYghah7qNmILi0Z4OwPujsJ7crUkDJZKLTsceX0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= +github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 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/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= +github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b h1:+y4hCMc/WKsDbAPsOQZgBSaSZ26uh2afyaWeVg/3s/c= 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= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= @@ -39,19 +70,24 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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/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= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/x/mobileproxy/.gitignore b/x/mobileproxy/.gitignore index e2e7327c..61849141 100644 --- a/x/mobileproxy/.gitignore +++ b/x/mobileproxy/.gitignore @@ -1 +1,2 @@ /out +/.task diff --git a/x/mobileproxy/README.md b/x/mobileproxy/README.md index 31cb82e2..aa3e95a6 100644 --- a/x/mobileproxy/README.md +++ b/x/mobileproxy/README.md @@ -6,13 +6,13 @@ This package enables the use Go Mobile to generate a mobile library to run a loc From the `x/` directory: -```bash +```sh go build -o "$(pwd)/out/" golang.org/x/mobile/cmd/gomobile golang.org/x/mobile/cmd/gobind ``` ## Build the iOS and Android libraries with [`gomobile bind`](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile#hdr-Build_a_library_for_Android_and_iOS) -```bash +```sh PATH="$(pwd)/out:$PATH" gomobile bind -ldflags='-s -w' -target=ios -iosversion=11.0 -o "$(pwd)/out/mobileproxy.xcframework" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy PATH="$(pwd)/out:$PATH" gomobile bind -ldflags='-s -w' -target=android -androidapi=21 -o "$(pwd)/out/mobileproxy.aar" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy ``` @@ -379,6 +379,22 @@ On Android, you will likely have to implement [WebViewClient.shouldInterceptRequ On iOS, we are still looking for ideas. There's [WKWebViewConfiguration.setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler), but the documentation says it can't be used to intercept HTTPS. If you know how to use a proxy with the WKWebView, please let us know! +## Build the Mobileproxy with Psiphon included + +Some developers want to have both the Outline SDK and the Psiphon library in the same +app. Because they both use Go Mobile and define shared symbols, they must be built +together. + +``` +git clone --filter=blob:none git@github.com:Jigsaw-Code/outline-sdk +cd outline-sdk/x/mobileproxy +go run github.com/go-task/task/v3/cmd/task -p -v ios+psiphon android+psiphon +``` + +If you are using a specific branch of the Outline SDK, make sure to call `git checkout ` after `cd mobileproxy+psiphon`. + +The built libraries will be at `out/mobileproxy+psiphon.xcframework` and `out/mobileproxy+psiphon.aar`. + ## Clean up ```bash diff --git a/x/mobileproxy/Taskfile.yml b/x/mobileproxy/Taskfile.yml new file mode 100644 index 00000000..14d1b48c --- /dev/null +++ b/x/mobileproxy/Taskfile.yml @@ -0,0 +1,65 @@ +version: '3' + +run: when_changed + +vars: + OUT_DIR: "{{.USER_WORKING_DIR}}/out" + +tasks: + gomobile: + cmds: + - mkdir -p "{{.OUT_DIR}}" + - go build -o "{{.OUT_DIR}}" golang.org/x/mobile/cmd/gomobile golang.org/x/mobile/cmd/gobind + sources: ["go.mod"] + generates: ["out/gomobile", "out/gobind"] + + ios: + desc: "Builds the iOS mobileproxy.xcframework library" + deps: [gomobile] + cmds: + - |- + PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ + -ldflags='-s -w' \ + -target=ios -iosversion=11.0 \ + -o "{{.OUT_DIR}}/mobileproxy.xcframework" \ + github.com/Jigsaw-Code/outline-sdk/x/mobileproxy + + ios+psiphon: + desc: "Builds the iOS mobileproxy.xcframework library with Psiphon" + deps: [gomobile] + dir: "mobileproxy+psiphon" + cmds: + - |- + PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ + -tags PSIPHON_DISABLE_QUIC -ldflags='-s -w' \ + -target=ios -iosversion=11.0 \ + -o "{{.OUT_DIR}}/mobileproxy+psiphon.xcframework" \ + github.com/Jigsaw-Code/outline-sdk/x/mobileproxy github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi + + android: + desc: "Builds the Android mobileproxy.aar library" + deps: [gomobile] + cmds: + - |- + PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ + -ldflags='-s -w' \ + -target=android -androidapi=21 \ + -o "{{.OUT_DIR}}/mobileproxy.aar" \ + github.com/Jigsaw-Code/outline-sdk/x/mobileproxy + + android+psiphon: + desc: "Builds the Android mobileproxy.aar library with Psiphon" + deps: [gomobile] + dir: "mobileproxy+psiphon" + cmds: + - |- + PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ + -tags PSIPHON_DISABLE_QUIC -ldflags='-s -w' \ + -target=android -androidapi=21 \ + -o "{{.OUT_DIR}}/mobileproxy+psiphon.aar" \ + github.com/Jigsaw-Code/outline-sdk/x/mobileproxy github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi + + clean: + desc: "Cleans output directory" + cmds: + - rm -rf {{.OUT_DIR}} diff --git a/x/mobileproxy/mobileproxy+psiphon/go.mod b/x/mobileproxy/mobileproxy+psiphon/go.mod new file mode 100644 index 00000000..47d803d6 --- /dev/null +++ b/x/mobileproxy/mobileproxy+psiphon/go.mod @@ -0,0 +1,104 @@ +// Fake module, just to pin the dependencies. +module mobileproxy.local + +go 1.20 + +// Use checked out code. +replace github.com/Jigsaw-Code/outline-sdk/x => ../.. + +require ( + github.com/Jigsaw-Code/outline-sdk/x v0.0.0-00010101000000-000000000000 + github.com/Psiphon-Labs/psiphon-tunnel-core v0.0.14-beta-ios.0.20240130163824-f406d7f78492 +) + +require ( + filippo.io/bigmod v0.0.1 // indirect + filippo.io/keygen v0.0.0-20230306160926-5201437acf8e // indirect + github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/Jigsaw-Code/outline-sdk v0.0.11 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 // indirect + github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464 // indirect + github.com/Psiphon-Labs/qtls-go1-19 v0.0.0-20230608213623-d58aa73e519a // indirect + github.com/Psiphon-Labs/qtls-go1-20 v0.0.0-20230608214729-dd57d6787acf // indirect + github.com/Psiphon-Labs/quic-go v0.0.0-20230626192210-73f29effc9da // indirect + github.com/Psiphon-Labs/tls-tris v0.0.0-20230824155421-58bf6d336a9a // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/armon/go-proxyproto v0.1.0 // indirect + github.com/bifurcation/mint v0.0.0-20210616192047-fd18df995463 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/cognusion/go-cache-lru v0.0.0-20170419142635-f73e2280ecea // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/dgraph-io/badger v1.6.2 // indirect + github.com/dgraph-io/ristretto v0.0.2 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 // indirect + github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/florianl/go-nfqueue v1.3.1 // indirect + github.com/flynn/noise v1.0.0 // indirect + github.com/gaukas/godicttls v0.0.4 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-task/task/v3 v3.34.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/grafov/m3u8 v0.12.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/juju/ratelimit v1.0.2 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-zglob v0.0.4 // indirect + github.com/miekg/dns v1.1.58 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mroth/weightedrand v1.0.0 // indirect + github.com/onsi/ginkgo/v2 v2.15.0 // indirect + github.com/onsi/gomega v1.31.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/sctp v1.8.8 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/transport/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/radovskyb/watcher v1.0.7 // indirect + github.com/refraction-networking/conjure v0.7.11-0.20240130155008-c8df96195ab2 // indirect + github.com/refraction-networking/ed25519 v0.1.2 // indirect + github.com/refraction-networking/gotapdance v1.7.10 // indirect + github.com/refraction-networking/obfs4 v0.1.2 // indirect + github.com/refraction-networking/utls v1.3.3 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sajari/fuzzy v1.0.0 // indirect + github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 // indirect + github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.5.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mobile v0.0.0-20240213143359-d1f7d3436075 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + mvdan.cc/sh/v3 v3.7.0 // indirect +) diff --git a/x/mobileproxy/mobileproxy+psiphon/go.sum b/x/mobileproxy/mobileproxy+psiphon/go.sum new file mode 100644 index 00000000..b5c0049f --- /dev/null +++ b/x/mobileproxy/mobileproxy+psiphon/go.sum @@ -0,0 +1,349 @@ +filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= +filippo.io/bigmod v0.0.1/go.mod h1:KyzqAbH7bRH6MOuOF1TPfUjvLoi0mRF2bIyD2ouRNQI= +filippo.io/keygen v0.0.0-20230306160926-5201437acf8e h1:+xwUCyMiCWKWsI0RowhzB4sngpUdMHgU6lLuWJCX5Dg= +filippo.io/keygen v0.0.0-20230306160926-5201437acf8e/go.mod h1:ZGSiF/b2hd6MRghF/cid0vXw8pXykRTmIu+JSPw/NCQ= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/Jigsaw-Code/outline-sdk v0.0.11 h1:dJ2QMQJJmQ1J4/XvJ9lWpdhg40SKNBmmKffV5wyL00I= +github.com/Jigsaw-Code/outline-sdk v0.0.11/go.mod h1:m+KaWzU05VOIdAC0MVnV0HwyzdzH4WIJ8w7eaMrPp70= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk= +github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 h1:Hx/NCZTnvoKZuIBwSmxE58KKoNLXIGG6hBJYN7pj9Ag= +github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7/go.mod h1:alTtZBo3j4AWFvUrAH6F5ZaHcTj4G5Y01nHz8dkU6vU= +github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464 h1:VmnMMMheFXwLV0noxYhbJbLmkV4iaVW3xNnj6xcCNHo= +github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464/go.mod h1:Pe5BqN2DdIdChorAXl6bDaQd/wghpCleJfid2NoSli0= +github.com/Psiphon-Labs/psiphon-tunnel-core v0.0.14-beta-ios.0.20240130163824-f406d7f78492 h1:za/5Tcl2CBMAWGdFDjporDidLmhdb3rfypYRNn7jit0= +github.com/Psiphon-Labs/psiphon-tunnel-core v0.0.14-beta-ios.0.20240130163824-f406d7f78492/go.mod h1:p9U9GHpMgLoCGmZqzEJmcCSH9LwKnUT3xXudqKHv1Xo= +github.com/Psiphon-Labs/qtls-go1-19 v0.0.0-20230608213623-d58aa73e519a h1:O8D+GcEoZwutcERaABP2AM3RDvswBVtNmBWvlBn5wiw= +github.com/Psiphon-Labs/qtls-go1-19 v0.0.0-20230608213623-d58aa73e519a/go.mod h1:81bbD3bvEvi3BSamZb30PgvPvqwSLfEPqwwmq5sx7fc= +github.com/Psiphon-Labs/qtls-go1-20 v0.0.0-20230608214729-dd57d6787acf h1:bGS+WxWdHHuf42hn3M1GFSJbzCgtKNVTuiRqwCo3zyc= +github.com/Psiphon-Labs/qtls-go1-20 v0.0.0-20230608214729-dd57d6787acf/go.mod h1:wUiSd0qyefymNlikc99B2rRC01YPN1uUvDMytMOGmF8= +github.com/Psiphon-Labs/quic-go v0.0.0-20230626192210-73f29effc9da h1:TI2+ExyFR3A0kPrFHfaM6y3RybP0HGfP9N1R8hfZzfk= +github.com/Psiphon-Labs/quic-go v0.0.0-20230626192210-73f29effc9da/go.mod h1:wTIxqsKVrEQIxVIIYOEHuscY+PM3h6Wz79u5aF60fo0= +github.com/Psiphon-Labs/tls-tris v0.0.0-20230824155421-58bf6d336a9a h1:BOfU6ghaMsT/c40sWHmf3PXNwIendYXzL6tRv6NbPog= +github.com/Psiphon-Labs/tls-tris v0.0.0-20230824155421-58bf6d336a9a/go.mod h1:v3y9GXFo9Sf2mO6auD2ExGG7oDgrK8TI7eb49ZnUxrE= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-proxyproto v0.1.0 h1:TWWcSsjco7o2itn6r25/5AqKBiWmsiuzsUDLT/MTl7k= +github.com/armon/go-proxyproto v0.1.0/go.mod h1:Xj90dce2VKbHzRAeiVQAMBtj4M5oidoXJ8lmgyW21mw= +github.com/bifurcation/mint v0.0.0-20210616192047-fd18df995463 h1:NuDsFBmluKEXOfDKFZbDyzb6ey9gGTsbCyvj8oveLA8= +github.com/bifurcation/mint v0.0.0-20210616192047-fd18df995463/go.mod h1:7hjOhy2FAdPeu39W6XOfgCq45As0yHINm9k3dGIHttA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cognusion/go-cache-lru v0.0.0-20170419142635-f73e2280ecea h1:9C2rdYRp8Vzwhm3sbFX0yYfB+70zKFRjn7cnPCucHSw= +github.com/cognusion/go-cache-lru v0.0.0-20170419142635-f73e2280ecea/go.mod h1:MdyNkAe06D7xmJsf+MsLvbZKYNXuOHLKJrvw+x4LlcQ= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5 h1:iGoePcl8bIDJxxRAL2Q4E4Rt35z5m917RJb8lAvdrQw= +github.com/elazarl/goproxy/ext v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/florianl/go-nfqueue v1.3.1 h1:khQ9fYCrjbu5CF8dZF55G2RTIEIQRI0Aj5k3msJR6Gw= +github.com/florianl/go-nfqueue v1.3.1/go.mod h1:aHWbgkhryJxF5XxYvJ3oRZpdD4JP74Zu/hP1zuhja+M= +github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= +github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-task/task/v3 v3.34.1 h1:yAAxUM54zoaHv+OtDnGgkWSVeiRuaOCn1lPUXPQQA0o= +github.com/go-task/task/v3 v3.34.1/go.mod h1:DqrukYghah7qNmILi0Z4OwPujsJ7crUkDJZKLTsceX0= +github.com/gobwas/glob v0.2.4-0.20180402141543-f00a7392b439 h1:T6zlOdzrYuHf6HUKujm9bzkzbZ5Iv/xf6rs8BHZDpoI= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4= +github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= +github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2 h1:sq+a5mb8zHbmHhrIH06oqIMGsanjpbxNgxEgZVfgpvQ= +github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2/go.mod h1:TmeOqAKoDinfPfSohs14CO3VcEf7o+Bem6JiNe05yrQ= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= +github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= +github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= +github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= +github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI= +github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mroth/weightedrand v1.0.0 h1:V8JeHChvl2MP1sAoXq4brElOcza+jxLkRuwvtQu8L3E= +github.com/mroth/weightedrand v1.0.0/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= +github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/pebbe/zmq4 v1.2.10 h1:wQkqRZ3CZeABIeidr3e8uQZMMH5YAykA/WN0L5zkd1c= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U= +github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA= +github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/refraction-networking/conjure v0.7.11-0.20240130155008-c8df96195ab2 h1:m2ZH6WV69otVmBpWbk8et3MypHFsjcYXTNrknQKS/PY= +github.com/refraction-networking/conjure v0.7.11-0.20240130155008-c8df96195ab2/go.mod h1:7KuAtYfSL0K0WpCScjN9YKiOZ4AQ/8IzSjUtVwWbSv8= +github.com/refraction-networking/ed25519 v0.1.2 h1:08kJZUkAlY7a7cZGosl1teGytV+QEoNxPO7NnRvAB+g= +github.com/refraction-networking/ed25519 v0.1.2/go.mod h1:nxYLUAYt/hmNpAh64PNSQ/tQ9gTIB89wCaGKJlRtZ9I= +github.com/refraction-networking/gotapdance v1.7.10 h1:vPtvuihP95SqrnnpX//KI1PTqrXCvNnOQslrG4gxsRY= +github.com/refraction-networking/gotapdance v1.7.10/go.mod h1:N7Xmt+/bLv+1VctiBHtsaL6YBknW2ox5LRLzPTISMzY= +github.com/refraction-networking/obfs4 v0.1.2 h1:J842O4fGSkd2W8ogYj0KN6gqVVY+Cpqodw9qFGL7wVU= +github.com/refraction-networking/obfs4 v0.1.2/go.mod h1:wAl/+gWiLsrcykJA3nKJHx89f5/gXGM8UKvty7+mvbM= +github.com/refraction-networking/utls v1.3.3 h1:f/TBLX7KBciRyFH3bwupp+CE4fzoYKCirhdRcC490sw= +github.com/refraction-networking/utls v1.3.3/go.mod h1:DlecWW1LMlMJu+9qpzzQqdHDT/C2LAe03EdpLUz/RL8= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= +github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= +github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 h1:ML7ZNtcln5UBo5Wv7RIv9Xg3Pr5VuRCWLFXEwda54Y4= +github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507/go.mod h1:DbI1gxrXI2jRGw7XGEUZQOOMd6PsnKzRrCKabvvMrwM= +github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= +github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 h1:9sreu9e9KOihf2Y0NbpyfWhd1XFDcL4GTkPYL4IvMrg= +github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78/go.mod h1:HazXTRLhXFyq80TQp7PUXi6BKE6mS+ydEdzEqNBKopQ= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.5.0 h1:rzdY78Ox2T+VlXcxGxELF+6VyUXlZBhmRqZu5etLm+c= +gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/goptlib v1.5.0/go.mod h1:70bhd4JKW/+1HLfm+TMrgHJsUHG4coelMWwiVEJ2gAg= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mobile v0.0.0-20240213143359-d1f7d3436075 h1:iZzqyDd8gFkJZpsJNzveyScRBcQlsveheh6Q77LzhPY= +golang.org/x/mobile v0.0.0-20240213143359-d1f7d3436075/go.mod h1:Y8Bnziw2dX69ZhYuqQB8Ihyjks1Q6fMmbg17j9+ISNA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/x/mobileproxy/mobileproxy+psiphon/tools.go b/x/mobileproxy/mobileproxy+psiphon/tools.go new file mode 100644 index 00000000..42ab0033 --- /dev/null +++ b/x/mobileproxy/mobileproxy+psiphon/tools.go @@ -0,0 +1,26 @@ +// 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. + +//go:build tools +// +build tools + +// See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module +// and https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md + +package tools + +import ( + _ "github.com/Jigsaw-Code/outline-sdk/x/mobileproxy" + _ "github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi" +) diff --git a/x/mobileproxy/tools.go b/x/mobileproxy/tools.go index 0ac21c6f..f0e6cfc4 100644 --- a/x/mobileproxy/tools.go +++ b/x/mobileproxy/tools.go @@ -21,6 +21,7 @@ package tools import ( + _ "github.com/go-task/task/v3/cmd/task" _ "golang.org/x/mobile/cmd/gobind" _ "golang.org/x/mobile/cmd/gomobile" ) From 4a1e96dd0f20ec249050190ec2735ab064b8d69a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 18:38:34 -0500 Subject: [PATCH 12/15] Fix --- x/go.mod | 2 +- x/go.sum | 4 ++-- x/smart/resolver_dialer.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x/go.mod b/x/go.mod index f22da790..b471bfa7 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.20 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.11 + github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5 github.com/go-task/task/v3 v3.34.1 github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b github.com/stretchr/testify v1.8.4 diff --git a/x/go.sum b/x/go.sum index 2f7f5b1c..a8369172 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,5 +1,5 @@ -github.com/Jigsaw-Code/outline-sdk v0.0.11 h1:dJ2QMQJJmQ1J4/XvJ9lWpdhg40SKNBmmKffV5wyL00I= -github.com/Jigsaw-Code/outline-sdk v0.0.11/go.mod h1:m+KaWzU05VOIdAC0MVnV0HwyzdzH4WIJ8w7eaMrPp70= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5 h1:+W2bHCUm9TRJ44Bq0jeFm3DD4UGYFEvGEdcmQdykHi8= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= diff --git a/x/smart/resolver_dialer.go b/x/smart/resolver_dialer.go index 14e528ed..e928848a 100644 --- a/x/smart/resolver_dialer.go +++ b/x/smart/resolver_dialer.go @@ -58,7 +58,7 @@ func resolveIP(ctx context.Context, resolver dns.Resolver, rrType dnsmessage.Typ func newResolverStreamDialer(resolver dns.Resolver, dialer transport.StreamDialer) transport.StreamDialer { return &transport.HappyEyeballsStreamDialer{ Dialer: dialer, - Resolve: transport.NewDualStackHappyEyeballsResolveFunc( + Resolve: transport.NewParallelHappyEyeballsResolveFunc( func(ctx context.Context, hostname string) ([]netip.Addr, error) { return resolveIP(ctx, resolver, dnsmessage.TypeAAAA, hostname) }, From cd30fe3409127501e2836907c8343d7cdc644850 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 18:41:21 -0500 Subject: [PATCH 13/15] Add checkout --- x/mobileproxy/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x/mobileproxy/README.md b/x/mobileproxy/README.md index aa3e95a6..81f02343 100644 --- a/x/mobileproxy/README.md +++ b/x/mobileproxy/README.md @@ -388,10 +388,11 @@ together. ``` git clone --filter=blob:none git@github.com:Jigsaw-Code/outline-sdk cd outline-sdk/x/mobileproxy +git checkout fortuna-dns go run github.com/go-task/task/v3/cmd/task -p -v ios+psiphon android+psiphon ``` -If you are using a specific branch of the Outline SDK, make sure to call `git checkout ` after `cd mobileproxy+psiphon`. +If you are using a different branch of the Outline SDK, make sure to change the `git checkout ` as needed. The built libraries will be at `out/mobileproxy+psiphon.xcframework` and `out/mobileproxy+psiphon.aar`. From ffb803436a096c3ff7a198794ea4551147252e97 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 19:04:18 -0500 Subject: [PATCH 14/15] Tweaks --- x/mobileproxy/README.md | 2 ++ x/mobileproxy/Taskfile.yml | 17 +++++++++++++++-- x/mobileproxy/mobileproxy+psiphon/go.mod | 2 +- x/mobileproxy/mobileproxy+psiphon/go.sum | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/x/mobileproxy/README.md b/x/mobileproxy/README.md index 81f02343..f5899b05 100644 --- a/x/mobileproxy/README.md +++ b/x/mobileproxy/README.md @@ -396,6 +396,8 @@ If you are using a different branch of the Outline SDK, make sure to change the The built libraries will be at `out/mobileproxy+psiphon.xcframework` and `out/mobileproxy+psiphon.aar`. +Keep in mind that this build doesn't include Psiphon's convenience [Java](https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java) and [Objective-C](https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h) wrappers. You will need to call the Go Mobile-generated code based on their [Go API](https://pkg.go.dev/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi). + ## Clean up ```bash diff --git a/x/mobileproxy/Taskfile.yml b/x/mobileproxy/Taskfile.yml index 14d1b48c..f9891fa3 100644 --- a/x/mobileproxy/Taskfile.yml +++ b/x/mobileproxy/Taskfile.yml @@ -31,7 +31,13 @@ tasks: cmds: - |- PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ - -tags PSIPHON_DISABLE_QUIC -ldflags='-s -w' \ + -tags PSIPHON_DISABLE_QUIC \ + -ldflags="-s -w + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildRepo=github.com/Jigsaw-Code/outline-sdk + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildDate={{now | date "2006-01-02T15:04:05-07:00"}} + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildRev=$(git rev-parse --short HEAD) + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.goVersion=$(go version | cut -d " " -f 3) + " \ -target=ios -iosversion=11.0 \ -o "{{.OUT_DIR}}/mobileproxy+psiphon.xcframework" \ github.com/Jigsaw-Code/outline-sdk/x/mobileproxy github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi @@ -49,12 +55,19 @@ tasks: android+psiphon: desc: "Builds the Android mobileproxy.aar library with Psiphon" + # See https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/MobileLibrary/Android/make.bash deps: [gomobile] dir: "mobileproxy+psiphon" cmds: - |- PATH="{{.OUT_DIR}}:$PATH" gomobile bind \ - -tags PSIPHON_DISABLE_QUIC -ldflags='-s -w' \ + -tags PSIPHON_DISABLE_QUIC \ + -ldflags="-s -w + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildRepo=github.com/Jigsaw-Code/outline-sdk + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildDate={{now | date "2006-01-02T15:04:05-07:00"}} + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.buildRev=$(git rev-parse --short HEAD) + -X github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo.goVersion=$(go version | cut -d " " -f 3) + " \ -target=android -androidapi=21 \ -o "{{.OUT_DIR}}/mobileproxy+psiphon.aar" \ github.com/Jigsaw-Code/outline-sdk/x/mobileproxy github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi diff --git a/x/mobileproxy/mobileproxy+psiphon/go.mod b/x/mobileproxy/mobileproxy+psiphon/go.mod index 47d803d6..67cc7170 100644 --- a/x/mobileproxy/mobileproxy+psiphon/go.mod +++ b/x/mobileproxy/mobileproxy+psiphon/go.mod @@ -15,7 +15,7 @@ require ( filippo.io/bigmod v0.0.1 // indirect filippo.io/keygen v0.0.0-20230306160926-5201437acf8e // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect - github.com/Jigsaw-Code/outline-sdk v0.0.11 // indirect + github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 // indirect github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464 // indirect diff --git a/x/mobileproxy/mobileproxy+psiphon/go.sum b/x/mobileproxy/mobileproxy+psiphon/go.sum index b5c0049f..cf9e9bde 100644 --- a/x/mobileproxy/mobileproxy+psiphon/go.sum +++ b/x/mobileproxy/mobileproxy+psiphon/go.sum @@ -6,8 +6,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/Jigsaw-Code/outline-sdk v0.0.11 h1:dJ2QMQJJmQ1J4/XvJ9lWpdhg40SKNBmmKffV5wyL00I= -github.com/Jigsaw-Code/outline-sdk v0.0.11/go.mod h1:m+KaWzU05VOIdAC0MVnV0HwyzdzH4WIJ8w7eaMrPp70= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5 h1:+W2bHCUm9TRJ44Bq0jeFm3DD4UGYFEvGEdcmQdykHi8= +github.com/Jigsaw-Code/outline-sdk v0.0.13-0.20240212211908-de32e39b12e5/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= From 9c245daebb5a03eae21edcf4dcf470fbf1f98258 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 13 Feb 2024 19:14:10 -0500 Subject: [PATCH 15/15] Add comment --- x/mobileproxy/Taskfile.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/x/mobileproxy/Taskfile.yml b/x/mobileproxy/Taskfile.yml index f9891fa3..535f913f 100644 --- a/x/mobileproxy/Taskfile.yml +++ b/x/mobileproxy/Taskfile.yml @@ -26,6 +26,7 @@ tasks: ios+psiphon: desc: "Builds the iOS mobileproxy.xcframework library with Psiphon" + # See https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/MobileLibrary/iOS/build-psiphon-framework.sh deps: [gomobile] dir: "mobileproxy+psiphon" cmds: