Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x/examples/fetch-psiphon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion x/examples/smart-proxy/config_broken.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion x/examples/smart-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
101 changes: 84 additions & 17 deletions x/smart/stream_dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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")
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}