Skip to content

Commit

Permalink
Add Websocket support
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Dec 26, 2024
1 parent 3962502 commit cacf818
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 42 deletions.
23 changes: 23 additions & 0 deletions client/go/outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ udp:
require.Equal(t, "example.com:53", result.Transport.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 := NewTransport(config)
require.Nil(t, result.Error, "Got %v", result.Error)
require.Equal(t, firstHop, result.Transport.Dialer.FirstHop)
require.Equal(t, firstHop, result.Transport.PacketListener.FirstHop)
}

func Test_NewClientFromJSON_Errors(t *testing.T) {
tests := []struct {
name string
Expand Down
45 changes: 4 additions & 41 deletions client/go/outline/config/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"net"
"net/http"

"github.com/Jigsaw-Code/outline-sdk/transport"
)
Expand Down Expand Up @@ -72,43 +73,6 @@ func (t *TransportPair) ListenPacket(ctx context.Context) (net.PacketConn, error
return t.PacketListener.ListenPacket(ctx)
}

// // NewClientProvider creates a [ProviderContainer] with the base instances properly initialized.
// func NewClientProvider() *ExtensibleProvider[*TransportClient], FunctionRegistry[] {
// clients := NewExtensibleProvider[*TransportClient](nil)
// return clients

// defaultStreamDialer := &Dialer[transport.StreamConn]{ConnectionProviderInfo{ConnTypeDirect, ""}, (&transport.TCPDialer{}).DialStream}
// defaultPacketDialer := &Dialer[net.Conn]{ConnectionProviderInfo{ConnTypeDirect, ""}, (&transport.UDPDialer{}).DialPacket}

// return &ProviderContainer{
// StreamDialers: NewExtensibleProvider(defaultStreamDialer),
// PacketDialers: NewExtensibleProvider(defaultPacketDialer),
// PacketListeners: NewExtensibleProvider(&PacketListener{ConnectionProviderInfo{ConnTypeDirect, ""}, &transport.UDPListener{}}),
// StreamEndpoints: NewExtensibleProvider[*Endpoint[transport.StreamConn]](nil),
// PacketEndpoints: NewExtensibleProvider[*Endpoint[net.Conn]](nil),
// }
// }

// // RegisterDefaultProviders registers a set of default providers with the providers in [ProviderContainer].
// func RegisterDefaultProviders(c *ProviderContainer) *ProviderContainer {
// registerDirectDialEndpoint(c.StreamEndpoints, "string", c.StreamDialers.NewInstance)
// registerDirectDialEndpoint(c.StreamEndpoints, "dial", c.StreamDialers.NewInstance)
// registerDirectDialEndpoint(c.PacketEndpoints, "string", c.PacketDialers.NewInstance)
// registerDirectDialEndpoint(c.PacketEndpoints, "dial", c.PacketDialers.NewInstance)

// registerShadowsocksStreamDialer(c.StreamDialers, ProviderTypeDefault, c.StreamEndpoints.NewInstance)
// registerShadowsocksStreamDialer(c.StreamDialers, "ss", c.StreamEndpoints.NewInstance)
// registerShadowsocksStreamDialer(c.StreamDialers, "string", c.StreamEndpoints.NewInstance)

// registerShadowsocksPacketDialer(c.PacketDialers, "ss", c.PacketEndpoints.NewInstance)
// registerShadowsocksPacketDialer(c.PacketDialers, "string", c.PacketEndpoints.NewInstance)

// registerShadowsocksPacketListener(c.PacketListeners, ProviderTypeDefault, c.PacketEndpoints.NewInstance)
// registerShadowsocksPacketListener(c.PacketListeners, "ss", c.PacketEndpoints.NewInstance)
// registerShadowsocksPacketListener(c.PacketListeners, "string", c.PacketEndpoints.NewInstance)
// return c
// }

func NewDefaultTransportProvider() *TypeParser[*TransportPair] {
var streamEndpoints *TypeParser[*Endpoint[transport.StreamConn]]
var packetEndpoints *TypeParser[*Endpoint[net.Conn]]
Expand Down Expand Up @@ -181,13 +145,12 @@ func NewDefaultTransportProvider() *TypeParser[*TransportPair] {
})

// Websocket support.
httpClient := http.DefaultClient
streamEndpoints.RegisterSubParser("websocket", func(ctx context.Context, input map[string]any) (*Endpoint[transport.StreamConn], error) {
// TODO
return nil, errors.ErrUnsupported
return parseWebsocketStreamEndpoint(ctx, input, httpClient)
})
packetEndpoints.RegisterSubParser("websocket", func(ctx context.Context, input map[string]any) (*Endpoint[net.Conn], error) {
// TODO
return nil, errors.ErrUnsupported
return parseWebsocketPacketEndpoint(ctx, input, httpClient)
})

transports.RegisterSubParser("tcpudp", func(ctx context.Context, config map[string]any) (*TransportPair, error) {
Expand Down
2 changes: 1 addition & 1 deletion client/go/outline/config/tcpudp.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type TCPUDPConfig struct {
func parseTCPUDPTransportPair(ctx context.Context, configMap map[string]any, parseSD ParseFunc[*Dialer[transport.StreamConn]], parsePL ParseFunc[*PacketListener]) (*TransportPair, error) {
var config TCPUDPConfig
if err := mapToAny(configMap, &config); err != nil {
return nil, fmt.Errorf("failed to parse TCPUDPConfig: %w", err)
return nil, fmt.Errorf("invalid config format: %w", err)
}

sd, err := parseSD(ctx, config.TCP)
Expand Down
100 changes: 100 additions & 0 deletions client/go/outline/config/websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2024 The Outline Authors
//
// 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
//
// http://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 (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/coder/websocket"
)

type WebsocketEndpointConfig struct {
URL string
HTTP_Client ConfigNode
}

func parseWebsocketStreamEndpoint(ctx context.Context, configMap map[string]any, httpClient *http.Client) (*Endpoint[transport.StreamConn], error) {
return parseWebsocketEndpoint(ctx, configMap, httpClient, func(c *websocket.Conn) transport.StreamConn {
return &netToStreamConn{websocket.NetConn(context.Background(), c, websocket.MessageBinary)}
})
}

func parseWebsocketPacketEndpoint(ctx context.Context, configMap map[string]any, httpClient *http.Client) (*Endpoint[net.Conn], error) {
return parseWebsocketEndpoint(ctx, configMap, httpClient, func(c *websocket.Conn) net.Conn {
return websocket.NetConn(context.Background(), c, websocket.MessageBinary)
})
}

func parseWebsocketEndpoint[ConnType any](_ context.Context, configMap map[string]any, httpClient *http.Client, wsToConn func(*websocket.Conn) ConnType) (*Endpoint[ConnType], error) {
var config WebsocketEndpointConfig
if err := mapToAny(configMap, &config); err != nil {
return nil, fmt.Errorf("invalid config format: %w", err)
}

url, err := url.Parse(config.URL)
if err != nil {
return nil, fmt.Errorf("url is invalid: %w", err)
}

if config.HTTP_Client != nil {
return nil, errors.New("http_client not yet supported")
}

port := url.Port()
if port == "" {
switch url.Scheme {
case "https", "wss":
port = "443"
case "http", "ws":
port = "80"
}
}

options := &websocket.DialOptions{HTTPClient: httpClient}
return &Endpoint[ConnType]{
ConnectionProviderInfo: ConnectionProviderInfo{ConnType: ConnTypeDirect, FirstHop: net.JoinHostPort(url.Hostname(), port)},
Connect: func(ctx context.Context) (ConnType, error) {
var zero ConnType
conn, _, err := websocket.Dial(ctx, config.URL, options)

if err != nil {
return zero, err
}
return wsToConn(conn), nil
},
}, nil
}

// netToStreamConn converts a [net.Conn] to a [transport.StreamConn].
type netToStreamConn struct {
net.Conn
}

var _ transport.StreamConn = (*netToStreamConn)(nil)

func (c *netToStreamConn) CloseRead() error {
// Do nothing.
return nil
}

func (c *netToStreamConn) CloseWrite() error {
return c.Close()
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22

require (
github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854
github.com/coder/websocket v1.8.12
github.com/eycorsican/go-tun2socks v1.16.11
github.com/go-task/task/v3 v3.36.0
github.com/google/addlicense v1.1.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand Down

0 comments on commit cacf818

Please sign in to comment.