Skip to content

Commit

Permalink
fix: introduce first-supported Endpoint (#2357)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Feb 3, 2025
1 parent 0fef7d5 commit d1cba53
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 1 deletion.
45 changes: 44 additions & 1 deletion client/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Outline uses a YAML-based configuration to define VPN parameters and handle TCP/
The top-level configuration specifies a [TunnelConfig](#TunnelConfig).

## Examples

A typical Shadowsocks configuration will look like this:

```yaml
Expand Down Expand Up @@ -93,6 +94,7 @@ transport:
```

Note that the Websocket endpoint can, in turn, take an endpoint, which can be leveraged to bypass DNS-based blocking:

```yaml
transport:
$type: tcpudp
Expand All @@ -115,6 +117,34 @@ transport:
secret: SS_SECRET
```

Note that Websockets is not yet supported on Windows. In order to have a single config for all platforms, use a `first-supported` for backwards-compatibility:

```yaml
transport:
$type: tcpudp
tcp:
$type: shadowsocks
endpoint:
$type: first-supported
endpoints:
- $type: websocket
url: wss://legendary-faster-packs-und.trycloudflare.com/SECRET_PATH/tcp
- ss.example.com:4321
cipher: chacha20-ietf-poly1305
secret: SS_SECRET
udp:
$type: shadowsocks
endpoint:
$type: first-supported
endpoints:
- $type: websocket
url: wss://legendary-faster-packs-und.trycloudflare.com/SECRET_PATH/udp
- ss.example.com:4321
cipher: chacha20-ietf-poly1305
secret: SS_SECRET
```

## Tunnels

### <a id=TunnelConfig></a>TunnelConfig
Expand Down Expand Up @@ -208,6 +238,7 @@ The _string_ Endpoint is the host:port address of the desired endpoint. The conn
Supported Interface types for Stream and Packet Endpoints:

- `dial`: [DialEndpointConfig](#DialEndpointConfig)
- `first-supported`: [FirstSupportedConfig](#FirstSupportedConfig)
- `websocket`: [WebsocketEndpointConfig](#WebsocketEndpointConfig)
<!-- TODO(fortuna): Add Shadowsocks endpoint
- `shadowsocks`: [ShadowsocksConfig](#ShadowsocksConfig)
Expand All @@ -222,7 +253,7 @@ Establishes connections by dialing a fixed address. It can take a dialer, which
**Fields:**

- `address` (_string_): the endpoint address to dial
- `dialer` ([DialerConfig](#dialers)): the dialer to use to dial the address
- `dialer` ([DialerConfig](#DialerConfig)): the dialer to use to dial the address

### <a id=WebsocketEndpointConfig></a>WebsocketEndpointConfig

Expand Down Expand Up @@ -250,6 +281,7 @@ The _null_ (absent) Dialer means the default Dialer, which uses direct TCP conne

Supported Interface types for Stream and Packer Dialers:

- `first-supported`: [FirstSupportedConfig](#FirstSupportedConfig)
- `shadowsocks`: [ShadowsocksConfig](#ShadowsocksConfig)

## Packet Listeners
Expand All @@ -264,6 +296,7 @@ The _null_ (absent) Packet Listener means the default Packet Listener, which is

Supported Interface types:

- `first-supported`: [FirstSupportedConfig](#FirstSupportedConfig)
- `shadowsocks`: [ShadowsocksPacketListenerConfig](#ShadowsocksConfig)

## Strategies
Expand Down Expand Up @@ -332,6 +365,16 @@ prefix: "POST "

## Meta Definitions

### <a id=FirstSupportedConfig></a>FirstSupportedConfig

Uses the first config that is supported by the application. This is a way to incorporate new configs while being backwards-compatible with old configs.

**Format:** _struct_

**Fields:**

- `options` ([EndpointConfig[]](#EndpointConfig) | [DialerConfig[]](#DialerConfig) | [PacketListenerConfig[]](#PacketListenerConfig)): list of options to consider

### <a id=Interface></a>Interface

Interfaces allow for choosing one of multiple implementations. It uses the `$type` field to specify the type that config represents.
Expand Down
46 changes: 46 additions & 0 deletions client/go/outline/config/config_first_supported.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 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"
)

type FirstSupportedConfig struct {
Options []any
}

func parseFirstSupported[Output any](ctx context.Context, configMap map[string]any, parseE ParseFunc[Output]) (Output, error) {
var zero Output
var config FirstSupportedConfig
if err := mapToAny(configMap, &config); err != nil {
return zero, fmt.Errorf("invalid config format: %w", err)
}

if len(config.Options) == 0 {
return zero, errors.New("empty list of options")
}

for _, ec := range config.Options {
endpoint, err := parseE(ctx, ec)
if errors.Is(err, errors.ErrUnsupported) {
continue
}
return endpoint, err
}
return zero, fmt.Errorf("no suported option found: %w", errors.ErrUnsupported)
}
17 changes: 17 additions & 0 deletions client/go/outline/config/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ func NewDefaultTransportProvider(tcpDialer transport.StreamDialer, udpDialer tra
return parseShadowsocksTransport(ctx, input, streamEndpoints.Parse, packetEndpoints.Parse)
})

// First-Supported support.
streamEndpoints.RegisterSubParser("first-supported", func(ctx context.Context, input map[string]any) (*Endpoint[transport.StreamConn], error) {
return parseFirstSupported(ctx, input, streamEndpoints.Parse)
})
packetEndpoints.RegisterSubParser("first-supported", func(ctx context.Context, input map[string]any) (*Endpoint[net.Conn], error) {
return parseFirstSupported(ctx, input, packetEndpoints.Parse)
})
streamDialers.RegisterSubParser("first-supported", func(ctx context.Context, input map[string]any) (*Dialer[transport.StreamConn], error) {
return parseFirstSupported(ctx, input, streamDialers.Parse)
})
packetDialers.RegisterSubParser("first-supported", func(ctx context.Context, input map[string]any) (*Dialer[net.Conn], error) {
return parseFirstSupported(ctx, input, packetDialers.Parse)
})
packetListeners.RegisterSubParser("first-supported", func(ctx context.Context, input map[string]any) (*PacketListener, error) {
return parseFirstSupported(ctx, input, packetListeners.Parse)
})

// Shadowsocks support.
streamDialers.RegisterSubParser("shadowsocks", func(ctx context.Context, input map[string]any) (*Dialer[transport.StreamConn], error) {
return parseShadowsocksStreamDialer(ctx, input, streamEndpoints.Parse)
Expand Down

0 comments on commit d1cba53

Please sign in to comment.