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: add WebSocket support to the existing outline-ss-server #225

Open
wants to merge 14 commits into
base: sbruens/udp-split-serving
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 83 additions & 26 deletions cmd/outline-ss-server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,71 +22,128 @@ import (
)

type ServiceConfig struct {
Listeners []ListenerConfig
Keys []KeyConfig
Dialer DialerConfig
Listeners []ListenerConfig `yaml:"listeners"`
Keys []KeyConfig `yaml:"keys"`
Dialer DialerConfig `yaml:"dialer"`
}

type ListenerType string

const listenerTypeTCP ListenerType = "tcp"
const (
listenerTypeTCP ListenerType = "tcp"

const listenerTypeUDP ListenerType = "udp"
listenerTypeUDP ListenerType = "udp"
listenerTypeWebsocketStream ListenerType = "websocket-stream"
listenerTypeWebsocketPacket ListenerType = "websocket-packet"
)

type WebServerConfig struct {
ID string `yaml:"id"`
Listeners []string `yaml:"listen"`
}

type ListenerConfig struct {
Type ListenerType
Address string
Type ListenerType `yaml:"type"`
Address string `yaml:"address,omitempty"`
WebServer string `yaml:"web_server,omitempty"`
Path string `yaml:"path,omitempty"`
}

type DialerConfig struct {
Fwmark uint
}

type KeyConfig struct {
ID string
Cipher string
Secret string
ID string `yaml:"id"`
Cipher string `yaml:"cipher"`
Secret string `yaml:"secret"`
}

type LegacyKeyServiceConfig struct {
KeyConfig `yaml:",inline"`
Port int
Port int `yaml:"port"`
}

type WebConfig struct {
Servers []WebServerConfig `yaml:"servers"`
}

type Config struct {
Services []ServiceConfig
Web WebConfig `yaml:"web"`
Services []ServiceConfig `yaml:"services"`

// Deprecated: `keys` exists for backward compatibility. Prefer to configure
// using the newer `services` format.
Keys []LegacyKeyServiceConfig
Keys []LegacyKeyServiceConfig `yaml:"keys"`
}

// Validate checks that the config is valid.
func (c *Config) Validate() error {
existingWebServers := make(map[string]bool)
for _, srv := range c.Web.Servers {
if srv.ID == "" {
return fmt.Errorf("web server must have an ID")
}
if _, exists := existingWebServers[srv.ID]; exists {
return fmt.Errorf("web server with ID `%s` already exists", srv.ID)
}
existingWebServers[srv.ID] = true

for _, addr := range srv.Listeners {
if err := validateAddress(addr); err != nil {
return fmt.Errorf("invalid listener for web server `%s`: %w", srv.ID, err)
}
}
}

existingListeners := make(map[string]bool)
for _, serviceConfig := range c.Services {
for _, lnConfig := range serviceConfig.Listeners {
// TODO: Support more listener types.
if lnConfig.Type != listenerTypeTCP && lnConfig.Type != listenerTypeUDP {
var key string
switch lnConfig.Type {
case listenerTypeTCP, listenerTypeUDP:
if err := validateAddress(lnConfig.Address); err != nil {
return err
}
key = fmt.Sprintf("%s/%s", lnConfig.Type, lnConfig.Address)
if _, exists := existingListeners[key]; exists {
return fmt.Errorf("listener of type `%s` with address `%s` already exists.", lnConfig.Type, lnConfig.Address)
}
case listenerTypeWebsocketStream, listenerTypeWebsocketPacket:
if lnConfig.WebServer == "" {
return fmt.Errorf("listener type `%s` requires a `web_server`", lnConfig.Type)
}
if lnConfig.Path == "" {
return fmt.Errorf("listener type `%s` requires a `path`", lnConfig.Type)
}
if _, exists := existingWebServers[lnConfig.WebServer]; !exists {
return fmt.Errorf("listener type `%s` references unknown web server `%s`", lnConfig.Type, lnConfig.WebServer)
}
key = fmt.Sprintf("%s/%s", lnConfig.Type, lnConfig.WebServer)
if _, exists := existingListeners[key]; exists {
return fmt.Errorf("listener of type `%s` with web server `%s` already exists.", lnConfig.Type, lnConfig.WebServer)
}
default:
return fmt.Errorf("unsupported listener type: %s", lnConfig.Type)
}
host, _, err := net.SplitHostPort(lnConfig.Address)
if err != nil {
return fmt.Errorf("invalid listener address `%s`: %v", lnConfig.Address, err)
}
if ip := net.ParseIP(host); ip == nil {
return fmt.Errorf("address must be IP, found: %s", host)
}
key := string(lnConfig.Type) + "/" + lnConfig.Address
if _, exists := existingListeners[key]; exists {
return fmt.Errorf("listener of type %s with address %s already exists.", lnConfig.Type, lnConfig.Address)
}

existingListeners[key] = true
}
}
return nil
}

func validateAddress(addr string) error {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("invalid listener address `%s`: %v", addr, err)
}
if ip := net.ParseIP(host); ip == nil {
return fmt.Errorf("address must be IP, found: %s", host)
}
return nil
}

// readConfig attempts to read a config from a filename and parses it as a [Config].
func readConfig(configData []byte) (*Config, error) {
config := Config{}
Expand Down
12 changes: 12 additions & 0 deletions cmd/outline-ss-server/config_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

web:
servers:
- id: my_web_server
listen:
- "[::]:8000"

services:
- listeners:
# TODO(sbruens): Allow a string-based listener config, as a convenient short-form
Expand All @@ -20,6 +26,12 @@ services:
address: "[::]:9000"
- type: udp
address: "[::]:9000"
- type: websocket-stream
web_server: my_web_server
path: "/tcp"
- type: websocket-packet
web_server: my_web_server
path: "/udp"
keys:
- id: user-0
cipher: chacha20-ietf-poly1305
Expand Down
Loading
Loading