Skip to content

Commit

Permalink
Use tcp and udp types for direct listeners.
Browse files Browse the repository at this point in the history
  • Loading branch information
sbruens committed Jul 8, 2024
1 parent 1f097be commit 80b25b1
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 135 deletions.
10 changes: 4 additions & 6 deletions cmd/outline-ss-server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ type ServiceConfig struct {

type ListenerType string

const listenerTypeDirect ListenerType = "direct"
const listenerTypeTCP ListenerType = "tcp"
const listenerTypeUDP ListenerType = "udp"

type ListenerConfig struct {
Type ListenerType
Expand Down Expand Up @@ -59,17 +60,14 @@ func (c *Config) Validate() error {
for _, serviceConfig := range c.Services {
for _, listenerConfig := range serviceConfig.Listeners {
// TODO: Support more listener types.
if listenerConfig.Type != listenerTypeDirect {
if listenerConfig.Type != listenerTypeTCP && listenerConfig.Type != listenerTypeUDP {
return fmt.Errorf("unsupported listener type: %s", listenerConfig.Type)
}

network, host, _, err := SplitNetworkAddr(listenerConfig.Address)
host, _, err := net.SplitHostPort(listenerConfig.Address)
if err != nil {
return fmt.Errorf("invalid listener address `%s`: %v", listenerConfig.Address, err)
}
if network != "tcp" && network != "udp" {
return fmt.Errorf("unsupported network: %s", network)
}
if ip := net.ParseIP(host); ip == nil {
return fmt.Errorf("address must be IP, found: %s", host)
}
Expand Down
16 changes: 8 additions & 8 deletions cmd/outline-ss-server/config_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ services:
- listeners:
# TODO(sbruens): Allow a string-based listener config, as a convenient short-form
# to create a direct listener, e.g. `- tcp/[::]:9000`.
- type: direct
address: "tcp/[::]:9000"
- type: direct
address: "udp/[::]:9000"
- type: tcp
address: "[::]:9000"
- type: udp
address: "[::]:9000"
keys:
- id: user-0
cipher: chacha20-ietf-poly1305
Expand All @@ -15,10 +15,10 @@ services:
secret: Secret1

- listeners:
- type: direct
address: "tcp/[::]:9001"
- type: direct
address: "udp/[::]:9001"
- type: tcp
address: "[::]:9001"
- type: udp
address: "[::]:9001"
keys:
- id: user-2
cipher: chacha20-ietf-poly1305
Expand Down
26 changes: 7 additions & 19 deletions cmd/outline-ss-server/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestValidateConfigFails(t *testing.T) {
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: "foo", Address: "tcp/[::]:9000"},
ListenerConfig{Type: "foo", Address: "[::]:9000"},
},
},
},
Expand All @@ -44,19 +44,7 @@ func TestValidateConfigFails(t *testing.T) {
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeDirect, Address: "tcp//[::]:9000"},
},
},
},
},
},
{
name: "WithUnsupportedNetworkType",
cfg: &Config{
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeDirect, Address: "foo/[::]:9000"},
ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"},
},
},
},
Expand All @@ -68,7 +56,7 @@ func TestValidateConfigFails(t *testing.T) {
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeDirect, Address: "tcp/example.com:9000"},
ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"},
},
},
},
Expand All @@ -92,8 +80,8 @@ func TestReadConfig(t *testing.T) {
Services: []ServiceConfig{
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeDirect, Address: "tcp/[::]:9000"},
ListenerConfig{Type: listenerTypeDirect, Address: "udp/[::]:9000"},
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"},
ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"},
},
Keys: []KeyConfig{
KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"},
Expand All @@ -102,8 +90,8 @@ func TestReadConfig(t *testing.T) {
},
ServiceConfig{
Listeners: []ListenerConfig{
ListenerConfig{Type: listenerTypeDirect, Address: "tcp/[::]:9001"},
ListenerConfig{Type: listenerTypeDirect, Address: "udp/[::]:9001"},
ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"},
ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"},
},
Keys: []KeyConfig{
KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"},
Expand Down
99 changes: 16 additions & 83 deletions cmd/outline-ss-server/listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ package main

import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -152,143 +149,79 @@ type globalListener struct {
deadlineMu sync.Mutex
}

type NetworkAddr struct {
network string
Host string
Port uint
}

// String returns a human-readable representation of the [NetworkAddr].
func (na *NetworkAddr) Network() string {
return na.network
}

// String returns a human-readable representation of the [NetworkAddr].
func (na *NetworkAddr) String() string {
return na.JoinHostPort()
}

// JoinHostPort is a convenience wrapper around [net.JoinHostPort].
func (na *NetworkAddr) JoinHostPort() string {
return net.JoinHostPort(na.Host, strconv.Itoa(int(na.Port)))
}

// Key returns a representative string useful to retrieve this entity from a
// map. This is used to uniquely identify reusable listeners.
func (na *NetworkAddr) Key() string {
return na.network + "/" + na.JoinHostPort()
}

// Listen creates a new listener for the [NetworkAddr].
// Listen creates a new listener for a given network and address.
//
// Listeners can overlap one another, because during config changes the new
// config is started before the old config is destroyed. This is done by using
// reusable listener wrappers, which do not actually close the underlying socket
// until all uses of the shared listener have been closed.
func (na *NetworkAddr) Listen(ctx context.Context, config net.ListenConfig) (any, error) {
switch na.network {
func Listen(ctx context.Context, network string, addr string, config net.ListenConfig) (any, error) {
lnKey := network + "/" + addr

switch network {

case "tcp":
listenersMu.Lock()
defer listenersMu.Unlock()

if lnGlobal, ok := listeners[na.Key()]; ok {
if lnGlobal, ok := listeners[lnKey]; ok {
lnGlobal.usage.Add(1)
return &sharedListener{
usage: &lnGlobal.usage,
deadline: &lnGlobal.deadline,
deadlineMu: &lnGlobal.deadlineMu,
key: na.Key(),
key: lnKey,
listener: lnGlobal.ln,
}, nil
}

ln, err := config.Listen(ctx, na.network, na.JoinHostPort())
ln, err := config.Listen(ctx, network, addr)
if err != nil {
return nil, err
}

lnGlobal := &globalListener{ln: ln}
lnGlobal.usage.Store(1)
listeners[na.Key()] = lnGlobal
listeners[lnKey] = lnGlobal

return &sharedListener{
usage: &lnGlobal.usage,
deadline: &lnGlobal.deadline,
deadlineMu: &lnGlobal.deadlineMu,
key: na.Key(),
key: lnKey,
listener: ln,
}, nil

case "udp":
listenersMu.Lock()
defer listenersMu.Unlock()

if lnGlobal, ok := listeners[na.Key()]; ok {
if lnGlobal, ok := listeners[lnKey]; ok {
lnGlobal.usage.Add(1)
return &sharedPacketConn{
usage: &lnGlobal.usage,
key: na.Key(),
key: lnKey,
PacketConn: lnGlobal.pc,
}, nil
}

pc, err := config.ListenPacket(ctx, na.network, na.JoinHostPort())
pc, err := config.ListenPacket(ctx, network, addr)
if err != nil {
return nil, err
}

lnGlobal := &globalListener{pc: pc}
lnGlobal.usage.Store(1)
listeners[na.Key()] = lnGlobal
listeners[lnKey] = lnGlobal

return &sharedPacketConn{
usage: &lnGlobal.usage,
key: na.Key(),
key: lnKey,
PacketConn: pc,
}, nil

default:
return nil, fmt.Errorf("unsupported network: %s", na.network)

}
}

// ParseNetworkAddr parses an address into a [NetworkAddr]. The input
// string is expected to be of the form "network/host:port" where any part is
// optional.
//
// Examples:
//
// tcp/127.0.0.1:8000
// udp/127.0.0.1:9000
func ParseNetworkAddr(addr string) (NetworkAddr, error) {
var host, port string
network, host, port, err := SplitNetworkAddr(addr)
if err != nil {
return NetworkAddr{}, err
}
if network == "" {
return NetworkAddr{}, errors.New("missing network")
}
p, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return NetworkAddr{}, fmt.Errorf("invalid port: %v", err)
}
return NetworkAddr{
network: network,
Host: host,
Port: uint(p),
}, nil
}
return nil, fmt.Errorf("unsupported network: %s", network)

// SplitNetworkAddr splits a into its network, host, and port components.
func SplitNetworkAddr(a string) (network, host, port string, err error) {
beforeSlash, afterSlash, slashFound := strings.Cut(a, "/")
if slashFound {
network = strings.ToLower(strings.TrimSpace(beforeSlash))
a = afterSlash
}
host, port, err = net.SplitHostPort(a)
return
}
10 changes: 4 additions & 6 deletions cmd/outline-ss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"container/list"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -92,12 +94,8 @@ func (s *SSServer) loadConfig(filename string) error {
ciphers: list.New(),
}
for _, network := range []string{"tcp", "udp"} {
addr := NetworkAddr{
network: network,
Host: "::",
Port: uint(legacyKeyServiceConfig.Port),
}
if err := legacyService.AddListener(addr); err != nil {
addr := net.JoinHostPort("::", strconv.Itoa(legacyKeyServiceConfig.Port))
if err := legacyService.AddListener(network, addr); err != nil {
return err
}
}
Expand Down
24 changes: 11 additions & 13 deletions cmd/outline-ss-server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ type Service struct {
ciphers *list.List // Values are *List of *service.CipherEntry.
}

func (s *Service) Serve(addr NetworkAddr, listener Listener, cipherList service.CipherList) error {
func (s *Service) Serve(lnKey string, listener Listener, cipherList service.CipherList) error {
switch ln := listener.(type) {
case net.Listener:
authFunc := service.NewShadowsocksStreamAuthenticator(cipherList, s.replayCache, s.m)
// TODO: Register initial data metrics at zero.
tcpHandler := service.NewTCPHandler(addr.Key(), authFunc, s.m, tcpReadTimeout)
tcpHandler := service.NewTCPHandler(lnKey, authFunc, s.m, tcpReadTimeout)
accept := func() (transport.StreamConn, error) {
c, err := ln.Accept()
if err == nil {
Expand Down Expand Up @@ -85,20 +85,21 @@ func (s *Service) Stop() error {
}

// AddListener adds a new listener to the service.
func (s *Service) AddListener(addr NetworkAddr) error {
func (s *Service) AddListener(network string, addr string) error {
// Create new listeners based on the configured network addresses.
cipherList := service.NewCipherList()
cipherList.Update(s.ciphers)

listener, err := addr.Listen(context.TODO(), net.ListenConfig{KeepAlive: 0})
listener, err := Listen(context.TODO(), network, addr, net.ListenConfig{KeepAlive: 0})
if err != nil {
//lint:ignore ST1005 Shadowsocks is capitalized.
return fmt.Errorf("Shadowsocks %s service failed to start on address %s: %w", addr.Network(), addr.String(), err)
return fmt.Errorf("Shadowsocks %s service failed to start on address %s: %w", network, addr, err)
}
s.listeners = append(s.listeners, listener)
logger.Infof("Shadowsocks %s service listening on %s", addr.Network(), addr.String())
if err = s.Serve(addr, listener, cipherList); err != nil {
return fmt.Errorf("failed to serve on %s listener on address %s: %w", addr.Network(), addr.String(), err)
logger.Infof("Shadowsocks %s service listening on %s", network, addr)
lnKey := network + "/" + addr
if err = s.Serve(lnKey, listener, cipherList); err != nil {
return fmt.Errorf("failed to serve on %s listener on address %s: %w", network, addr, err)
}
return nil
}
Expand Down Expand Up @@ -145,11 +146,8 @@ func NewService(config ServiceConfig, natTimeout time.Duration, m *outlineMetric
}

for _, listener := range config.Listeners {
addr, err := ParseNetworkAddr(listener.Address)
if err != nil {
return nil, fmt.Errorf("error parsing listener address `%s`: %v", listener.Address, err)
}
if err := s.AddListener(addr); err != nil {
network := string(listener.Type)
if err := s.AddListener(network, listener.Address); err != nil {
return nil, err
}
}
Expand Down

0 comments on commit 80b25b1

Please sign in to comment.