From 2c8cbb0a982e0ec51e1ee4d4a9193e7a9ca4953d Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 01:06:32 -0400 Subject: [PATCH 1/9] Revamp Config --- x/config/config.go | 251 ++++++++++++----------- x/config/config_test.go | 44 ++-- x/config/dns.go | 11 +- x/config/doc.go | 10 +- x/config/override.go | 16 +- x/config/shadowsocks.go | 16 +- x/config/socks5.go | 8 +- x/config/split.go | 37 ++++ x/config/tls.go | 8 +- x/config/tls_test.go | 2 +- x/examples/fetch-speed/main.go | 3 +- x/examples/fetch/main.go | 3 +- x/examples/http2transport/main.go | 3 +- x/examples/outline-cli/outline_device.go | 4 +- x/examples/resolve/main.go | 7 +- x/examples/smart-proxy/main.go | 6 +- x/examples/test-connectivity/main.go | 7 +- x/examples/ws2endpoint/main.go | 2 +- x/httpproxy/connect_handler.go | 14 +- x/mobileproxy/mobileproxy.go | 4 +- x/smart/stream_dialer.go | 6 +- 21 files changed, 274 insertions(+), 188 deletions(-) create mode 100644 x/config/split.go diff --git a/x/config/config.go b/x/config/config.go index 63d79563..0787e9a3 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -22,187 +22,190 @@ import ( "strings" "github.com/Jigsaw-Code/outline-sdk/transport" - "github.com/Jigsaw-Code/outline-sdk/transport/split" "github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag" ) -// ConfigParser enables the creation of stream and packet dialers based on a config. The config is +// ConfigToDialer enables the creation of stream and packet dialers based on a config. The config is // extensible by registering wrappers for config subtypes. -type ConfigParser struct { - sdWrapers map[string]WrapStreamDialerFunc - pdWrappers map[string]WrapPacketDialerFunc +type ConfigToDialer struct { + BaseStreamDialer transport.StreamDialer + BasePacketDialer transport.PacketDialer + sdBuilders map[string]NewStreamDialerFunc + pdBuilders map[string]NewPacketDialerFunc } -// NewDefaultConfigParser creates a [ConfigParser] with a set of default wrappers already registered. -func NewDefaultConfigParser() *ConfigParser { - p := new(ConfigParser) +// NewStreamDialerFunc wraps a Dialer based on the wrapConfig. +type NewStreamDialerFunc func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.StreamDialer, error) + +// NewPacketDialerFunc wraps a Dialer based on the wrapConfig. +type NewPacketDialerFunc func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.PacketDialer, error) + +// NewDefaultConfigToDialer creates a [ConfigToDialer] with a set of default wrappers already registered. +func NewDefaultConfigToDialer() *ConfigToDialer { + p := new(ConfigToDialer) + p.BaseStreamDialer = &transport.TCPDialer{} + p.BasePacketDialer = &transport.UDPDialer{} // Please keep the list in alphabetical order. p.RegisterStreamDialerWrapper("doh", wrapStreamDialerWithDOH) - p.RegisterPacketDialerWrapper("doh", func(baseDialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) { - return nil, errors.New("doh is not supported for PacketDialers") - }) p.RegisterStreamDialerWrapper("override", wrapStreamDialerWithOverride) p.RegisterPacketDialerWrapper("override", wrapPacketDialerWithOverride) p.RegisterStreamDialerWrapper("socks5", wrapStreamDialerWithSOCKS5) - p.RegisterPacketDialerWrapper("socks5", func(baseDialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) { - return nil, errors.New("socks5 is not supported for PacketDialers") - }) - p.RegisterStreamDialerWrapper("split", func(baseDialer transport.StreamDialer, wrapConfig *url.URL) (transport.StreamDialer, error) { - prefixBytesStr := wrapConfig.Opaque - prefixBytes, err := strconv.Atoi(prefixBytesStr) - if err != nil { - return nil, fmt.Errorf("prefixBytes is not a number: %v. Split config should be in split: format", prefixBytesStr) - } - return split.NewStreamDialer(baseDialer, int64(prefixBytes)) - }) - p.RegisterPacketDialerWrapper("split", func(baseDialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) { - return nil, errors.New("split is not supported for PacketDialers") - }) + p.RegisterStreamDialerWrapper("split", wrapStreamDialerWithSplit) p.RegisterStreamDialerWrapper("ss", wrapStreamDialerWithShadowsocks) p.RegisterPacketDialerWrapper("ss", wrapPacketDialerWithShadowsocks) p.RegisterStreamDialerWrapper("tls", wrapStreamDialerWithTLS) - p.RegisterPacketDialerWrapper("tls", func(baseDialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) { - return nil, errors.New("tls is not supported for PacketDialers") - }) - p.RegisterStreamDialerWrapper("tlsfrag", func(baseDialer transport.StreamDialer, wrapConfig *url.URL) (transport.StreamDialer, error) { + p.RegisterStreamDialerWrapper("tlsfrag", func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } lenStr := wrapConfig.Opaque fixedLen, err := strconv.Atoi(lenStr) if err != nil { return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag: format", lenStr) } - return tlsfrag.NewFixedLenStreamDialer(baseDialer, fixedLen) - }) - p.RegisterPacketDialerWrapper("tlsfrag", func(baseDialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) { - return nil, errors.New("tlsfrag is not supported for PacketDialers") + return tlsfrag.NewFixedLenStreamDialer(sd, fixedLen) }) - return p } -// WrapStreamDialerFunc wraps a [transport.StreamDialer] based on the wrapConfig. -type WrapStreamDialerFunc func(dialer transport.StreamDialer, wrapConfig *url.URL) (transport.StreamDialer, error) - // RegisterStreamDialerWrapper will register a wrapper for stream dialers under the given subtype. -func (p *ConfigParser) RegisterStreamDialerWrapper(subtype string, wrapper WrapStreamDialerFunc) error { - if p.sdWrapers == nil { - p.sdWrapers = make(map[string]WrapStreamDialerFunc) +func (p *ConfigToDialer) RegisterStreamDialerWrapper(subtype string, wrapper NewStreamDialerFunc) error { + if p.sdBuilders == nil { + p.sdBuilders = make(map[string]NewStreamDialerFunc) } - if _, found := p.sdWrapers[subtype]; found { + if _, found := p.sdBuilders[subtype]; found { return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) } - p.sdWrapers[subtype] = wrapper + p.sdBuilders[subtype] = wrapper return nil } -// WrapPacketDialerFunc wraps a [transport.PacketDialer] based on the wrapConfig. -type WrapPacketDialerFunc func(dialer transport.PacketDialer, wrapConfig *url.URL) (transport.PacketDialer, error) - // RegisterPacketDialerWrapper will register a wrapper for packet dialers under the given subtype. -func (p *ConfigParser) RegisterPacketDialerWrapper(subtype string, wrapper WrapPacketDialerFunc) error { - if p.pdWrappers == nil { - p.pdWrappers = make(map[string]WrapPacketDialerFunc) +func (p *ConfigToDialer) RegisterPacketDialerWrapper(subtype string, wrapper NewPacketDialerFunc) error { + if p.pdBuilders == nil { + p.pdBuilders = make(map[string]NewPacketDialerFunc) } - if _, found := p.pdWrappers[subtype]; found { - return fmt.Errorf("config parser %v for PacketDialer added twice", subtype) + if _, found := p.pdBuilders[subtype]; found { + return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) } - p.pdWrappers[subtype] = wrapper + p.pdBuilders[subtype] = wrapper return nil } -func parseConfigPart(oneDialerConfig string) (*url.URL, error) { - oneDialerConfig = strings.TrimSpace(oneDialerConfig) - if oneDialerConfig == "" { - return nil, errors.New("empty config part") +func parseConfig(configText string) ([]*url.URL, error) { + parts := strings.Split(strings.TrimSpace(configText), "|") + if len(parts) == 1 && parts[0] == "" { + return []*url.URL{}, nil } - // Make it ":" if it's only "" to parse as a URL. - if !strings.Contains(oneDialerConfig, ":") { - oneDialerConfig += ":" + urls := make([]*url.URL, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + return nil, errors.New("empty config part") + } + // Make it ":" if it's only "" to parse as a URL. + if !strings.Contains(part, ":") { + part += ":" + } + url, err := url.Parse(part) + if err != nil { + return nil, fmt.Errorf("part is not a valid URL: %w", err) + } + urls = append(urls, url) } - url, err := url.Parse(oneDialerConfig) + return urls, nil +} + +// WrapDialer creates a [Dialer] according to transportConfig, using dialer as the +// base [Dialer]. The given dialer must not be nil. +func (p *ConfigToDialer) NewStreamDialer(transportConfig string) (transport.StreamDialer, error) { + parts, err := parseConfig(transportConfig) if err != nil { - return nil, fmt.Errorf("part is not a valid URL: %w", err) + return nil, err } - return url, nil + return p.newStreamDialer(parts) } -// WrapStreamDialer creates a [transport.StreamDialer] according to transportConfig, using dialer as the -// base [transport.StreamDialer]. The given dialer must not be nil. -func (p *ConfigParser) WrapStreamDialer(dialer transport.StreamDialer, transportConfig string) (transport.StreamDialer, error) { - if dialer == nil { - return nil, errors.New("base dialer must not be nil") - } - transportConfig = strings.TrimSpace(transportConfig) - if transportConfig == "" { - return dialer, nil +// WrapDialer creates a [Dialer] according to transportConfig, using dialer as the +// base [Dialer]. The given dialer must not be nil. +func (p *ConfigToDialer) NewPacketDialer(transportConfig string) (transport.PacketDialer, error) { + parts, err := parseConfig(transportConfig) + if err != nil { + return nil, err } - for _, part := range strings.Split(transportConfig, "|") { - url, err := parseConfigPart(part) - if err != nil { - return nil, err - } - w, ok := p.sdWrapers[url.Scheme] - if !ok { - return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme) - } - dialer, err = w(dialer, url) - if err != nil { - return nil, err + return p.newPacketDialer(parts) +} + +func (p *ConfigToDialer) newStreamDialer(configParts []*url.URL) (transport.StreamDialer, error) { + if len(configParts) == 0 { + if p.BaseStreamDialer == nil { + return nil, fmt.Errorf("base StreamDialer must not be nil") } + return p.BaseStreamDialer, nil + } + thisURL := configParts[len(configParts)-1] + innerConfig := configParts[:len(configParts)-1] + w, ok := p.sdBuilders[thisURL.Scheme] + if !ok { + return nil, fmt.Errorf("config scheme '%v' is not supported for Stream Dialers", thisURL.Scheme) } - return dialer, nil + newSD := func() (transport.StreamDialer, error) { + return p.newStreamDialer(innerConfig) + } + newPD := func() (transport.PacketDialer, error) { + return p.newPacketDialer(innerConfig) + } + return w(newSD, newPD, thisURL) } -// WrapPacketDialer creates a [transport.PacketDialer] according to transportConfig, using dialer as the -// base [transport.PacketDialer]. The given dialer must not be nil. -func (p *ConfigParser) WrapPacketDialer(dialer transport.PacketDialer, transportConfig string) (transport.PacketDialer, error) { - if dialer == nil { - return nil, errors.New("base dialer must not be nil") +func (p *ConfigToDialer) newPacketDialer(configParts []*url.URL) (transport.PacketDialer, error) { + if len(configParts) == 0 { + if p.BasePacketDialer == nil { + return nil, fmt.Errorf("base PacketDialer must not be nil") + } + return p.BasePacketDialer, nil } - transportConfig = strings.TrimSpace(transportConfig) - if transportConfig == "" { - return dialer, nil + thisURL := configParts[len(configParts)-1] + innerConfig := configParts[:len(configParts)-1] + w, ok := p.pdBuilders[thisURL.Scheme] + if !ok { + return nil, fmt.Errorf("config scheme '%v' is not supported for Packet Dialers", thisURL.Scheme) } - for _, part := range strings.Split(transportConfig, "|") { - url, err := parseConfigPart(part) - if err != nil { - return nil, err - } - w, ok := p.pdWrappers[url.Scheme] - if !ok { - return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme) - } - dialer, err = w(dialer, url) - if err != nil { - return nil, err - } + newSD := func() (transport.StreamDialer, error) { + return p.newStreamDialer(innerConfig) + } + newPD := func() (transport.PacketDialer, error) { + return p.newPacketDialer(innerConfig) } - return dialer, nil + return w(newSD, newPD, thisURL) } // NewpacketListener creates a new [transport.PacketListener] according to the given config, // the config must contain only one "ss://" segment. // TODO: make NewPacketListener configurable. func NewPacketListener(transportConfig string) (transport.PacketListener, error) { - if transportConfig = strings.TrimSpace(transportConfig); transportConfig == "" { + parts, err := parseConfig(transportConfig) + if err != nil { + return nil, err + } + if len(parts) == 0 { return nil, errors.New("config is required") } - if strings.Contains(transportConfig, "|") { + if len(parts) > 1 { return nil, errors.New("multi-part config is not supported") } - url, err := parseConfigPart(transportConfig) - if err != nil { - return nil, fmt.Errorf("failed to parse config: %w", err) - } + url := parts[0] // Please keep scheme list sorted. switch strings.ToLower(url.Scheme) { case "ss": @@ -214,34 +217,34 @@ func NewPacketListener(transportConfig string) (transport.PacketListener, error) } func SanitizeConfig(transportConfig string) (string, error) { + parts, err := parseConfig(transportConfig) + if err != nil { + return "", err + } + // Do nothing if the config is empty - if transportConfig == "" { + if len(parts) == 0 { return "", nil } - // Split the string into parts - parts := strings.Split(transportConfig, "|") // Iterate through each part - for i, part := range parts { - u, err := parseConfigPart(part) - if err != nil { - return "", fmt.Errorf("failed to parse config part: %w", err) - } + textParts := make([]string, len(parts)) + for i, u := range parts { scheme := strings.ToLower(u.Scheme) switch scheme { case "ss": - parts[i], _ = sanitizeShadowsocksURL(u) + textParts[i], _ = sanitizeShadowsocksURL(u) case "socks5": - parts[i], _ = sanitizeSocks5URL(u) + textParts[i], _ = sanitizeSocks5URL(u) case "override", "split", "tls", "tlsfrag": // No sanitization needed - parts[i] = u.String() + textParts[i] = u.String() default: - parts[i] = scheme + "://UNKNOWN" + textParts[i] = scheme + "://UNKNOWN" } } // Join the parts back into a string - return strings.Join(parts, "|"), nil + return strings.Join(textParts, "|"), nil } func sanitizeSocks5URL(u *url.URL) (string, error) { diff --git a/x/config/config_test.go b/x/config/config_test.go index 01abfb17..ada5f458 100644 --- a/x/config/config_test.go +++ b/x/config/config_test.go @@ -1,3 +1,17 @@ +// 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 config import ( @@ -44,9 +58,10 @@ func TestSanitizeConfig(t *testing.T) { func TestShowsocksLagacyBase64URL(t *testing.T) { encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20")) - u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123") + urls, err := parseConfig("ss://" + string(encoded) + "#outline-123") require.NoError(t, err) - config, err := parseShadowsocksLegacyBase64URL(u) + require.Equal(t, 1, len(urls)) + config, err := parseShadowsocksLegacyBase64URL(urls[0]) require.Equal(t, "example.com:1234", config.serverAddress) require.Equal(t, "HTTP/1.1 ", string(config.prefix)) require.NoError(t, err) @@ -54,17 +69,19 @@ func TestShowsocksLagacyBase64URL(t *testing.T) { func TestParseShadowsocksURL(t *testing.T) { encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20")) - u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123") + urls, err := parseConfig("ss://" + string(encoded) + "#outline-123") require.NoError(t, err) - config, err := parseShadowsocksURL(u) + require.Equal(t, 1, len(urls)) + config, err := parseShadowsocksURL(urls[0]) require.Equal(t, "example.com:1234", config.serverAddress) require.Equal(t, "HTTP/1.1 ", string(config.prefix)) require.NoError(t, err) encoded = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567")) - u, err = parseConfigPart("ss://" + string(encoded) + "@example.com:1234?prefix=HTTP%2F1.1%20" + "#outline-123") + urls, err = parseConfig("ss://" + string(encoded) + "@example.com:1234?prefix=HTTP%2F1.1%20" + "#outline-123") require.NoError(t, err) - config, err = parseShadowsocksURL(u) + require.Equal(t, 1, len(urls)) + config, err = parseShadowsocksURL(urls[0]) require.Equal(t, "example.com:1234", config.serverAddress) require.Equal(t, "HTTP/1.1 ", string(config.prefix)) require.NoError(t, err) @@ -79,25 +96,28 @@ func TestSocks5URLSanitization(t *testing.T) { func TestParseShadowsocksSIP002URLUnsuccessful(t *testing.T) { encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20")) - u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123") + urls, err := parseConfig("ss://" + string(encoded) + "#outline-123") require.NoError(t, err) - _, err = parseShadowsocksSIP002URL(u) + require.Equal(t, 1, len(urls)) + _, err = parseShadowsocksSIP002URL(urls[0]) require.Error(t, err) } func TestParseShadowsocksSIP002URLUnsupportedCypher(t *testing.T) { configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwnTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20" - u, err := parseConfigPart(configString) + urls, err := parseConfig(configString) require.NoError(t, err) - _, err = parseShadowsocksSIP002URL(u) + require.Equal(t, 1, len(urls)) + _, err = parseShadowsocksSIP002URL(urls[0]) require.Error(t, err) } func TestParseShadowsocksSIP002URLSuccessful(t *testing.T) { configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20" - u, err := parseConfigPart(configString) + urls, err := parseConfig(configString) require.NoError(t, err) - config, err := parseShadowsocksSIP002URL(u) + require.Equal(t, 1, len(urls)) + config, err := parseShadowsocksSIP002URL(urls[0]) require.NoError(t, err) require.Equal(t, "example.com:1234", config.serverAddress) require.Equal(t, "HTTP/1.1 ", string(config.prefix)) diff --git a/x/config/dns.go b/x/config/dns.go index cb5ac22a..e6633818 100644 --- a/x/config/dns.go +++ b/x/config/dns.go @@ -25,12 +25,17 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) -func wrapStreamDialerWithDOH(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) { +func wrapStreamDialerWithDOH(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { query := configURL.Opaque values, err := url.ParseQuery(query) if err != nil { return nil, err } + sd, err := innerSD() + if err != nil { + return nil, err + } + var name, address string for key, values := range values { switch strings.ToLower(key) { @@ -61,6 +66,6 @@ func wrapStreamDialerWithDOH(innerDialer transport.StreamDialer, configURL *url. port = "443" } dohURL := url.URL{Scheme: "https", Host: net.JoinHostPort(name, port), Path: "/dns-query"} - resolver := dns.NewHTTPSResolver(innerDialer, address, dohURL.String()) - return dns.NewStreamDialer(resolver, innerDialer) + resolver := dns.NewHTTPSResolver(sd, address, dohURL.String()) + return dns.NewStreamDialer(resolver, sd) } diff --git a/x/config/doc.go b/x/config/doc.go index 501a7225..9bf1cb91 100644 --- a/x/config/doc.go +++ b/x/config/doc.go @@ -107,19 +107,19 @@ DPI Evasion - To add packet splitting to a Shadowsocks server for enhanced DPI e split:2|ss://[USERINFO]@[HOST]:[PORT] -Defining custom transport - You can define your custom transport by implementing and registering the [WrapStreamDialerFunc] and [WrapPacketDialerFunc] functions: +Defining custom transport - You can define your custom transport by implementing and registering the [NewStreamDialerFunc] and [NewPacketDialerFunc] functions: // create new config parser - // p := new(ConfigParser) + // p := new(ConfigToDialer) // or - p := NewDefaultConfigParser() + p := NewDefaultConfigToDialer() // register your custom dialer p.RegisterPacketDialerWrapper("custom", wrapStreamDialerWithCustom) p.RegisterStreamDialerWrapper("custom", wrapPacketDialerWithCustom) // then use it - dialer, err := p.WrapStreamDialer(innerDialer, "custom://config") + dialer, err := p.NewStreamDialer(innerDialer, "custom://config") -where wrapStreamDialerWithCustom and wrapPacketDialerWithCustom implement [WrapPacketDialerFunc] and [WrapStreamDialerFunc]. +where wrapStreamDialerWithCustom and wrapPacketDialerWithCustom implement [NewPacketDialerFunc] and [NewStreamDialerFunc]. [Onion Routing]: https://en.wikipedia.org/wiki/Onion_routing */ diff --git a/x/config/override.go b/x/config/override.go index daac524d..76c4a785 100644 --- a/x/config/override.go +++ b/x/config/override.go @@ -66,7 +66,11 @@ func newOverrideFromURL(configURL *url.URL) (func(string) (string, error), error }, nil } -func wrapStreamDialerWithOverride(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) { +func wrapStreamDialerWithOverride(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } override, err := newOverrideFromURL(configURL) if err != nil { return nil, err @@ -76,11 +80,15 @@ func wrapStreamDialerWithOverride(innerDialer transport.StreamDialer, configURL if err != nil { return nil, err } - return innerDialer.DialStream(ctx, addr) + return sd.DialStream(ctx, addr) }), nil } -func wrapPacketDialerWithOverride(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) { +func wrapPacketDialerWithOverride(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { + pd, err := innerPD() + if err != nil { + return nil, err + } override, err := newOverrideFromURL(configURL) if err != nil { return nil, err @@ -90,6 +98,6 @@ func wrapPacketDialerWithOverride(innerDialer transport.PacketDialer, configURL if err != nil { return nil, err } - return innerDialer.DialPacket(ctx, addr) + return pd.DialPacket(ctx, addr) }), nil } diff --git a/x/config/shadowsocks.go b/x/config/shadowsocks.go index 531bea9f..94760bed 100644 --- a/x/config/shadowsocks.go +++ b/x/config/shadowsocks.go @@ -25,12 +25,16 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" ) -func wrapStreamDialerWithShadowsocks(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) { +func wrapStreamDialerWithShadowsocks(innerSD func() (transport.StreamDialer, error), _ func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } config, err := parseShadowsocksURL(configURL) if err != nil { return nil, err } - endpoint := &transport.StreamDialerEndpoint{Dialer: innerDialer, Address: config.serverAddress} + endpoint := &transport.StreamDialerEndpoint{Dialer: sd, Address: config.serverAddress} dialer, err := shadowsocks.NewStreamDialer(endpoint, config.cryptoKey) if err != nil { return nil, err @@ -41,12 +45,16 @@ func wrapStreamDialerWithShadowsocks(innerDialer transport.StreamDialer, configU return dialer, nil } -func wrapPacketDialerWithShadowsocks(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) { +func wrapPacketDialerWithShadowsocks(_ func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { + pd, err := innerPD() + if err != nil { + return nil, err + } config, err := parseShadowsocksURL(configURL) if err != nil { return nil, err } - endpoint := &transport.PacketDialerEndpoint{Dialer: innerDialer, Address: config.serverAddress} + endpoint := &transport.PacketDialerEndpoint{Dialer: pd, Address: config.serverAddress} listener, err := shadowsocks.NewPacketListener(endpoint, config.cryptoKey) if err != nil { return nil, err diff --git a/x/config/socks5.go b/x/config/socks5.go index f3d79012..0788588a 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -7,8 +7,12 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport/socks5" ) -func wrapStreamDialerWithSOCKS5(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) { - endpoint := transport.StreamDialerEndpoint{Dialer: innerDialer, Address: configURL.Host} +func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), _ func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } + endpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} dialer, err := socks5.NewStreamDialer(&endpoint) if err != nil { return nil, err diff --git a/x/config/split.go b/x/config/split.go new file mode 100644 index 00000000..654c9ba9 --- /dev/null +++ b/x/config/split.go @@ -0,0 +1,37 @@ +// 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 config + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/split" +) + +func wrapStreamDialerWithSplit(innerSD func() (transport.StreamDialer, error), _ func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } + prefixBytesStr := configURL.Opaque + prefixBytes, err := strconv.Atoi(prefixBytesStr) + if err != nil { + return nil, fmt.Errorf("prefixBytes is not a number: %v. Split config should be in split: format", prefixBytesStr) + } + return split.NewStreamDialer(sd, int64(prefixBytes)) +} diff --git a/x/config/tls.go b/x/config/tls.go index b896f455..3086249b 100644 --- a/x/config/tls.go +++ b/x/config/tls.go @@ -50,10 +50,14 @@ func parseOptions(configURL *url.URL) ([]tls.ClientOption, error) { return options, nil } -func wrapStreamDialerWithTLS(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) { +func wrapStreamDialerWithTLS(innerSD func() (transport.StreamDialer, error), _ func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } options, err := parseOptions(configURL) if err != nil { return nil, err } - return tls.NewStreamDialer(innerDialer, options...) + return tls.NewStreamDialer(sd, options...) } diff --git a/x/config/tls_test.go b/x/config/tls_test.go index 4ef07eb6..c0a0bcfc 100644 --- a/x/config/tls_test.go +++ b/x/config/tls_test.go @@ -26,7 +26,7 @@ import ( func TestTLS(t *testing.T) { tlsURL, err := url.Parse("tls") require.NoError(t, err) - _, err = wrapStreamDialerWithTLS(&transport.TCPDialer{}, tlsURL) + _, err = wrapStreamDialerWithTLS(func() (transport.StreamDialer, error) { return &transport.TCPDialer{}, nil }, nil, tlsURL) require.NoError(t, err) } diff --git a/x/examples/fetch-speed/main.go b/x/examples/fetch-speed/main.go index e05d7ecb..65807062 100644 --- a/x/examples/fetch-speed/main.go +++ b/x/examples/fetch-speed/main.go @@ -27,7 +27,6 @@ import ( "strings" "time" - "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/x/config" ) @@ -59,7 +58,7 @@ func main() { os.Exit(1) } - dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + dialer, err := config.NewDefaultConfigToDialer().NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create dialer: %v\n", err) } diff --git a/x/examples/fetch/main.go b/x/examples/fetch/main.go index a3400b6f..228014c7 100644 --- a/x/examples/fetch/main.go +++ b/x/examples/fetch/main.go @@ -27,7 +27,6 @@ import ( "strings" "time" - "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/x/config" ) @@ -70,7 +69,7 @@ func main() { os.Exit(1) } - dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + dialer, err := config.NewDefaultConfigToDialer().NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create dialer: %v\n", err) } diff --git a/x/examples/http2transport/main.go b/x/examples/http2transport/main.go index 214d1144..f8299725 100644 --- a/x/examples/http2transport/main.go +++ b/x/examples/http2transport/main.go @@ -24,7 +24,6 @@ import ( "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" ) @@ -35,7 +34,7 @@ func main() { urlProxyPrefixFlag := flag.String("urlProxyPrefix", "/proxy", "Path where to run the URL proxy. Set to empty (\"\") to disable it.") flag.Parse() - dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + dialer, err := config.NewDefaultConfigToDialer().NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create dialer: %v", err) diff --git a/x/examples/outline-cli/outline_device.go b/x/examples/outline-cli/outline_device.go index 69f03bec..330ba024 100644 --- a/x/examples/outline-cli/outline_device.go +++ b/x/examples/outline-cli/outline_device.go @@ -39,7 +39,7 @@ type OutlineDevice struct { svrIP net.IP } -var configParser = config.NewDefaultConfigParser() +var configToDialer = config.NewDefaultConfigToDialer() func NewOutlineDevice(transportConfig string) (od *OutlineDevice, err error) { ip, err := resolveShadowsocksServerIPFromConfig(transportConfig) @@ -50,7 +50,7 @@ func NewOutlineDevice(transportConfig string) (od *OutlineDevice, err error) { svrIP: ip, } - if od.sd, err = configParser.WrapStreamDialer(&transport.TCPDialer{}, transportConfig); err != nil { + if od.sd, err = configToDialer.NewStreamDialer(transportConfig); err != nil { return nil, fmt.Errorf("failed to create TCP dialer: %w", err) } if od.pp, err = newOutlinePacketProxy(transportConfig); err != nil { diff --git a/x/examples/resolve/main.go b/x/examples/resolve/main.go index 4daab84b..0364a781 100644 --- a/x/examples/resolve/main.go +++ b/x/examples/resolve/main.go @@ -27,7 +27,6 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/dns" - "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/x/config" "golang.org/x/net/dns/dnsmessage" ) @@ -67,15 +66,15 @@ func main() { resolverAddr := *resolverFlag var resolver dns.Resolver - configParser := config.NewDefaultConfigParser() + configToDialer := config.NewDefaultConfigToDialer() if *tcpFlag { - streamDialer, err := configParser.WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + streamDialer, err := configToDialer.NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create stream dialer: %v", err) } resolver = dns.NewTCPResolver(streamDialer, resolverAddr) } else { - packetDialer, err := configParser.WrapPacketDialer(&transport.UDPDialer{}, *transportFlag) + packetDialer, err := configToDialer.NewPacketDialer(*transportFlag) if err != nil { log.Fatalf("Could not create packet dialer: %v", err) } diff --git a/x/examples/smart-proxy/main.go b/x/examples/smart-proxy/main.go index b3f91f30..b695566e 100644 --- a/x/examples/smart-proxy/main.go +++ b/x/examples/smart-proxy/main.go @@ -86,12 +86,12 @@ func main() { log.Fatalf("Could not read config: %v", err) } - configParser := config.NewDefaultConfigParser() - packetDialer, err := configParser.WrapPacketDialer(&transport.UDPDialer{}, *transportFlag) + configToDialer := config.NewDefaultConfigToDialer() + packetDialer, err := configToDialer.NewPacketDialer(*transportFlag) if err != nil { log.Fatalf("Could not create packet dialer: %v", err) } - streamDialer, err := configParser.WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + streamDialer, err := configToDialer.NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create stream dialer: %v", err) } diff --git a/x/examples/test-connectivity/main.go b/x/examples/test-connectivity/main.go index 5e8f707c..45b4ea8d 100644 --- a/x/examples/test-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -31,7 +31,6 @@ import ( "time" "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/connectivity" "github.com/Jigsaw-Code/outline-sdk/x/report" @@ -162,7 +161,7 @@ func main() { success := false jsonEncoder := json.NewEncoder(os.Stdout) jsonEncoder.SetEscapeHTML(false) - configParser := config.NewDefaultConfigParser() + configToDialer := config.NewDefaultConfigToDialer() for _, resolverHost := range strings.Split(*resolverFlag, ",") { resolverHost := strings.TrimSpace(resolverHost) resolverAddress := net.JoinHostPort(resolverHost, "53") @@ -171,13 +170,13 @@ func main() { var resolver dns.Resolver switch proto { case "tcp": - streamDialer, err := configParser.WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + streamDialer, err := configToDialer.NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Failed to create StreamDialer: %v", err) } resolver = dns.NewTCPResolver(streamDialer, resolverAddress) case "udp": - packetDialer, err := configParser.WrapPacketDialer(&transport.UDPDialer{}, *transportFlag) + packetDialer, err := configToDialer.NewPacketDialer(*transportFlag) if err != nil { log.Fatalf("Failed to create PacketDialer: %v", err) } diff --git a/x/examples/ws2endpoint/main.go b/x/examples/ws2endpoint/main.go index 678daf71..155af82b 100644 --- a/x/examples/ws2endpoint/main.go +++ b/x/examples/ws2endpoint/main.go @@ -37,7 +37,7 @@ func main() { pathPrefix := flag.String("path", "/", "Path where to run the Websocket forwarder") flag.Parse() - dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag) + dialer, err := config.NewDefaultConfigToDialer().NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Could not create dialer: %v", err) } diff --git a/x/httpproxy/connect_handler.go b/x/httpproxy/connect_handler.go index 25845d25..28183c5e 100644 --- a/x/httpproxy/connect_handler.go +++ b/x/httpproxy/connect_handler.go @@ -51,14 +51,12 @@ func (d *sanitizeErrorDialer) DialStream(ctx context.Context, addr string) (tran } type connectHandler struct { - dialer *sanitizeErrorDialer + dialer *sanitizeErrorDialer + dialerConfig *config.ConfigToDialer } var _ http.Handler = (*connectHandler)(nil) -// TODO(fortuna): Inject the config parser -var configParser = config.NewDefaultConfigParser() - func (h *connectHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http.Request) { if proxyReq.Method != http.MethodConnect { proxyResp.Header().Add("Allow", "CONNECT") @@ -79,7 +77,7 @@ func (h *connectHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http // Dial the target. transportConfig := proxyReq.Header.Get("Transport") - dialer, err := configParser.WrapStreamDialer(h.dialer, transportConfig) + dialer, err := h.dialerConfig.NewStreamDialer(transportConfig) if err != nil { // Because we sanitize the base dialer error, it's safe to return error details here. http.Error(proxyResp, fmt.Sprintf("Invalid config in Transport header: %v", err), http.StatusBadRequest) @@ -140,5 +138,9 @@ func (h *connectHandler) ServeHTTP(proxyResp http.ResponseWriter, proxyReq *http func NewConnectHandler(dialer transport.StreamDialer) http.Handler { // We sanitize the errors from the input Dialer because we don't want to leak sensitive details // of the base dialer (e.g. access key credentials) to the user. - return &connectHandler{&sanitizeErrorDialer{dialer}} + sd := &sanitizeErrorDialer{dialer} + // TODO(fortuna): Inject the config parser + dialerConfig := config.NewDefaultConfigToDialer() + dialerConfig.BaseStreamDialer = sd + return &connectHandler{sd, dialerConfig} } diff --git a/x/mobileproxy/mobileproxy.go b/x/mobileproxy/mobileproxy.go index 81a793ee..14397864 100644 --- a/x/mobileproxy/mobileproxy.go +++ b/x/mobileproxy/mobileproxy.go @@ -152,12 +152,12 @@ type StreamDialer struct { transport.StreamDialer } -var configParser = config.NewDefaultConfigParser() +var configToDialer = config.NewDefaultConfigToDialer() // NewStreamDialerFromConfig creates a [StreamDialer] based on the given config. // The config format is specified in https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format. func NewStreamDialerFromConfig(transportConfig string) (*StreamDialer, error) { - dialer, err := configParser.WrapStreamDialer(&transport.TCPDialer{}, transportConfig) + dialer, err := configToDialer.NewStreamDialer(transportConfig) if err != nil { return nil, err } diff --git a/x/smart/stream_dialer.go b/x/smart/stream_dialer.go index 5f53e709..f6db7a77 100644 --- a/x/smart/stream_dialer.go +++ b/x/smart/stream_dialer.go @@ -227,12 +227,12 @@ func (f *StrategyFinder) findDNS(ctx context.Context, testDomains []string, dnsC return resolver.Resolver, nil } -var configParser = config.NewDefaultConfigParser() - func (f *StrategyFinder) findTLS(ctx context.Context, 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") } + var configToDialer = config.NewDefaultConfigToDialer() + configToDialer.BaseStreamDialer = baseDialer ctx, searchDone := context.WithCancel(ctx) defer searchDone() @@ -242,7 +242,7 @@ func (f *StrategyFinder) findTLS(ctx context.Context, testDomains []string, base Config string } result, err := raceTests(ctx, 250*time.Millisecond, tlsConfig, func(transportCfg string) (*SearchResult, error) { - tlsDialer, err := configParser.WrapStreamDialer(baseDialer, transportCfg) + tlsDialer, err := configToDialer.NewStreamDialer(transportCfg) if err != nil { return nil, fmt.Errorf("WrapStreamDialer failed: %w", err) } From c074c4bbf32fa3642a9faa98d3af864040dec9ec Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 10:12:28 -0400 Subject: [PATCH 2/9] Fix comments and names --- x/config/config.go | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/x/config/config.go b/x/config/config.go index 0787e9a3..b4fbeb04 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -34,10 +34,10 @@ type ConfigToDialer struct { pdBuilders map[string]NewPacketDialerFunc } -// NewStreamDialerFunc wraps a Dialer based on the wrapConfig. +// NewStreamDialerFunc wraps a Dialer based on the wrapConfig. The innerSD and innerPD functions can provide a base Stream and Packet Dialers if needed. type NewStreamDialerFunc func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.StreamDialer, error) -// NewPacketDialerFunc wraps a Dialer based on the wrapConfig. +// NewPacketDialerFunc wraps a Dialer based on the wrapConfig. The innerSD and innerPD functions can provide a base Stream and Packet Dialers if needed. type NewPacketDialerFunc func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.PacketDialer, error) // NewDefaultConfigToDialer creates a [ConfigToDialer] with a set of default wrappers already registered. @@ -47,21 +47,21 @@ func NewDefaultConfigToDialer() *ConfigToDialer { p.BasePacketDialer = &transport.UDPDialer{} // Please keep the list in alphabetical order. - p.RegisterStreamDialerWrapper("doh", wrapStreamDialerWithDOH) + p.RegisterStreamDialerType("doh", wrapStreamDialerWithDOH) - p.RegisterStreamDialerWrapper("override", wrapStreamDialerWithOverride) - p.RegisterPacketDialerWrapper("override", wrapPacketDialerWithOverride) + p.RegisterStreamDialerType("override", wrapStreamDialerWithOverride) + p.RegisterPacketDialerType("override", wrapPacketDialerWithOverride) - p.RegisterStreamDialerWrapper("socks5", wrapStreamDialerWithSOCKS5) + p.RegisterStreamDialerType("socks5", wrapStreamDialerWithSOCKS5) - p.RegisterStreamDialerWrapper("split", wrapStreamDialerWithSplit) + p.RegisterStreamDialerType("split", wrapStreamDialerWithSplit) - p.RegisterStreamDialerWrapper("ss", wrapStreamDialerWithShadowsocks) - p.RegisterPacketDialerWrapper("ss", wrapPacketDialerWithShadowsocks) + p.RegisterStreamDialerType("ss", wrapStreamDialerWithShadowsocks) + p.RegisterPacketDialerType("ss", wrapPacketDialerWithShadowsocks) - p.RegisterStreamDialerWrapper("tls", wrapStreamDialerWithTLS) + p.RegisterStreamDialerType("tls", wrapStreamDialerWithTLS) - p.RegisterStreamDialerWrapper("tlsfrag", func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.StreamDialer, error) { + p.RegisterStreamDialerType("tlsfrag", func(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), wrapConfig *url.URL) (transport.StreamDialer, error) { sd, err := innerSD() if err != nil { return nil, err @@ -76,8 +76,8 @@ func NewDefaultConfigToDialer() *ConfigToDialer { return p } -// RegisterStreamDialerWrapper will register a wrapper for stream dialers under the given subtype. -func (p *ConfigToDialer) RegisterStreamDialerWrapper(subtype string, wrapper NewStreamDialerFunc) error { +// RegisterStreamDialerType will register a wrapper for stream dialers under the given subtype. +func (p *ConfigToDialer) RegisterStreamDialerType(subtype string, newDialer NewStreamDialerFunc) error { if p.sdBuilders == nil { p.sdBuilders = make(map[string]NewStreamDialerFunc) } @@ -85,12 +85,12 @@ func (p *ConfigToDialer) RegisterStreamDialerWrapper(subtype string, wrapper New if _, found := p.sdBuilders[subtype]; found { return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) } - p.sdBuilders[subtype] = wrapper + p.sdBuilders[subtype] = newDialer return nil } -// RegisterPacketDialerWrapper will register a wrapper for packet dialers under the given subtype. -func (p *ConfigToDialer) RegisterPacketDialerWrapper(subtype string, wrapper NewPacketDialerFunc) error { +// RegisterPacketDialerType will register a wrapper for packet dialers under the given subtype. +func (p *ConfigToDialer) RegisterPacketDialerType(subtype string, newDialer NewPacketDialerFunc) error { if p.pdBuilders == nil { p.pdBuilders = make(map[string]NewPacketDialerFunc) } @@ -98,7 +98,7 @@ func (p *ConfigToDialer) RegisterPacketDialerWrapper(subtype string, wrapper New if _, found := p.pdBuilders[subtype]; found { return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) } - p.pdBuilders[subtype] = wrapper + p.pdBuilders[subtype] = newDialer return nil } @@ -126,7 +126,7 @@ func parseConfig(configText string) ([]*url.URL, error) { return urls, nil } -// WrapDialer creates a [Dialer] according to transportConfig, using dialer as the +// NewStreamDialer creates a [Dialer] according to transportConfig, using dialer as the // base [Dialer]. The given dialer must not be nil. func (p *ConfigToDialer) NewStreamDialer(transportConfig string) (transport.StreamDialer, error) { parts, err := parseConfig(transportConfig) @@ -136,7 +136,7 @@ func (p *ConfigToDialer) NewStreamDialer(transportConfig string) (transport.Stre return p.newStreamDialer(parts) } -// WrapDialer creates a [Dialer] according to transportConfig, using dialer as the +// NewPacketDialer creates a [Dialer] according to transportConfig, using dialer as the // base [Dialer]. The given dialer must not be nil. func (p *ConfigToDialer) NewPacketDialer(transportConfig string) (transport.PacketDialer, error) { parts, err := parseConfig(transportConfig) @@ -155,7 +155,7 @@ func (p *ConfigToDialer) newStreamDialer(configParts []*url.URL) (transport.Stre } thisURL := configParts[len(configParts)-1] innerConfig := configParts[:len(configParts)-1] - w, ok := p.sdBuilders[thisURL.Scheme] + newDialer, ok := p.sdBuilders[thisURL.Scheme] if !ok { return nil, fmt.Errorf("config scheme '%v' is not supported for Stream Dialers", thisURL.Scheme) } @@ -165,7 +165,7 @@ func (p *ConfigToDialer) newStreamDialer(configParts []*url.URL) (transport.Stre newPD := func() (transport.PacketDialer, error) { return p.newPacketDialer(innerConfig) } - return w(newSD, newPD, thisURL) + return newDialer(newSD, newPD, thisURL) } func (p *ConfigToDialer) newPacketDialer(configParts []*url.URL) (transport.PacketDialer, error) { @@ -177,7 +177,7 @@ func (p *ConfigToDialer) newPacketDialer(configParts []*url.URL) (transport.Pack } thisURL := configParts[len(configParts)-1] innerConfig := configParts[:len(configParts)-1] - w, ok := p.pdBuilders[thisURL.Scheme] + newDialer, ok := p.pdBuilders[thisURL.Scheme] if !ok { return nil, fmt.Errorf("config scheme '%v' is not supported for Packet Dialers", thisURL.Scheme) } @@ -187,7 +187,7 @@ func (p *ConfigToDialer) newPacketDialer(configParts []*url.URL) (transport.Pack newPD := func() (transport.PacketDialer, error) { return p.newPacketDialer(innerConfig) } - return w(newSD, newPD, thisURL) + return newDialer(newSD, newPD, thisURL) } // NewpacketListener creates a new [transport.PacketListener] according to the given config, From d8a0becc77368c9609d0b902c3d93aec62fa102f Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 12:39:46 -0400 Subject: [PATCH 3/9] Improvements --- x/config/config.go | 2 ++ x/config/shadowsocks.go | 15 +++++++++++--- x/config/shadowsocks_test.go | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 x/config/shadowsocks_test.go diff --git a/x/config/config.go b/x/config/config.go index b4fbeb04..cb864d42 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -28,7 +28,9 @@ import ( // ConfigToDialer enables the creation of stream and packet dialers based on a config. The config is // extensible by registering wrappers for config subtypes. type ConfigToDialer struct { + // Base StreamDialer to create direct stream connections. If you need direct stream connections, this must not be nil. BaseStreamDialer transport.StreamDialer + // Base PacketDialer to create direct packet connections. If you need direct packet connections, this must not be nil. BasePacketDialer transport.PacketDialer sdBuilders map[string]NewStreamDialerFunc pdBuilders map[string]NewPacketDialerFunc diff --git a/x/config/shadowsocks.go b/x/config/shadowsocks.go index 94760bed..33d93229 100644 --- a/x/config/shadowsocks.go +++ b/x/config/shadowsocks.go @@ -179,10 +179,19 @@ func parseStringPrefix(utf8Str string) ([]byte, error) { } func sanitizeShadowsocksURL(u *url.URL) (string, error) { - const redactedPlaceholder = "REDACTED" config, err := parseShadowsocksURL(u) if err != nil { - return "ss://ERROR", err + return "", err } - return "ss://" + redactedPlaceholder + "@" + config.serverAddress + "?prefix=" + url.PathEscape(string(config.prefix)), nil + values := make(url.Values) + if prefix := u.Query().Get("prefix"); prefix != "" { + values.Add("prefix", prefix) + } + cleanURL := url.URL{ + Scheme: "ss", + User: url.User("REDACTED"), + Host: config.serverAddress, + RawQuery: values.Encode(), + } + return cleanURL.String(), nil } diff --git a/x/config/shadowsocks_test.go b/x/config/shadowsocks_test.go new file mode 100644 index 00000000..0fad540b --- /dev/null +++ b/x/config/shadowsocks_test.go @@ -0,0 +1,38 @@ +// 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 config + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_sanitizeShadowsocksURL(t *testing.T) { + ssURL, err := url.Parse("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888") + require.NoError(t, err) + sanitized, err := sanitizeShadowsocksURL(ssURL) + require.NoError(t, err) + require.Equal(t, "ss://REDACTED@192.168.100.1:8888", sanitized) +} + +func Test_sanitizeShadowsocksURL_withPrefix(t *testing.T) { + ssURL, err := url.Parse("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888?prefix=foo") + require.NoError(t, err) + sanitized, err := sanitizeShadowsocksURL(ssURL) + require.NoError(t, err) + require.Equal(t, "ss://REDACTED@192.168.100.1:8888?prefix=foo", sanitized) +} From aadfe78272069c2a76c2ebebbb9f09cbef5fc034 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 12:53:37 -0400 Subject: [PATCH 4/9] Add Do53 --- x/config/config.go | 1 + x/config/dns.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/x/config/config.go b/x/config/config.go index cb864d42..d11c8447 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -49,6 +49,7 @@ func NewDefaultConfigToDialer() *ConfigToDialer { p.BasePacketDialer = &transport.UDPDialer{} // Please keep the list in alphabetical order. + p.RegisterStreamDialerType("do53", wrapStreamDialerWithDO53) p.RegisterStreamDialerType("doh", wrapStreamDialerWithDOH) p.RegisterStreamDialerType("override", wrapStreamDialerWithOverride) diff --git a/x/config/dns.go b/x/config/dns.go index e6633818..b47b9dac 100644 --- a/x/config/dns.go +++ b/x/config/dns.go @@ -15,6 +15,7 @@ package config import ( + "context" "errors" "fmt" "net" @@ -23,8 +24,58 @@ import ( "github.com/Jigsaw-Code/outline-sdk/dns" "github.com/Jigsaw-Code/outline-sdk/transport" + "golang.org/x/net/dns/dnsmessage" ) +func wrapStreamDialerWithDO53(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } + pd, err := innerPD() + if err != nil { + return nil, err + } + query := configURL.Opaque + values, err := url.ParseQuery(query) + if err != nil { + return nil, err + } + var address string + for key, values := range values { + switch strings.ToLower(key) { + case "address": + if len(values) != 1 { + return nil, fmt.Errorf("address option must has one value, found %v", len(values)) + } + address = values[0] + default: + return nil, fmt.Errorf("unsupported option %v", key) + + } + } + if address == "" { + return nil, errors.New("must set an address") + } + _, _, err = net.SplitHostPort(address) + if err != nil { + address = net.JoinHostPort(address, "53") + } + udpResolver := dns.NewUDPResolver(pd, address) + tcpResolver := dns.NewTCPResolver(sd, address) + resolver := dns.FuncResolver(func(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) { + msg, err := udpResolver.Query(ctx, q) + if err != nil { + return nil, err + } + if !msg.Header.Truncated { + return msg, err + } + return tcpResolver.Query(ctx, q) + }) + return dns.NewStreamDialer(resolver, sd) +} + func wrapStreamDialerWithDOH(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.StreamDialer, error) { query := configURL.Opaque values, err := url.ParseQuery(query) From 34a83c98afd2a3b624189e049c3a84fa5a8efe8a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 12:57:54 -0400 Subject: [PATCH 5/9] Add line --- x/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/x/config/config.go b/x/config/config.go index d11c8447..2d0939b4 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -50,6 +50,7 @@ func NewDefaultConfigToDialer() *ConfigToDialer { // Please keep the list in alphabetical order. p.RegisterStreamDialerType("do53", wrapStreamDialerWithDO53) + p.RegisterStreamDialerType("doh", wrapStreamDialerWithDOH) p.RegisterStreamDialerType("override", wrapStreamDialerWithOverride) From c0fe3aca7cac645d525d5a2d5421142741a4748e Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 13:09:47 -0400 Subject: [PATCH 6/9] Fix test --- x/config/config.go | 10 ++++++++-- x/config/config_test.go | 5 ++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x/config/config.go b/x/config/config.go index 2d0939b4..c5d9d8ec 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -237,9 +237,15 @@ func SanitizeConfig(transportConfig string) (string, error) { scheme := strings.ToLower(u.Scheme) switch scheme { case "ss": - textParts[i], _ = sanitizeShadowsocksURL(u) + textParts[i], err = sanitizeShadowsocksURL(u) + if err != nil { + return "", err + } case "socks5": - textParts[i], _ = sanitizeSocks5URL(u) + textParts[i], err = sanitizeSocks5URL(u) + if err != nil { + return "", err + } case "override", "split", "tls", "tlsfrag": // No sanitization needed textParts[i] = u.String() diff --git a/x/config/config_test.go b/x/config/config_test.go index ada5f458..2308c34c 100644 --- a/x/config/config_test.go +++ b/x/config/config_test.go @@ -28,13 +28,12 @@ func TestSanitizeConfig(t *testing.T) { // Test that a invalid cypher is rejected. sanitizedConfig, err := SanitizeConfig("split:5|ss://jhvdsjkfhvkhsadvf@example.com:1234?prefix=HTTP%2F1.1%20") - require.NoError(t, err) - require.Equal(t, "split:5|ss://ERROR", sanitizedConfig) + require.Error(t, err) // Test that a valid config is accepted and user info is redacted. sanitizedConfig, err = SanitizeConfig("split:5|ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20") require.NoError(t, err) - require.Equal(t, "split:5|ss://REDACTED@example.com:1234?prefix=HTTP%2F1.1%20", sanitizedConfig) + require.Equal(t, "split:5|ss://REDACTED@example.com:1234?prefix=HTTP%2F1.1+", sanitizedConfig) // Test sanitizer with unknown transport. sanitizedConfig, err = SanitizeConfig("split:5|vless://ac08785d-203d-4db4-915c-eb4e23435fd62@example.com:443?path=%2Fvless&security=tls&encryption=none&alpn=h2&host=sub.hello.com&fp=chrome&type=ws&sni=sub.hello.com#vless-ws-tls-cdn") From c2ea9f375b18f18556411484554f68722f6d0eb3 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 16:11:59 -0400 Subject: [PATCH 7/9] Update --- x/config/dns.go | 2 +- x/config/doc.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/x/config/dns.go b/x/config/dns.go index b47b9dac..218bea7e 100644 --- a/x/config/dns.go +++ b/x/config/dns.go @@ -69,7 +69,7 @@ func wrapStreamDialerWithDO53(innerSD func() (transport.StreamDialer, error), in return nil, err } if !msg.Header.Truncated { - return msg, err + return msg, nil } return tcpResolver.Query(ctx, q) }) diff --git a/x/config/doc.go b/x/config/doc.go index 9bf1cb91..6152b628 100644 --- a/x/config/doc.go +++ b/x/config/doc.go @@ -44,6 +44,13 @@ SOCKS5 proxy (currently streams only, package [github.com/Jigsaw-Code/outline-sd USERINFO field is optional and only required if username and password authentication is used. It is in the format of username:password. +DNS resolution (streams only, package [github.com/Jigsaw-Code/outline-sdk/dns]) + +It takes a host:port address. If the port is missing, it will use 53. The resulting dialer will use the input dialer with +Happy Eyeballs to connect to the destination. + + do53:address=[ADDRESS] + DNS-over-HTTPS resolution (streams only, package [github.com/Jigsaw-Code/outline-sdk/dns]) It takes a host name and a host:port address. The name will be used in the SNI and Host header, while the address is used to connect From 69ebaea588d479e5df5582aea44ef57022c8da68 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 3 May 2024 16:14:15 -0400 Subject: [PATCH 8/9] Add comment. --- x/config/dns.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/config/dns.go b/x/config/dns.go index 218bea7e..941fba41 100644 --- a/x/config/dns.go +++ b/x/config/dns.go @@ -71,6 +71,8 @@ func wrapStreamDialerWithDO53(innerSD func() (transport.StreamDialer, error), in if !msg.Header.Truncated { return msg, nil } + // If the message is truncated, retry over TCP. + // See https://datatracker.ietf.org/doc/html/rfc1123#page-75. return tcpResolver.Query(ctx, q) }) return dns.NewStreamDialer(resolver, sd) From 8b5e5f0691baa498ebb26cda03184751f3c93c3a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Mon, 6 May 2024 19:59:14 -0400 Subject: [PATCH 9/9] Add copyright --- x/config/socks5.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x/config/socks5.go b/x/config/socks5.go index 0788588a..9820a148 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -1,3 +1,17 @@ +// 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 config import (