Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x): Support YAML config in the Smart Dialer #352

Merged
merged 8 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
52 changes: 0 additions & 52 deletions x/examples/smart-proxy/config.json

This file was deleted.

91 changes: 91 additions & 0 deletions x/examples/smart-proxy/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# DNS strategies
dns:
# DNS-over-HTTPS

# Quad9
- https:
name: 2620:fe::fe
- https:
name: 9.9.9.9
# Google
- https:
name: 2001:4860:4860::8888
- https:
name: 8.8.8.8
# Cloudflare
- https:
name: 2606:4700:4700::1111
- https:
name: 1.1.1.1
# Wikimedia DNS
- https:
name: 2001:67c:930::1
- https:
name: 185.71.138.138

# DNS-over-TLS

# Quad9
- tls:
name: 2620:fe::fe
- tls:
name: 9.9.9.9
# Google
- tls:
name: 2001:4860:4860::8888
- tls:
name: 8.8.8.8
# Cloudflare
- tls:
name: 2606:4700:4700::1111
- tls:
name: 1.1.1.1
# Wikimedia DNS
- tls:
name: 2001:67c:930::1
- tls:
name: 185.71.138.138
# Quad9
- tcp:
address: 2620:fe::fe
- tcp:
address: 9.9.9.9

# DNS-over-TCP

# Google
- tcp:
address: 2001:4860:4860::8888
- tcp:
address: 8.8.8.8
# Cloudflare
- tcp:
address: 2606:4700:4700::1111
- tcp:
address: 1.1.1.1

# DNS-over-UDP

# Quad9
- udp:
address: 2620:fe::fe
- udp:
address: 9.9.9.9
# Google
- udp:
address: 2001:4860:4860::8888
- udp:
address: 8.8.8.8
# Cloudflare
- udp:
address: 2606:4700:4700::1111
- udp:
address: 1.1.1.1

# TLS strategies
tls:
- "" # Direct dialer
- split:1 # TCP stream split at position 1
- split:2,20*5 # TCP stream split at position 2, followed by 20 blocks of length 5.
- split:200|disorder:1 # TCP stream split at position 1, and send the second packet (packet #1) with zero TTL at first.
- tlsfrag:1 # TLS Record Fragmentation at position 1
19 changes: 0 additions & 19 deletions x/examples/smart-proxy/config_broken.json

This file was deleted.

29 changes: 29 additions & 0 deletions x/examples/smart-proxy/config_broken.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
dns:
# We get censored DNS responses when we send queries to an IP in China.
- udp:
address: china.cn
# We get censored DNS responses when we send queries to a resolver in Iran.
- udp:
address: ns1.tic.ir
- tcp:
address: ns1.tic.ir
# We get censored DNS responses when we send queries to an IP in Turkmenistan.
- udp:
address: tmcell.tm
# We get censored DNS responses when we send queries to a resolver in Russia.
- udp:
address: dns1.transtelecom.net.
# Testing captive portal.
- tls:
name: captive-portal.badssl.com
address: captive-portal.badssl.com:443
# Testing forged TLS certificate.
- https:
name: mitm-software.badssl.com

tls:
- ""
- split:1
- split:2
- split:5
- tlsfrag:1
2 changes: 1 addition & 1 deletion x/examples/smart-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func supportsHappyEyeballs(dialer transport.StreamDialer) bool {
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")
configFlag := flag.String("config", "config.yaml", "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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ FOUNDATION_EXPORT MobileproxyStringList* _Nullable MobileproxyNewListFromLines(N
that will use the selected strategy.
It uses testDomains to find a strategy that works when accessing those domains.
The strategies to search are given in the searchConfig. An example can be found in
https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.json
https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.yaml
*/
FOUNDATION_EXPORT MobileproxyStreamDialer* _Nullable MobileproxyNewSmartStreamDialer(MobileproxyStringList* _Nullable testDomains, NSString* _Nullable searchConfig, id<MobileproxyLogWriter> _Nullable logWriter, NSError* _Nullable* _Nullable error);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ FOUNDATION_EXPORT MobileproxyStringList* _Nullable MobileproxyNewListFromLines(N
that will use the selected strategy.
It uses testDomains to find a strategy that works when accessing those domains.
The strategies to search are given in the searchConfig. An example can be found in
https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.json
https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.yaml
*/
FOUNDATION_EXPORT MobileproxyStreamDialer* _Nullable MobileproxyNewSmartStreamDialer(MobileproxyStringList* _Nullable testDomains, NSString* _Nullable searchConfig, id<MobileproxyLogWriter> _Nullable logWriter, NSError* _Nullable* _Nullable error);

Expand Down
2 changes: 1 addition & 1 deletion x/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
golang.org/x/net v0.28.0
golang.org/x/sys v0.23.0
golang.org/x/term v0.23.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -79,5 +80,4 @@ require (
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 1 addition & 1 deletion x/mobileproxy/mobileproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func toWriter(logWriter LogWriter) io.Writer {
// that will use the selected strategy.
// It uses testDomains to find a strategy that works when accessing those domains.
// The strategies to search are given in the searchConfig. An example can be found in
// https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.json
// https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.yaml
func NewSmartStreamDialer(testDomains *StringList, searchConfig string, logWriter LogWriter) (*StreamDialer, error) {
logBytesWriter := toWriter(logWriter)
// TODO: inject the base dialer for tests.
Expand Down
52 changes: 26 additions & 26 deletions x/smart/stream_dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package smart
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -29,6 +28,7 @@ import (
"github.com/Jigsaw-Code/outline-sdk/dns"
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/configurl"
"gopkg.in/yaml.v3"
)

// To test one strategy:
Expand Down Expand Up @@ -62,46 +62,46 @@ func (f *StrategyFinder) logCtx(ctx context.Context, format string, a ...any) {
f.log(format, a...)
}

type httpsEntryJSON struct {
type httpsEntryConfig struct {
// Domain name of the host.
Name string `json:"name,omitempty"`
Name string `yaml:"name,omitempty"`
// Host:port. Defaults to Name:443.
Address string `json:"address,omitempty"`
Address string `yaml:"address,omitempty"`
}

type tlsEntryJSON struct {
type tlsEntryConfig struct {
// Domain name of the host.
Name string `json:"name,omitempty"`
Name string `yaml:"name,omitempty"`
// Host:port. Defaults to Name:853.
Address string `json:"address,omitempty"`
Address string `yaml:"address,omitempty"`
}

type udpEntryJSON struct {
type udpEntryConfig struct {
// Host:port.
Address string `json:"address,omitempty"`
Address string `yaml:"address,omitempty"`
}

type tcpEntryJSON struct {
type tcpEntryConfig struct {
// Host:port.
Address string `json:"address,omitempty"`
Address string `yaml:"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 dnsEntryConfig struct {
System *struct{} `yaml:"system,omitempty"`
HTTPS *httpsEntryConfig `yaml:"https,omitempty"`
TLS *tlsEntryConfig `yaml:"tls,omitempty"`
UDP *udpEntryConfig `yaml:"udp,omitempty"`
TCP *tcpEntryConfig `yaml:"tcp,omitempty"`
}

type configJSON struct {
DNS []dnsEntryJSON `json:"dns,omitempty"`
TLS []string `json:"tls,omitempty"`
type configConfig struct {
DNS []dnsEntryConfig `yaml:"dns,omitempty"`
TLS []string `yaml:"tls,omitempty"`
}

// newDNSResolverFromEntry creates a [dns.Resolver] based on the config, returning the resolver and
// a boolean indicating whether the resolver is secure (TLS, HTTPS) and a possible error.
func (f *StrategyFinder) newDNSResolverFromEntry(entry dnsEntryJSON) (dns.Resolver, bool, error) {
func (f *StrategyFinder) newDNSResolverFromEntry(entry dnsEntryConfig) (dns.Resolver, bool, error) {
if entry.System != nil {
return nil, false, nil
} else if cfg := entry.HTTPS; cfg != nil {
Expand Down Expand Up @@ -165,13 +165,13 @@ type smartResolver struct {
Secure bool
}

func (f *StrategyFinder) dnsConfigToResolver(dnsConfig []dnsEntryJSON) ([]*smartResolver, error) {
func (f *StrategyFinder) dnsConfigToResolver(dnsConfig []dnsEntryConfig) ([]*smartResolver, error) {
if len(dnsConfig) == 0 {
return nil, errors.New("no DNS config entry")
}
rts := make([]*smartResolver, 0, len(dnsConfig))
for ei, entry := range dnsConfig {
idBytes, err := json.Marshal(entry)
idBytes, err := yaml.Marshal(entry)
if err != nil {
return nil, fmt.Errorf("cannot serialize entry %v: %w", ei, err)
}
Expand All @@ -185,7 +185,7 @@ func (f *StrategyFinder) dnsConfigToResolver(dnsConfig []dnsEntryJSON) ([]*smart
return rts, nil
}

func (f *StrategyFinder) findDNS(ctx context.Context, testDomains []string, dnsConfig []dnsEntryJSON) (dns.Resolver, error) {
func (f *StrategyFinder) findDNS(ctx context.Context, testDomains []string, dnsConfig []dnsEntryConfig) (dns.Resolver, error) {
resolvers, err := f.dnsConfigToResolver(dnsConfig)
if err != nil {
return nil, err
Expand Down Expand Up @@ -296,8 +296,8 @@ func (f *StrategyFinder) findTLS(ctx context.Context, testDomains []string, base
// 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)
var parsedConfig configConfig
err := yaml.Unmarshal(configBytes, &parsedConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse config: %v", err)
}
Expand Down
Loading