diff --git a/cmd/outline-ss-server/config.go b/cmd/outline-ss-server/config.go
index 6c22efd7..f677d198 100644
--- a/cmd/outline-ss-server/config.go
+++ b/cmd/outline-ss-server/config.go
@@ -58,10 +58,12 @@ type LegacyKeyServiceConfig struct {
 	Port      int `yaml:"port"`
 }
 
+type WebConfig struct {
+	Servers []WebServerConfig `yaml:"servers"`
+}
+
 type Config struct {
-	Web struct {
-		Servers []WebServerConfig `yaml:"servers"`
-	} `yaml:"web"`
+	Web WebConfig `yaml:"web"`
 	Services []ServiceConfig `yaml:"services"`
 
 	// Deprecated: `keys` exists for backward compatibility. Prefer to configure
@@ -103,14 +105,17 @@ func (c *Config) Validate() error {
 				}
 			case listenerTypeWebsocketStream, listenerTypeWebsocketPacket:
 				if lnConfig.WebServer == "" {
-					return fmt.Errorf("listener type `%s` requires an http server reference", lnConfig.Type)
+					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 http server `%s` already exists.", lnConfig.Type, lnConfig.WebServer)
+					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)
diff --git a/cmd/outline-ss-server/config_test.go b/cmd/outline-ss-server/config_test.go
index ce40e249..7c71cf08 100644
--- a/cmd/outline-ss-server/config_test.go
+++ b/cmd/outline-ss-server/config_test.go
@@ -16,178 +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"},
+							},
 						},
 					},
-					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: "WithWebSocketWithoutOptions",
-			cfg: &Config{
-				Services: []ServiceConfig{
-					ServiceConfig{
-						Listeners: []ListenerConfig{
-							ListenerConfig{Type: listenerTypeWebSocket, Address: "[::]:9000"},
+			{
+				name: "WebServerDuplicateID",
+				cfg: &Config{
+					Web: WebConfig{
+						Servers: []WebServerConfig{
+							{
+								ID:        "foo",
+								Listeners: []string{"[::]:8000"},
+							},
+							{
+								ID:        "foo",
+								Listeners: []string{"[::]:8001"},
+							},
 						},
 					},
+					Services: []ServiceConfig{},
 				},
+				errStr: "already exists",
 			},
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			err := tc.cfg.Validate()
-			require.Error(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"},
-					ListenerConfig{
-						Type:    listenerTypeWebSocket,
-						Address: "[::]:8000",
-						Options: []ConfigOption{
+			{
+				name: "WebServerInvalidAddress",
+				cfg: &Config{
+					Web: WebConfig{
+						Servers: []WebServerConfig{
 							{
-								Path:           "/tcp",
-								ConnectionType: connectionTypeStream,
+								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{
 							{
-								Path:           "/udp",
-								ConnectionType: connectionTypePacket,
+								ID:        "foo",
+								Listeners: []string{"[::]:8000"},
+							},
+						},
+					},
+					Services: []ServiceConfig{
+						{
+							Listeners: []ListenerConfig{
+								{
+									Type:      listenerTypeWebsocketStream,
+									WebServer: "foo",
+								},
 							},
 						},
 					},
 				},
-				Keys: []KeyConfig{
-					KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"},
-					KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"},
+				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",
 			},
-			ServiceConfig{
-				Listeners: []ListenerConfig{
-					ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"},
-					ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"},
+		}
+
+		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"},
+					},
 				},
-				Keys: []KeyConfig{
-					KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"},
+			},
+			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",
+						},
+					},
 				},
 			},
-		},
-	}
-	require.Equal(t, expected, *config)
+		}
+		err := config.Validate()
+		require.NoError(t, err)
+	})
 }
 
-func TestReadConfigParsesDeprecatedFormat(t *testing.T) {
-	config, err := readConfigFile("./config_example.deprecated.yml")
+func TestReadConfig(t *testing.T) {
+
+	t.Run("ExampleFile", func(t *testing.T) {
+		config, err := readConfigFile("./config_example.yml")
 
-	require.NoError(t, err)
-	expected := Config{
-		Keys: []LegacyKeyServiceConfig{
-			LegacyKeyServiceConfig{
-				KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"},
-				Port:      9000,
+		require.NoError(t, err)
+		expected := Config{
+			Web: WebConfig{
+				Servers: []WebServerConfig{
+					WebServerConfig{ID: "my_web_server", Listeners: []string{"[::]:8000"}},
+				},
 			},
-			LegacyKeyServiceConfig{
-				KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"},
-				Port:      9000,
+			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"},
+					},
+				},
+				ServiceConfig{
+					Listeners: []ListenerConfig{
+						ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"},
+						ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"},
+					},
+					Keys: []KeyConfig{
+						KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"},
+					},
+				},
 			},
-			LegacyKeyServiceConfig{
-				KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"},
-				Port:      9001,
+		}
+		require.Equal(t, expected, *config)
+	})
+
+	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.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)
+}