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(client): introduce extensible YAML config in Go #2306

Open
wants to merge 49 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c615901
Initial Implementation
fortuna Nov 22, 2024
a4c894d
Add DialEndpoint
fortuna Nov 25, 2024
0d1b9ec
Wire new config
fortuna Nov 26, 2024
e81cb45
Add URL
fortuna Nov 26, 2024
455a2ad
Wire Endpoint
fortuna Nov 27, 2024
5a85e73
Cleanup
fortuna Nov 27, 2024
87eaa89
Tweaks
fortuna Dec 2, 2024
fbddc57
Merge branch 'master' into fortuna-ws-config
fortuna Dec 6, 2024
131e564
Progress
fortuna Dec 6, 2024
a26708c
Make it work
fortuna Dec 9, 2024
1f89c98
Simplify
fortuna Dec 10, 2024
0aa3dbc
More simplification
fortuna Dec 10, 2024
e3738e7
Merge branch 'master' into fortuna-ws-config
fortuna Dec 10, 2024
bc20623
Revert Go version
fortuna Dec 10, 2024
cde35c5
Progress
fortuna Dec 10, 2024
0128549
Fixes
fortuna Dec 10, 2024
e05542f
Fix tests
fortuna Dec 11, 2024
6c199bf
Fix
fortuna Dec 11, 2024
dbaf0a2
Add test
fortuna Dec 11, 2024
cffd70a
Add default provider
fortuna Dec 11, 2024
d46455e
Clean up
fortuna Dec 10, 2024
f63d2b9
Update tests
fortuna Dec 20, 2024
8df547f
Revamp config
fortuna Dec 24, 2024
52dcd84
Clean up and TODOs
fortuna Dec 24, 2024
5b80741
Naming
fortuna Dec 26, 2024
abcbb42
Clean up test
fortuna Dec 26, 2024
70a4a28
More tests
fortuna Dec 26, 2024
5ed3701
Better test
fortuna Dec 26, 2024
6141ff2
Add tcpudp
fortuna Dec 26, 2024
3962502
More tests
fortuna Dec 26, 2024
cacf818
Add Websocket support
fortuna Dec 26, 2024
2c59ed5
Fix
fortuna Dec 27, 2024
7304fdc
Lint fixes
fortuna Dec 27, 2024
b3f7dd6
It works!
fortuna Dec 27, 2024
5b0de18
Fix display
fortuna Dec 30, 2024
fa05c42
Try Coder
fortuna Jan 2, 2025
79c0a73
Comment
fortuna Jan 6, 2025
24f0544
Remove Websocket
fortuna Jan 6, 2025
777da9b
Merge branch 'master' into fortuna-ws-config
fortuna Jan 6, 2025
612d5db
Fix Android
fortuna Jan 6, 2025
ebb993f
Add net
fortuna Jan 6, 2025
01d95d2
Fix Electron
fortuna Jan 6, 2025
486aabb
Review changes
fortuna Jan 6, 2025
fdd54d5
Merge branch 'master' into fortuna-ws-config
fortuna Jan 6, 2025
5ab7ea9
fixes
fortuna Jan 6, 2025
d18deae
Fix Linux
fortuna Jan 6, 2025
b4a6e1f
Fixes
fortuna Jan 7, 2025
025f102
Tests
fortuna Jan 7, 2025
3334519
Lint
fortuna Jan 7, 2025
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we still accepting the old JSON format here? Existing dynamic keys are still using JSON.

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},
}
}
199 changes: 198 additions & 1 deletion client/go/outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,204 @@

package outline

import "testing"
import (
"testing"

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

func Test_NewTransport_SS_URL(t *testing.T) {
config := "ss://[email protected]:4321/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to test multiple new-line separated ss keys, too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't support multiple keys yet.

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)
}

/*
TODO: Add Websocket support
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