diff --git a/cmd/outline-ss-server/config.go b/cmd/outline-ss-server/config.go index 0ed7295d..9b8ec31f 100644 --- a/cmd/outline-ss-server/config.go +++ b/cmd/outline-ss-server/config.go @@ -22,20 +22,31 @@ 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 { @@ -43,50 +54,96 @@ type DialerConfig struct { } 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{} diff --git a/cmd/outline-ss-server/config_example.yml b/cmd/outline-ss-server/config_example.yml index 8dddbb6f..3392ec0a 100644 --- a/cmd/outline-ss-server/config_example.yml +++ b/cmd/outline-ss-server/config_example.yml @@ -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 @@ -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 diff --git a/cmd/outline-ss-server/config_test.go b/cmd/outline-ss-server/config_test.go index f183ff5a..7c71cf08 100644 --- a/cmd/outline-ss-server/config_test.go +++ b/cmd/outline-ss-server/config_test.go @@ -16,152 +16,361 @@ package main import ( "os" + "strings" "testing" "github.com/stretchr/testify/require" ) -func TestValidateConfigFails(t *testing.T) { - tests := []struct { - name string - cfg *Config - }{ - { - name: "WithUnknownListenerType", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: "foo", Address: "[::]:9000"}, +func TestConfigValidate(t *testing.T) { + t.Run("InvalidConfig", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + errStr string + }{ + { + name: "UnknownListenerType", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: "foo", Address: "[::]:9000"}, + }, }, }, }, + errStr: "unsupported listener type", }, - }, - { - name: "WithInvalidListenerAddress", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"}, + { + name: "InvalidListenerAddress", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"}, + }, }, }, }, + errStr: "invalid listener address", }, - }, - { - name: "WithHostnameAddress", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"}, + { + name: "HostnameAddress", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"}, + }, }, }, }, + errStr: "address must be IP", }, - }, - { - name: "WithDuplicateListeners", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + { + name: "DuplicateListeners", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, + }, + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, + }, + }, + }, + errStr: "already exists", + }, + { + name: "WebServerMissingID", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{}, + }, + errStr: "web server must have an ID", + }, + { + name: "WebServerDuplicateID", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + { + ID: "foo", + Listeners: []string{"[::]:8001"}, + }, + }, + }, + Services: []ServiceConfig{}, + }, + errStr: "already exists", + }, + { + name: "WebServerInvalidAddress", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{":invalid"}, + }, + }, + }, + Services: []ServiceConfig{}, + }, + errStr: "invalid listener for web server `foo`", + }, + { + name: "WebsocketListenerMissingWebServer", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + Path: "/tcp", + }, + }, + }, + }, + }, + errStr: "requires a `web_server`", + }, + { + name: "WebsocketListenerUnknownWebServer", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "unknown_server", + Path: "/tcp", + }, + }, + }, + }, + }, + errStr: "unknown web server `unknown_server`", + }, + { + name: "WebsocketListenerMissingPath", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "foo", + }, + }, }, }, - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, + errStr: "requires a `path`", + }, + { + name: "ListenerInvalidType", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: "invalid-type", + WebServer: "foo", + Path: "/tcp", + }, + }, }, }, }, + errStr: "unsupported listener type: invalid-type", }, - }, - } + } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.cfg.Validate() - require.Error(t, err) - }) - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.cfg.Validate() + require.Error(t, err) + if !isStrInError(err, tc.errStr) { + t.Errorf("Config.Validate() error=`%v`, expected=`%v`", err, tc.errStr) + } + }) + } + }) + + t.Run("ValidConfig", func(t *testing.T) { + config := Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "my_web_server", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "my_web_server", + Path: "/tcp", + }, + { + Type: listenerTypeWebsocketPacket, + WebServer: "my_web_server", + Path: "/udp", + }, + }, + Keys: []KeyConfig{ + { + ID: "user-0", + Cipher: "chacha20-ietf-poly1305", + Secret: "Secret0", + }, + }, + }, + }, + } + err := config.Validate() + require.NoError(t, err) + }) } func TestReadConfig(t *testing.T) { - config, err := readConfigFile("./config_example.yml") - require.NoError(t, err) - expected := Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, - ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"}, - }, - Keys: []KeyConfig{ - KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"}, - KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"}, + t.Run("ExampleFile", func(t *testing.T) { + config, err := readConfigFile("./config_example.yml") + + require.NoError(t, err) + expected := Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + WebServerConfig{ID: "my_web_server", Listeners: []string{"[::]:8000"}}, }, }, - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"}, - ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"}, + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"}, + ListenerConfig{Type: listenerTypeWebsocketStream, WebServer: "my_web_server", Path: "/tcp"}, + ListenerConfig{Type: listenerTypeWebsocketPacket, WebServer: "my_web_server", Path: "/udp"}, + }, + Keys: []KeyConfig{ + KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"}, + KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"}, + }, }, - Keys: []KeyConfig{ - KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"}, + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"}, + ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"}, + }, + Keys: []KeyConfig{ + KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"}, + }, }, }, - }, - } - require.Equal(t, expected, *config) -} + } + require.Equal(t, expected, *config) + }) -func TestReadConfigParsesDeprecatedFormat(t *testing.T) { - config, err := readConfigFile("./config_example.deprecated.yml") + t.Run("ParsesDeprecatedFormat", func(t *testing.T) { + config, err := readConfigFile("./config_example.deprecated.yml") - require.NoError(t, err) - expected := Config{ - Keys: []LegacyKeyServiceConfig{ - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"}, - Port: 9000, - }, - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"}, - Port: 9000, - }, - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"}, - Port: 9001, + require.NoError(t, err) + expected := Config{ + Keys: []LegacyKeyServiceConfig{ + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"}, + Port: 9000, + }, + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"}, + Port: 9000, + }, + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"}, + Port: 9001, + }, }, - }, - } - require.Equal(t, expected, *config) -} + } + require.Equal(t, expected, *config) + }) -func TestReadConfigFromEmptyFile(t *testing.T) { - file, _ := os.CreateTemp("", "empty.yaml") + t.Run("FromEmptyFile", func(t *testing.T) { + file, _ := os.CreateTemp("", "empty.yaml") - config, err := readConfigFile(file.Name()) + config, err := readConfigFile(file.Name()) - require.NoError(t, err) - require.ElementsMatch(t, Config{}, config) -} + require.NoError(t, err) + require.ElementsMatch(t, Config{}, config) + }) -func TestReadConfigFromIncorrectFormatFails(t *testing.T) { - file, _ := os.CreateTemp("", "empty.yaml") - file.WriteString("foo") + t.Run("FromIncorrectFormatFails", func(t *testing.T) { + file, _ := os.CreateTemp("", "empty.yaml") + file.WriteString("foo") - config, err := readConfigFile(file.Name()) + config, err := readConfigFile(file.Name()) - require.Error(t, err) - require.ElementsMatch(t, Config{}, config) + require.Error(t, err) + require.ElementsMatch(t, Config{}, config) + }) } func readConfigFile(filename string) (*Config, error) { configData, _ := os.ReadFile(filename) return readConfig(configData) } + +func isStrInError(err error, str string) bool { + return err != nil && strings.Contains(err.Error(), str) +} diff --git a/cmd/outline-ss-server/main.go b/cmd/outline-ss-server/main.go index d41bf685..a496f8cd 100644 --- a/cmd/outline-ss-server/main.go +++ b/cmd/outline-ss-server/main.go @@ -16,6 +16,8 @@ package main import ( "container/list" + "context" + "errors" "flag" "fmt" "log/slog" @@ -23,14 +25,17 @@ import ( "net/http" "os" "os/signal" + "strings" "sync" "syscall" "time" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" "github.com/lmittmann/tint" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/net/websocket" "golang.org/x/term" "github.com/Jigsaw-Code/outline-ss-server/ipinfo" @@ -60,6 +65,16 @@ func init() { ) } +type HTTPStreamListener struct { + service.StreamListener +} + +var _ net.Listener = (*HTTPStreamListener)(nil) + +func (t *HTTPStreamListener) Accept() (net.Conn, error) { + return t.StreamListener.AcceptStream() +} + type OutlineServer struct { stopConfig func() error lnManager service.ListenerManager @@ -184,6 +199,11 @@ func (ls *listenerSet) Len() int { return len(ls.listenerCloseFuncs) } +type connWithDone struct { + net.Conn + doneCh chan struct{} +} + func (s *OutlineServer) runConfig(config Config) (func() error, error) { startErrCh := make(chan error) stopErrCh := make(chan error) @@ -199,6 +219,29 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { }() startErrCh <- func() error { + // Start configured web servers. + webServers := make(map[string]*http.ServeMux) + for _, srvConfig := range config.Web.Servers { + mux := http.NewServeMux() + for _, addr := range srvConfig.Listeners { + server := &http.Server{Addr: addr, Handler: mux} + ln, err := lnSet.ListenStream(addr) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", addr, err) + } + go func() { + defer server.Shutdown(context.Background()) + err := server.Serve(&HTTPStreamListener{ln}) + if err != nil && err != http.ErrServerClosed && !isErrClosing(err) { + slog.Error("Failed to run web server.", "err", err, "ID", srvConfig.ID) + } + }() + slog.Info("Web server started.", "ID", srvConfig.ID, "address", addr) + } + webServers[srvConfig.ID] = mux + } + + // Start legacy services. totalCipherCount := len(config.Keys) portCiphers := make(map[int]*list.List) // Values are *List of *CipherEntry. for _, keyConfig := range config.Keys { @@ -245,6 +288,7 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) } + // Start services with listeners. for _, serviceConfig := range config.Services { ciphers, err := newCipherListFromConfig(serviceConfig) if err != nil { @@ -287,6 +331,56 @@ func (s *OutlineServer) runConfig(config Config) (func() error, error) { return serviceConfig.Dialer.Fwmark }()) go service.PacketServe(pc, ssService.NewPacketAssociation, s.serverMetrics) + case listenerTypeWebsocketStream: + if _, exists := webServers[lnConfig.WebServer]; !exists { + return fmt.Errorf("listener type `%s` references unknown web server `%s`", lnConfig.Type, lnConfig.WebServer) + } + mux := webServers[lnConfig.WebServer] + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := func(wsConn *websocket.Conn) { + defer wsConn.Close() + ctx, contextCancel := context.WithCancel(context.Background()) + defer contextCancel() + raddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if err != nil { + slog.Error("failed to upgrade", "err", err) + w.WriteHeader(http.StatusBadGateway) + return + } + conn := &streamConn{&wrappedConn{Conn: wsConn, raddr: raddr}} + ssService.HandleStream(ctx, conn) + } + websocket.Handler(handler).ServeHTTP(w, r) + }) + mux.Handle(lnConfig.Path, http.StripPrefix(lnConfig.Path, handler)) + case listenerTypeWebsocketPacket: + if _, exists := webServers[lnConfig.WebServer]; !exists { + return fmt.Errorf("listener type `%s` references unknown web server `%s`", lnConfig.Type, lnConfig.WebServer) + } + mux := webServers[lnConfig.WebServer] + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := func(wsConn *websocket.Conn) { + defer wsConn.Close() + raddr, err := net.ResolveUDPAddr("udp", r.RemoteAddr) + if err != nil { + slog.Error("failed to upgrade", "err", err) + w.WriteHeader(http.StatusBadGateway) + return + } + conn := &wrappedConn{Conn: wsConn, raddr: raddr} + assoc, err := ssService.NewConnAssociation(conn) + if err != nil { + slog.Error("failed to upgrade", "err", err) + w.WriteHeader(http.StatusBadGateway) + return + } + assoc.Handle() + } + websocket.Handler(handler).ServeHTTP(w, r) + }) + mux.Handle(lnConfig.Path, http.StripPrefix(lnConfig.Path, handler)) + default: + return errors.New("unsupported listener configuration") } } totalCipherCount += len(serviceConfig.Keys) @@ -328,6 +422,10 @@ func (s *OutlineServer) Stop() error { return nil } +func isErrClosing(err error) bool { + return strings.Contains(err.Error(), "use of closed network connection") +} + // RunOutlineServer starts an Outline server running, and returns the server or an error. func RunOutlineServer(filename string, natTimeout time.Duration, serverMetrics *serverMetrics, serviceMetrics service.ServiceMetrics, replayHistory int) (*OutlineServer, error) { server := &OutlineServer{ @@ -354,6 +452,31 @@ func RunOutlineServer(filename string, natTimeout time.Duration, serverMetrics * return server, nil } +// TODO: Create a dedicated `ClientConn` struct with `ClientAddr` and `Conn`. +// wrappedConn overrides [websocket.Conn]'s remote address handling. +type wrappedConn struct { + *websocket.Conn + raddr net.Addr +} + +func (c wrappedConn) RemoteAddr() net.Addr { + return c.raddr +} + +type streamConn struct { + net.Conn +} + +var _ transport.StreamConn = (*streamConn)(nil) + +func (c *streamConn) CloseRead() error { + return c.Close() +} + +func (c *streamConn) CloseWrite() error { + return c.Close() +} + func main() { slog.SetDefault(slog.New(logHandler)) diff --git a/service/udp.go b/service/udp.go index dc0708c9..7b5d2e18 100644 --- a/service/udp.go +++ b/service/udp.go @@ -367,7 +367,7 @@ func (m *natmap) Close() error { // ConnAssociation represents a UDP association that handles incoming and // outgoing packets from a [net.Conn] and forwards them to a target connection, -// and vice versa. Used by Caddy. +// and vice versa. Used by Caddy and outline-ss-server's WebSockets handler. type ConnAssociation interface { // Handle reads data from the association and handles incoming and outgoing // packets.