diff --git a/x/examples/fetch-psiphon/README.md b/x/examples/fetch-psiphon/README.md index 098b3d22..6865c1e6 100644 --- a/x/examples/fetch-psiphon/README.md +++ b/x/examples/fetch-psiphon/README.md @@ -5,7 +5,7 @@ This fetch tool illustrates how to use Psiphon as a stream dialer. Usage: ```sh -go run github.com/Jigsaw-Code/outline-sdk/x/examples/fetch-psiphon@latest -config config.json https://ipinfo.io +go run -tags psiphon github.com/Jigsaw-Code/outline-sdk/x/examples/fetch-psiphon@latest -config config.json https://ipinfo.io ``` You will need a config file of a Psiphon server. You can run one yourself and generate the config as per the diff --git a/x/examples/smart-proxy/config_broken.yaml b/x/examples/smart-proxy/config_broken.yaml index 5d2a7da8..d4c8f847 100644 --- a/x/examples/smart-proxy/config_broken.yaml +++ b/x/examples/smart-proxy/config_broken.yaml @@ -23,5 +23,19 @@ tls: - tlsfrag:1 fallback: - # Nonexistent server + # Nonexistent Outline Server - ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTprSzdEdHQ0MkJLOE9hRjBKYjdpWGFK@1.2.3.4:9999/?outline=1 + # Nonexistant Psiphon Config JSON. Not yet supported + - psiphon: { + "LocalHttpProxyPort":8081, + "LocalSocksProxyPort":1081, + "PropagationChannelId":"FFFFFFFFFFFFFFFF", + "RemoteServerListDownloadFilename":"remote_server_list", + "RemoteServerListSignaturePublicKey":"MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAt7Ls+/39r+T6zNW7GiVpJfzq/xvL9SBH5rIFnk0RXYEYavax3WS6HOD35eTAqn8AniOwiH+DOkvgSKF2caqk/y1dfq47Pdymtwzp9ikpB1C5OfAysXzBiwVJlCdajBKvBZDerV1cMvRzCKvKwRmvDmHgphQQ7WfXIGbRbmmk6opMBh3roE42KcotLFtqp0RRwLtcBRNtCdsrVsjiI1Lqz/lH+T61sGjSjQ3CHMuZYSQJZo/KrvzgQXpkaCTdbObxHqb6/+i1qaVOfEsvjoiyzTxJADvSytVtcTjijhPEV6XskJVHE1Zgl+7rATr/pDQkw6DPCNBS1+Y6fy7GstZALQXwEDN/qhQI9kWkHijT8ns+i1vGg00Mk/6J75arLhqcodWsdeG/M/moWgqQAnlZAGVtJI1OgeF5fsPpXu4kctOfuZlGjVZXQNW34aOzm8r8S0eVZitPlbhcPiR4gT/aSMz/wd8lZlzZYsje/Jr8u/YtlwjjreZrGRmG8KMOzukV3lLmMppXFMvl4bxv6YFEmIuTsOhbLTwFgh7KYNjodLj/LsqRVfwz31PgWQFTEPICV7GCvgVlPRxnofqKSjgTWI4mxDhBpVcATvaoBl1L/6WLbFvBsoAUBItWwctO2xalKxF5szhGm8lccoc5MZr8kfE0uxMgsxz4er68iCID+rsCAQM=", + "RemoteServerListUrl":"https://s3.amazonaws.com//psiphon/web/mjr4-p23r-puwl/server_list_compressed", + "SponsorId":"FFFFFFFFFFFFFFFF", + "UseIndistinguishableTLS":true + } + # Nonexistant local socks5 proxy + - socks5://192.168.1.10:1080 + diff --git a/x/examples/smart-proxy/main.go b/x/examples/smart-proxy/main.go index 88aaafcd..99c14b06 100644 --- a/x/examples/smart-proxy/main.go +++ b/x/examples/smart-proxy/main.go @@ -127,7 +127,7 @@ func main() { 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) + debugLog.Printf("Failed to dial %v: %v, %w\n", address, dialer, err) } return conn, err }) diff --git a/x/smart/stream_dialer.go b/x/smart/stream_dialer.go index 9b24894a..760df5f9 100644 --- a/x/smart/stream_dialer.go +++ b/x/smart/stream_dialer.go @@ -17,17 +17,21 @@ package smart import ( "context" "crypto/tls" + "encoding/json" "errors" "fmt" "io" "net" "net/url" + "os" + "path" "sync" "time" "github.com/Jigsaw-Code/outline-sdk/dns" "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/x/configurl" + "github.com/Jigsaw-Code/outline-sdk/x/psiphon" "gopkg.in/yaml.v3" ) @@ -94,10 +98,13 @@ type dnsEntryConfig struct { TCP *tcpEntryConfig `yaml:"tcp,omitempty"` } +// can be either a configurl string or {psiphon: psiphonConfig} +type fallbackEntryConfig any + type configConfig struct { - DNS []dnsEntryConfig `yaml:"dns,omitempty"` - TLS []string `yaml:"tls,omitempty"` - FALLBACK []string `yaml:"fallback,omitempty"` + DNS []dnsEntryConfig `yaml:"dns,omitempty"` + TLS []string `yaml:"tls,omitempty"` + Fallback []fallbackEntryConfig `yaml:"fallback,omitempty"` } // newDNSResolverFromEntry creates a [dns.Resolver] based on the config, returning the resolver and @@ -186,8 +193,34 @@ func (f *StrategyFinder) dnsConfigToResolver(dnsConfig []dnsEntryConfig) ([]*sma return rts, nil } +func (f *StrategyFinder) getPsiphonDialer(ctx context.Context, psiphonJSON []byte) (transport.StreamDialer, error) { + config := &psiphon.DialerConfig{ProviderConfig: psiphonJSON} + + cacheBaseDir, err := os.UserCacheDir() + if err != nil { + return nil, fmt.Errorf("Failed to get the user cache directory: %w", err) + } + + config.DataRootDirectory = path.Join(cacheBaseDir, "psiphon") + if err := os.MkdirAll(config.DataRootDirectory, 0700); err != nil { + return nil, fmt.Errorf("Failed to create storage directory: %w", err) + } + f.logCtx(ctx, "Using data store in %v\n", config.DataRootDirectory) + + dialer := psiphon.GetSingletonDialer() + if err := dialer.Start(ctx, config); err != nil { + return nil, fmt.Errorf("failed to start psiphon dialer: %w", err) + } + + return dialer, nil +} + // Test that a dialer is able to access all the given test domains. Returns nil if all tests succeed func (f *StrategyFinder) testDialer(ctx context.Context, dialer transport.StreamDialer, testDomains []string, transportCfg string) error { + f.logCtx(ctx, "------------ \n") + f.logCtx(ctx, "logging dialer: %+v, %v", dialer, testDomains) + f.logCtx(ctx, "------------ \n") + for _, testDomain := range testDomains { startTime := time.Now() @@ -305,8 +338,8 @@ func (f *StrategyFinder) findTLS(ctx context.Context, testDomains []string, base } // Return the fastest fallback dialer that is able to access all the testDomans -func (f *StrategyFinder) findFallback(ctx context.Context, testDomains []string, fallbackConfig []string) (transport.StreamDialer, error) { - if len(fallbackConfig) == 0 { +func (f *StrategyFinder) findFallback(ctx context.Context, testDomains []string, fallbackConfigs []fallbackEntryConfig) (transport.StreamDialer, error) { + if len(fallbackConfigs) == 0 { return nil, errors.New("no fallback was specified") } @@ -315,27 +348,61 @@ func (f *StrategyFinder) findFallback(ctx context.Context, testDomains []string, raceStart := time.Now() type SearchResult struct { Dialer transport.StreamDialer - Config string + Config fallbackEntryConfig } var configModule = configurl.NewDefaultProviders() - fallback, err := raceTests(ctx, 250*time.Millisecond, fallbackConfig, func(fallbackUrl string) (*SearchResult, error) { - dialer, err := configModule.NewStreamDialer(ctx, fallbackUrl) - if err != nil { - return nil, fmt.Errorf("getStreamDialer failed: %w", err) - } + fallback, err := raceTests(ctx, 250*time.Millisecond, fallbackConfigs, func(fallbackConfig fallbackEntryConfig) (*SearchResult, error) { + switch v := fallbackConfig.(type) { + case string: + fallbackUrl := v + dialer, err := configModule.NewStreamDialer(ctx, fallbackUrl) + if err != nil { + return nil, fmt.Errorf("getStreamDialer failed: %w", err) + } - err = f.testDialer(ctx, dialer, testDomains, fallbackUrl) - if err != nil { - return nil, err + err = f.testDialer(ctx, dialer, testDomains, fallbackUrl) + if err != nil { + return nil, err + } + + return &SearchResult{dialer, fallbackConfig}, nil + case map[string]interface{}: + psiphonConfig, ok := v["psiphon"] + if !ok { + return nil, fmt.Errorf("unknown fallback type: %v", v) + } + psiphonJSON, err := json.Marshal(psiphonConfig) + if err != nil { + return nil, fmt.Errorf("Error marshaling to JSON: %v, %v", psiphonConfig, err) + } + + dialer, err := f.getPsiphonDialer(ctx, psiphonJSON) + if err != nil { + return nil, fmt.Errorf("getPsiphonDialer failed: %w", err) + } + + err = f.testDialer(ctx, dialer, testDomains, string(psiphonJSON)) + if err != nil { + f.logCtx(ctx, "error testing dialer: %v, %v", psiphonConfig, err) + return nil, err + } + + f.logCtx(ctx, "returning valid dialer: %+v, %v \n", dialer, psiphonConfig) + return &SearchResult{dialer, string(psiphonJSON)}, nil + + return nil, fmt.Errorf("unknown fallback type: %v", v) + default: + return nil, fmt.Errorf("unknown fallback type: %v", v) } - return &SearchResult{dialer, fallbackUrl}, nil + // If neither, it's an unknown type + return nil, fmt.Errorf("unknown fallback type: %v", fallbackConfig) }) if err != nil { return nil, fmt.Errorf("could not find a working fallback: %w", err) } - f.log("🏆 selected fallback '%v' in %0.2fs\n\n", fallback.Config, time.Since(raceStart).Seconds()) + f.log("🏆 selected fallback '%+v' '%v' in %0.2fs\n\n", fallback.Dialer, fallback.Config, time.Since(raceStart).Seconds()) return fallback.Dialer, nil } @@ -383,7 +450,7 @@ func (f *StrategyFinder) NewDialer(ctx context.Context, testDomains []string, co dialer, err := f.newProxylessDialer(ctx, testDomains, parsedConfig) if err != nil { - return f.findFallback(ctx, testDomains, parsedConfig.FALLBACK) + return f.findFallback(ctx, testDomains, parsedConfig.Fallback) } return dialer, err }