Skip to content

Commit

Permalink
feat: enable custom transports in configs (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Apr 19, 2024
1 parent 47952bc commit 2318bba
Show file tree
Hide file tree
Showing 19 changed files with 168 additions and 103 deletions.
193 changes: 113 additions & 80 deletions x/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,94 @@ import (
"github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag"
)

// ConfigParser 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
}

// NewDefaultConfigParser creates a [ConfigParser] with a set of default wrappers already registered.
func NewDefaultConfigParser() *ConfigParser {
p := new(ConfigParser)

// Please keep the list in alphabetical order.

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:<number> 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("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) {
lenStr := wrapConfig.Opaque
fixedLen, err := strconv.Atoi(lenStr)
if err != nil {
return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag:<number> 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 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)
}

if _, found := p.sdWrapers[subtype]; found {
return fmt.Errorf("config parser %v for StreamDialer added twice", subtype)
}
p.sdWrapers[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)
}

if _, found := p.pdWrappers[subtype]; found {
return fmt.Errorf("config parser %v for PacketDialer added twice", subtype)
}
p.pdWrappers[subtype] = wrapper
return nil
}

func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
oneDialerConfig = strings.TrimSpace(oneDialerConfig)
if oneDialerConfig == "" {
Expand All @@ -42,118 +130,63 @@ func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
return url, nil
}

// NewStreamDialer creates a new [transport.StreamDialer] according to the given config.
func NewStreamDialer(transportConfig string) (transport.StreamDialer, error) {
return WrapStreamDialer(&transport.TCPDialer{}, transportConfig)
}

// WrapStreamDialer created a [transport.StreamDialer] according to transportConfig, using dialer as the
// WrapStreamDialer creates a [transport.StreamDialer] according to transportConfig, using dialer as the
// base [transport.StreamDialer]. The given dialer must not be nil.
func WrapStreamDialer(dialer transport.StreamDialer, transportConfig string) (transport.StreamDialer, error) {
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
}
var err error
for _, part := range strings.Split(transportConfig, "|") {
dialer, err = newStreamDialerFromPart(dialer, part)
url, err := parseConfigPart(part)
if err != nil {
return nil, err
}
}
return dialer, nil
}

func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig string) (transport.StreamDialer, error) {
url, err := parseConfigPart(oneDialerConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse config part: %w", err)
}

// Please keep scheme list sorted.
switch strings.ToLower(url.Scheme) {
case "override":
return newOverrideStreamDialerFromURL(innerDialer, url)

case "socks5":
return newSOCKS5StreamDialerFromURL(innerDialer, url)

case "split":
prefixBytesStr := url.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:<number> format", prefixBytesStr)
w, ok := p.sdWrapers[url.Scheme]
if !ok {
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
return split.NewStreamDialer(innerDialer, int64(prefixBytes))

case "ss":
return newShadowsocksStreamDialerFromURL(innerDialer, url)

case "tls":
return newTlsStreamDialerFromURL(innerDialer, url)

case "tlsfrag":
lenStr := url.Opaque
fixedLen, err := strconv.Atoi(lenStr)
dialer, err = w(dialer, url)
if err != nil {
return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag:<number> format", lenStr)
return nil, err
}
return tlsfrag.NewFixedLenStreamDialer(innerDialer, fixedLen)

default:
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
return dialer, nil
}

// NewPacketDialer creates a new [transport.PacketDialer] according to the given config.
func NewPacketDialer(transportConfig string) (dialer transport.PacketDialer, err error) {
dialer = &transport.UDPDialer{}
// 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")
}
transportConfig = strings.TrimSpace(transportConfig)
if transportConfig == "" {
return dialer, nil
}
for _, part := range strings.Split(transportConfig, "|") {
dialer, err = newPacketDialerFromPart(dialer, part)
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
}
}
return dialer, nil
}

func newPacketDialerFromPart(innerDialer transport.PacketDialer, oneDialerConfig string) (transport.PacketDialer, error) {
url, err := parseConfigPart(oneDialerConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse config part: %w", err)
}

// Please keep scheme list sorted.
switch strings.ToLower(url.Scheme) {
case "override":
return newOverridePacketDialerFromURL(innerDialer, url)

case "socks5":
return nil, errors.New("socks5 is not supported for PacketDialers")

case "split":
return nil, errors.New("split is not supported for PacketDialers")

case "ss":
return newShadowsocksPacketDialerFromURL(innerDialer, url)

case "tls":
return nil, errors.New("tls is not yet supported for PacketDialers")

default:
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
}

// 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 == "" {
return nil, errors.New("config is required")
Expand Down
14 changes: 14 additions & 0 deletions x/config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ 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:
// create new config parser
// p := new(ConfigParser)
// or
p := NewDefaultConfigParser()
// register your custom dialer
p.RegisterPacketDialerWrapper("custom", wrapStreamDialerWithCustom)
p.RegisterStreamDialerWrapper("custom", wrapPacketDialerWithCustom)
// then use it
dialer, err := p.WrapStreamDialer(innerDialer, "custom://config")
where wrapStreamDialerWithCustom and wrapPacketDialerWithCustom implement [WrapPacketDialerFunc] and [WrapStreamDialerFunc].
[Onion Routing]: https://en.wikipedia.org/wiki/Onion_routing
*/
package config
4 changes: 2 additions & 2 deletions x/config/override.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func newOverrideFromURL(configURL *url.URL) (func(string) (string, error), error
}, nil
}

func newOverrideStreamDialerFromURL(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
func wrapStreamDialerWithOverride(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
override, err := newOverrideFromURL(configURL)
if err != nil {
return nil, err
Expand All @@ -80,7 +80,7 @@ func newOverrideStreamDialerFromURL(innerDialer transport.StreamDialer, configUR
}), nil
}

func newOverridePacketDialerFromURL(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) {
func wrapPacketDialerWithOverride(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) {
override, err := newOverrideFromURL(configURL)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions x/config/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
)

func newShadowsocksStreamDialerFromURL(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
func wrapStreamDialerWithShadowsocks(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
config, err := parseShadowsocksURL(configURL)
if err != nil {
return nil, err
Expand All @@ -41,7 +41,7 @@ func newShadowsocksStreamDialerFromURL(innerDialer transport.StreamDialer, confi
return dialer, nil
}

func newShadowsocksPacketDialerFromURL(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) {
func wrapPacketDialerWithShadowsocks(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) {
config, err := parseShadowsocksURL(configURL)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion x/config/socks5.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/Jigsaw-Code/outline-sdk/transport/socks5"
)

func newSOCKS5StreamDialerFromURL(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
func wrapStreamDialerWithSOCKS5(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
endpoint := transport.StreamDialerEndpoint{Dialer: innerDialer, Address: configURL.Host}
dialer, err := socks5.NewStreamDialer(&endpoint)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion x/config/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func parseOptions(configURL *url.URL) ([]tls.ClientOption, error) {
return options, nil
}

func newTlsStreamDialerFromURL(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
func wrapStreamDialerWithTLS(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
options, err := parseOptions(configURL)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion x/config/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
func TestTLS(t *testing.T) {
tlsURL, err := url.Parse("tls")
require.NoError(t, err)
_, err = newTlsStreamDialerFromURL(&transport.TCPDialer{}, tlsURL)
_, err = wrapStreamDialerWithTLS(&transport.TCPDialer{}, tlsURL)
require.NoError(t, err)
}

Expand Down
5 changes: 3 additions & 2 deletions x/examples/fetch-speed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/config"
)

Expand All @@ -43,7 +44,7 @@ func main() {
verboseFlag := flag.Bool("v", false, "Enable debug output")
transportFlag := flag.String("transport", "", "Transport config")
methodFlag := flag.String("method", "GET", "The HTTP method to use")
timeoutFlag := flag.Duration("timeout", 10 * time.Second, "The HTTP timeout value")
timeoutFlag := flag.Duration("timeout", 10*time.Second, "The HTTP timeout value")

flag.Parse()

Expand All @@ -58,7 +59,7 @@ func main() {
os.Exit(1)
}

dialer, err := config.NewStreamDialer(*transportFlag)
dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag)
if err != nil {
log.Fatalf("Could not create dialer: %v\n", err)
}
Expand Down
3 changes: 2 additions & 1 deletion x/examples/fetch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/config"
)

Expand Down Expand Up @@ -68,7 +69,7 @@ func main() {
os.Exit(1)
}

dialer, err := config.NewStreamDialer(*transportFlag)
dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag)
if err != nil {
log.Fatalf("Could not create dialer: %v\n", err)
}
Expand Down
4 changes: 3 additions & 1 deletion x/examples/http2transport/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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"
)
Expand All @@ -34,7 +35,8 @@ 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.NewStreamDialer(*transportFlag)
dialer, err := config.NewDefaultConfigParser().WrapStreamDialer(&transport.TCPDialer{}, *transportFlag)

if err != nil {
log.Fatalf("Could not create dialer: %v", err)
}
Expand Down
Loading

0 comments on commit 2318bba

Please sign in to comment.