Skip to content

Commit 13bdb23

Browse files
authored
feat: add WebSocket support to the existing outline-ss-server (#225)
This allows clients to connect to Shadowsocks over WebSockets.
1 parent 6a15c34 commit 13bdb23

File tree

6 files changed

+531
-146
lines changed

6 files changed

+531
-146
lines changed

cmd/outline-ss-server/config.go

Lines changed: 173 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@
1515
package main
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"net"
21+
"reflect"
22+
"strings"
2023

24+
"github.com/go-viper/mapstructure/v2"
2125
"gopkg.in/yaml.v3"
2226
)
2327

28+
type Validator interface {
29+
// Validate checks that the type is valid.
30+
validate() error
31+
}
32+
2433
type ServiceConfig struct {
2534
Listeners []ListenerConfig
2635
Keys []KeyConfig
@@ -29,15 +38,143 @@ type ServiceConfig struct {
2938

3039
type ListenerType string
3140

32-
const listenerTypeTCP ListenerType = "tcp"
41+
const (
42+
TCPListenerType = ListenerType("tcp")
43+
UDPListenerType = ListenerType("udp")
44+
WebsocketStreamListenerType = ListenerType("websocket-stream")
45+
WebsocketPacketListenerType = ListenerType("websocket-packet")
46+
)
47+
48+
type WebServerConfig struct {
49+
// Unique identifier of the web server to be referenced in Websocket connections.
50+
ID string
3351

34-
const listenerTypeUDP ListenerType = "udp"
52+
// List of listener addresses (e.g., ":8080", "localhost:8081"). Should be localhost for HTTP.
53+
Listeners []string `yaml:"listen"`
54+
}
3555

56+
// ListenerConfig holds the configuration for a listener. It supports different
57+
// listener types, configured via the embedded type and unmarshalled based on
58+
// the "type" field in the YAML/JSON configuration. Only one of the fields will
59+
// be set, corresponding to the listener type.
3660
type ListenerConfig struct {
37-
Type ListenerType
61+
// TCP configuration for the listener.
62+
TCP *TCPUDPConfig
63+
// UDP configuration for the listener.
64+
UDP *TCPUDPConfig
65+
// Websocket stream configuration for the listener.
66+
WebsocketStream *WebsocketConfig
67+
// Websocket packet configuration for the listener.
68+
WebsocketPacket *WebsocketConfig
69+
}
70+
71+
var _ Validator = (*ListenerConfig)(nil)
72+
var _ yaml.Unmarshaler = (*ListenerConfig)(nil)
73+
74+
// Define a map to associate listener types with [ListenerConfig] field names.
75+
var listenerTypeMap = map[ListenerType]string{
76+
TCPListenerType: "TCP",
77+
UDPListenerType: "UDP",
78+
WebsocketStreamListenerType: "WebsocketStream",
79+
WebsocketPacketListenerType: "WebsocketPacket",
80+
}
81+
82+
func (c *ListenerConfig) UnmarshalYAML(value *yaml.Node) error {
83+
var raw map[string]interface{}
84+
if err := value.Decode(&raw); err != nil {
85+
return err
86+
}
87+
88+
// Remove the "type" field so we can decode directly into the target struct.
89+
rawType, ok := raw["type"]
90+
if !ok {
91+
return errors.New("`type` field required")
92+
}
93+
lnTypeStr, ok := rawType.(string)
94+
if !ok {
95+
return fmt.Errorf("`type` is not a string, but %T", rawType)
96+
}
97+
lnType := ListenerType(lnTypeStr)
98+
delete(raw, "type")
99+
100+
fieldName, ok := listenerTypeMap[lnType]
101+
if !ok {
102+
return fmt.Errorf("invalid listener type: %v", lnType)
103+
}
104+
v := reflect.ValueOf(c).Elem()
105+
field := v.FieldByName(fieldName)
106+
if !field.IsValid() {
107+
return fmt.Errorf("invalid field name: %s for type: %s", fieldName, lnType)
108+
}
109+
fieldType := field.Type()
110+
if fieldType.Kind() != reflect.Ptr || fieldType.Elem().Kind() != reflect.Struct {
111+
return fmt.Errorf("field %s is not a pointer to a struct", fieldName)
112+
}
113+
114+
configValue := reflect.New(fieldType.Elem())
115+
field.Set(configValue)
116+
if err := mapstructure.Decode(raw, configValue.Interface()); err != nil {
117+
return fmt.Errorf("failed to decode map: %w", err)
118+
}
119+
return nil
120+
}
121+
122+
func (c *ListenerConfig) validate() error {
123+
v := reflect.ValueOf(c).Elem()
124+
125+
for i := 0; i < v.NumField(); i++ {
126+
field := v.Field(i)
127+
if field.Kind() == reflect.Ptr && field.IsNil() {
128+
continue
129+
}
130+
if validator, ok := field.Interface().(Validator); ok {
131+
if err := validator.validate(); err != nil {
132+
return fmt.Errorf("invalid config: %v", err)
133+
}
134+
}
135+
}
136+
return nil
137+
}
138+
139+
type TCPUDPConfig struct {
140+
// Address for the TCP or UDP listener. Should be in the format host:port.
38141
Address string
39142
}
40143

144+
var _ Validator = (*TCPUDPConfig)(nil)
145+
146+
func (c *TCPUDPConfig) validate() error {
147+
if c.Address == "" {
148+
return errors.New("`address` must be specified")
149+
}
150+
if err := validateAddress(c.Address); err != nil {
151+
return fmt.Errorf("invalid address: %v", err)
152+
}
153+
return nil
154+
}
155+
156+
type WebsocketConfig struct {
157+
// Web server unique identifier to use for the websocket connection.
158+
WebServer string `mapstructure:"web_server"`
159+
// Path for the websocket connection.
160+
Path string
161+
}
162+
163+
var _ Validator = (*WebsocketConfig)(nil)
164+
165+
func (c *WebsocketConfig) validate() error {
166+
if c.WebServer == "" {
167+
return errors.New("`web_server` must be specified")
168+
}
169+
if c.Path == "" {
170+
return errors.New("`path` must be specified")
171+
}
172+
if !strings.HasPrefix(c.Path, "/") {
173+
return errors.New("`path` must start with `/`")
174+
}
175+
return nil
176+
}
177+
41178
type DialerConfig struct {
42179
Fwmark uint
43180
}
@@ -53,40 +190,54 @@ type LegacyKeyServiceConfig struct {
53190
Port int
54191
}
55192

193+
type WebConfig struct {
194+
Servers []WebServerConfig `yaml:"servers"`
195+
}
196+
56197
type Config struct {
198+
Web WebConfig
57199
Services []ServiceConfig
58200

59201
// Deprecated: `keys` exists for backward compatibility. Prefer to configure
60202
// using the newer `services` format.
61203
Keys []LegacyKeyServiceConfig
62204
}
63205

64-
// Validate checks that the config is valid.
65-
func (c *Config) Validate() error {
66-
existingListeners := make(map[string]bool)
67-
for _, serviceConfig := range c.Services {
68-
for _, lnConfig := range serviceConfig.Listeners {
69-
// TODO: Support more listener types.
70-
if lnConfig.Type != listenerTypeTCP && lnConfig.Type != listenerTypeUDP {
71-
return fmt.Errorf("unsupported listener type: %s", lnConfig.Type)
72-
}
73-
host, _, err := net.SplitHostPort(lnConfig.Address)
74-
if err != nil {
75-
return fmt.Errorf("invalid listener address `%s`: %v", lnConfig.Address, err)
76-
}
77-
if ip := net.ParseIP(host); ip == nil {
78-
return fmt.Errorf("address must be IP, found: %s", host)
206+
var _ Validator = (*Config)(nil)
207+
208+
func (c *Config) validate() error {
209+
for _, srv := range c.Web.Servers {
210+
if srv.ID == "" {
211+
return fmt.Errorf("web server must have an ID")
212+
}
213+
for _, addr := range srv.Listeners {
214+
if err := validateAddress(addr); err != nil {
215+
return fmt.Errorf("invalid listener for web server `%s`: %w", srv.ID, err)
79216
}
80-
key := string(lnConfig.Type) + "/" + lnConfig.Address
81-
if _, exists := existingListeners[key]; exists {
82-
return fmt.Errorf("listener of type %s with address %s already exists.", lnConfig.Type, lnConfig.Address)
217+
}
218+
}
219+
220+
for _, service := range c.Services {
221+
for _, ln := range service.Listeners {
222+
if err := ln.validate(); err != nil {
223+
return fmt.Errorf("invalid listener: %v", err)
83224
}
84-
existingListeners[key] = true
85225
}
86226
}
87227
return nil
88228
}
89229

230+
func validateAddress(addr string) error {
231+
host, _, err := net.SplitHostPort(addr)
232+
if err != nil {
233+
return err
234+
}
235+
if ip := net.ParseIP(host); ip == nil {
236+
return fmt.Errorf("address must be IP, found: %s", host)
237+
}
238+
return nil
239+
}
240+
90241
// readConfig attempts to read a config from a filename and parses it as a [Config].
91242
func readConfig(configData []byte) (*Config, error) {
92243
config := Config{}

cmd/outline-ss-server/config_example.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
web:
16+
servers:
17+
- id: my_web_server
18+
listen:
19+
- "127.0.0.1:8000"
20+
1521
services:
1622
- listeners:
1723
# TODO(sbruens): Allow a string-based listener config, as a convenient short-form
@@ -20,6 +26,12 @@ services:
2026
address: "[::]:9000"
2127
- type: udp
2228
address: "[::]:9000"
29+
- type: websocket-stream
30+
web_server: my_web_server
31+
path: "/SECRET/tcp" # Prevent probing by serving under a secret path.
32+
- type: websocket-packet
33+
web_server: my_web_server
34+
path: "/SECRET/udp" # Prevent probing by serving under a secret path.
2335
keys:
2436
- id: user-0
2537
cipher: chacha20-ietf-poly1305

0 commit comments

Comments
 (0)