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

chore(client): introduce extensible YAML config in Go #2306

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
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
77 changes: 22 additions & 55 deletions client/go/outline/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,20 @@
package outline

import (
"fmt"
"net"
"context"

"github.com/Jigsaw-Code/outline-apps/client/go/outline/config"
"github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors"
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
"github.com/eycorsican/go-tun2socks/common/log"
)

// Client provides a transparent container for [transport.StreamDialer] and [transport.PacketListener]
// that is exportable (as an opaque object) via gobind.
// It's used by the connectivity test and the tun2socks handlers.
// TODO: Rename to Transport. Needs to update per-platform code.
type Client struct {
transport.StreamDialer
transport.PacketListener
*config.Dialer[transport.StreamConn]
*config.PacketListener
}

// NewClientResult represents the result of [NewClientAndReturnError].
Expand All @@ -42,61 +41,29 @@ type NewClientResult struct {

// NewClient creates a new Outline client from a configuration string.
func NewClient(transportConfig string) *NewClientResult {
config, err := parseConfigFromJSON(transportConfig)
transportYAML, err := config.ParseConfigYAML(transportConfig)
if err != nil {
return &NewClientResult{Error: platerrors.ToPlatformError(err)}
}
prefixBytes, err := ParseConfigPrefixFromString(config.Prefix)
if err != nil {
return &NewClientResult{Error: platerrors.ToPlatformError(err)}
}

client, err := newShadowsocksClient(config.Host, int(config.Port), config.Method, config.Password, prefixBytes)
return &NewClientResult{
Client: client,
Error: platerrors.ToPlatformError(err),
}
}

func newShadowsocksClient(host string, port int, cipherName, password string, prefix []byte) (*Client, error) {
if err := validateConfig(host, port, cipherName, password); err != nil {
return nil, err
}

// TODO: consider using net.LookupIP to get a list of IPs, and add logic for optimal selection.
proxyAddress := net.JoinHostPort(host, fmt.Sprint(port))

cryptoKey, err := shadowsocks.NewEncryptionKey(cipherName, password)
if err != nil {
return nil, newIllegalConfigErrorWithDetails("cipher&password pair is not valid",
"cipher|password", cipherName+"|"+password, "valid combination", err)
}

// We disable Keep-Alive as per https://datatracker.ietf.org/doc/html/rfc1122#page-101, which states that it should only be
// enabled in server applications. This prevents the device from unnecessarily waking up to send keep alives.
streamDialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress, Dialer: net.Dialer{KeepAlive: -1}}, cryptoKey)
if err != nil {
return nil, platerrors.PlatformError{
Code: platerrors.SetupTrafficHandlerFailed,
Message: "failed to create TCP traffic handler",
Details: platerrors.ErrorDetails{"proxy-protocol": "shadowsocks", "handler": "tcp"},
Cause: platerrors.ToPlatformError(err),
return &NewClientResult{
Error: &platerrors.PlatformError{
Code: platerrors.IllegalConfig,
Message: "config is not valid YAML",
Cause: platerrors.ToPlatformError(err),
},
}
}
if len(prefix) > 0 {
log.Debugf("Using salt prefix: %s", string(prefix))
streamDialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(prefix)
}

packetListener, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress}, cryptoKey)
transportPair, err := config.NewDefaultTransportProvider().Parse(context.Background(), transportYAML)
if err != nil {
return nil, platerrors.PlatformError{
Code: platerrors.SetupTrafficHandlerFailed,
Message: "failed to create UDP traffic handler",
Details: platerrors.ErrorDetails{"proxy-protocol": "shadowsocks", "handler": "udp"},
Cause: platerrors.ToPlatformError(err),
return &NewClientResult{
Error: &platerrors.PlatformError{
Code: platerrors.IllegalConfig,
Message: "failed to create transport",
Cause: platerrors.ToPlatformError(err),
},
}
}

return &Client{StreamDialer: streamDialer, PacketListener: packetListener}, nil
return &NewClientResult{
Client: &Client{Dialer: transportPair.StreamDialer, PacketListener: transportPair.PacketListener},
}
}
196 changes: 195 additions & 1 deletion client/go/outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,201 @@

package outline

import "testing"
import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_NewTransport_SS_URL(t *testing.T) {
config := "ss://[email protected]:4321/"
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Legacy_JSON(t *testing.T) {
config := `{
"server": "example.com",
"server_port": 4321,
"method": "chacha20-ietf-poly1305",
"password": "SECRET"
}`
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Flexible_JSON(t *testing.T) {
config := `{
# Comment
server: example.com,
server_port: 4321,
method: chacha20-ietf-poly1305,
password: SECRET
}`
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_YAML(t *testing.T) {
config := `# Comment
server: example.com
server_port: 4321
method: chacha20-ietf-poly1305
password: SECRET`
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Explicit_endpoint(t *testing.T) {
config := `
endpoint:
$parser: dial
address: example.com:4321
cipher: chacha20-ietf-poly1305
secret: SECRET`
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Multihop_URL(t *testing.T) {
config := `
endpoint:
$parser: dial
address: exit.example.com:4321
dialer: ss://[email protected]:4321/
cipher: chacha20-ietf-poly1305
secret: SECRET`
firstHop := "entry.example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Multihop_Explicit(t *testing.T) {
config := `
endpoint:
$parser: dial
address: exit.example.com:4321
dialer:
$parser: shadowsocks
endpoint: entry.example.com:4321
cipher: chacha20-ietf-poly1305
secret: ENTRY_SECRET
cipher: chacha20-ietf-poly1305
secret: EXIT_SECRET`
firstHop := "entry.example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Explicit_TCPUDP(t *testing.T) {
config := `
$parser: tcpudp
tcp:
$parser: shadowsocks
endpoint: example.com:80
cipher: chacha20-ietf-poly1305
secret: SECRET
prefix: "POST "
udp:
$parser: shadowsocks
endpoint: example.com:53
cipher: chacha20-ietf-poly1305
secret: SECRET`

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, "example.com:80", result.Client.Dialer.FirstHop)
require.Equal(t, "example.com:53", result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_YAML_Reuse(t *testing.T) {
config := `
$parser: tcpudp
udp: &base
$parser: shadowsocks
endpoint: example.com:4321
cipher: chacha20-ietf-poly1305
secret: SECRET
tcp:
<<: *base
prefix: "POST "`
firstHop := "example.com:4321"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_YAML_Partial_Reuse(t *testing.T) {
config := `
$parser: tcpudp
tcp:
$parser: shadowsocks
endpoint: example.com:80
<<: &cipher
cipher: chacha20-ietf-poly1305
secret: SECRET
prefix: "POST "
udp:
$parser: shadowsocks
endpoint: example.com:53
<<: *cipher`

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, "example.com:80", result.Client.Dialer.FirstHop)
require.Equal(t, "example.com:53", result.Client.PacketListener.FirstHop)
}

func Test_NewTransport_Websocket(t *testing.T) {
config := `
$parser: tcpudp
tcp: &base
$parser: shadowsocks
endpoint:
$parser: websocket
url: https://entrypoint.cdn.example.com/tcp
cipher: chacha20-ietf-poly1305
secret: SECRET
udp:
<<: *base
endpoint:
$parser: websocket
url: https://entrypoint.cdn.example.com/udp`
firstHop := "entrypoint.cdn.example.com:443"

result := NewClient(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Client.Dialer.FirstHop)
require.Equal(t, firstHop, result.Client.PacketListener.FirstHop)
}

func Test_NewClientFromJSON_Errors(t *testing.T) {
tests := []struct {
Expand Down
Loading
Loading