Browse Source

add e2e tests for v1 config (#3608)

fatedier 1 year ago
parent
commit
7cd02f5bd8
61 changed files with 3697 additions and 159 deletions
  1. 2 0
      Release.md
  2. 1 1
      cmd/frpc/sub/root.go
  3. 7 4
      cmd/frps/root.go
  4. 0 9
      conf/frpc.ini
  5. 0 0
      conf/frpc_legacy_full.ini
  6. 0 2
      conf/frps.ini
  7. 0 0
      conf/frps_legacy_full.ini
  8. 1 1
      go.mod
  9. 2 1
      go.sum
  10. 1 1
      hack/run-e2e.sh
  11. 4 4
      pkg/auth/auth.go
  12. 9 9
      pkg/config/legacy/conversion.go
  13. 1 2
      pkg/config/load.go
  14. 1 1
      pkg/config/v1/client.go
  15. 3 2
      pkg/config/v1/plugin.go
  16. 9 9
      pkg/config/v1/server.go
  17. 1 1
      pkg/config/v1/validation/client.go
  18. 1 1
      pkg/config/v1/visitor.go
  19. 3 0
      pkg/plugin/client/plugin.go
  20. 1 1
      server/dashboard_api.go
  21. 4 4
      server/service.go
  22. 6 3
      test/e2e/e2e_test.go
  23. 5 4
      test/e2e/examples.go
  24. 15 1
      test/e2e/framework/consts/consts.go
  25. 8 2
      test/e2e/framework/process.go
  26. 12 12
      test/e2e/legacy/basic/basic.go
  27. 6 6
      test/e2e/legacy/basic/client.go
  28. 3 3
      test/e2e/legacy/basic/client_server.go
  29. 0 0
      test/e2e/legacy/basic/cmd.go
  30. 2 2
      test/e2e/legacy/basic/config.go
  31. 9 9
      test/e2e/legacy/basic/http.go
  32. 8 8
      test/e2e/legacy/basic/server.go
  33. 4 4
      test/e2e/legacy/basic/tcpmux.go
  34. 2 2
      test/e2e/legacy/basic/xtcp.go
  35. 5 5
      test/e2e/legacy/features/bandwidth_limit.go
  36. 0 0
      test/e2e/legacy/features/chaos.go
  37. 6 6
      test/e2e/legacy/features/group.go
  38. 0 0
      test/e2e/legacy/features/heartbeat.go
  39. 2 2
      test/e2e/legacy/features/monitor.go
  40. 6 6
      test/e2e/legacy/features/real_ip.go
  41. 14 14
      test/e2e/legacy/plugin/client.go
  42. 17 17
      test/e2e/legacy/plugin/server.go
  43. 0 0
      test/e2e/legacy/plugin/utils.go
  44. 524 0
      test/e2e/v1/basic/basic.go
  45. 136 0
      test/e2e/v1/basic/client.go
  46. 325 0
      test/e2e/v1/basic/client_server.go
  47. 109 0
      test/e2e/v1/basic/cmd.go
  48. 84 0
      test/e2e/v1/basic/config.go
  49. 388 0
      test/e2e/v1/basic/http.go
  50. 192 0
      test/e2e/v1/basic/server.go
  51. 223 0
      test/e2e/v1/basic/tcpmux.go
  52. 53 0
      test/e2e/v1/basic/xtcp.go
  53. 108 0
      test/e2e/v1/features/bandwidth_limit.go
  54. 64 0
      test/e2e/v1/features/chaos.go
  55. 267 0
      test/e2e/v1/features/group.go
  56. 47 0
      test/e2e/v1/features/heartbeat.go
  57. 55 0
      test/e2e/v1/features/monitor.go
  58. 154 0
      test/e2e/v1/features/real_ip.go
  59. 331 0
      test/e2e/v1/plugin/client.go
  60. 415 0
      test/e2e/v1/plugin/server.go
  61. 41 0
      test/e2e/v1/plugin/utils.go

+ 2 - 0
Release.md

@@ -1 +1,3 @@
 ### Features
 ### Features
+
+* Configuration: We now support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. #2521

+ 1 - 1
cmd/frpc/sub/root.go

@@ -186,7 +186,7 @@ func parseClientCommonCfgFromCmd() (*v1.ClientCommonConfig, error) {
 
 
 	cfg.Complete()
 	cfg.Complete()
 
 
-	err, warning := validation.ValidateClientCommonConfig(cfg)
+	warning, err := validation.ValidateClientCommonConfig(cfg)
 	if warning != nil {
 	if warning != nil {
 		fmt.Printf("WARNING: %v\n", warning)
 		fmt.Printf("WARNING: %v\n", warning)
 	}
 	}

+ 7 - 4
cmd/frps/root.go

@@ -108,7 +108,8 @@ var rootCmd = &cobra.Command{
 		if cfgFile != "" {
 		if cfgFile != "" {
 			svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
 			svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
 			if err != nil {
 			if err != nil {
-				return err
+				fmt.Println(err)
+				os.Exit(1)
 			}
 			}
 			if isLegacyFormat {
 			if isLegacyFormat {
 				fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
 				fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
@@ -116,7 +117,8 @@ var rootCmd = &cobra.Command{
 			}
 			}
 		} else {
 		} else {
 			if svrCfg, err = parseServerConfigFromCmd(); err != nil {
 			if svrCfg, err = parseServerConfigFromCmd(); err != nil {
-				return err
+				fmt.Println(err)
+				os.Exit(1)
 			}
 			}
 		}
 		}
 
 
@@ -125,7 +127,8 @@ var rootCmd = &cobra.Command{
 			fmt.Printf("WARNING: %v\n", warning)
 			fmt.Printf("WARNING: %v\n", warning)
 		}
 		}
 		if err != nil {
 		if err != nil {
-			return err
+			fmt.Println(err)
+			os.Exit(1)
 		}
 		}
 
 
 		if err := runServer(svrCfg); err != nil {
 		if err := runServer(svrCfg); err != nil {
@@ -168,7 +171,7 @@ func parseServerConfigFromCmd() (*v1.ServerConfig, error) {
 	cfg.Log.MaxDays = logMaxDays
 	cfg.Log.MaxDays = logMaxDays
 	cfg.Log.DisablePrintColor = disableLogColor
 	cfg.Log.DisablePrintColor = disableLogColor
 	cfg.SubDomainHost = subDomainHost
 	cfg.SubDomainHost = subDomainHost
-	cfg.TLS.Force = tlsOnly
+	cfg.Transport.TLS.Force = tlsOnly
 	cfg.MaxPortsPerClient = maxPortsPerClient
 	cfg.MaxPortsPerClient = maxPortsPerClient
 
 
 	// Only token authentication is supported in cmd mode
 	// Only token authentication is supported in cmd mode

+ 0 - 9
conf/frpc.ini

@@ -1,9 +0,0 @@
-[common]
-server_addr = 127.0.0.1
-server_port = 7000
-
-[ssh]
-type = tcp
-local_ip = 127.0.0.1
-local_port = 22
-remote_port = 6000

+ 0 - 0
conf/frpc_full.ini → conf/frpc_legacy_full.ini


+ 0 - 2
conf/frps.ini

@@ -1,2 +0,0 @@
-[common]
-bind_port = 7000

+ 0 - 0
conf/frps_full.ini → conf/frps_legacy_full.ini


+ 1 - 1
go.mod

@@ -3,7 +3,6 @@ module github.com/fatedier/frp
 go 1.20
 go 1.20
 
 
 require (
 require (
-	github.com/BurntSushi/toml v0.3.1
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
 	github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
@@ -15,6 +14,7 @@ require (
 	github.com/hashicorp/yamux v0.1.1
 	github.com/hashicorp/yamux v0.1.1
 	github.com/onsi/ginkgo/v2 v2.11.0
 	github.com/onsi/ginkgo/v2 v2.11.0
 	github.com/onsi/gomega v1.27.8
 	github.com/onsi/gomega v1.27.8
+	github.com/pelletier/go-toml/v2 v2.1.0
 	github.com/pion/stun v0.6.1
 	github.com/pion/stun v0.6.1
 	github.com/pires/go-proxyproto v0.7.0
 	github.com/pires/go-proxyproto v0.7.0
 	github.com/prometheus/client_golang v1.16.0
 	github.com/prometheus/client_golang v1.16.0

+ 2 - 1
go.sum

@@ -1,7 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -93,6 +92,8 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU
 github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
 github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
 github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
 github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
 github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
 github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
 github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
 github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
 github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=

+ 1 - 1
hack/run-e2e.sh

@@ -6,7 +6,7 @@ ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
 ginkgo_command=$(which ginkgo 2>/dev/null)
 ginkgo_command=$(which ginkgo 2>/dev/null)
 if [ -z "$ginkgo_command" ]; then
 if [ -z "$ginkgo_command" ]; then
     echo "ginkgo not found, try to install..."
     echo "ginkgo not found, try to install..."
-    go install github.com/onsi/ginkgo/v2/ginkgo@v2.8.3
+    go install github.com/onsi/ginkgo/v2/ginkgo@v2.11.0
 fi
 fi
 
 
 debug=false
 debug=false

+ 4 - 4
pkg/auth/auth.go

@@ -31,9 +31,9 @@ type Setter interface {
 func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
 func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
 	switch cfg.Method {
 	switch cfg.Method {
 	case consts.TokenAuthMethod:
 	case consts.TokenAuthMethod:
-		authProvider = NewTokenAuth(cfg.AdditionalAuthScopes, cfg.Token)
+		authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
 	case consts.OidcAuthMethod:
 	case consts.OidcAuthMethod:
-		authProvider = NewOidcAuthSetter(cfg.AdditionalAuthScopes, cfg.OIDC)
+		authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
 	default:
 	default:
 		panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
 		panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
 	}
 	}
@@ -49,9 +49,9 @@ type Verifier interface {
 func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
 func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
 	switch cfg.Method {
 	switch cfg.Method {
 	case consts.TokenAuthMethod:
 	case consts.TokenAuthMethod:
-		authVerifier = NewTokenAuth(cfg.AdditionalAuthScopes, cfg.Token)
+		authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
 	case consts.OidcAuthMethod:
 	case consts.OidcAuthMethod:
-		authVerifier = NewOidcAuthVerifier(cfg.AdditionalAuthScopes, cfg.OIDC)
+		authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC)
 	}
 	}
 	return authVerifier
 	return authVerifier
 }
 }

+ 9 - 9
pkg/config/legacy/conversion.go

@@ -29,10 +29,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
 	out.Auth.Method = conf.ClientConfig.AuthenticationMethod
 	out.Auth.Method = conf.ClientConfig.AuthenticationMethod
 	out.Auth.Token = conf.ClientConfig.Token
 	out.Auth.Token = conf.ClientConfig.Token
 	if conf.ClientConfig.AuthenticateHeartBeats {
 	if conf.ClientConfig.AuthenticateHeartBeats {
-		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeHeartBeats)
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
 	}
 	}
 	if conf.ClientConfig.AuthenticateNewWorkConns {
 	if conf.ClientConfig.AuthenticateNewWorkConns {
-		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeNewWorkConns)
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
 	}
 	}
 	out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
 	out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
 	out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
 	out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
@@ -89,10 +89,10 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
 	out.Auth.Method = conf.ServerConfig.AuthenticationMethod
 	out.Auth.Method = conf.ServerConfig.AuthenticationMethod
 	out.Auth.Token = conf.ServerConfig.Token
 	out.Auth.Token = conf.ServerConfig.Token
 	if conf.ServerConfig.AuthenticateHeartBeats {
 	if conf.ServerConfig.AuthenticateHeartBeats {
-		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeHeartBeats)
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
 	}
 	}
 	if conf.ServerConfig.AuthenticateNewWorkConns {
 	if conf.ServerConfig.AuthenticateNewWorkConns {
-		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeNewWorkConns)
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
 	}
 	}
 	out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
 	out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
 	out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
 	out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
@@ -146,12 +146,12 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
 	out.Transport.MaxPoolCount = conf.MaxPoolCount
 	out.Transport.MaxPoolCount = conf.MaxPoolCount
 	out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
 	out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
 
 
-	out.MaxPortsPerClient = conf.MaxPortsPerClient
+	out.Transport.TLS.Force = conf.TLSOnly
+	out.Transport.TLS.CertFile = conf.TLSCertFile
+	out.Transport.TLS.KeyFile = conf.TLSKeyFile
+	out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
 
 
-	out.TLS.Force = conf.TLSOnly
-	out.TLS.CertFile = conf.TLSCertFile
-	out.TLS.KeyFile = conf.TLSKeyFile
-	out.TLS.TrustedCaFile = conf.TLSTrustedCaFile
+	out.MaxPortsPerClient = conf.MaxPortsPerClient
 
 
 	for _, v := range conf.HTTPPlugins {
 	for _, v := range conf.HTTPPlugins {
 		out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{
 		out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{

+ 1 - 2
pkg/config/load.go

@@ -23,7 +23,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
-	"github.com/BurntSushi/toml"
+	toml "github.com/pelletier/go-toml/v2"
 	"github.com/samber/lo"
 	"github.com/samber/lo"
 	"gopkg.in/ini.v1"
 	"gopkg.in/ini.v1"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -119,7 +119,6 @@ func LoadConfigure(b []byte, c any) error {
 			return err
 			return err
 		}
 		}
 	}
 	}
-
 	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
 	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
 	return decoder.Decode(c)
 	return decoder.Decode(c)
 }
 }

+ 1 - 1
pkg/config/v1/client.go

@@ -168,7 +168,7 @@ type AuthClientConfig struct {
 	Method string `json:"method,omitempty"`
 	Method string `json:"method,omitempty"`
 	// Specify whether to include auth info in additional scope.
 	// Specify whether to include auth info in additional scope.
 	// Current supported scopes are: "HeartBeats", "NewWorkConns".
 	// Current supported scopes are: "HeartBeats", "NewWorkConns".
-	AdditionalAuthScopes []AuthScope `json:"additionalAuthScopes,omitempty"`
+	AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
 	// Token specifies the authorization token used to create keys to be sent
 	// Token specifies the authorization token used to create keys to be sent
 	// to the server. The server must have a matching token for authorization
 	// to the server. The server must have a matching token for authorization
 	// to succeed.  By default, this value is "".
 	// to succeed.  By default, this value is "".

+ 3 - 2
pkg/config/v1/plugin.go

@@ -46,10 +46,11 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
 	if !ok {
 	if !ok {
 		return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
 		return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
 	}
 	}
-	if err := json.Unmarshal(b, v); err != nil {
+	options := reflect.New(v).Interface().(ClientPluginOptions)
+	if err := json.Unmarshal(b, options); err != nil {
 		return err
 		return err
 	}
 	}
-	c.ClientPluginOptions = v
+	c.ClientPluginOptions = options
 	return nil
 	return nil
 }
 }
 
 

+ 9 - 9
pkg/config/v1/server.go

@@ -76,8 +76,6 @@ type ServerConfig struct {
 
 
 	Transport ServerTransportConfig `json:"transport,omitempty"`
 	Transport ServerTransportConfig `json:"transport,omitempty"`
 
 
-	TLS TLSServerConfig `json:"tls,omitempty"`
-
 	// DetailedErrorsToClient defines whether to send the specific error (with
 	// DetailedErrorsToClient defines whether to send the specific error (with
 	// debug info) to frpc. By default, this value is true.
 	// debug info) to frpc. By default, this value is true.
 	DetailedErrorsToClient *bool `json:"detailedErrorsToClient,omitempty"`
 	DetailedErrorsToClient *bool `json:"detailedErrorsToClient,omitempty"`
@@ -109,9 +107,6 @@ func (c *ServerConfig) Complete() {
 	if c.ProxyBindAddr == "" {
 	if c.ProxyBindAddr == "" {
 		c.ProxyBindAddr = c.BindAddr
 		c.ProxyBindAddr = c.BindAddr
 	}
 	}
-	if c.TLS.TrustedCaFile != "" {
-		c.TLS.Force = true
-	}
 
 
 	if c.WebServer.Port > 0 {
 	if c.WebServer.Port > 0 {
 		c.WebServer.Addr = util.EmptyOr(c.WebServer.Addr, "0.0.0.0")
 		c.WebServer.Addr = util.EmptyOr(c.WebServer.Addr, "0.0.0.0")
@@ -125,10 +120,10 @@ func (c *ServerConfig) Complete() {
 }
 }
 
 
 type AuthServerConfig struct {
 type AuthServerConfig struct {
-	Method               string               `json:"method,omitempty"`
-	AdditionalAuthScopes []AuthScope          `json:"additionalAuthScopes,omitempty"`
-	Token                string               `json:"token,omitempty"`
-	OIDC                 AuthOIDCServerConfig `json:"oidc,omitempty"`
+	Method           string               `json:"method,omitempty"`
+	AdditionalScopes []AuthScope          `json:"additionalScopes,omitempty"`
+	Token            string               `json:"token,omitempty"`
+	OIDC             AuthOIDCServerConfig `json:"oidc,omitempty"`
 }
 }
 
 
 func (c *AuthServerConfig) Complete() {
 func (c *AuthServerConfig) Complete() {
@@ -171,6 +166,8 @@ type ServerTransportConfig struct {
 	HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
 	HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
 	// QUIC options.
 	// QUIC options.
 	QUIC QUICOptions `json:"quic,omitempty"`
 	QUIC QUICOptions `json:"quic,omitempty"`
+	// TLS specifies TLS settings for the connection from the client.
+	TLS TLSServerConfig `json:"tls,omitempty"`
 }
 }
 
 
 func (c *ServerTransportConfig) Complete() {
 func (c *ServerTransportConfig) Complete() {
@@ -180,6 +177,9 @@ func (c *ServerTransportConfig) Complete() {
 	c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
 	c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
 	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
 	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
 	c.QUIC.Complete()
 	c.QUIC.Complete()
+	if c.TLS.TrustedCaFile != "" {
+		c.TLS.Force = true
+	}
 }
 }
 
 
 type TLSServerConfig struct {
 type TLSServerConfig struct {

+ 1 - 1
pkg/config/v1/validation/client.go

@@ -71,7 +71,7 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigu
 		warning, err := ValidateClientCommonConfig(c)
 		warning, err := ValidateClientCommonConfig(c)
 		warnings = AppendError(warnings, warning)
 		warnings = AppendError(warnings, warning)
 		if err != nil {
 		if err != nil {
-			return err, warnings
+			return warnings, err
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
pkg/config/v1/visitor.go

@@ -35,7 +35,7 @@ type VisitorBaseConfig struct {
 	Name      string           `json:"name"`
 	Name      string           `json:"name"`
 	Type      string           `json:"type"`
 	Type      string           `json:"type"`
 	Transport VisitorTransport `json:"transport,omitempty"`
 	Transport VisitorTransport `json:"transport,omitempty"`
-	SecretKey string           `json:"sk,omitempty"`
+	SecretKey string           `json:"secretKey,omitempty"`
 	// if the server user is not set, it defaults to the current user
 	// if the server user is not set, it defaults to the current user
 	ServerUser string `json:"serverUser,omitempty"`
 	ServerUser string `json:"serverUser,omitempty"`
 	ServerName string `json:"serverName,omitempty"`
 	ServerName string `json:"serverName,omitempty"`

+ 3 - 0
pkg/plugin/client/plugin.go

@@ -32,6 +32,9 @@ var creators = make(map[string]CreatorFn)
 type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error)
 type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error)
 
 
 func Register(name string, fn CreatorFn) {
 func Register(name string, fn CreatorFn) {
+	if _, exist := creators[name]; exist {
+		panic(fmt.Sprintf("plugin [%s] is already registered", name))
+	}
 	creators[name] = fn
 	creators[name] = fn
 }
 }
 
 

+ 1 - 1
server/dashboard_api.go

@@ -86,7 +86,7 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
 		MaxPortsPerClient:     svr.cfg.MaxPortsPerClient,
 		MaxPortsPerClient:     svr.cfg.MaxPortsPerClient,
 		HeartBeatTimeout:      svr.cfg.Transport.HeartbeatTimeout,
 		HeartBeatTimeout:      svr.cfg.Transport.HeartbeatTimeout,
 		AllowPortsStr:         types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
 		AllowPortsStr:         types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
-		TLSOnly:               svr.cfg.TLS.Force,
+		TLSOnly:               svr.cfg.Transport.TLS.Force,
 
 
 		TotalTrafficIn:  serverStats.TotalTrafficIn,
 		TotalTrafficIn:  serverStats.TotalTrafficIn,
 		TotalTrafficOut: serverStats.TotalTrafficOut,
 		TotalTrafficOut: serverStats.TotalTrafficOut,

+ 4 - 4
server/service.go

@@ -108,9 +108,9 @@ type Service struct {
 
 
 func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 	tlsConfig, err := transport.NewServerTLSConfig(
 	tlsConfig, err := transport.NewServerTLSConfig(
-		cfg.TLS.CertFile,
-		cfg.TLS.KeyFile,
-		cfg.TLS.TrustedCaFile)
+		cfg.Transport.TLS.CertFile,
+		cfg.Transport.TLS.KeyFile,
+		cfg.Transport.TLS.TrustedCaFile)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -455,7 +455,7 @@ func (svr *Service) HandleListener(l net.Listener) {
 		log.Trace("start check TLS connection...")
 		log.Trace("start check TLS connection...")
 		originConn := c
 		originConn := c
 		var isTLS, custom bool
 		var isTLS, custom bool
-		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLS.Force, connReadTimeout)
+		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout)
 		if err != nil {
 		if err != nil {
 			log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
 			log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
 			originConn.Close()
 			originConn.Close()

+ 6 - 3
test/e2e/e2e_test.go

@@ -10,10 +10,13 @@ import (
 
 
 	"github.com/fatedier/frp/pkg/util/log"
 	"github.com/fatedier/frp/pkg/util/log"
 	// test source
 	// test source
-	_ "github.com/fatedier/frp/test/e2e/basic"
-	_ "github.com/fatedier/frp/test/e2e/features"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework"
-	_ "github.com/fatedier/frp/test/e2e/plugin"
+	_ "github.com/fatedier/frp/test/e2e/legacy/basic"
+	_ "github.com/fatedier/frp/test/e2e/legacy/features"
+	_ "github.com/fatedier/frp/test/e2e/legacy/plugin"
+	_ "github.com/fatedier/frp/test/e2e/v1/basic"
+	_ "github.com/fatedier/frp/test/e2e/v1/features"
+	_ "github.com/fatedier/frp/test/e2e/v1/plugin"
 )
 )
 
 
 // handleFlags sets up all flags and parses the command line.
 // handleFlags sets up all flags and parses the command line.

+ 5 - 4
test/e2e/examples.go

@@ -19,10 +19,11 @@ var _ = ginkgo.Describe("[Feature: Example]", func() {
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
-			[tcp]
-			type = tcp
-			local_port = {{ .%s }}
-			remote_port = %d
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
 			`, framework.TCPEchoServerPort, remotePort)
 			`, framework.TCPEchoServerPort, remotePort)
 
 
 			f.RunProcesses([]string{serverConf}, []string{clientConf})
 			f.RunProcesses([]string{serverConf}, []string{clientConf})

+ 15 - 1
test/e2e/framework/consts/consts.go

@@ -18,12 +18,23 @@ var (
 	PortClientAdmin string
 	PortClientAdmin string
 
 
 	DefaultServerConfig = `
 	DefaultServerConfig = `
+bindPort = {{ .%s }}
+log.level = "trace"
+`
+
+	DefaultClientConfig = `
+serverAddr = "127.0.0.1"
+serverPort = {{ .%s }}
+log.level = "trace"
+`
+
+	LegacyDefaultServerConfig = `
 	[common]
 	[common]
 	bind_port = {{ .%s }}
 	bind_port = {{ .%s }}
 	log_level = trace
 	log_level = trace
 	`
 	`
 
 
-	DefaultClientConfig = `
+	LegacyDefaultClientConfig = `
 	[common]
 	[common]
 	server_addr = 127.0.0.1
 	server_addr = 127.0.0.1
 	server_port = {{ .%s }}
 	server_port = {{ .%s }}
@@ -34,6 +45,9 @@ var (
 func init() {
 func init() {
 	PortServerName = port.GenName("Server")
 	PortServerName = port.GenName("Server")
 	PortClientAdmin = port.GenName("ClientAdmin")
 	PortClientAdmin = port.GenName("ClientAdmin")
+	LegacyDefaultServerConfig = fmt.Sprintf(LegacyDefaultServerConfig, port.GenName("Server"))
+	LegacyDefaultClientConfig = fmt.Sprintf(LegacyDefaultClientConfig, port.GenName("Server"))
+
 	DefaultServerConfig = fmt.Sprintf(DefaultServerConfig, port.GenName("Server"))
 	DefaultServerConfig = fmt.Sprintf(DefaultServerConfig, port.GenName("Server"))
 	DefaultClientConfig = fmt.Sprintf(DefaultClientConfig, port.GenName("Server"))
 	DefaultClientConfig = fmt.Sprintf(DefaultClientConfig, port.GenName("Server"))
 }
 }

+ 8 - 2
test/e2e/framework/process.go

@@ -29,7 +29,10 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
 		err = os.WriteFile(path, []byte(outs[i]), 0o666)
 		err = os.WriteFile(path, []byte(outs[i]), 0o666)
 		ExpectNoError(err)
 		ExpectNoError(err)
-		flog.Trace("[%s] %s", path, outs[i])
+
+		if TestContext.Debug {
+			flog.Debug("[%s] %s", path, outs[i])
+		}
 
 
 		p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs)
 		p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs)
 		f.serverConfPaths = append(f.serverConfPaths, path)
 		f.serverConfPaths = append(f.serverConfPaths, path)
@@ -46,7 +49,10 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
 		err = os.WriteFile(path, []byte(outs[index]), 0o666)
 		err = os.WriteFile(path, []byte(outs[index]), 0o666)
 		ExpectNoError(err)
 		ExpectNoError(err)
-		flog.Trace("[%s] %s", path, outs[index])
+
+		if TestContext.Debug {
+			flog.Debug("[%s] %s", path, outs[index])
+		}
 
 
 		p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs)
 		p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs)
 		f.clientConfPaths = append(f.clientConfPaths, path)
 		f.clientConfPaths = append(f.clientConfPaths, path)

+ 12 - 12
test/e2e/basic/basic.go → test/e2e/legacy/basic/basic.go

@@ -25,8 +25,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 		for _, t := range types {
 		for _, t := range types {
 			proxyType := t
 			proxyType := t
 			ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() {
 			ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() {
-				serverConf := consts.DefaultServerConfig
-				clientConf := consts.DefaultClientConfig
+				serverConf := consts.LegacyDefaultServerConfig
+				clientConf := consts.LegacyDefaultClientConfig
 
 
 				localPortName := ""
 				localPortName := ""
 				protocol := "tcp"
 				protocol := "tcp"
@@ -96,13 +96,13 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 
 
 	ginkgo.Describe("HTTP", func() {
 	ginkgo.Describe("HTTP", func() {
 		ginkgo.It("proxy to HTTP server", func() {
 		ginkgo.It("proxy to HTTP server", func() {
-			serverConf := consts.DefaultServerConfig
+			serverConf := consts.LegacyDefaultServerConfig
 			vhostHTTPPort := f.AllocPort()
 			vhostHTTPPort := f.AllocPort()
 			serverConf += fmt.Sprintf(`
 			serverConf += fmt.Sprintf(`
 			vhost_http_port = %d
 			vhost_http_port = %d
 			`, vhostHTTPPort)
 			`, vhostHTTPPort)
 
 
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			getProxyConf := func(proxyName string, customDomains string, extra string) string {
 			getProxyConf := func(proxyName string, customDomains string, extra string) string {
 				return fmt.Sprintf(`
 				return fmt.Sprintf(`
@@ -178,14 +178,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 
 
 	ginkgo.Describe("HTTPS", func() {
 	ginkgo.Describe("HTTPS", func() {
 		ginkgo.It("proxy to HTTPS server", func() {
 		ginkgo.It("proxy to HTTPS server", func() {
-			serverConf := consts.DefaultServerConfig
+			serverConf := consts.LegacyDefaultServerConfig
 			vhostHTTPSPort := f.AllocPort()
 			vhostHTTPSPort := f.AllocPort()
 			serverConf += fmt.Sprintf(`
 			serverConf += fmt.Sprintf(`
 			vhost_https_port = %d
 			vhost_https_port = %d
 			`, vhostHTTPSPort)
 			`, vhostHTTPSPort)
 
 
 			localPort := f.AllocPort()
 			localPort := f.AllocPort()
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 			getProxyConf := func(proxyName string, customDomains string, extra string) string {
 			getProxyConf := func(proxyName string, customDomains string, extra string) string {
 				return fmt.Sprintf(`
 				return fmt.Sprintf(`
 				[%s]
 				[%s]
@@ -281,10 +281,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 		for _, t := range types {
 		for _, t := range types {
 			proxyType := t
 			proxyType := t
 			ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() {
 			ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() {
-				serverConf := consts.DefaultServerConfig
-				clientServerConf := consts.DefaultClientConfig + "\nuser = user1"
-				clientVisitorConf := consts.DefaultClientConfig + "\nuser = user1"
-				clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = user2"
+				serverConf := consts.LegacyDefaultServerConfig
+				clientServerConf := consts.LegacyDefaultClientConfig + "\nuser = user1"
+				clientVisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user1"
+				clientUser2VisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user2"
 
 
 				localPortName := ""
 				localPortName := ""
 				protocol := "tcp"
 				protocol := "tcp"
@@ -439,8 +439,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 
 
 	ginkgo.Describe("TCPMUX", func() {
 	ginkgo.Describe("TCPMUX", func() {
 		ginkgo.It("Type tcpmux", func() {
 		ginkgo.It("Type tcpmux", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			tcpmuxHTTPConnectPortName := port.GenName("TCPMUX")
 			tcpmuxHTTPConnectPortName := port.GenName("TCPMUX")
 			serverConf += fmt.Sprintf(`
 			serverConf += fmt.Sprintf(`

+ 6 - 6
test/e2e/basic/client.go → test/e2e/legacy/basic/client.go

@@ -18,7 +18,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	ginkgo.It("Update && Reload API", func() {
 	ginkgo.It("Update && Reload API", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 
 
 		adminPort := f.AllocPort()
 		adminPort := f.AllocPort()
 
 
@@ -26,7 +26,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 		p2Port := f.AllocPort()
 		p2Port := f.AllocPort()
 		p3Port := f.AllocPort()
 		p3Port := f.AllocPort()
 
 
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		admin_port = %d
 		admin_port = %d
 
 
 		[p1]
 		[p1]
@@ -80,10 +80,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 	})
 	})
 
 
 	ginkgo.It("healthz", func() {
 	ginkgo.It("healthz", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 
 
 		dashboardPort := f.AllocPort()
 		dashboardPort := f.AllocPort()
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		admin_addr = 0.0.0.0
 		admin_addr = 0.0.0.0
 		admin_port = %d
 		admin_port = %d
 		admin_user = admin
 		admin_user = admin
@@ -103,11 +103,11 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 	})
 	})
 
 
 	ginkgo.It("stop", func() {
 	ginkgo.It("stop", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 
 
 		adminPort := f.AllocPort()
 		adminPort := f.AllocPort()
 		testPort := f.AllocPort()
 		testPort := f.AllocPort()
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		admin_port = %d
 		admin_port = %d
 
 
 		[test]
 		[test]

+ 3 - 3
test/e2e/basic/client_server.go → test/e2e/legacy/basic/client_server.go

@@ -33,8 +33,8 @@ func renderBindPortConfig(protocol string) string {
 }
 }
 
 
 func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {
 func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {
-	serverConf := consts.DefaultServerConfig
-	clientConf := consts.DefaultClientConfig
+	serverConf := consts.LegacyDefaultServerConfig
+	clientConf := consts.LegacyDefaultClientConfig
 	if configures.clientPrefix != "" {
 	if configures.clientPrefix != "" {
 		clientConf = configures.clientPrefix
 		clientConf = configures.clientPrefix
 	}
 	}
@@ -64,7 +64,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur
 
 
 	clientConfs := []string{clientConf}
 	clientConfs := []string{clientConf}
 	if configures.client2 != "" {
 	if configures.client2 != "" {
-		client2Conf := consts.DefaultClientConfig
+		client2Conf := consts.LegacyDefaultClientConfig
 		if configures.client2Prefix != "" {
 		if configures.client2Prefix != "" {
 			client2Conf = configures.client2Prefix
 			client2Conf = configures.client2Prefix
 		}
 		}

+ 0 - 0
test/e2e/basic/cmd.go → test/e2e/legacy/basic/cmd.go


+ 2 - 2
test/e2e/basic/config.go → test/e2e/legacy/basic/config.go

@@ -15,8 +15,8 @@ var _ = ginkgo.Describe("[Feature: Config]", func() {
 
 
 	ginkgo.Describe("Template", func() {
 	ginkgo.Describe("Template", func() {
 		ginkgo.It("render by env", func() {
 		ginkgo.It("render by env", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			portName := port.GenName("TCP")
 			portName := port.GenName("TCP")
 			serverConf += fmt.Sprintf(`
 			serverConf += fmt.Sprintf(`

+ 9 - 9
test/e2e/basic/http.go → test/e2e/legacy/basic/http.go

@@ -19,7 +19,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	getDefaultServerConf := func(vhostHTTPPort int) string {
 	getDefaultServerConf := func(vhostHTTPPort int) string {
-		conf := consts.DefaultServerConfig + `
+		conf := consts.LegacyDefaultServerConfig + `
 		vhost_http_port = %d
 		vhost_http_port = %d
 		`
 		`
 		return fmt.Sprintf(conf, vhostHTTPPort)
 		return fmt.Sprintf(conf, vhostHTTPPort)
@@ -41,7 +41,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		barPort := f.AllocPort()
 		barPort := f.AllocPort()
 		f.RunServer("", newHTTPServer(barPort, "bar"))
 		f.RunServer("", newHTTPServer(barPort, "bar"))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[foo]
 			[foo]
 			type = http
 			type = http
@@ -91,7 +91,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		otherPort := f.AllocPort()
 		otherPort := f.AllocPort()
 		f.RunServer("", newHTTPServer(otherPort, "other"))
 		f.RunServer("", newHTTPServer(otherPort, "other"))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[foo]
 			[foo]
 			type = http
 			type = http
@@ -142,7 +142,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		vhostHTTPPort := f.AllocPort()
 		vhostHTTPPort := f.AllocPort()
 		serverConf := getDefaultServerConf(vhostHTTPPort)
 		serverConf := getDefaultServerConf(vhostHTTPPort)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = http
 			type = http
@@ -180,7 +180,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		vhostHTTPPort := f.AllocPort()
 		vhostHTTPPort := f.AllocPort()
 		serverConf := getDefaultServerConf(vhostHTTPPort)
 		serverConf := getDefaultServerConf(vhostHTTPPort)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = http
 			type = http
@@ -225,7 +225,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		barPort := f.AllocPort()
 		barPort := f.AllocPort()
 		f.RunServer("", newHTTPServer(barPort, "bar"))
 		f.RunServer("", newHTTPServer(barPort, "bar"))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[foo]
 			[foo]
 			type = http
 			type = http
@@ -270,7 +270,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		)
 		)
 		f.RunServer("", localServer)
 		f.RunServer("", localServer)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = http
 			type = http
@@ -303,7 +303,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		)
 		)
 		f.RunServer("", localServer)
 		f.RunServer("", localServer)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = http
 			type = http
@@ -352,7 +352,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 
 
 		f.RunServer("", localServer)
 		f.RunServer("", localServer)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = http
 			type = http

+ 8 - 8
test/e2e/basic/server.go → test/e2e/legacy/basic/server.go

@@ -18,8 +18,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	ginkgo.It("Ports Whitelist", func() {
 	ginkgo.It("Ports Whitelist", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		serverConf += `
 		serverConf += `
 			allow_ports = 20000-25000,25002,30000-50000
 			allow_ports = 20000-25000,25002,30000-50000
@@ -81,8 +81,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 	})
 	})
 
 
 	ginkgo.It("Alloc Random Port", func() {
 	ginkgo.It("Alloc Random Port", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		adminPort := f.AllocPort()
 		adminPort := f.AllocPort()
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
@@ -125,13 +125,13 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 	})
 	})
 
 
 	ginkgo.It("Port Reuse", func() {
 	ginkgo.It("Port Reuse", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 		// Use same port as PortServer
 		// Use same port as PortServer
 		serverConf += fmt.Sprintf(`
 		serverConf += fmt.Sprintf(`
 		vhost_http_port = {{ .%s }}
 		vhost_http_port = {{ .%s }}
 		`, consts.PortServerName)
 		`, consts.PortServerName)
 
 
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		[http]
 		[http]
 		type = http
 		type = http
 		local_port = {{ .%s }}
 		local_port = {{ .%s }}
@@ -146,7 +146,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 	})
 	})
 
 
 	ginkgo.It("healthz", func() {
 	ginkgo.It("healthz", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 		dashboardPort := f.AllocPort()
 		dashboardPort := f.AllocPort()
 
 
 		// Use same port as PortServer
 		// Use same port as PortServer
@@ -158,7 +158,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 		dashboard_pwd = admin
 		dashboard_pwd = admin
 		`, consts.PortServerName, dashboardPort)
 		`, consts.PortServerName, dashboardPort)
 
 
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		[http]
 		[http]
 		type = http
 		type = http
 		local_port = {{ .%s }}
 		local_port = {{ .%s }}

+ 4 - 4
test/e2e/basic/tcpmux.go → test/e2e/legacy/basic/tcpmux.go

@@ -20,7 +20,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	getDefaultServerConf := func(httpconnectPort int) string {
 	getDefaultServerConf := func(httpconnectPort int) string {
-		conf := consts.DefaultServerConfig + `
+		conf := consts.LegacyDefaultServerConfig + `
 		tcpmux_httpconnect_port = %d
 		tcpmux_httpconnect_port = %d
 		`
 		`
 		return fmt.Sprintf(conf, httpconnectPort)
 		return fmt.Sprintf(conf, httpconnectPort)
@@ -53,7 +53,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
 		otherPort := f.AllocPort()
 		otherPort := f.AllocPort()
 		f.RunServer("", newServer(otherPort, "other"))
 		f.RunServer("", newServer(otherPort, "other"))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[foo]
 			[foo]
 			type = tcpmux
 			type = tcpmux
@@ -110,7 +110,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
 		fooPort := f.AllocPort()
 		fooPort := f.AllocPort()
 		f.RunServer("", newServer(fooPort, "foo"))
 		f.RunServer("", newServer(fooPort, "foo"))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = tcpmux
 			type = tcpmux
@@ -195,7 +195,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
 		f.RunServer("", newServer(localPort))
 		f.RunServer("", newServer(localPort))
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 			[test]
 			[test]
 			type = tcpmux
 			type = tcpmux

+ 2 - 2
test/e2e/basic/xtcp.go → test/e2e/legacy/basic/xtcp.go

@@ -16,8 +16,8 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	ginkgo.It("Fallback To STCP", func() {
 	ginkgo.It("Fallback To STCP", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		bindPortName := port.GenName("XTCP")
 		bindPortName := port.GenName("XTCP")
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`

+ 5 - 5
test/e2e/features/bandwidth_limit.go → test/e2e/legacy/features/bandwidth_limit.go

@@ -10,17 +10,17 @@ import (
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	plugintest "github.com/fatedier/frp/test/e2e/legacy/plugin"
 	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
 	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
-	plugintest "github.com/fatedier/frp/test/e2e/plugin"
 )
 )
 
 
 var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
 var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
 	f := framework.NewDefaultFramework()
 	f := framework.NewDefaultFramework()
 
 
 	ginkgo.It("Proxy Bandwidth Limit by Client", func() {
 	ginkgo.It("Proxy Bandwidth Limit by Client", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
 		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))
 		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))
@@ -69,13 +69,13 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
 
 
 		f.RunServer("", pluginServer)
 		f.RunServer("", pluginServer)
 
 
-		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 		[plugin.test]
 		[plugin.test]
 		addr = 127.0.0.1:%d
 		addr = 127.0.0.1:%d
 		path = /handler
 		path = /handler
 		ops = NewProxy
 		ops = NewProxy
 		`, pluginPort)
 		`, pluginPort)
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
 		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))
 		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))

+ 0 - 0
test/e2e/features/chaos.go → test/e2e/legacy/features/chaos.go


+ 6 - 6
test/e2e/features/group.go → test/e2e/legacy/features/group.go

@@ -62,8 +62,8 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
 
 
 	ginkgo.Describe("Load Balancing", func() {
 	ginkgo.Describe("Load Balancing", func() {
 		ginkgo.It("TCP", func() {
 		ginkgo.It("TCP", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			fooPort := f.AllocPort()
 			fooPort := f.AllocPort()
 			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
 			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
@@ -114,8 +114,8 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
 
 
 	ginkgo.Describe("Health Check", func() {
 	ginkgo.Describe("Health Check", func() {
 		ginkgo.It("TCP", func() {
 		ginkgo.It("TCP", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			fooPort := f.AllocPort()
 			fooPort := f.AllocPort()
 			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
 			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
@@ -180,10 +180,10 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
 
 
 		ginkgo.It("HTTP", func() {
 		ginkgo.It("HTTP", func() {
 			vhostPort := f.AllocPort()
 			vhostPort := f.AllocPort()
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			vhost_http_port = %d
 			vhost_http_port = %d
 			`, vhostPort)
 			`, vhostPort)
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			fooPort := f.AllocPort()
 			fooPort := f.AllocPort()
 			fooServer := newHTTPServer(fooPort, "foo")
 			fooServer := newHTTPServer(fooPort, "foo")

+ 0 - 0
test/e2e/features/heartbeat.go → test/e2e/legacy/features/heartbeat.go


+ 2 - 2
test/e2e/features/monitor.go → test/e2e/legacy/features/monitor.go

@@ -18,13 +18,13 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() {
 
 
 	ginkgo.It("Prometheus metrics", func() {
 	ginkgo.It("Prometheus metrics", func() {
 		dashboardPort := f.AllocPort()
 		dashboardPort := f.AllocPort()
-		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 		enable_prometheus = true
 		enable_prometheus = true
 		dashboard_addr = 0.0.0.0
 		dashboard_addr = 0.0.0.0
 		dashboard_port = %d
 		dashboard_port = %d
 		`, dashboardPort)
 		`, dashboardPort)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		remotePort := f.AllocPort()
 		remotePort := f.AllocPort()
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 		[tcp]
 		[tcp]

+ 6 - 6
test/e2e/features/real_ip.go → test/e2e/legacy/features/real_ip.go

@@ -23,7 +23,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 
 
 	ginkgo.It("HTTP X-Forwarded-For", func() {
 	ginkgo.It("HTTP X-Forwarded-For", func() {
 		vhostHTTPPort := f.AllocPort()
 		vhostHTTPPort := f.AllocPort()
-		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 		vhost_http_port = %d
 		vhost_http_port = %d
 		`, vhostHTTPPort)
 		`, vhostHTTPPort)
 
 
@@ -36,7 +36,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 		)
 		)
 		f.RunServer("", localServer)
 		f.RunServer("", localServer)
 
 
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
 		[test]
 		[test]
 		type = http
 		type = http
@@ -56,8 +56,8 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 
 
 	ginkgo.Describe("Proxy Protocol", func() {
 	ginkgo.Describe("Proxy Protocol", func() {
 		ginkgo.It("TCP", func() {
 		ginkgo.It("TCP", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			localPort := f.AllocPort()
 			localPort := f.AllocPort()
 			localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort),
 			localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort),
@@ -107,11 +107,11 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() {
 
 
 		ginkgo.It("HTTP", func() {
 		ginkgo.It("HTTP", func() {
 			vhostHTTPPort := f.AllocPort()
 			vhostHTTPPort := f.AllocPort()
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 		vhost_http_port = %d
 		vhost_http_port = %d
 		`, vhostHTTPPort)
 		`, vhostHTTPPort)
 
 
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			localPort := f.AllocPort()
 			localPort := f.AllocPort()
 			var srcAddrRecord string
 			var srcAddrRecord string

+ 14 - 14
test/e2e/plugin/client.go → test/e2e/legacy/plugin/client.go

@@ -21,8 +21,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 
 
 	ginkgo.Describe("UnixDomainSocket", func() {
 	ginkgo.Describe("UnixDomainSocket", func() {
 		ginkgo.It("Expose a unix domain socket echo server", func() {
 		ginkgo.It("Expose a unix domain socket echo server", func() {
-			serverConf := consts.DefaultServerConfig
-			clientConf := consts.DefaultClientConfig
+			serverConf := consts.LegacyDefaultServerConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			getProxyConf := func(proxyName string, portName string, extra string) string {
 			getProxyConf := func(proxyName string, portName string, extra string) string {
 				return fmt.Sprintf(`
 				return fmt.Sprintf(`
@@ -77,8 +77,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 	})
 	})
 
 
 	ginkgo.It("http_proxy", func() {
 	ginkgo.It("http_proxy", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		remotePort := f.AllocPort()
 		remotePort := f.AllocPort()
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
@@ -109,8 +109,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 	})
 	})
 
 
 	ginkgo.It("socks5 proxy", func() {
 	ginkgo.It("socks5 proxy", func() {
-		serverConf := consts.DefaultServerConfig
-		clientConf := consts.DefaultClientConfig
+		serverConf := consts.LegacyDefaultServerConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		remotePort := f.AllocPort()
 		remotePort := f.AllocPort()
 		clientConf += fmt.Sprintf(`
 		clientConf += fmt.Sprintf(`
@@ -137,10 +137,10 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 
 
 	ginkgo.It("static_file", func() {
 	ginkgo.It("static_file", func() {
 		vhostPort := f.AllocPort()
 		vhostPort := f.AllocPort()
-		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 		vhost_http_port = %d
 		vhost_http_port = %d
 		`, vhostPort)
 		`, vhostPort)
-		clientConf := consts.DefaultClientConfig
+		clientConf := consts.LegacyDefaultClientConfig
 
 
 		remotePort := f.AllocPort()
 		remotePort := f.AllocPort()
 		f.WriteTempFile("test_static_file", "foo")
 		f.WriteTempFile("test_static_file", "foo")
@@ -185,14 +185,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 	})
 	})
 
 
 	ginkgo.It("http2https", func() {
 	ginkgo.It("http2https", func() {
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 		vhostHTTPPort := f.AllocPort()
 		vhostHTTPPort := f.AllocPort()
 		serverConf += fmt.Sprintf(`
 		serverConf += fmt.Sprintf(`
 		vhost_http_port = %d
 		vhost_http_port = %d
 		`, vhostHTTPPort)
 		`, vhostHTTPPort)
 
 
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		[http2https]
 		[http2https]
 		type = http
 		type = http
 		custom_domains = example.com
 		custom_domains = example.com
@@ -227,14 +227,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
 		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
 		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
 		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
 
 
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 		vhostHTTPSPort := f.AllocPort()
 		vhostHTTPSPort := f.AllocPort()
 		serverConf += fmt.Sprintf(`
 		serverConf += fmt.Sprintf(`
 		vhost_https_port = %d
 		vhost_https_port = %d
 		`, vhostHTTPSPort)
 		`, vhostHTTPSPort)
 
 
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		[https2http]
 		[https2http]
 		type = https
 		type = https
 		custom_domains = example.com
 		custom_domains = example.com
@@ -271,14 +271,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
 		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
 		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
 		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
 		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
 
 
-		serverConf := consts.DefaultServerConfig
+		serverConf := consts.LegacyDefaultServerConfig
 		vhostHTTPSPort := f.AllocPort()
 		vhostHTTPSPort := f.AllocPort()
 		serverConf += fmt.Sprintf(`
 		serverConf += fmt.Sprintf(`
 		vhost_https_port = %d
 		vhost_https_port = %d
 		`, vhostHTTPSPort)
 		`, vhostHTTPSPort)
 
 
 		localPort := f.AllocPort()
 		localPort := f.AllocPort()
-		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 		[https2https]
 		[https2https]
 		type = https
 		type = https
 		custom_domains = example.com
 		custom_domains = example.com

+ 17 - 17
test/e2e/plugin/server.go → test/e2e/legacy/plugin/server.go

@@ -44,13 +44,13 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.user-manager]
 			[plugin.user-manager]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
 			ops = Login
 			ops = Login
 			`, localPort)
 			`, localPort)
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
@@ -63,7 +63,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			`, framework.TCPEchoServerPort, remotePort)
 			`, framework.TCPEchoServerPort, remotePort)
 
 
 			remotePort2 := f.AllocPort()
 			remotePort2 := f.AllocPort()
-			invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+			invalidTokenClientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(`
 			[tcp2]
 			[tcp2]
 			type = tcp
 			type = tcp
 			local_port = {{ .%s }}
 			local_port = {{ .%s }}
@@ -102,13 +102,13 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
 			ops = NewProxy
 			ops = NewProxy
 			`, localPort)
 			`, localPort)
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
@@ -137,13 +137,13 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
 			ops = NewProxy
 			ops = NewProxy
 			`, localPort)
 			`, localPort)
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
 			[tcp]
 			[tcp]
@@ -178,13 +178,13 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
 			ops = CloseProxy
 			ops = CloseProxy
 			`, localPort)
 			`, localPort)
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
@@ -230,7 +230,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
@@ -238,7 +238,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			`, localPort)
 			`, localPort)
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
 			heartbeat_interval = 1
 			heartbeat_interval = 1
 			authenticate_heartbeats = true
 			authenticate_heartbeats = true
@@ -280,7 +280,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
@@ -288,7 +288,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			`, localPort)
 			`, localPort)
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
 			[tcp]
 			[tcp]
 			type = tcp
 			type = tcp
@@ -325,7 +325,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = 127.0.0.1:%d
 			addr = 127.0.0.1:%d
 			path = /handler
 			path = /handler
@@ -333,7 +333,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			`, localPort)
 			`, localPort)
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
 			[tcp]
 			[tcp]
 			type = tcp
 			type = tcp
@@ -372,7 +372,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 
 
 			f.RunServer("", pluginServer)
 			f.RunServer("", pluginServer)
 
 
-			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(`
 			[plugin.test]
 			[plugin.test]
 			addr = https://127.0.0.1:%d
 			addr = https://127.0.0.1:%d
 			path = /handler
 			path = /handler
@@ -380,7 +380,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			`, localPort)
 			`, localPort)
 
 
 			remotePort := f.AllocPort()
 			remotePort := f.AllocPort()
-			clientConf := consts.DefaultClientConfig
+			clientConf := consts.LegacyDefaultClientConfig
 			clientConf += fmt.Sprintf(`
 			clientConf += fmt.Sprintf(`
 			[tcp]
 			[tcp]
 			type = tcp
 			type = tcp

+ 0 - 0
test/e2e/plugin/utils.go → test/e2e/legacy/plugin/utils.go


+ 524 - 0
test/e2e/v1/basic/basic.go

@@ -0,0 +1,524 @@
+package basic
+
+import (
+	"crypto/tls"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: Basic]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("TCP && UDP", func() {
+		types := []string{"tcp", "udp"}
+		for _, t := range types {
+			proxyType := t
+			ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() {
+				serverConf := consts.DefaultServerConfig
+				clientConf := consts.DefaultClientConfig
+
+				localPortName := ""
+				protocol := "tcp"
+				switch proxyType {
+				case "tcp":
+					localPortName = framework.TCPEchoServerPort
+					protocol = "tcp"
+				case "udp":
+					localPortName = framework.UDPEchoServerPort
+					protocol = "udp"
+				}
+				getProxyConf := func(proxyName string, portName string, extra string) string {
+					return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "%s"
+				localPort = {{ .%s }}
+				remotePort = {{ .%s }}
+				`+extra, proxyName, proxyType, localPortName, portName)
+				}
+
+				tests := []struct {
+					proxyName   string
+					portName    string
+					extraConfig string
+				}{
+					{
+						proxyName: "normal",
+						portName:  port.GenName("Normal"),
+					},
+					{
+						proxyName:   "with-encryption",
+						portName:    port.GenName("WithEncryption"),
+						extraConfig: "transport.useEncryption = true",
+					},
+					{
+						proxyName:   "with-compression",
+						portName:    port.GenName("WithCompression"),
+						extraConfig: "transport.useCompression = true",
+					},
+					{
+						proxyName: "with-encryption-and-compression",
+						portName:  port.GenName("WithEncryptionAndCompression"),
+						extraConfig: `
+						transport.useEncryption = true
+						transport.useCompression = true
+						`,
+					},
+				}
+
+				// build all client config
+				for _, test := range tests {
+					clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n"
+				}
+				// run frps and frpc
+				f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+				for _, test := range tests {
+					framework.NewRequestExpect(f).
+						Protocol(protocol).
+						PortName(test.portName).
+						Explain(test.proxyName).
+						Ensure()
+				}
+			})
+		}
+	})
+
+	ginkgo.Describe("HTTP", func() {
+		ginkgo.It("proxy to HTTP server", func() {
+			serverConf := consts.DefaultServerConfig
+			vhostHTTPPort := f.AllocPort()
+			serverConf += fmt.Sprintf(`
+			vhostHTTPPort = %d
+			`, vhostHTTPPort)
+
+			clientConf := consts.DefaultClientConfig
+
+			getProxyConf := func(proxyName string, customDomains string, extra string) string {
+				return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "http"
+				localPort = {{ .%s }}
+				customDomains = %s
+				`+extra, proxyName, framework.HTTPSimpleServerPort, customDomains)
+			}
+
+			tests := []struct {
+				proxyName     string
+				customDomains string
+				extraConfig   string
+			}{
+				{
+					proxyName: "normal",
+				},
+				{
+					proxyName:   "with-encryption",
+					extraConfig: "transport.useEncryption = true",
+				},
+				{
+					proxyName:   "with-compression",
+					extraConfig: "transport.useCompression = true",
+				},
+				{
+					proxyName: "with-encryption-and-compression",
+					extraConfig: `
+					transport.useEncryption = true
+					transport.useCompression = true
+					`,
+				},
+				{
+					proxyName:     "multiple-custom-domains",
+					customDomains: `["a.example.com", "b.example.com"]`,
+				},
+			}
+
+			// build all client config
+			for i, test := range tests {
+				if tests[i].customDomains == "" {
+					tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com")
+				}
+				clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n"
+			}
+			// run frps and frpc
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			for _, test := range tests {
+				for _, domain := range strings.Split(test.customDomains, ",") {
+					domain = strings.TrimSpace(domain)
+					domain = strings.TrimLeft(domain, "[\"")
+					domain = strings.TrimRight(domain, "]\"")
+					framework.NewRequestExpect(f).
+						Explain(test.proxyName + "-" + domain).
+						Port(vhostHTTPPort).
+						RequestModify(func(r *request.Request) {
+							r.HTTP().HTTPHost(domain)
+						}).
+						Ensure()
+				}
+			}
+
+			// not exist host
+			framework.NewRequestExpect(f).
+				Explain("not exist host").
+				Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("not-exist.example.com")
+				}).
+				Ensure(framework.ExpectResponseCode(404))
+		})
+	})
+
+	ginkgo.Describe("HTTPS", func() {
+		ginkgo.It("proxy to HTTPS server", func() {
+			serverConf := consts.DefaultServerConfig
+			vhostHTTPSPort := f.AllocPort()
+			serverConf += fmt.Sprintf(`
+			vhostHTTPSPort = %d
+			`, vhostHTTPSPort)
+
+			localPort := f.AllocPort()
+			clientConf := consts.DefaultClientConfig
+			getProxyConf := func(proxyName string, customDomains string, extra string) string {
+				return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "https"
+				localPort = %d
+				customDomains = %s
+				`+extra, proxyName, localPort, customDomains)
+			}
+
+			tests := []struct {
+				proxyName     string
+				customDomains string
+				extraConfig   string
+			}{
+				{
+					proxyName: "normal",
+				},
+				{
+					proxyName:   "with-encryption",
+					extraConfig: "transport.useEncryption = true",
+				},
+				{
+					proxyName:   "with-compression",
+					extraConfig: "transport.useCompression = true",
+				},
+				{
+					proxyName: "with-encryption-and-compression",
+					extraConfig: `
+						transport.useEncryption = true
+						transport.useCompression = true
+						`,
+				},
+				{
+					proxyName:     "multiple-custom-domains",
+					customDomains: `["a.example.com", "b.example.com"]`,
+				},
+			}
+
+			// build all client config
+			for i, test := range tests {
+				if tests[i].customDomains == "" {
+					tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com")
+				}
+				clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n"
+			}
+			// run frps and frpc
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+			framework.ExpectNoError(err)
+			localServer := httpserver.New(
+				httpserver.WithBindPort(localPort),
+				httpserver.WithTLSConfig(tlsConfig),
+				httpserver.WithResponse([]byte("test")),
+			)
+			f.RunServer("", localServer)
+
+			for _, test := range tests {
+				for _, domain := range strings.Split(test.customDomains, ",") {
+					domain = strings.TrimSpace(domain)
+					domain = strings.TrimLeft(domain, "[\"")
+					domain = strings.TrimRight(domain, "]\"")
+					framework.NewRequestExpect(f).
+						Explain(test.proxyName + "-" + domain).
+						Port(vhostHTTPSPort).
+						RequestModify(func(r *request.Request) {
+							r.HTTPS().HTTPHost(domain).TLSConfig(&tls.Config{
+								ServerName:         domain,
+								InsecureSkipVerify: true,
+							})
+						}).
+						ExpectResp([]byte("test")).
+						Ensure()
+				}
+			}
+
+			// not exist host
+			notExistDomain := "not-exist.example.com"
+			framework.NewRequestExpect(f).
+				Explain("not exist host").
+				Port(vhostHTTPSPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTPS().HTTPHost(notExistDomain).TLSConfig(&tls.Config{
+						ServerName:         notExistDomain,
+						InsecureSkipVerify: true,
+					})
+				}).
+				ExpectError(true).
+				Ensure()
+		})
+	})
+
+	ginkgo.Describe("STCP && SUDP && XTCP", func() {
+		types := []string{"stcp", "sudp", "xtcp"}
+		for _, t := range types {
+			proxyType := t
+			ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() {
+				serverConf := consts.DefaultServerConfig
+				clientServerConf := consts.DefaultClientConfig + "\nuser = \"user1\""
+				clientVisitorConf := consts.DefaultClientConfig + "\nuser = \"user1\""
+				clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = \"user2\""
+
+				localPortName := ""
+				protocol := "tcp"
+				switch proxyType {
+				case "stcp":
+					localPortName = framework.TCPEchoServerPort
+					protocol = "tcp"
+				case "sudp":
+					localPortName = framework.UDPEchoServerPort
+					protocol = "udp"
+				case "xtcp":
+					localPortName = framework.TCPEchoServerPort
+					protocol = "tcp"
+					ginkgo.Skip("stun server is not stable")
+				}
+
+				correctSK := "abc"
+				wrongSK := "123"
+
+				getProxyServerConf := func(proxyName string, extra string) string {
+					return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "%s"
+				secretKey = "%s"
+				localPort = {{ .%s }}
+				`+extra, proxyName, proxyType, correctSK, localPortName)
+				}
+				getProxyVisitorConf := func(proxyName string, portName, visitorSK, extra string) string {
+					return fmt.Sprintf(`
+				[[visitors]]
+				name = "%s"
+				type = "%s"
+				serverName = "%s"
+				secretKey = "%s"
+				bindPort = {{ .%s }}
+				`+extra, proxyName, proxyType, proxyName, visitorSK, portName)
+				}
+
+				tests := []struct {
+					proxyName          string
+					bindPortName       string
+					visitorSK          string
+					commonExtraConfig  string
+					proxyExtraConfig   string
+					visitorExtraConfig string
+					expectError        bool
+					deployUser2Client  bool
+					// skipXTCP is used to skip xtcp test case
+					skipXTCP bool
+				}{
+					{
+						proxyName:    "normal",
+						bindPortName: port.GenName("Normal"),
+						visitorSK:    correctSK,
+						skipXTCP:     true,
+					},
+					{
+						proxyName:         "with-encryption",
+						bindPortName:      port.GenName("WithEncryption"),
+						visitorSK:         correctSK,
+						commonExtraConfig: "transport.useEncryption = true",
+						skipXTCP:          true,
+					},
+					{
+						proxyName:         "with-compression",
+						bindPortName:      port.GenName("WithCompression"),
+						visitorSK:         correctSK,
+						commonExtraConfig: "transport.useCompression = true",
+						skipXTCP:          true,
+					},
+					{
+						proxyName:    "with-encryption-and-compression",
+						bindPortName: port.GenName("WithEncryptionAndCompression"),
+						visitorSK:    correctSK,
+						commonExtraConfig: `
+						transport.useEncryption = true
+						transport.useCompression = true
+						`,
+						skipXTCP: true,
+					},
+					{
+						proxyName:    "with-error-sk",
+						bindPortName: port.GenName("WithErrorSK"),
+						visitorSK:    wrongSK,
+						expectError:  true,
+					},
+					{
+						proxyName:          "allowed-user",
+						bindPortName:       port.GenName("AllowedUser"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   `allowUsers = ["another", "user2"]`,
+						visitorExtraConfig: `serverUser = "user1"`,
+						deployUser2Client:  true,
+					},
+					{
+						proxyName:          "not-allowed-user",
+						bindPortName:       port.GenName("NotAllowedUser"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   `allowUsers = ["invalid"]`,
+						visitorExtraConfig: `serverUser = "user1"`,
+						expectError:        true,
+					},
+					{
+						proxyName:          "allow-all",
+						bindPortName:       port.GenName("AllowAll"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   `allowUsers = ["*"]`,
+						visitorExtraConfig: `serverUser = "user1"`,
+						deployUser2Client:  true,
+					},
+				}
+
+				// build all client config
+				for _, test := range tests {
+					clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n"
+				}
+				for _, test := range tests {
+					config := getProxyVisitorConf(
+						test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig,
+					) + "\n"
+					if test.deployUser2Client {
+						clientUser2VisitorConf += config
+					} else {
+						clientVisitorConf += config
+					}
+				}
+				// run frps and frpc
+				f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf})
+
+				for _, test := range tests {
+					timeout := time.Second
+					if t == "xtcp" {
+						if test.skipXTCP {
+							continue
+						}
+						timeout = 10 * time.Second
+					}
+					framework.NewRequestExpect(f).
+						RequestModify(func(r *request.Request) {
+							r.Timeout(timeout)
+						}).
+						Protocol(protocol).
+						PortName(test.bindPortName).
+						Explain(test.proxyName).
+						ExpectError(test.expectError).
+						Ensure()
+				}
+			})
+		}
+	})
+
+	ginkgo.Describe("TCPMUX", func() {
+		ginkgo.It("Type tcpmux", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			tcpmuxHTTPConnectPortName := port.GenName("TCPMUX")
+			serverConf += fmt.Sprintf(`
+			tcpmuxHTTPConnectPort = {{ .%s }}
+			`, tcpmuxHTTPConnectPortName)
+
+			getProxyConf := func(proxyName string, extra string) string {
+				return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "tcpmux"
+				multiplexer = "httpconnect"
+				localPort = {{ .%s }}
+				customDomains = ["%s"]
+				`+extra, proxyName, port.GenName(proxyName), proxyName)
+			}
+
+			tests := []struct {
+				proxyName   string
+				extraConfig string
+			}{
+				{
+					proxyName: "normal",
+				},
+				{
+					proxyName:   "with-encryption",
+					extraConfig: "transport.useEncryption = true",
+				},
+				{
+					proxyName:   "with-compression",
+					extraConfig: "transport.useCompression = true",
+				},
+				{
+					proxyName: "with-encryption-and-compression",
+					extraConfig: `
+					transport.useEncryption = true
+					transport.useCompression = true
+					`,
+				},
+			}
+
+			// build all client config
+			for _, test := range tests {
+				clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n"
+
+				localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName)))
+				f.RunServer(port.GenName(test.proxyName), localServer)
+			}
+
+			// run frps and frpc
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			// Request without HTTP connect should get error
+			framework.NewRequestExpect(f).
+				PortName(tcpmuxHTTPConnectPortName).
+				ExpectError(true).
+				Explain("request without HTTP connect expect error").
+				Ensure()
+
+			proxyURL := fmt.Sprintf("http://127.0.0.1:%d", f.PortByName(tcpmuxHTTPConnectPortName))
+			// Request with incorrect connect hostname
+			framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+				r.Addr("invalid").Proxy(proxyURL)
+			}).ExpectError(true).Explain("request without HTTP connect expect error").Ensure()
+
+			// Request with correct connect hostname
+			for _, test := range tests {
+				framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+					r.Addr(test.proxyName).Proxy(proxyURL)
+				}).ExpectResp([]byte(test.proxyName)).Explain(test.proxyName).Ensure()
+			}
+		})
+	})
+})

+ 136 - 0
test/e2e/v1/basic/client.go

@@ -0,0 +1,136 @@
+package basic
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+	clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client"
+)
+
+var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("Update && Reload API", func() {
+		serverConf := consts.DefaultServerConfig
+
+		adminPort := f.AllocPort()
+
+		p1Port := f.AllocPort()
+		p2Port := f.AllocPort()
+		p3Port := f.AllocPort()
+
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		webServer.port = %d
+
+		[[proxies]]
+		name = "p1"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = %d
+
+		[[proxies]]
+		name = "p2"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = %d
+
+		[[proxies]]
+		name = "p3"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = %d
+		`, adminPort,
+			framework.TCPEchoServerPort, p1Port,
+			framework.TCPEchoServerPort, p2Port,
+			framework.TCPEchoServerPort, p3Port)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(p1Port).Ensure()
+		framework.NewRequestExpect(f).Port(p2Port).Ensure()
+		framework.NewRequestExpect(f).Port(p3Port).Ensure()
+
+		client := clientsdk.New("127.0.0.1", adminPort)
+		conf, err := client.GetConfig()
+		framework.ExpectNoError(err)
+
+		newP2Port := f.AllocPort()
+		// change p2 port and remove p3 proxy
+		newClientConf := strings.ReplaceAll(conf, strconv.Itoa(p2Port), strconv.Itoa(newP2Port))
+		p3Index := strings.LastIndex(newClientConf, "[[proxies]]")
+		if p3Index >= 0 {
+			newClientConf = newClientConf[:p3Index]
+		}
+
+		err = client.UpdateConfig(newClientConf)
+		framework.ExpectNoError(err)
+
+		err = client.Reload()
+		framework.ExpectNoError(err)
+		time.Sleep(time.Second)
+
+		framework.NewRequestExpect(f).Port(p1Port).Explain("p1 port").Ensure()
+		framework.NewRequestExpect(f).Port(p2Port).Explain("original p2 port").ExpectError(true).Ensure()
+		framework.NewRequestExpect(f).Port(newP2Port).Explain("new p2 port").Ensure()
+		framework.NewRequestExpect(f).Port(p3Port).Explain("p3 port").ExpectError(true).Ensure()
+	})
+
+	ginkgo.It("healthz", func() {
+		serverConf := consts.DefaultServerConfig
+
+		dashboardPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+        webServer.addr = "0.0.0.0"
+		webServer.port = %d
+		webServer.user = "admin"
+		webServer.password = "admin"
+		`, dashboardPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/healthz")
+		}).Port(dashboardPort).ExpectResp([]byte("")).Ensure()
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/")
+		}).Port(dashboardPort).
+			Ensure(framework.ExpectResponseCode(401))
+	})
+
+	ginkgo.It("stop", func() {
+		serverConf := consts.DefaultServerConfig
+
+		adminPort := f.AllocPort()
+		testPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		webServer.port = %d
+
+		[[proxies]]
+		name = "test"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = %d
+		`, adminPort, framework.TCPEchoServerPort, testPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(testPort).Ensure()
+
+		client := clientsdk.New("127.0.0.1", adminPort)
+		err := client.Stop()
+		framework.ExpectNoError(err)
+
+		time.Sleep(3 * time.Second)
+
+		// frpc stopped so the port is not listened, expect error
+		framework.NewRequestExpect(f).Port(testPort).ExpectError(true).Ensure()
+	})
+})

+ 325 - 0
test/e2e/v1/basic/client_server.go

@@ -0,0 +1,325 @@
+package basic
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/cert"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+)
+
+type generalTestConfigures struct {
+	server        string
+	client        string
+	clientPrefix  string
+	client2       string
+	client2Prefix string
+	testDelay     time.Duration
+	expectError   bool
+}
+
+func renderBindPortConfig(protocol string) string {
+	if protocol == "kcp" {
+		return fmt.Sprintf(`kcpBindPort = {{ .%s }}`, consts.PortServerName)
+	} else if protocol == "quic" {
+		return fmt.Sprintf(`quicBindPort = {{ .%s }}`, consts.PortServerName)
+	}
+	return ""
+}
+
+func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {
+	serverConf := consts.DefaultServerConfig
+	clientConf := consts.DefaultClientConfig
+	if configures.clientPrefix != "" {
+		clientConf = configures.clientPrefix
+	}
+
+	serverConf += fmt.Sprintf(`
+	%s
+	`, configures.server)
+
+	tcpPortName := port.GenName("TCP")
+	udpPortName := port.GenName("UDP")
+	clientConf += fmt.Sprintf(`
+		%s
+
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = {{ .%s }}
+
+		[[proxies]]
+		name = "udp"
+		type = "udp"
+		localPort = {{ .%s }}
+		remotePort = {{ .%s }}
+		`, configures.client,
+		framework.TCPEchoServerPort, tcpPortName,
+		framework.UDPEchoServerPort, udpPortName,
+	)
+
+	clientConfs := []string{clientConf}
+	if configures.client2 != "" {
+		client2Conf := consts.DefaultClientConfig
+		if configures.client2Prefix != "" {
+			client2Conf = configures.client2Prefix
+		}
+		client2Conf += fmt.Sprintf(`
+			%s
+		`, configures.client2)
+		clientConfs = append(clientConfs, client2Conf)
+	}
+
+	f.RunProcesses([]string{serverConf}, clientConfs)
+
+	if configures.testDelay > 0 {
+		time.Sleep(configures.testDelay)
+	}
+
+	framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure()
+	framework.NewRequestExpect(f).Protocol("udp").
+		PortName(udpPortName).ExpectError(configures.expectError).Explain("udp proxy").Ensure()
+}
+
+// defineClientServerTest test a normal tcp and udp proxy with specified TestConfigures.
+func defineClientServerTest(desc string, f *framework.Framework, configures *generalTestConfigures) {
+	ginkgo.It(desc, func() {
+		runClientServerTest(f, configures)
+	})
+}
+
+var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("Protocol", func() {
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
+		for _, protocol := range supportProtocols {
+			configures := &generalTestConfigures{
+				server: fmt.Sprintf(`
+				%s
+				`, renderBindPortConfig(protocol)),
+				client: fmt.Sprintf(`transport.protocol = "%s"`, protocol),
+			}
+			defineClientServerTest(protocol, f, configures)
+		}
+	})
+
+	// wss is special, it needs to be tested separately.
+	// frps only supports ws, so there should be a proxy to terminate TLS before frps.
+	ginkgo.Describe("Protocol wss", func() {
+		wssPort := f.AllocPort()
+		configures := &generalTestConfigures{
+			clientPrefix: fmt.Sprintf(`
+				serverAddr = "127.0.0.1"
+				serverPort = %d
+				loginFailExit = false
+				transport.protocol = "wss"
+				log.level = "trace"
+			`, wssPort),
+			// Due to the fact that frps cannot directly accept wss connections, we use the https2http plugin of another frpc to terminate TLS.
+			client2: fmt.Sprintf(`
+				[[proxies]]
+				name = "wss2ws"
+				type = "tcp"
+				remotePort = %d
+				[proxies.plugin]
+				type = "https2http"
+				localAddr = "127.0.0.1:{{ .%s }}"
+			`, wssPort, consts.PortServerName),
+			testDelay: 10 * time.Second,
+		}
+
+		defineClientServerTest("wss", f, configures)
+	})
+
+	ginkgo.Describe("Authentication", func() {
+		defineClientServerTest("Token Correct", f, &generalTestConfigures{
+			server: `auth.token = "123456"`,
+			client: `auth.token = "123456"`,
+		})
+
+		defineClientServerTest("Token Incorrect", f, &generalTestConfigures{
+			server:      `auth.token = "123456"`,
+			client:      `auth.token = "invalid"`,
+			expectError: true,
+		})
+	})
+
+	ginkgo.Describe("TLS", func() {
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
+		for _, protocol := range supportProtocols {
+			tmp := protocol
+			// Since v0.50.0, the default value of tls_enable has been changed to true.
+			// Therefore, here it needs to be set as false to test the scenario of turning it off.
+			defineClientServerTest("Disable TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
+				server: fmt.Sprintf(`
+				%s
+				`, renderBindPortConfig(protocol)),
+				client: fmt.Sprintf(`transport.tls.enable = false
+				transport.protocol = "%s"
+				`, protocol),
+			})
+		}
+
+		defineClientServerTest("enable tls force, client with TLS", f, &generalTestConfigures{
+			server: "transport.tls.force = true",
+		})
+		defineClientServerTest("enable tls force, client without TLS", f, &generalTestConfigures{
+			server:      "transport.tls.force = true",
+			client:      "transport.tls.enable = false",
+			expectError: true,
+		})
+	})
+
+	ginkgo.Describe("TLS with custom certificate", func() {
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
+
+		var (
+			caCrtPath                    string
+			serverCrtPath, serverKeyPath string
+			clientCrtPath, clientKeyPath string
+		)
+		ginkgo.JustBeforeEach(func() {
+			generator := &cert.SelfSignedCertGenerator{}
+			artifacts, err := generator.Generate("127.0.0.1")
+			framework.ExpectNoError(err)
+
+			caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert))
+			serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert))
+			serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key))
+			generator.SetCA(artifacts.CACert, artifacts.CAKey)
+			_, err = generator.Generate("127.0.0.1")
+			framework.ExpectNoError(err)
+			clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert))
+			clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key))
+		})
+
+		for _, protocol := range supportProtocols {
+			tmp := protocol
+
+			ginkgo.It("one-way authentication: "+tmp, func() {
+				runClientServerTest(f, &generalTestConfigures{
+					server: fmt.Sprintf(`
+						%s
+						transport.tls.trustedCaFile = "%s"
+					`, renderBindPortConfig(tmp), caCrtPath),
+					client: fmt.Sprintf(`
+						transport.protocol = "%s"
+						transport.tls.certFile = "%s"
+						transport.tls.keyFile = "%s"
+					`, tmp, clientCrtPath, clientKeyPath),
+				})
+			})
+
+			ginkgo.It("mutual authentication: "+tmp, func() {
+				runClientServerTest(f, &generalTestConfigures{
+					server: fmt.Sprintf(`
+						%s
+						transport.tls.certFile = "%s"
+						transport.tls.keyFile = "%s"
+						transport.tls.trustedCaFile = "%s"
+					`, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath),
+					client: fmt.Sprintf(`
+						transport.protocol = "%s"
+						transport.tls.certFile = "%s"
+						transport.tls.keyFile = "%s"
+						transport.tls.trustedCaFile = "%s"
+					`, tmp, clientCrtPath, clientKeyPath, caCrtPath),
+				})
+			})
+		}
+	})
+
+	ginkgo.Describe("TLS with custom certificate and specified server name", func() {
+		var (
+			caCrtPath                    string
+			serverCrtPath, serverKeyPath string
+			clientCrtPath, clientKeyPath string
+		)
+		ginkgo.JustBeforeEach(func() {
+			generator := &cert.SelfSignedCertGenerator{}
+			artifacts, err := generator.Generate("example.com")
+			framework.ExpectNoError(err)
+
+			caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert))
+			serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert))
+			serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key))
+			generator.SetCA(artifacts.CACert, artifacts.CAKey)
+			_, err = generator.Generate("example.com")
+			framework.ExpectNoError(err)
+			clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert))
+			clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key))
+		})
+
+		ginkgo.It("mutual authentication", func() {
+			runClientServerTest(f, &generalTestConfigures{
+				server: fmt.Sprintf(`
+				transport.tls.certFile = "%s"
+				transport.tls.keyFile = "%s"
+				transport.tls.trustedCaFile = "%s"
+				`, serverCrtPath, serverKeyPath, caCrtPath),
+				client: fmt.Sprintf(`
+				transport.tls.serverName = "example.com"
+				transport.tls.certFile = "%s"
+				transport.tls.keyFile = "%s"
+				transport.tls.trustedCaFile = "%s"
+				`, clientCrtPath, clientKeyPath, caCrtPath),
+			})
+		})
+
+		ginkgo.It("mutual authentication with incorrect server name", func() {
+			runClientServerTest(f, &generalTestConfigures{
+				server: fmt.Sprintf(`
+				transport.tls.certFile = "%s"
+				transport.tls.keyFile = "%s"
+				transport.tls.trustedCaFile = "%s"
+				`, serverCrtPath, serverKeyPath, caCrtPath),
+				client: fmt.Sprintf(`
+				transport.tls.serverName = "invalid.com"
+				transport.tls.certFile = "%s"
+				transport.tls.keyFile = "%s"
+				transport.tls.trustedCaFile = "%s"
+				`, clientCrtPath, clientKeyPath, caCrtPath),
+				expectError: true,
+			})
+		})
+	})
+
+	ginkgo.Describe("TLS with disable_custom_tls_first_byte set to false", func() {
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
+		for _, protocol := range supportProtocols {
+			tmp := protocol
+			defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
+				server: fmt.Sprintf(`
+					%s
+					`, renderBindPortConfig(protocol)),
+				client: fmt.Sprintf(`
+					transport.protocol = "%s"
+					transport.tls.disableCustomTLSFirstByte = false
+					`, protocol),
+			})
+		}
+	})
+
+	ginkgo.Describe("IPv6 bind address", func() {
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
+		for _, protocol := range supportProtocols {
+			tmp := protocol
+			defineClientServerTest("IPv6 bind address: "+strings.ToUpper(tmp), f, &generalTestConfigures{
+				server: fmt.Sprintf(`
+					bindAddr = "::"
+					%s
+					`, renderBindPortConfig(protocol)),
+				client: fmt.Sprintf(`
+					transport.protocol = "%s"
+					`, protocol),
+			})
+		}
+	})
+})

+ 109 - 0
test/e2e/v1/basic/cmd.go

@@ -0,0 +1,109 @@
+package basic
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+const (
+	ConfigValidStr = "syntax is ok"
+)
+
+var _ = ginkgo.Describe("[Feature: Cmd]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("Verify", func() {
+		ginkgo.It("frps valid", func() {
+			path := f.GenerateConfigFile(`
+			bindAddr = "0.0.0.0"
+			bindPort = 7000
+			`)
+			_, output, err := f.RunFrps("verify", "-c", path)
+			framework.ExpectNoError(err)
+			framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output)
+		})
+		ginkgo.It("frps invalid", func() {
+			path := f.GenerateConfigFile(`
+			bindAddr = "0.0.0.0"
+			bindPort = 70000
+			`)
+			_, output, err := f.RunFrps("verify", "-c", path)
+			framework.ExpectNoError(err)
+			framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output)
+		})
+		ginkgo.It("frpc valid", func() {
+			path := f.GenerateConfigFile(`
+			serverAddr = "0.0.0.0"
+			serverPort = 7000
+			`)
+			_, output, err := f.RunFrpc("verify", "-c", path)
+			framework.ExpectNoError(err)
+			framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output)
+		})
+		ginkgo.It("frpc invalid", func() {
+			path := f.GenerateConfigFile(`
+			serverAddr = "0.0.0.0"
+			serverPort = 7000
+			transport.protocol = "invalid"
+			`)
+			_, output, err := f.RunFrpc("verify", "-c", path)
+			framework.ExpectNoError(err)
+			framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output)
+		})
+	})
+
+	ginkgo.Describe("Single proxy", func() {
+		ginkgo.It("TCP", func() {
+			serverPort := f.AllocPort()
+			_, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort))
+			framework.ExpectNoError(err)
+
+			localPort := f.PortByName(framework.TCPEchoServerPort)
+			remotePort := f.AllocPort()
+			_, _, err = f.RunFrpc("tcp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test",
+				"-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "tcp_test")
+			framework.ExpectNoError(err)
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+		})
+
+		ginkgo.It("UDP", func() {
+			serverPort := f.AllocPort()
+			_, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort))
+			framework.ExpectNoError(err)
+
+			localPort := f.PortByName(framework.UDPEchoServerPort)
+			remotePort := f.AllocPort()
+			_, _, err = f.RunFrpc("udp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test",
+				"-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "udp_test")
+			framework.ExpectNoError(err)
+
+			framework.NewRequestExpect(f).Protocol("udp").
+				Port(remotePort).Ensure()
+		})
+
+		ginkgo.It("HTTP", func() {
+			serverPort := f.AllocPort()
+			vhostHTTPPort := f.AllocPort()
+			_, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost_http_port", strconv.Itoa(vhostHTTPPort))
+			framework.ExpectNoError(err)
+
+			_, _, err = f.RunFrpc("http", "-s", "127.0.0.1:"+strconv.Itoa(serverPort), "-t", "123", "-u", "test",
+				"-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)),
+				"--custom_domain", "test.example.com")
+			framework.ExpectNoError(err)
+
+			framework.NewRequestExpect(f).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("test.example.com")
+				}).
+				Ensure()
+		})
+	})
+})

+ 84 - 0
test/e2e/v1/basic/config.go

@@ -0,0 +1,84 @@
+package basic
+
+import (
+	"fmt"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+)
+
+var _ = ginkgo.Describe("[Feature: Config]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("Template", func() {
+		ginkgo.It("render by env", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			portName := port.GenName("TCP")
+			serverConf += fmt.Sprintf(`
+			auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}"
+			`, "`", "`")
+
+			clientConf += fmt.Sprintf(`
+			auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}"
+
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = {{ .%s }}
+			`, "`", "`", framework.TCPEchoServerPort, portName)
+
+			f.SetEnvs([]string{"FRP_TOKEN=123"})
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).PortName(portName).Ensure()
+		})
+	})
+
+	ginkgo.Describe("Includes", func() {
+		ginkgo.It("split tcp proxies into different files", func() {
+			serverPort := f.AllocPort()
+			serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(`
+			bindAddr = "0.0.0.0"
+			bindPort = %d
+			`, serverPort))
+
+			remotePort := f.AllocPort()
+			proxyConfigPath := f.GenerateConfigFile(fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			`, f.PortByName(framework.TCPEchoServerPort), remotePort))
+
+			remotePort2 := f.AllocPort()
+			proxyConfigPath2 := f.GenerateConfigFile(fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp2"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			`, f.PortByName(framework.TCPEchoServerPort), remotePort2))
+
+			clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(`
+			serverPort = %d
+			includes = ["%s","%s"]
+			`, serverPort, proxyConfigPath, proxyConfigPath2))
+
+			_, _, err := f.RunFrps("-c", serverConfigPath)
+			framework.ExpectNoError(err)
+
+			_, _, err = f.RunFrpc("-c", clientConfigPath)
+			framework.ExpectNoError(err)
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+			framework.NewRequestExpect(f).Port(remotePort2).Ensure()
+		})
+	})
+})

+ 388 - 0
test/e2e/v1/basic/http.go

@@ -0,0 +1,388 @@
+package basic
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"github.com/gorilla/websocket"
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: HTTP]", func() {
+	f := framework.NewDefaultFramework()
+
+	getDefaultServerConf := func(vhostHTTPPort int) string {
+		conf := consts.DefaultServerConfig + `
+		vhostHTTPPort = %d
+		`
+		return fmt.Sprintf(conf, vhostHTTPPort)
+	}
+	newHTTPServer := func(port int, respContent string) *httpserver.Server {
+		return httpserver.New(
+			httpserver.WithBindPort(port),
+			httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))),
+		)
+	}
+
+	ginkgo.It("HTTP route by locations", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(fooPort, "foo"))
+
+		barPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(barPort, "bar"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			locations = ["/","/foo"]
+
+			[[proxies]]
+			name = "bar"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			locations = ["/bar"]
+			`, fooPort, barPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		tests := []struct {
+			path       string
+			expectResp string
+			desc       string
+		}{
+			{path: "/foo", expectResp: "foo", desc: "foo path"},
+			{path: "/bar", expectResp: "bar", desc: "bar path"},
+			{path: "/other", expectResp: "foo", desc: "other path"},
+		}
+
+		for _, test := range tests {
+			framework.NewRequestExpect(f).Explain(test.desc).Port(vhostHTTPPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("normal.example.com").HTTPPath(test.path)
+				}).
+				ExpectResp([]byte(test.expectResp)).
+				Ensure()
+		}
+	})
+
+	ginkgo.It("HTTP route by HTTP user", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(fooPort, "foo"))
+
+		barPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(barPort, "bar"))
+
+		otherPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(otherPort, "other"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			routeByHTTPUser = "user1"
+
+			[[proxies]]
+			name = "bar"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			routeByHTTPUser = "user2"
+
+			[[proxies]]
+			name = "catchAll"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			`, fooPort, barPort, otherPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// user1
+		framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user1", "")
+			}).
+			ExpectResp([]byte("foo")).
+			Ensure()
+
+		// user2
+		framework.NewRequestExpect(f).Explain("user2").Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user2", "")
+			}).
+			ExpectResp([]byte("bar")).
+			Ensure()
+
+		// other user
+		framework.NewRequestExpect(f).Explain("other user").Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user3", "")
+			}).
+			ExpectResp([]byte("other")).
+			Ensure()
+	})
+
+	ginkgo.It("HTTP Basic Auth", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = {{ .%s }}
+			customDomains = ["normal.example.com"]
+			httpUser = "test"
+			httpPassword = "test"
+			`, framework.HTTPSimpleServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// not set auth header
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).
+			Ensure(framework.ExpectResponseCode(401))
+
+		// set incorrect auth header
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "invalid")
+			}).
+			Ensure(framework.ExpectResponseCode(401))
+
+		// set correct auth header
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "test")
+			}).
+			Ensure()
+	})
+
+	ginkgo.It("Wildcard domain", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = {{ .%s }}
+			customDomains = ["*.example.com"]
+			`, framework.HTTPSimpleServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// not match host
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("not-match.test.com")
+			}).
+			Ensure(framework.ExpectResponseCode(404))
+
+		// test.example.com match *.example.com
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("test.example.com")
+			}).
+			Ensure()
+
+		// sub.test.example.com match *.example.com
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("sub.test.example.com")
+			}).
+			Ensure()
+	})
+
+	ginkgo.It("Subdomain", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+		serverConf += `
+		subdomainHost = "example.com"
+		`
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(fooPort, "foo"))
+
+		barPort := f.AllocPort()
+		f.RunServer("", newHTTPServer(barPort, "bar"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "http"
+			localPort = %d
+			subdomain = "foo"
+
+			[[proxies]]
+			name = "bar"
+			type = "http"
+			localPort = %d
+			subdomain = "bar"
+			`, fooPort, barPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// foo
+		framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("foo.example.com")
+			}).
+			ExpectResp([]byte("foo")).
+			Ensure()
+
+		// bar
+		framework.NewRequestExpect(f).Explain("bar subdomain").Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("bar.example.com")
+			}).
+			ExpectResp([]byte("bar")).
+			Ensure()
+	})
+
+	ginkgo.It("Modify headers", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				_, _ = w.Write([]byte(req.Header.Get("X-From-Where")))
+			})),
+		)
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			requestHeaders.set.x-from-where = "frp"
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// not set auth header
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).
+			ExpectResp([]byte("frp")). // local http server will write this X-From-Where header to response body
+			Ensure()
+	})
+
+	ginkgo.It("Host Header Rewrite", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				_, _ = w.Write([]byte(req.Host))
+			})),
+		)
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			hostHeaderRewrite = "rewrite.example.com"
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).
+			ExpectResp([]byte("rewrite.example.com")). // local http server will write host header to response body
+			Ensure()
+	})
+
+	ginkgo.It("Websocket protocol", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		upgrader := websocket.Upgrader{}
+
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				c, err := upgrader.Upgrade(w, req, nil)
+				if err != nil {
+					return
+				}
+				defer c.Close()
+				for {
+					mt, message, err := c.ReadMessage()
+					if err != nil {
+						break
+					}
+					err = c.WriteMessage(mt, message)
+					if err != nil {
+						break
+					}
+				}
+			})),
+		)
+
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["127.0.0.1"]
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)}
+		c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
+		framework.ExpectNoError(err)
+
+		err = c.WriteMessage(websocket.TextMessage, []byte(consts.TestString))
+		framework.ExpectNoError(err)
+
+		_, msg, err := c.ReadMessage()
+		framework.ExpectNoError(err)
+		framework.ExpectEqualValues(consts.TestString, string(msg))
+	})
+})

+ 192 - 0
test/e2e/v1/basic/server.go

@@ -0,0 +1,192 @@
+package basic
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+	clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client"
+)
+
+var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("Ports Whitelist", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		serverConf += `
+		allowPorts = [
+		  { start = 20000, end = 25000 },
+		  { single = 25002 },
+		  { start = 30000, end = 50000 },
+		]
+		`
+
+		tcpPortName := port.GenName("TCP", port.WithRangePorts(20000, 25000))
+		udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000))
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp-allowded-in-range"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = {{ .%s }}
+			`, framework.TCPEchoServerPort, tcpPortName)
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp-port-not-allowed"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = 25001
+			`, framework.TCPEchoServerPort)
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp-port-unavailable"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = {{ .%s }}
+			`, framework.TCPEchoServerPort, consts.PortServerName)
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "udp-allowed-in-range"
+			type = "udp"
+			localPort = {{ .%s }}
+			remotePort = {{ .%s }}
+			`, framework.UDPEchoServerPort, udpPortName)
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "udp-port-not-allowed"
+			type = "udp"
+			localPort = {{ .%s }}
+			remotePort = 25003
+			`, framework.UDPEchoServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// TCP
+		// Allowed in range
+		framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
+
+		// Not Allowed
+		framework.NewRequestExpect(f).Port(25001).ExpectError(true).Ensure()
+
+		// Unavailable, already bind by frps
+		framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
+
+		// UDP
+		// Allowed in range
+		framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure()
+
+		// Not Allowed
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.UDP().Port(25003)
+		}).ExpectError(true).Ensure()
+	})
+
+	ginkgo.It("Alloc Random Port", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		adminPort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+		webServer.port = %d
+
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		localPort = {{ .%s }}
+
+		[[proxies]]
+		name = "udp"
+		type = "udp"
+		localPort = {{ .%s }}
+		`, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		client := clientsdk.New("127.0.0.1", adminPort)
+
+		// tcp random port
+		status, err := client.GetProxyStatus("tcp")
+		framework.ExpectNoError(err)
+
+		_, portStr, err := net.SplitHostPort(status.RemoteAddr)
+		framework.ExpectNoError(err)
+		port, err := strconv.Atoi(portStr)
+		framework.ExpectNoError(err)
+
+		framework.NewRequestExpect(f).Port(port).Ensure()
+
+		// udp random port
+		status, err = client.GetProxyStatus("udp")
+		framework.ExpectNoError(err)
+
+		_, portStr, err = net.SplitHostPort(status.RemoteAddr)
+		framework.ExpectNoError(err)
+		port, err = strconv.Atoi(portStr)
+		framework.ExpectNoError(err)
+
+		framework.NewRequestExpect(f).Protocol("udp").Port(port).Ensure()
+	})
+
+	ginkgo.It("Port Reuse", func() {
+		serverConf := consts.DefaultServerConfig
+		// Use same port as PortServer
+		serverConf += fmt.Sprintf(`
+		vhostHTTPPort = {{ .%s }}
+		`, consts.PortServerName)
+
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[[proxies]]
+		name = "http"
+		type = "http"
+		localPort = {{ .%s }}
+		customDomains = ["example.com"]
+		`, framework.HTTPSimpleServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPHost("example.com")
+		}).PortName(consts.PortServerName).Ensure()
+	})
+
+	ginkgo.It("healthz", func() {
+		serverConf := consts.DefaultServerConfig
+		dashboardPort := f.AllocPort()
+
+		// Use same port as PortServer
+		serverConf += fmt.Sprintf(`
+		vhostHTTPPort = {{ .%s }}
+		webServer.addr = "0.0.0.0"
+		webServer.port = %d
+		webServer.user = "admin"
+		webServer.password = "admin"
+		`, consts.PortServerName, dashboardPort)
+
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[[proxies]]
+		name = "http"
+		type = "http"
+		localPort = {{ .%s }}
+		customDomains = ["example.com"]
+		`, framework.HTTPSimpleServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/healthz")
+		}).Port(dashboardPort).ExpectResp([]byte("")).Ensure()
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/")
+		}).Port(dashboardPort).
+			Ensure(framework.ExpectResponseCode(401))
+	})
+})

+ 223 - 0
test/e2e/v1/basic/tcpmux.go

@@ -0,0 +1,223 @@
+package basic
+
+import (
+	"bufio"
+	"fmt"
+	"net"
+	"net/http"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/pkg/util/util"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+	"github.com/fatedier/frp/test/e2e/pkg/rpc"
+)
+
+var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
+	f := framework.NewDefaultFramework()
+
+	getDefaultServerConf := func(httpconnectPort int) string {
+		conf := consts.DefaultServerConfig + `
+		tcpmuxHTTPConnectPort = %d
+		`
+		return fmt.Sprintf(conf, httpconnectPort)
+	}
+	newServer := func(port int, respContent string) *streamserver.Server {
+		return streamserver.New(
+			streamserver.TCP,
+			streamserver.WithBindPort(port),
+			streamserver.WithRespContent([]byte(respContent)),
+		)
+	}
+
+	proxyURLWithAuth := func(username, password string, port int) string {
+		if username == "" {
+			return fmt.Sprintf("http://127.0.0.1:%d", port)
+		}
+		return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, port)
+	}
+
+	ginkgo.It("Route by HTTP user", func() {
+		vhostPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostPort)
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newServer(fooPort, "foo"))
+
+		barPort := f.AllocPort()
+		f.RunServer("", newServer(barPort, "bar"))
+
+		otherPort := f.AllocPort()
+		f.RunServer("", newServer(otherPort, "other"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "tcpmux"
+			multiplexer = "httpconnect"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			routeByHTTPUser = "user1"
+
+			[[proxies]]
+			name = "bar"
+			type = "tcpmux"
+			multiplexer = "httpconnect"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			routeByHTTPUser = "user2"
+
+			[[proxies]]
+			name = "catchAll"
+			type = "tcpmux"
+			multiplexer = "httpconnect"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			`, fooPort, barPort, otherPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// user1
+		framework.NewRequestExpect(f).Explain("user1").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user1", "", vhostPort))
+			}).
+			ExpectResp([]byte("foo")).
+			Ensure()
+
+		// user2
+		framework.NewRequestExpect(f).Explain("user2").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user2", "", vhostPort))
+			}).
+			ExpectResp([]byte("bar")).
+			Ensure()
+
+		// other user
+		framework.NewRequestExpect(f).Explain("other user").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user3", "", vhostPort))
+			}).
+			ExpectResp([]byte("other")).
+			Ensure()
+	})
+
+	ginkgo.It("Proxy auth", func() {
+		vhostPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostPort)
+
+		fooPort := f.AllocPort()
+		f.RunServer("", newServer(fooPort, "foo"))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "tcpmux"
+			multiplexer = "httpconnect"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			httpUser = "test"
+			httpPassword = "test"
+		`, fooPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// not set auth header
+		framework.NewRequestExpect(f).Explain("no auth").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort))
+			}).
+			ExpectError(true).
+			Ensure()
+
+		// set incorrect auth header
+		framework.NewRequestExpect(f).Explain("incorrect auth").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "invalid", vhostPort))
+			}).
+			ExpectError(true).
+			Ensure()
+
+		// set correct auth header
+		framework.NewRequestExpect(f).Explain("correct auth").
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "test", vhostPort))
+			}).
+			ExpectResp([]byte("foo")).
+			Ensure()
+	})
+
+	ginkgo.It("TCPMux Passthrough", func() {
+		vhostPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostPort)
+		serverConf += `
+		tcpmuxPassthrough = true
+		`
+
+		var (
+			respErr            error
+			connectRequestHost string
+		)
+		newServer := func(port int) *streamserver.Server {
+			return streamserver.New(
+				streamserver.TCP,
+				streamserver.WithBindPort(port),
+				streamserver.WithCustomHandler(func(conn net.Conn) {
+					defer conn.Close()
+
+					// read HTTP CONNECT request
+					bufioReader := bufio.NewReader(conn)
+					req, err := http.ReadRequest(bufioReader)
+					if err != nil {
+						respErr = err
+						return
+					}
+					connectRequestHost = req.Host
+
+					// return ok response
+					res := util.OkResponse()
+					if res.Body != nil {
+						defer res.Body.Close()
+					}
+					_ = res.Write(conn)
+
+					buf, err := rpc.ReadBytes(conn)
+					if err != nil {
+						respErr = err
+						return
+					}
+					_, _ = rpc.WriteBytes(conn, buf)
+				}),
+			)
+		}
+
+		localPort := f.AllocPort()
+		f.RunServer("", newServer(localPort))
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "tcpmux"
+			multiplexer = "httpconnect"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).
+			RequestModify(func(r *request.Request) {
+				r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)).Body([]byte("frp"))
+			}).
+			ExpectResp([]byte("frp")).
+			Ensure()
+		framework.ExpectNoError(respErr)
+		framework.ExpectEqualValues(connectRequestHost, "normal.example.com")
+	})
+})

+ 53 - 0
test/e2e/v1/basic/xtcp.go

@@ -0,0 +1,53 @@
+package basic
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: XTCP]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("Fallback To STCP", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		bindPortName := port.GenName("XTCP")
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "stcp"
+			localPort = {{ .%s }}
+
+			[[visitors]]
+			name = "foo-visitor"
+			type = "stcp"
+			serverName = "foo"
+			bindPort = -1
+
+			[[visitors]]
+			name = "bar-visitor"
+			type = "xtcp"
+			serverName = "bar"
+			bindPort = {{ .%s }}
+			keepTunnelOpen = true
+			fallbackTo = "foo-visitor"
+			fallbackTimeoutMs = 200
+			`, framework.TCPEchoServerPort, bindPortName)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+		framework.NewRequestExpect(f).
+			RequestModify(func(r *request.Request) {
+				r.Timeout(time.Second)
+			}).
+			PortName(bindPortName).
+			Ensure()
+	})
+})

+ 108 - 0
test/e2e/v1/features/bandwidth_limit.go

@@ -0,0 +1,108 @@
+package features
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	plugin "github.com/fatedier/frp/pkg/plugin/server"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	plugintest "github.com/fatedier/frp/test/e2e/legacy/plugin"
+	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("Proxy Bandwidth Limit by Client", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		localPort := f.AllocPort()
+		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))
+		f.RunServer("", localServer)
+
+		remotePort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			transport.bandwidthLimit = "10KB"
+			`, localPort, remotePort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		content := strings.Repeat("a", 50*1024) // 5KB
+		start := time.Now()
+		framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) {
+			r.Body([]byte(content)).Timeout(30 * time.Second)
+		}).ExpectResp([]byte(content)).Ensure()
+
+		duration := time.Since(start)
+		framework.Logf("request duration: %s", duration.String())
+
+		framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String())
+	})
+
+	ginkgo.It("Proxy Bandwidth Limit by Server", func() {
+		// new test plugin server
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.NewProxyContent{}
+			return &r
+		}
+		pluginPort := f.AllocPort()
+		handler := func(req *plugin.Request) *plugin.Response {
+			var ret plugin.Response
+			content := req.Content.(*plugin.NewProxyContent)
+			content.BandwidthLimit = "10KB"
+			content.BandwidthLimitMode = "server"
+			ret.Content = content
+			return &ret
+		}
+		pluginServer := plugintest.NewHTTPPluginServer(pluginPort, newFunc, handler, nil)
+
+		f.RunServer("", pluginServer)
+
+		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		[[httpPlugins]]
+		name = "test"
+		addr = "127.0.0.1:%d"
+		path = "/handler"
+		ops = ["NewProxy"]
+		`, pluginPort)
+		clientConf := consts.DefaultClientConfig
+
+		localPort := f.AllocPort()
+		localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort))
+		f.RunServer("", localServer)
+
+		remotePort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			`, localPort, remotePort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		content := strings.Repeat("a", 50*1024) // 5KB
+		start := time.Now()
+		framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) {
+			r.Body([]byte(content)).Timeout(30 * time.Second)
+		}).ExpectResp([]byte(content)).Ensure()
+
+		duration := time.Since(start)
+		framework.Logf("request duration: %s", duration.String())
+
+		framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String())
+	})
+})

+ 64 - 0
test/e2e/v1/features/chaos.go

@@ -0,0 +1,64 @@
+package features
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+)
+
+var _ = ginkgo.Describe("[Feature: Chaos]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("reconnect after frps restart", func() {
+		serverPort := f.AllocPort()
+		serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(`
+		bindAddr = "0.0.0.0"
+		bindPort = %d
+		`, serverPort))
+
+		remotePort := f.AllocPort()
+		clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(`
+		serverPort = %d
+		log.level = "trace"
+
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		localPort = %d
+		remotePort = %d
+		`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort))
+
+		// 1. start frps and frpc, expect request success
+		ps, _, err := f.RunFrps("-c", serverConfigPath)
+		framework.ExpectNoError(err)
+
+		pc, _, err := f.RunFrpc("-c", clientConfigPath)
+		framework.ExpectNoError(err)
+		framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+		// 2. stop frps, expect request failed
+		_ = ps.Stop()
+		time.Sleep(200 * time.Millisecond)
+		framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure()
+
+		// 3. restart frps, expect request success
+		_, _, err = f.RunFrps("-c", serverConfigPath)
+		framework.ExpectNoError(err)
+		time.Sleep(2 * time.Second)
+		framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+		// 4. stop frpc, expect request failed
+		_ = pc.Stop()
+		time.Sleep(200 * time.Millisecond)
+		framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure()
+
+		// 5. restart frpc, expect request success
+		_, _, err = f.RunFrpc("-c", clientConfigPath)
+		framework.ExpectNoError(err)
+		time.Sleep(time.Second)
+		framework.NewRequestExpect(f).Port(remotePort).Ensure()
+	})
+})

+ 267 - 0
test/e2e/v1/features/group.go

@@ -0,0 +1,267 @@
+package features
+
+import (
+	"fmt"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: Group]", func() {
+	f := framework.NewDefaultFramework()
+
+	newHTTPServer := func(port int, respContent string) *httpserver.Server {
+		return httpserver.New(
+			httpserver.WithBindPort(port),
+			httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))),
+		)
+	}
+
+	validateFooBarResponse := func(resp *request.Response) bool {
+		if string(resp.Content) == "foo" || string(resp.Content) == "bar" {
+			return true
+		}
+		return false
+	}
+
+	doFooBarHTTPRequest := func(vhostPort int, host string) []string {
+		results := []string{}
+		var wait sync.WaitGroup
+		var mu sync.Mutex
+		expectFn := func() {
+			framework.NewRequestExpect(f).Port(vhostPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost(host)
+				}).
+				Ensure(validateFooBarResponse, func(resp *request.Response) bool {
+					mu.Lock()
+					defer mu.Unlock()
+					results = append(results, string(resp.Content))
+					return true
+				})
+		}
+		for i := 0; i < 10; i++ {
+			wait.Add(1)
+			go func() {
+				defer wait.Done()
+				expectFn()
+			}()
+		}
+
+		wait.Wait()
+		return results
+	}
+
+	ginkgo.Describe("Load Balancing", func() {
+		ginkgo.It("TCP", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			fooPort := f.AllocPort()
+			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
+			f.RunServer("", fooServer)
+
+			barPort := f.AllocPort()
+			barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar")))
+			f.RunServer("", barServer)
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+
+			[[proxies]]
+			name = "bar"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+			`, fooPort, remotePort, barPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			fooCount := 0
+			barCount := 0
+			for i := 0; i < 10; i++ {
+				framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool {
+					switch string(resp.Content) {
+					case "foo":
+						fooCount++
+					case "bar":
+						barCount++
+					default:
+						return false
+					}
+					return true
+				})
+			}
+
+			framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
+		})
+	})
+
+	ginkgo.Describe("Health Check", func() {
+		ginkgo.It("TCP", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			fooPort := f.AllocPort()
+			fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo")))
+			f.RunServer("", fooServer)
+
+			barPort := f.AllocPort()
+			barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar")))
+			f.RunServer("", barServer)
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+			healthCheck.type = "tcp"
+			healthCheck.intervalSeconds = 1
+
+			[[proxies]]
+			name = "bar"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+			healthCheck.type = "tcp"
+			healthCheck.intervalSeconds = 1
+			`, fooPort, remotePort, barPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			// check foo and bar is ok
+			results := []string{}
+			for i := 0; i < 10; i++ {
+				framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool {
+					results = append(results, string(resp.Content))
+					return true
+				})
+			}
+			framework.ExpectContainElements(results, []string{"foo", "bar"})
+
+			// close bar server, check foo is ok
+			barServer.Close()
+			time.Sleep(2 * time.Second)
+			for i := 0; i < 10; i++ {
+				framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure()
+			}
+
+			// resume bar server, check foo and bar is ok
+			f.RunServer("", barServer)
+			time.Sleep(2 * time.Second)
+			results = []string{}
+			for i := 0; i < 10; i++ {
+				framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool {
+					results = append(results, string(resp.Content))
+					return true
+				})
+			}
+			framework.ExpectContainElements(results, []string{"foo", "bar"})
+		})
+
+		ginkgo.It("HTTP", func() {
+			vhostPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			vhostHTTPPort = %d
+			`, vhostPort)
+			clientConf := consts.DefaultClientConfig
+
+			fooPort := f.AllocPort()
+			fooServer := newHTTPServer(fooPort, "foo")
+			f.RunServer("", fooServer)
+
+			barPort := f.AllocPort()
+			barServer := newHTTPServer(barPort, "bar")
+			f.RunServer("", barServer)
+
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "foo"
+			type = "http"
+			localPort = %d
+			customDomains = ["example.com"]
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+			healthCheck.type = "http"
+			healthCheck.intervalSeconds = 1
+			healthCheck.path = "/healthz"
+
+			[[proxies]]
+			name = "bar"
+			type = "http"
+			localPort = %d
+			customDomains = ["example.com"]
+			loadBalancer.group = "test"
+			loadBalancer.groupKey = "123"
+			healthCheck.type = "http"
+			healthCheck.intervalSeconds = 1
+			healthCheck.path = "/healthz"
+			`, fooPort, barPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			// send first HTTP request
+			var contents []string
+			framework.NewRequestExpect(f).Port(vhostPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("example.com")
+				}).
+				Ensure(func(resp *request.Response) bool {
+					contents = append(contents, string(resp.Content))
+					return true
+				})
+
+			// send second HTTP request, should be forwarded to another service
+			framework.NewRequestExpect(f).Port(vhostPort).
+				RequestModify(func(r *request.Request) {
+					r.HTTP().HTTPHost("example.com")
+				}).
+				Ensure(func(resp *request.Response) bool {
+					contents = append(contents, string(resp.Content))
+					return true
+				})
+
+			framework.ExpectContainElements(contents, []string{"foo", "bar"})
+
+			// check foo and bar is ok
+			results := doFooBarHTTPRequest(vhostPort, "example.com")
+			framework.ExpectContainElements(results, []string{"foo", "bar"})
+
+			// close bar server, check foo is ok
+			barServer.Close()
+			time.Sleep(2 * time.Second)
+			results = doFooBarHTTPRequest(vhostPort, "example.com")
+			framework.ExpectContainElements(results, []string{"foo"})
+			framework.ExpectNotContainElements(results, []string{"bar"})
+
+			// resume bar server, check foo and bar is ok
+			f.RunServer("", barServer)
+			time.Sleep(2 * time.Second)
+			results = doFooBarHTTPRequest(vhostPort, "example.com")
+			framework.ExpectContainElements(results, []string{"foo", "bar"})
+		})
+	})
+})

+ 47 - 0
test/e2e/v1/features/heartbeat.go

@@ -0,0 +1,47 @@
+package features
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+)
+
+var _ = ginkgo.Describe("[Feature: Heartbeat]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("disable application layer heartbeat", func() {
+		serverPort := f.AllocPort()
+		serverConf := fmt.Sprintf(`
+		bindAddr = "0.0.0.0"
+		bindPort = %d
+		transport.heartbeatTimeout = -1
+		transport.tcpMuxKeepaliveInterval = 2
+		`, serverPort)
+
+		remotePort := f.AllocPort()
+		clientConf := fmt.Sprintf(`
+		serverPort = %d
+		log.level = "trace"
+		transport.heartbeatInterval = -1
+		transport.heartbeatTimeout = -1
+		transport.tcpMuxKeepaliveInterval = 2
+
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		localPort = %d
+		remotePort = %d
+		`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
+
+		// run frps and frpc
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
+
+		time.Sleep(5 * time.Second)
+		framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
+	})
+})

+ 55 - 0
test/e2e/v1/features/monitor.go

@@ -0,0 +1,55 @@
+package features
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/pkg/util/log"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: Monitor]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("Prometheus metrics", func() {
+		dashboardPort := f.AllocPort()
+		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		enablePrometheus = true
+		webServer.addr = "0.0.0.0"
+		webServer.port = %d
+		`, dashboardPort)
+
+		clientConf := consts.DefaultClientConfig
+		remotePort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		localPort = {{ .%s }}
+		remotePort = %d
+		`, framework.TCPEchoServerPort, remotePort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(remotePort).Ensure()
+		time.Sleep(500 * time.Millisecond)
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().Port(dashboardPort).HTTPPath("/metrics")
+		}).Ensure(func(resp *request.Response) bool {
+			log.Trace("prometheus metrics response: \n%s", resp.Content)
+			if resp.Code != 200 {
+				return false
+			}
+			if !strings.Contains(string(resp.Content), "traffic_in") {
+				return false
+			}
+			return true
+		})
+	})
+})

+ 154 - 0
test/e2e/v1/features/real_ip.go

@@ -0,0 +1,154 @@
+package features
+
+import (
+	"bufio"
+	"fmt"
+	"net"
+	"net/http"
+
+	"github.com/onsi/ginkgo/v2"
+	pp "github.com/pires/go-proxyproto"
+
+	"github.com/fatedier/frp/pkg/util/log"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+	"github.com/fatedier/frp/test/e2e/pkg/rpc"
+)
+
+var _ = ginkgo.Describe("[Feature: Real IP]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.It("HTTP X-Forwarded-For", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostHTTPPort)
+
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				_, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For")))
+			})),
+		)
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "test"
+		type = "http"
+		localPort = %d
+		customDomains = ["normal.example.com"]
+		`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).
+			ExpectResp([]byte("127.0.0.1")).
+			Ensure()
+	})
+
+	ginkgo.Describe("Proxy Protocol", func() {
+		ginkgo.It("TCP", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			localPort := f.AllocPort()
+			localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort),
+				streamserver.WithCustomHandler(func(c net.Conn) {
+					defer c.Close()
+					rd := bufio.NewReader(c)
+					ppHeader, err := pp.Read(rd)
+					if err != nil {
+						log.Error("read proxy protocol error: %v", err)
+						return
+					}
+
+					for {
+						if _, err := rpc.ReadBytes(rd); err != nil {
+							return
+						}
+
+						buf := []byte(ppHeader.SourceAddr.String())
+						_, _ = rpc.WriteBytes(c, buf)
+					}
+				}))
+			f.RunServer("", localServer)
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = %d
+			remotePort = %d
+			transport.proxyProtocolVersion = "v2"
+			`, localPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool {
+				log.Trace("ProxyProtocol get SourceAddr: %s", string(resp.Content))
+				addr, err := net.ResolveTCPAddr("tcp", string(resp.Content))
+				if err != nil {
+					return false
+				}
+				if addr.IP.String() != "127.0.0.1" {
+					return false
+				}
+				return true
+			})
+		})
+
+		ginkgo.It("HTTP", func() {
+			vhostHTTPPort := f.AllocPort()
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostHTTPPort)
+
+			clientConf := consts.DefaultClientConfig
+
+			localPort := f.AllocPort()
+			var srcAddrRecord string
+			localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort),
+				streamserver.WithCustomHandler(func(c net.Conn) {
+					defer c.Close()
+					rd := bufio.NewReader(c)
+					ppHeader, err := pp.Read(rd)
+					if err != nil {
+						log.Error("read proxy protocol error: %v", err)
+						return
+					}
+					srcAddrRecord = ppHeader.SourceAddr.String()
+				}))
+			f.RunServer("", localServer)
+
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			transport.proxyProtocolVersion = "v2"
+			`, localPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).Ensure(framework.ExpectResponseCode(404))
+
+			log.Trace("ProxyProtocol get SourceAddr: %s", srcAddrRecord)
+			addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord)
+			framework.ExpectNoError(err, srcAddrRecord)
+			framework.ExpectEqualValues("127.0.0.1", addr.IP.String())
+		})
+	})
+})

+ 331 - 0
test/e2e/v1/plugin/client.go

@@ -0,0 +1,331 @@
+package plugin
+
+import (
+	"crypto/tls"
+	"fmt"
+	"strconv"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+	"github.com/fatedier/frp/test/e2e/pkg/cert"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+)
+
+var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("UnixDomainSocket", func() {
+		ginkgo.It("Expose a unix domain socket echo server", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			getProxyConf := func(proxyName string, portName string, extra string) string {
+				return fmt.Sprintf(`
+				[[proxies]]
+				name = "%s"
+				type = "tcp"
+				remotePort = {{ .%s }}
+				[proxies.plugin]
+				type = "unix_domain_socket"
+				unixPath = "{{ .%s }}"
+				`+extra, proxyName, portName, framework.UDSEchoServerAddr)
+			}
+
+			tests := []struct {
+				proxyName   string
+				portName    string
+				extraConfig string
+			}{
+				{
+					proxyName: "normal",
+					portName:  port.GenName("Normal"),
+				},
+				{
+					proxyName:   "with-encryption",
+					portName:    port.GenName("WithEncryption"),
+					extraConfig: "transport.useEncryption = true",
+				},
+				{
+					proxyName:   "with-compression",
+					portName:    port.GenName("WithCompression"),
+					extraConfig: "transport.useCompression = true",
+				},
+				{
+					proxyName: "with-encryption-and-compression",
+					portName:  port.GenName("WithEncryptionAndCompression"),
+					extraConfig: `
+					transport.useEncryption = true
+					transport.useCompression = true
+					`,
+				},
+			}
+
+			// build all client config
+			for _, test := range tests {
+				clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n"
+			}
+			// run frps and frpc
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			for _, test := range tests {
+				framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure()
+			}
+		})
+	})
+
+	ginkgo.It("http_proxy", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		remotePort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		remotePort = %d
+		[proxies.plugin]
+		type = "http_proxy"
+		httpUser = "abc"
+		httpPassword = "123"
+		`, remotePort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// http proxy, no auth info
+		framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) {
+			r.HTTP().Proxy("http://127.0.0.1:" + strconv.Itoa(remotePort))
+		}).Ensure(framework.ExpectResponseCode(407))
+
+		// http proxy, correct auth
+		framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) {
+			r.HTTP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort))
+		}).Ensure()
+
+		// connect TCP server by CONNECT method
+		framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) {
+			r.TCP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort))
+		})
+	})
+
+	ginkgo.It("socks5 proxy", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		remotePort := f.AllocPort()
+		clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		remotePort = %d
+		[proxies.plugin]
+		type = "socks5"
+		username = "abc"
+		password = "123"
+		`, remotePort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// http proxy, no auth info
+		framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) {
+			r.TCP().Proxy("socks5://127.0.0.1:" + strconv.Itoa(remotePort))
+		}).ExpectError(true).Ensure()
+
+		// http proxy, correct auth
+		framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) {
+			r.TCP().Proxy("socks5://abc:123@127.0.0.1:" + strconv.Itoa(remotePort))
+		}).Ensure()
+	})
+
+	ginkgo.It("static_file", func() {
+		vhostPort := f.AllocPort()
+		serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostPort)
+		clientConf := consts.DefaultClientConfig
+
+		remotePort := f.AllocPort()
+		f.WriteTempFile("test_static_file", "foo")
+		clientConf += fmt.Sprintf(`
+		[[proxies]]
+		name = "tcp"
+		type = "tcp"
+		remotePort = %d
+		[proxies.plugin]
+		type = "static_file"
+		localPath = "%s"
+
+		[[proxies]]
+		name = "http"
+		type = "http"
+		customDomains = ["example.com"]
+		[proxies.plugin]
+		type = "static_file"
+		localPath = "%s"
+
+		[[proxies]]
+		name = "http-with-auth"
+		type = "http"
+		customDomains = ["other.example.com"]
+		[proxies.plugin]
+		type = "static_file"
+		localPath = "%s"
+		httpUser = "abc"
+		httpPassword = "123"
+		`, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// from tcp proxy
+		framework.NewRequestExpect(f).Request(
+			framework.NewHTTPRequest().HTTPPath("/test_static_file").Port(remotePort),
+		).ExpectResp([]byte("foo")).Ensure()
+
+		// from http proxy without auth
+		framework.NewRequestExpect(f).Request(
+			framework.NewHTTPRequest().HTTPHost("example.com").HTTPPath("/test_static_file").Port(vhostPort),
+		).ExpectResp([]byte("foo")).Ensure()
+
+		// from http proxy with auth
+		framework.NewRequestExpect(f).Request(
+			framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPAuth("abc", "123"),
+		).ExpectResp([]byte("foo")).Ensure()
+	})
+
+	ginkgo.It("http2https", func() {
+		serverConf := consts.DefaultServerConfig
+		vhostHTTPPort := f.AllocPort()
+		serverConf += fmt.Sprintf(`
+		vhostHTTPPort = %d
+		`, vhostHTTPPort)
+
+		localPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[[proxies]]
+		name = "http2https"
+		type = "http"
+		customDomains = ["example.com"]
+		[proxies.plugin]
+		type = "http2https"
+		localAddr = "127.0.0.1:%d"
+		`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+		framework.ExpectNoError(err)
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithTLSConfig(tlsConfig),
+			httpserver.WithResponse([]byte("test")),
+		)
+		f.RunServer("", localServer)
+
+		framework.NewRequestExpect(f).
+			Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("example.com")
+			}).
+			ExpectResp([]byte("test")).
+			Ensure()
+	})
+
+	ginkgo.It("https2http", func() {
+		generator := &cert.SelfSignedCertGenerator{}
+		artifacts, err := generator.Generate("example.com")
+		framework.ExpectNoError(err)
+		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
+		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
+
+		serverConf := consts.DefaultServerConfig
+		vhostHTTPSPort := f.AllocPort()
+		serverConf += fmt.Sprintf(`
+		vhostHTTPSPort = %d
+		`, vhostHTTPSPort)
+
+		localPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[[proxies]]
+		name = "https2http"
+		type = "https"
+		customDomains = ["example.com"]
+		[proxies.plugin]
+		type = "https2http"
+		localAddr = "127.0.0.1:%d"
+		crtPath = "%s"
+		keyPath = "%s"
+		`, localPort, crtPath, keyPath)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithResponse([]byte("test")),
+		)
+		f.RunServer("", localServer)
+
+		framework.NewRequestExpect(f).
+			Port(vhostHTTPSPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
+					ServerName:         "example.com",
+					InsecureSkipVerify: true,
+				})
+			}).
+			ExpectResp([]byte("test")).
+			Ensure()
+	})
+
+	ginkgo.It("https2https", func() {
+		generator := &cert.SelfSignedCertGenerator{}
+		artifacts, err := generator.Generate("example.com")
+		framework.ExpectNoError(err)
+		crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
+		keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
+
+		serverConf := consts.DefaultServerConfig
+		vhostHTTPSPort := f.AllocPort()
+		serverConf += fmt.Sprintf(`
+		vhostHTTPSPort = %d
+		`, vhostHTTPSPort)
+
+		localPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[[proxies]]
+		name = "https2https"
+		type = "https"
+		customDomains = ["example.com"]
+		[proxies.plugin]
+		type = "https2https"
+		localAddr = "127.0.0.1:%d"
+		crtPath = "%s"
+		keyPath = "%s"
+		`, localPort, crtPath, keyPath)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+		framework.ExpectNoError(err)
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithResponse([]byte("test")),
+			httpserver.WithTLSConfig(tlsConfig),
+		)
+		f.RunServer("", localServer)
+
+		framework.NewRequestExpect(f).
+			Port(vhostHTTPSPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
+					ServerName:         "example.com",
+					InsecureSkipVerify: true,
+				})
+			}).
+			ExpectResp([]byte("test")).
+			Ensure()
+	})
+})

+ 415 - 0
test/e2e/v1/plugin/server.go

@@ -0,0 +1,415 @@
+package plugin
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+
+	plugin "github.com/fatedier/frp/pkg/plugin/server"
+	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+)
+
+var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
+	f := framework.NewDefaultFramework()
+
+	ginkgo.Describe("Login", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.LoginContent{}
+			return &r
+		}
+
+		ginkgo.It("Auth for custom meta token", func() {
+			localPort := f.AllocPort()
+
+			clientAddressGot := false
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.LoginContent)
+				if content.ClientAddress != "" {
+					clientAddressGot = true
+				}
+				if content.Metas["token"] == "123" {
+					ret.Unchange = true
+				} else {
+					ret.Reject = true
+					ret.RejectReason = "invalid token"
+				}
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "user-manager"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["Login"]
+			`, localPort)
+			clientConf := consts.DefaultClientConfig
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			metadatas.token = "123"
+
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			remotePort2 := f.AllocPort()
+			invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp2"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort2)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf, invalidTokenClientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+			framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure()
+
+			framework.ExpectTrue(clientAddressGot)
+		})
+	})
+
+	ginkgo.Describe("NewProxy", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.NewProxyContent{}
+			return &r
+		}
+
+		ginkgo.It("Validate Info", func() {
+			localPort := f.AllocPort()
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.NewProxyContent)
+				if content.ProxyName == "tcp" {
+					ret.Unchange = true
+				} else {
+					ret.Reject = true
+				}
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["NewProxy"]
+			`, localPort)
+			clientConf := consts.DefaultClientConfig
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+		})
+
+		ginkgo.It("Mofify RemotePort", func() {
+			localPort := f.AllocPort()
+			remotePort := f.AllocPort()
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.NewProxyContent)
+				content.RemotePort = remotePort
+				ret.Content = content
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["NewProxy"]
+			`, localPort)
+			clientConf := consts.DefaultClientConfig
+
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = 0
+			`, framework.TCPEchoServerPort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+		})
+	})
+
+	ginkgo.Describe("CloseProxy", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.CloseProxyContent{}
+			return &r
+		}
+
+		ginkgo.It("Validate Info", func() {
+			localPort := f.AllocPort()
+			var recordProxyName string
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.CloseProxyContent)
+				recordProxyName = content.ProxyName
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["CloseProxy"]
+			`, localPort)
+			clientConf := consts.DefaultClientConfig
+
+			remotePort := f.AllocPort()
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			_, clients := f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+			for _, c := range clients {
+				_ = c.Stop()
+			}
+
+			time.Sleep(1 * time.Second)
+
+			framework.ExpectEqual(recordProxyName, "tcp")
+		})
+	})
+
+	ginkgo.Describe("Ping", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.PingContent{}
+			return &r
+		}
+
+		ginkgo.It("Validate Info", func() {
+			localPort := f.AllocPort()
+
+			var record string
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.PingContent)
+				record = content.Ping.PrivilegeKey
+				ret.Unchange = true
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["Ping"]
+			`, localPort)
+
+			remotePort := f.AllocPort()
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+			transport.heartbeatInterval = 1
+			auth.additionalScopes = ["HeartBeats"]
+
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+			time.Sleep(3 * time.Second)
+			framework.ExpectNotEqual("", record)
+		})
+	})
+
+	ginkgo.Describe("NewWorkConn", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.NewWorkConnContent{}
+			return &r
+		}
+
+		ginkgo.It("Validate Info", func() {
+			localPort := f.AllocPort()
+
+			var record string
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.NewWorkConnContent)
+				record = content.NewWorkConn.RunID
+				ret.Unchange = true
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["NewWorkConn"]
+			`, localPort)
+
+			remotePort := f.AllocPort()
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+			framework.ExpectNotEqual("", record)
+		})
+	})
+
+	ginkgo.Describe("NewUserConn", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.NewUserConnContent{}
+			return &r
+		}
+		ginkgo.It("Validate Info", func() {
+			localPort := f.AllocPort()
+
+			var record string
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.NewUserConnContent)
+				record = content.RemoteAddr
+				ret.Unchange = true
+				return &ret
+			}
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "127.0.0.1:%d"
+			path = "/handler"
+			ops = ["NewUserConn"]
+			`, localPort)
+
+			remotePort := f.AllocPort()
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+			framework.ExpectNotEqual("", record)
+		})
+	})
+
+	ginkgo.Describe("HTTPS Protocol", func() {
+		newFunc := func() *plugin.Request {
+			var r plugin.Request
+			r.Content = &plugin.NewUserConnContent{}
+			return &r
+		}
+		ginkgo.It("Validate Login Info, disable tls verify", func() {
+			localPort := f.AllocPort()
+
+			var record string
+			handler := func(req *plugin.Request) *plugin.Response {
+				var ret plugin.Response
+				content := req.Content.(*plugin.NewUserConnContent)
+				record = content.RemoteAddr
+				ret.Unchange = true
+				return &ret
+			}
+			tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+			framework.ExpectNoError(err)
+			pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig)
+
+			f.RunServer("", pluginServer)
+
+			serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+			[[httpPlugins]]
+			name = "test"
+			addr = "https://127.0.0.1:%d"
+			path = "/handler"
+			ops = ["NewUserConn"]
+			`, localPort)
+
+			remotePort := f.AllocPort()
+			clientConf := consts.DefaultClientConfig
+			clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "tcp"
+			type = "tcp"
+			localPort = {{ .%s }}
+			remotePort = %d
+			`, framework.TCPEchoServerPort, remotePort)
+
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			framework.NewRequestExpect(f).Port(remotePort).Ensure()
+
+			framework.ExpectNotEqual("", record)
+		})
+	})
+})

+ 41 - 0
test/e2e/v1/plugin/utils.go

@@ -0,0 +1,41 @@
+package plugin
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"io"
+	"net/http"
+
+	plugin "github.com/fatedier/frp/pkg/plugin/server"
+	"github.com/fatedier/frp/pkg/util/log"
+	"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+)
+
+type Handler func(req *plugin.Request) *plugin.Response
+
+type NewPluginRequest func() *plugin.Request
+
+func NewHTTPPluginServer(port int, newFunc NewPluginRequest, handler Handler, tlsConfig *tls.Config) *httpserver.Server {
+	return httpserver.New(
+		httpserver.WithBindPort(port),
+		httpserver.WithTLSConfig(tlsConfig),
+		httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			r := newFunc()
+			buf, err := io.ReadAll(req.Body)
+			if err != nil {
+				w.WriteHeader(500)
+				return
+			}
+			log.Trace("plugin request: %s", string(buf))
+			err = json.Unmarshal(buf, &r)
+			if err != nil {
+				w.WriteHeader(500)
+				return
+			}
+			resp := handler(r)
+			buf, _ = json.Marshal(resp)
+			log.Trace("plugin response: %s", string(buf))
+			_, _ = w.Write(buf)
+		})),
+	)
+}