Bläddra i källkod

Merge pull request #3845 from fatedier/dev

bump version to v0.53.0
fatedier 1 år sedan
förälder
incheckning
051299ec25
100 ändrade filer med 3274 tillägg och 1542 borttagningar
  1. 1 1
      .github/workflows/golangci-lint.yml
  2. 1 0
      .gitignore
  3. 1 1
      .golangci.yml
  4. 2 2
      Makefile
  5. 58 2
      README.md
  6. 5 1
      README_zh.md
  7. 9 1
      Release.md
  8. 0 85
      client/admin.go
  9. 46 9
      client/admin_api.go
  10. 227 0
      client/connector.go
  11. 137 205
      client/control.go
  12. 13 4
      client/proxy/proxy.go
  13. 23 6
      client/proxy/proxy_manager.go
  14. 4 0
      client/proxy/proxy_wrapper.go
  15. 4 2
      client/proxy/sudp.go
  16. 5 3
      client/proxy/udp.go
  17. 5 3
      client/proxy/xtcp.go
  18. 238 354
      client/service.go
  19. 2 2
      client/visitor/sudp.go
  20. 4 4
      client/visitor/visitor.go
  21. 14 4
      client/visitor/visitor_manager.go
  22. 3 3
      client/visitor/xtcp.go
  23. 2 2
      cmd/frpc/sub/admin.go
  24. 0 125
      cmd/frpc/sub/flags.go
  25. 2 1
      cmd/frpc/sub/nathole.go
  26. 4 3
      cmd/frpc/sub/proxy.go
  27. 16 11
      cmd/frpc/sub/root.go
  28. 2 2
      cmd/frpc/sub/verify.go
  29. 0 110
      cmd/frps/flags.go
  30. 6 4
      cmd/frps/root.go
  31. 1 1
      cmd/frps/verify.go
  32. 1 1
      conf/frpc_full_example.toml
  33. 8 0
      conf/frps_full_example.toml
  34. 1 1
      conf/legacy/frpc_legacy_full.ini
  35. BIN
      doc/pic/donate-alipay.png
  36. BIN
      doc/pic/sponsor_asocks.jpg
  37. BIN
      doc/pic/sponsor_nango.png
  38. 160 0
      doc/ssh_tunnel_gateway.md
  39. 6 6
      go.mod
  40. 12 11
      go.sum
  41. 31 0
      pkg/auth/pass.go
  42. 244 0
      pkg/config/flags.go
  43. 1 1
      pkg/config/legacy/client.go
  44. 2 2
      pkg/config/legacy/parse.go
  45. 1 1
      pkg/config/legacy/server.go
  46. 41 25
      pkg/config/load.go
  47. 132 11
      pkg/config/load_test.go
  48. 1 1
      pkg/config/v1/client.go
  49. 15 1
      pkg/config/v1/common.go
  50. 15 1
      pkg/config/v1/plugin.go
  51. 9 2
      pkg/config/v1/proxy.go
  52. 15 1
      pkg/config/v1/server.go
  53. 2 2
      pkg/config/v1/validation/client.go
  54. 9 2
      pkg/config/v1/visitor.go
  55. 14 0
      pkg/metrics/metrics.go
  56. 103 0
      pkg/msg/handler.go
  57. 12 0
      pkg/msg/msg.go
  58. 4 2
      pkg/plugin/client/http2https.go
  59. 4 2
      pkg/plugin/client/http_proxy.go
  60. 4 2
      pkg/plugin/client/https2http.go
  61. 4 2
      pkg/plugin/client/https2https.go
  62. 4 2
      pkg/plugin/client/socks5.go
  63. 6 4
      pkg/plugin/client/static_file.go
  64. 2 0
      pkg/plugin/client/unix_domain_socket.go
  65. 13 4
      pkg/sdk/client/client.go
  66. 143 0
      pkg/ssh/gateway.go
  67. 383 0
      pkg/ssh/server.go
  68. 31 0
      pkg/ssh/terminal.go
  69. 3 1
      pkg/transport/message.go
  70. 12 0
      pkg/transport/tls.go
  71. 1 1
      pkg/util/http/http.go
  72. 126 0
      pkg/util/http/server.go
  73. 16 0
      pkg/util/net/conn.go
  74. 33 0
      pkg/util/net/dns.go
  75. 7 7
      pkg/util/net/http.go
  76. 4 1
      pkg/util/net/listener.go
  77. 5 5
      pkg/util/tcpmux/httpconnect.go
  78. 1 1
      pkg/util/version/version.go
  79. 2 2
      pkg/util/version/version_test.go
  80. 10 10
      pkg/util/vhost/http.go
  81. 2 2
      pkg/util/vhost/resource.go
  82. 2 2
      pkg/util/vhost/vhost.go
  83. 197 0
      pkg/util/wait/backoff.go
  84. 50 9
      pkg/util/xlog/xlog.go
  85. 107 0
      pkg/virtual/client.go
  86. 148 245
      server/control.go
  87. 0 99
      server/dashboard.go
  88. 38 5
      server/dashboard_api.go
  89. 3 3
      server/proxy/http.go
  90. 2 2
      server/proxy/proxy.go
  91. 2 2
      server/proxy/udp.go
  92. 126 86
      server/service.go
  93. 7 8
      server/visitor/visitor.go
  94. 6 6
      test/e2e/framework/framework.go
  95. 1 1
      test/e2e/legacy/basic/client.go
  96. 2 2
      test/e2e/legacy/basic/tcpmux.go
  97. 1 1
      test/e2e/legacy/plugin/server.go
  98. 2 2
      test/e2e/pkg/request/request.go
  99. 89 0
      test/e2e/pkg/ssh/client.go
  100. 1 1
      test/e2e/v1/basic/client.go

+ 1 - 1
.github/workflows/golangci-lint.yml

@@ -22,7 +22,7 @@ jobs:
         uses: golangci/golangci-lint-action@v3
         with:
           # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
-          version: v1.53
+          version: v1.55
 
           # Optional: golangci-lint command line arguments.
           # args: --issues-exit-code=0

+ 1 - 0
.gitignore

@@ -33,6 +33,7 @@ lastversion/
 dist/
 .idea/
 .vscode/
+.autogen_ssh_key
 
 # Cache
 *.swp

+ 1 - 1
.golangci.yml

@@ -1,5 +1,5 @@
 service:
-  golangci-lint-version: 1.51.x # use the fixed version to not introduce new linters unexpectedly
+  golangci-lint-version: 1.55.x # use the fixed version to not introduce new linters unexpectedly
   
 run:
   concurrency: 4

+ 2 - 2
Makefile

@@ -26,10 +26,10 @@ vet:
 	go vet ./...
 
 frps:
-	env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps
+	env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
 
 frpc:
-	env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc
+	env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
 
 test: gotest
 

+ 58 - 2
README.md

@@ -11,6 +11,10 @@
   <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
     <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
   </a>
+  <a>&nbsp</a>
+  <a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
+    <img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
+  </a>
 </p>
 <!--gold sponsors end-->
 
@@ -42,7 +46,7 @@ frp also offers a P2P connect mode.
     * [Using Environment Variables](#using-environment-variables)
     * [Split Configures Into Different Files](#split-configures-into-different-files)
     * [Server Dashboard](#server-dashboard)
-    * [Admin UI](#admin-ui)
+    * [Client Admin UI](#client-admin-ui)
     * [Monitor](#monitor)
         * [Prometheus](#prometheus)
     * [Authenticating the Client](#authenticating-the-client)
@@ -71,9 +75,10 @@ frp also offers a P2P connect mode.
     * [Custom Subdomain Names](#custom-subdomain-names)
     * [URL Routing](#url-routing)
     * [TCP Port Multiplexing](#tcp-port-multiplexing)
-    * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy)
+    * [Connecting to frps via PROXY](#connecting-to-frps-via-proxy)
     * [Client Plugins](#client-plugins)
     * [Server Manage Plugins](#server-manage-plugins)
+    * [SSH Tunnel Gateway](#ssh-tunnel-gateway)
 * [Contributing](#contributing)
 * [Donation](#donation)
     * [GitHub Sponsors](#github-sponsors)
@@ -505,6 +510,7 @@ includes = ["./confd/*.toml"]
 
 ```toml
 # ./confd/test.toml
+
 [[proxies]]
 name = "ssh"
 type = "tcp"
@@ -616,6 +622,7 @@ The features are off by default. You can turn on encryption and/or compression:
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "ssh"
 type = "tcp"
@@ -771,6 +778,7 @@ We would like to try to allow multiple proxies bind a same remote port with diff
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "ssh"
 type = "tcp"
@@ -876,6 +884,7 @@ This feature is only available for types `tcp`, `http`, `tcpmux` now.
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "test1"
 type = "tcp"
@@ -911,6 +920,7 @@ With health check type **tcp**, the service port will be pinged (TCPing):
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "test1"
 type = "tcp"
@@ -930,6 +940,7 @@ With health check type **http**, an HTTP request will be sent to the service and
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "http"
@@ -954,6 +965,7 @@ However, speaking of web servers and HTTP requests, your web server might rely o
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "http"
@@ -970,6 +982,7 @@ Similar to `Host`, You can override other HTTP request headers with proxy type `
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "http"
@@ -997,6 +1010,7 @@ Here is an example for https service:
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "https"
@@ -1019,6 +1033,7 @@ It can only be enabled when proxy type is http.
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "http"
@@ -1043,6 +1058,7 @@ Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web"
 type = "http"
@@ -1062,6 +1078,7 @@ frp supports forwarding HTTP requests to different backend web services by url r
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "web01"
 type = "http"
@@ -1147,6 +1164,7 @@ Using plugin **http_proxy**:
 
 ```toml
 # frpc.toml
+
 [[proxies]]
 name = "http_proxy"
 type = "tcp"
@@ -1165,6 +1183,44 @@ Read the [document](/doc/server_plugin.md).
 
 Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin).
 
+### SSH Tunnel Gateway
+
+*added in v0.53.0*
+
+frp supports listening to an SSH port on the frps side and achieves TCP protocol proxying through the SSH -R protocol, without relying on frpc.
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 2200
+```
+
+When running `./frps -c frps.toml`, a private key file named `.autogen_ssh_key` will be automatically created in the current working directory. This generated private key file will be used by the SSH server in frps.
+
+Executing the command
+
+```bash
+ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
+```
+
+sets up a proxy on frps that forwards the local 8080 service to the port 9090.
+
+```bash
+frp (via SSH) (Ctrl+C to quit)
+
+User:
+ProxyName: test-tcp
+Type: tcp
+RemoteAddress: :9090
+```
+
+This is equivalent to:
+
+```bash
+frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
+```
+
+Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
+
 ## Contributing
 
 Interested in getting involved? We would like to help you!

+ 5 - 1
README_zh.md

@@ -13,6 +13,10 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
   <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
     <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
   </a>
+  <a>&nbsp</a>
+  <a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
+    <img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
+  </a>
 </p>
 <!--gold sponsors end-->
 
@@ -84,7 +88,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 
 ### 知识星球
 
-如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
+如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
 
 ![zsxq](/doc/pic/zsxq.jpg)
 

+ 9 - 1
Release.md

@@ -1,3 +1,11 @@
+### Features
+
+* The new command line parameter `--strict_config` has been added to enable strict configuration validation mode. It will throw an error for unknown fields instead of ignoring them. In future versions, we will set the default value of this parameter to true to avoid misconfigurations.
+* Support `SSH reverse tunneling`. With this feature, you can expose your local service without running frpc, only using SSH. The SSH reverse tunnel agent has many functional limitations compared to the frpc agent. The currently supported proxy types are tcp, http, https, tcpmux, and stcp.
+* The frpc tcpmux command line parameters have been updated to support configuring `http_user` and `http_pwd`.
+* The frpc stcp/sudp/xtcp command line parameters have been updated to support configuring `allow_users`.
+
 ### Fixes
 
-* `admin_user` is not effective in the INI configuration.
+* frpc: Return code 1 when the first login attempt fails and exits.
+* When auth.method is `oidc` and auth.additionalScopes contains `HeartBeats`, if obtaining AccessToken fails, the application will be unresponsive.

+ 0 - 85
client/admin.go

@@ -1,85 +0,0 @@
-// Copyright 2017 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"net"
-	"net/http"
-	"net/http/pprof"
-	"time"
-
-	"github.com/gorilla/mux"
-
-	"github.com/fatedier/frp/assets"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
-)
-
-var (
-	httpServerReadTimeout  = 60 * time.Second
-	httpServerWriteTimeout = 60 * time.Second
-)
-
-func (svr *Service) RunAdminServer(address string) (err error) {
-	// url router
-	router := mux.NewRouter()
-
-	router.HandleFunc("/healthz", svr.healthz)
-
-	// debug
-	if svr.cfg.WebServer.PprofEnable {
-		router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
-		router.HandleFunc("/debug/pprof/profile", pprof.Profile)
-		router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
-		router.HandleFunc("/debug/pprof/trace", pprof.Trace)
-		router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
-	}
-
-	subRouter := router.NewRoute().Subrouter()
-	user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
-	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-
-	// api, see admin_api.go
-	subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
-	subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
-	subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
-	subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
-	subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
-
-	// view
-	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
-	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
-	})
-
-	server := &http.Server{
-		Addr:         address,
-		Handler:      router,
-		ReadTimeout:  httpServerReadTimeout,
-		WriteTimeout: httpServerWriteTimeout,
-	}
-	if address == "" {
-		address = ":http"
-	}
-	ln, err := net.Listen("tcp", address)
-	if err != nil {
-		return err
-	}
-
-	go func() {
-		_ = server.Serve(ln)
-	}()
-	return
-}

+ 46 - 9
client/admin_api.go

@@ -31,7 +31,9 @@ import (
 	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/config/v1/validation"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 type GeneralResponse struct {
@@ -39,14 +41,42 @@ type GeneralResponse struct {
 	Msg  string
 }
 
+func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
+	helper.Router.HandleFunc("/healthz", svr.healthz)
+	subRouter := helper.Router.NewRoute().Subrouter()
+
+	subRouter.Use(helper.AuthMiddleware.Middleware)
+
+	// api, see admin_api.go
+	subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
+	subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
+	subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
+	subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
+	subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
+
+	// view
+	subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
+	subRouter.PathPrefix("/static/").Handler(
+		netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
+	).Methods("GET")
+	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
+	})
+}
+
 // /healthz
 func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
 	w.WriteHeader(200)
 }
 
 // GET /api/reload
-func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
+func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
+	strictConfigMode := false
+	strictStr := r.URL.Query().Get("strictConfig")
+	if strictStr != "" {
+		strictConfigMode, _ = strconv.ParseBool(strictStr)
+	}
 
 	log.Info("api request [/api/reload]")
 	defer func() {
@@ -57,21 +87,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
 		}
 	}()
 
-	cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile)
+	cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
 	if err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)
 		return
 	}
-	if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil {
+	if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)
 		return
 	}
 
-	if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
+	if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
 		res.Code = 500
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)
@@ -144,9 +174,16 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
 		_, _ = w.Write(buf)
 	}()
 
-	ps := svr.ctl.pm.GetAllProxyStatus()
+	svr.ctlMu.RLock()
+	ctl := svr.ctl
+	svr.ctlMu.RUnlock()
+	if ctl == nil {
+		return
+	}
+
+	ps := ctl.pm.GetAllProxyStatus()
 	for _, status := range ps {
-		res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr))
+		res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
 	}
 
 	for _, arrs := range res {
@@ -172,14 +209,14 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
 		}
 	}()
 
-	if svr.cfgFile == "" {
+	if svr.configFilePath == "" {
 		res.Code = 400
 		res.Msg = "frpc has no config file path"
 		log.Warn("%s", res.Msg)
 		return
 	}
 
-	content, err := os.ReadFile(svr.cfgFile)
+	content, err := os.ReadFile(svr.configFilePath)
 	if err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
@@ -218,7 +255,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil {
+	if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil {
 		res.Code = 500
 		res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
 		log.Warn("%s", res.Msg)

+ 227 - 0
client/connector.go

@@ -0,0 +1,227 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"context"
+	"crypto/tls"
+	"io"
+	"net"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	libdial "github.com/fatedier/golib/net/dial"
+	fmux "github.com/hashicorp/yamux"
+	quic "github.com/quic-go/quic-go"
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/transport"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+	"github.com/fatedier/frp/pkg/util/xlog"
+)
+
+// Connector is a interface for establishing connections to the server.
+type Connector interface {
+	Open() error
+	Connect() (net.Conn, error)
+	Close() error
+}
+
+// defaultConnectorImpl is the default implementation of Connector for normal frpc.
+type defaultConnectorImpl struct {
+	ctx context.Context
+	cfg *v1.ClientCommonConfig
+
+	muxSession *fmux.Session
+	quicConn   quic.Connection
+	closeOnce  sync.Once
+}
+
+func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
+	return &defaultConnectorImpl{
+		ctx: ctx,
+		cfg: cfg,
+	}
+}
+
+// Open opens a underlying connection to the server.
+// The underlying connection is either a TCP connection or a QUIC connection.
+// After the underlying connection is established, you can call Connect() to get a stream.
+// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect().
+func (c *defaultConnectorImpl) Open() error {
+	xl := xlog.FromContextSafe(c.ctx)
+
+	// special for quic
+	if strings.EqualFold(c.cfg.Transport.Protocol, "quic") {
+		var tlsConfig *tls.Config
+		var err error
+		sn := c.cfg.Transport.TLS.ServerName
+		if sn == "" {
+			sn = c.cfg.ServerAddr
+		}
+		if lo.FromPtr(c.cfg.Transport.TLS.Enable) {
+			tlsConfig, err = transport.NewClientTLSConfig(
+				c.cfg.Transport.TLS.CertFile,
+				c.cfg.Transport.TLS.KeyFile,
+				c.cfg.Transport.TLS.TrustedCaFile,
+				sn)
+		} else {
+			tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
+		}
+		if err != nil {
+			xl.Warn("fail to build tls configuration, err: %v", err)
+			return err
+		}
+		tlsConfig.NextProtos = []string{"frp"}
+
+		conn, err := quic.DialAddr(
+			c.ctx,
+			net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
+			tlsConfig, &quic.Config{
+				MaxIdleTimeout:     time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
+				MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams),
+				KeepAlivePeriod:    time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
+			})
+		if err != nil {
+			return err
+		}
+		c.quicConn = conn
+		return nil
+	}
+
+	if !lo.FromPtr(c.cfg.Transport.TCPMux) {
+		return nil
+	}
+
+	conn, err := c.realConnect()
+	if err != nil {
+		return err
+	}
+
+	fmuxCfg := fmux.DefaultConfig()
+	fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
+	fmuxCfg.LogOutput = io.Discard
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
+	session, err := fmux.Client(conn, fmuxCfg)
+	if err != nil {
+		return err
+	}
+	c.muxSession = session
+	return nil
+}
+
+// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
+func (c *defaultConnectorImpl) Connect() (net.Conn, error) {
+	if c.quicConn != nil {
+		stream, err := c.quicConn.OpenStreamSync(context.Background())
+		if err != nil {
+			return nil, err
+		}
+		return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil
+	} else if c.muxSession != nil {
+		stream, err := c.muxSession.OpenStream()
+		if err != nil {
+			return nil, err
+		}
+		return stream, nil
+	}
+
+	return c.realConnect()
+}
+
+func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
+	xl := xlog.FromContextSafe(c.ctx)
+	var tlsConfig *tls.Config
+	var err error
+	tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable)
+	if c.cfg.Transport.Protocol == "wss" {
+		tlsEnable = true
+	}
+	if tlsEnable {
+		sn := c.cfg.Transport.TLS.ServerName
+		if sn == "" {
+			sn = c.cfg.ServerAddr
+		}
+
+		tlsConfig, err = transport.NewClientTLSConfig(
+			c.cfg.Transport.TLS.CertFile,
+			c.cfg.Transport.TLS.KeyFile,
+			c.cfg.Transport.TLS.TrustedCaFile,
+			sn)
+		if err != nil {
+			xl.Warn("fail to build tls configuration, err: %v", err)
+			return nil, err
+		}
+	}
+
+	proxyType, addr, auth, err := libdial.ParseProxyURL(c.cfg.Transport.ProxyURL)
+	if err != nil {
+		xl.Error("fail to parse proxy url")
+		return nil, err
+	}
+	dialOptions := []libdial.DialOption{}
+	protocol := c.cfg.Transport.Protocol
+	switch protocol {
+	case "websocket":
+		protocol = "tcp"
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+			Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
+		}))
+		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
+	case "wss":
+		protocol = "tcp"
+		dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
+		// Make sure that if it is wss, the websocket hook is executed after the tls hook.
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
+	default:
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+			Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
+		}))
+		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
+	}
+
+	if c.cfg.Transport.ConnectServerLocalIP != "" {
+		dialOptions = append(dialOptions, libdial.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
+	}
+	dialOptions = append(dialOptions,
+		libdial.WithProtocol(protocol),
+		libdial.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
+		libdial.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
+		libdial.WithProxy(proxyType, addr),
+		libdial.WithProxyAuth(auth),
+	)
+	conn, err := libdial.DialContext(
+		c.ctx,
+		net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
+		dialOptions...,
+	)
+	return conn, err
+}
+
+func (c *defaultConnectorImpl) Close() error {
+	c.closeOnce.Do(func() {
+		if c.quicConn != nil {
+			_ = c.quicConn.CloseWithError(0, "")
+		}
+		if c.muxSession != nil {
+			_ = c.muxSession.Close()
+		}
+	})
+	return nil
+}

+ 137 - 205
client/control.go

@@ -16,13 +16,10 @@ package client
 
 import (
 	"context"
-	"io"
 	"net"
-	"runtime/debug"
+	"sync/atomic"
 	"time"
 
-	"github.com/fatedier/golib/control/shutdown"
-	"github.com/fatedier/golib/crypto"
 	"github.com/samber/lo"
 
 	"github.com/fatedier/frp/client/proxy"
@@ -31,101 +28,99 @@ import (
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+	"github.com/fatedier/frp/pkg/util/wait"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
+type SessionContext struct {
+	// The client common configuration.
+	Common *v1.ClientCommonConfig
+
+	// Unique ID obtained from frps.
+	// It should be attached to the login message when reconnecting.
+	RunID string
+	// Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
+	Conn net.Conn
+	// Indicates whether the connection is encrypted.
+	ConnEncrypted bool
+	// Sets authentication based on selected method
+	AuthSetter auth.Setter
+	// Connector is used to create new connections, which could be real TCP connections or virtual streams.
+	Connector Connector
+}
+
 type Control struct {
 	// service context
 	ctx context.Context
 	xl  *xlog.Logger
 
-	// Unique ID obtained from frps.
-	// It should be attached to the login message when reconnecting.
-	runID string
+	// session context
+	sessionCtx *SessionContext
 
 	// manage all proxies
-	pxyCfgs []v1.ProxyConfigurer
-	pm      *proxy.Manager
+	pm *proxy.Manager
 
 	// manage all visitors
 	vm *visitor.Manager
 
-	// control connection
-	conn net.Conn
-
-	cm *ConnectionManager
+	doneCh chan struct{}
 
-	// put a message in this channel to send it over control connection to server
-	sendCh chan (msg.Message)
-
-	// read from this channel to get the next message sent by server
-	readCh chan (msg.Message)
-
-	// goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed
-	closedCh chan struct{}
-
-	closedDoneCh chan struct{}
-
-	// last time got the Pong message
-	lastPong time.Time
-
-	// The client configuration
-	clientCfg *v1.ClientCommonConfig
-
-	readerShutdown     *shutdown.Shutdown
-	writerShutdown     *shutdown.Shutdown
-	msgHandlerShutdown *shutdown.Shutdown
-
-	// sets authentication based on selected method
-	authSetter auth.Setter
+	// of time.Time, last time got the Pong message
+	lastPong atomic.Value
 
+	// The role of msgTransporter is similar to HTTP2.
+	// It allows multiple messages to be sent simultaneously on the same control connection.
+	// The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type.
 	msgTransporter transport.MessageTransporter
+
+	// msgDispatcher is a wrapper for control connection.
+	// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
+	msgDispatcher *msg.Dispatcher
 }
 
-func NewControl(
-	ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
-	clientCfg *v1.ClientCommonConfig,
-	pxyCfgs []v1.ProxyConfigurer,
-	visitorCfgs []v1.VisitorConfigurer,
-	authSetter auth.Setter,
-) *Control {
+func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
 	// new xlog instance
 	ctl := &Control{
-		ctx:                ctx,
-		xl:                 xlog.FromContextSafe(ctx),
-		runID:              runID,
-		conn:               conn,
-		cm:                 cm,
-		pxyCfgs:            pxyCfgs,
-		sendCh:             make(chan msg.Message, 100),
-		readCh:             make(chan msg.Message, 100),
-		closedCh:           make(chan struct{}),
-		closedDoneCh:       make(chan struct{}),
-		clientCfg:          clientCfg,
-		readerShutdown:     shutdown.New(),
-		writerShutdown:     shutdown.New(),
-		msgHandlerShutdown: shutdown.New(),
-		authSetter:         authSetter,
+		ctx:        ctx,
+		xl:         xlog.FromContextSafe(ctx),
+		sessionCtx: sessionCtx,
+		doneCh:     make(chan struct{}),
 	}
-	ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
-	ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
+	ctl.lastPong.Store(time.Now())
 
-	ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
-	ctl.vm.Reload(visitorCfgs)
-	return ctl
+	if sessionCtx.ConnEncrypted {
+		cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
+		if err != nil {
+			return nil, err
+		}
+		ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
+	} else {
+		ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
+	}
+	ctl.registerMsgHandlers()
+	ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
+
+	ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter)
+	ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter)
+	return ctl, nil
 }
 
-func (ctl *Control) Run() {
+func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) {
 	go ctl.worker()
 
 	// start all proxies
-	ctl.pm.Reload(ctl.pxyCfgs)
+	ctl.pm.UpdateAll(proxyCfgs)
 
 	// start all visitors
-	go ctl.vm.Run()
+	ctl.vm.UpdateAll(visitorCfgs)
+}
+
+func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+	ctl.pm.SetInWorkConnCallback(cb)
 }
 
-func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
+func (ctl *Control) handleReqWorkConn(_ msg.Message) {
 	xl := ctl.xl
 	workConn, err := ctl.connectServer()
 	if err != nil {
@@ -134,9 +129,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
 	}
 
 	m := &msg.NewWorkConn{
-		RunID: ctl.runID,
+		RunID: ctl.sessionCtx.RunID,
 	}
-	if err = ctl.authSetter.SetNewWorkConn(m); err != nil {
+	if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
 		xl.Warn("error during NewWorkConn authentication: %v", err)
 		return
 	}
@@ -162,8 +157,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
 	ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
 }
 
-func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
+func (ctl *Control) handleNewProxyResp(m msg.Message) {
 	xl := ctl.xl
+	inMsg := m.(*msg.NewProxyResp)
 	// Server will return NewProxyResp message to each NewProxy message.
 	// Start a new proxy handler if no error got
 	err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
@@ -174,8 +170,9 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
 	}
 }
 
-func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
+func (ctl *Control) handleNatHoleResp(m msg.Message) {
 	xl := ctl.xl
+	inMsg := m.(*msg.NatHoleResp)
 
 	// Dispatch the NatHoleResp message to the related proxy.
 	ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
@@ -184,6 +181,25 @@ func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
 	}
 }
 
+func (ctl *Control) handlePong(m msg.Message) {
+	xl := ctl.xl
+	inMsg := m.(*msg.Pong)
+
+	if inMsg.Error != "" {
+		xl.Error("Pong message contains error: %s", inMsg.Error)
+		ctl.closeSession()
+		return
+	}
+	ctl.lastPong.Store(time.Now())
+	xl.Debug("receive heartbeat from server")
+}
+
+// closeSession closes the control connection.
+func (ctl *Control) closeSession() {
+	ctl.sessionCtx.Conn.Close()
+	ctl.sessionCtx.Connector.Close()
+}
+
 func (ctl *Control) Close() error {
 	return ctl.GracefulClose(0)
 }
@@ -194,170 +210,86 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
 
 	time.Sleep(d)
 
-	ctl.conn.Close()
-	ctl.cm.Close()
+	ctl.closeSession()
 	return nil
 }
 
-// ClosedDoneCh returns a channel that will be closed after all resources are released
-func (ctl *Control) ClosedDoneCh() <-chan struct{} {
-	return ctl.closedDoneCh
+// Done returns a channel that will be closed after all resources are released
+func (ctl *Control) Done() <-chan struct{} {
+	return ctl.doneCh
 }
 
 // connectServer return a new connection to frps
-func (ctl *Control) connectServer() (conn net.Conn, err error) {
-	return ctl.cm.Connect()
+func (ctl *Control) connectServer() (net.Conn, error) {
+	return ctl.sessionCtx.Connector.Connect()
 }
 
-// reader read all messages from frps and send to readCh
-func (ctl *Control) reader() {
-	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
-	defer ctl.readerShutdown.Done()
-	defer close(ctl.closedCh)
-
-	encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
-	for {
-		m, err := msg.ReadMsg(encReader)
-		if err != nil {
-			if err == io.EOF {
-				xl.Debug("read from control connection EOF")
-				return
-			}
-			xl.Warn("read error: %v", err)
-			ctl.conn.Close()
-			return
-		}
-		ctl.readCh <- m
-	}
+func (ctl *Control) registerMsgHandlers() {
+	ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
+	ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
+	ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
+	ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
 }
 
-// writer writes messages got from sendCh to frps
-func (ctl *Control) writer() {
+// headerWorker sends heartbeat to server and check heartbeat timeout.
+func (ctl *Control) heartbeatWorker() {
 	xl := ctl.xl
-	defer ctl.writerShutdown.Done()
-	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
-	if err != nil {
-		xl.Error("crypto new writer error: %v", err)
-		ctl.conn.Close()
-		return
-	}
-	for {
-		m, ok := <-ctl.sendCh
-		if !ok {
-			xl.Info("control writer is closing")
-			return
-		}
 
-		if err := msg.WriteMsg(encWriter, m); err != nil {
-			xl.Warn("write message to control connection error: %v", err)
-			return
+	// TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
+	// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
+	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
+		// send heartbeat to server
+		sendHeartBeat := func() error {
+			xl.Debug("send heartbeat to server")
+			pingMsg := &msg.Ping{}
+			if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
+				xl.Warn("error during ping authentication: %v, skip sending ping message", err)
+				return err
+			}
+			_ = ctl.msgDispatcher.Send(pingMsg)
+			return nil
 		}
-	}
-}
 
-// msgHandler handles all channel events and performs corresponding operations.
-func (ctl *Control) msgHandler() {
-	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
-	defer ctl.msgHandlerShutdown.Done()
-
-	var hbSendCh <-chan time.Time
-	// TODO(fatedier): disable heartbeat if TCPMux is enabled.
-	// Just keep it here to keep compatible with old version frps.
-	if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
-		hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second)
-		defer hbSend.Stop()
-		hbSendCh = hbSend.C
+		go wait.BackoffUntil(sendHeartBeat,
+			wait.NewFastBackoffManager(wait.FastBackoffOptions{
+				Duration:           time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
+				InitDurationIfFail: time.Second,
+				Factor:             2.0,
+				Jitter:             0.1,
+				MaxDuration:        time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
+			}),
+			true, ctl.doneCh,
+		)
 	}
 
-	var hbCheckCh <-chan time.Time
 	// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
-	if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 &&
-		!lo.FromPtr(ctl.clientCfg.Transport.TCPMux) {
-		hbCheck := time.NewTicker(time.Second)
-		defer hbCheck.Stop()
-		hbCheckCh = hbCheck.C
-	}
+	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 &&
+		!lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) {
 
-	ctl.lastPong = time.Now()
-	for {
-		select {
-		case <-hbSendCh:
-			// send heartbeat to server
-			xl.Debug("send heartbeat to server")
-			pingMsg := &msg.Ping{}
-			if err := ctl.authSetter.SetPing(pingMsg); err != nil {
-				xl.Warn("error during ping authentication: %v", err)
-				return
-			}
-			ctl.sendCh <- pingMsg
-		case <-hbCheckCh:
-			if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
+		go wait.Until(func() {
+			if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
 				xl.Warn("heartbeat timeout")
-				// let reader() stop
-				ctl.conn.Close()
+				ctl.closeSession()
 				return
 			}
-		case rawMsg, ok := <-ctl.readCh:
-			if !ok {
-				return
-			}
-
-			switch m := rawMsg.(type) {
-			case *msg.ReqWorkConn:
-				go ctl.HandleReqWorkConn(m)
-			case *msg.NewProxyResp:
-				ctl.HandleNewProxyResp(m)
-			case *msg.NatHoleResp:
-				ctl.HandleNatHoleResp(m)
-			case *msg.Pong:
-				if m.Error != "" {
-					xl.Error("Pong contains error: %s", m.Error)
-					ctl.conn.Close()
-					return
-				}
-				ctl.lastPong = time.Now()
-				xl.Debug("receive heartbeat from server")
-			}
-		}
+		}, time.Second, ctl.doneCh)
 	}
 }
 
-// If controler is notified by closedCh, reader and writer and handler will exit
 func (ctl *Control) worker() {
-	go ctl.msgHandler()
-	go ctl.reader()
-	go ctl.writer()
-
-	<-ctl.closedCh
-	// close related channels and wait until other goroutines done
-	close(ctl.readCh)
-	ctl.readerShutdown.WaitDone()
-	ctl.msgHandlerShutdown.WaitDone()
+	go ctl.heartbeatWorker()
+	go ctl.msgDispatcher.Run()
 
-	close(ctl.sendCh)
-	ctl.writerShutdown.WaitDone()
+	<-ctl.msgDispatcher.Done()
+	ctl.closeSession()
 
 	ctl.pm.Close()
 	ctl.vm.Close()
-
-	close(ctl.closedDoneCh)
-	ctl.cm.Close()
+	close(ctl.doneCh)
 }
 
-func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
-	ctl.vm.Reload(visitorCfgs)
-	ctl.pm.Reload(pxyCfgs)
+func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
+	ctl.vm.UpdateAll(visitorCfgs)
+	ctl.pm.UpdateAll(proxyCfgs)
 	return nil
 }

+ 13 - 4
client/proxy/proxy.go

@@ -47,10 +47,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v
 // Proxy defines how to handle work connections for different proxy type.
 type Proxy interface {
 	Run() error
-
 	// InWorkConn accept work connections registered to server.
 	InWorkConn(net.Conn, *msg.StartWorkConn)
-
+	SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
 	Close()
 }
 
@@ -89,7 +88,8 @@ type BaseProxy struct {
 	limiter        *rate.Limiter
 	// proxyPlugin is used to handle connections instead of dialing to local service.
 	// It's only validate for TCP protocol now.
-	proxyPlugin plugin.Plugin
+	proxyPlugin        plugin.Plugin
+	inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool
 
 	mu  sync.RWMutex
 	xl  *xlog.Logger
@@ -113,7 +113,16 @@ func (pxy *BaseProxy) Close() {
 	}
 }
 
+func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+	pxy.inWorkConnCallback = cb
+}
+
 func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
+	if pxy.inWorkConnCallback != nil {
+		if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) {
+			return
+		}
+	}
 	pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
 }
 
@@ -132,7 +141,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 		})
 	}
 
-	xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
+	xl.Trace("handle tcp work connection, useEncryption: %t, useCompression: %t",
 		baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
 	if baseCfg.Transport.UseEncryption {
 		remote, err = libio.WithEncryption(remote, encKey)

+ 23 - 6
client/proxy/proxy_manager.go

@@ -31,8 +31,9 @@ import (
 )
 
 type Manager struct {
-	proxies        map[string]*Wrapper
-	msgTransporter transport.MessageTransporter
+	proxies            map[string]*Wrapper
+	msgTransporter     transport.MessageTransporter
+	inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
 
 	closed bool
 	mu     sync.RWMutex
@@ -71,6 +72,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri
 	return nil
 }
 
+func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+	pm.inWorkConnCallback = cb
+}
+
 func (pm *Manager) Close() {
 	pm.mu.Lock()
 	defer pm.mu.Unlock()
@@ -115,9 +120,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
 	return ps
 }
 
-func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
+func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
+	pm.mu.RLock()
+	defer pm.mu.RUnlock()
+	if pxy, ok := pm.proxies[name]; ok {
+		return pxy.GetStatus(), true
+	}
+	return nil, false
+}
+
+func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
 	xl := xlog.FromContextSafe(pm.ctx)
-	pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string {
+	proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
 		return c.GetBaseConfig().Name
 	})
 	pm.mu.Lock()
@@ -126,7 +140,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
 	delPxyNames := make([]string, 0)
 	for name, pxy := range pm.proxies {
 		del := false
-		cfg, ok := pxyCfgsMap[name]
+		cfg, ok := proxyCfgsMap[name]
 		if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
 			del = true
 		}
@@ -142,10 +156,13 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
 	}
 
 	addPxyNames := make([]string, 0)
-	for _, cfg := range pxyCfgs {
+	for _, cfg := range proxyCfgs {
 		name := cfg.GetBaseConfig().Name
 		if _, ok := pm.proxies[name]; !ok {
 			pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
+			if pm.inWorkConnCallback != nil {
+				pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
+			}
 			pm.proxies[name] = pxy
 			addPxyNames = append(addPxyNames, name)
 

+ 4 - 0
client/proxy/proxy_wrapper.go

@@ -121,6 +121,10 @@ func NewWrapper(
 	return pw
 }
 
+func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+	pw.pxy.SetInWorkConnCallback(cb)
+}
+
 func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
 	pw.mu.Lock()
 	defer pw.mu.Unlock()

+ 4 - 2
client/proxy/sudp.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package proxy
 
 import (
@@ -29,7 +31,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -99,7 +101,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
 	if pxy.cfg.Transport.UseCompression {
 		rwc = libio.WithCompression(rwc)
 	}
-	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
+	conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
 
 	workConn := conn
 	readCh := make(chan *msg.UDPPacket, 1024)

+ 5 - 3
client/proxy/udp.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package proxy
 
 import (
@@ -28,7 +30,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -89,7 +91,7 @@ func (pxy *UDPProxy) Close() {
 func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
 	xl := pxy.xl
 	xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
-	// close resources releated with old workConn
+	// close resources related with old workConn
 	pxy.Close()
 
 	var rwc io.ReadWriteCloser = conn
@@ -110,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
 	if pxy.cfg.Transport.UseCompression {
 		rwc = libio.WithCompression(rwc)
 	}
-	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
+	conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
 
 	pxy.mu.Lock()
 	pxy.workConn = conn

+ 5 - 3
client/proxy/xtcp.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package proxy
 
 import (
@@ -27,7 +29,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	"github.com/fatedier/frp/pkg/transport"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -131,7 +133,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 	}
 	defer lConn.Close()
 
-	remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
+	remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
 	if err != nil {
 		xl.Warn("create kcp connection from udp connection error: %v", err)
 		return
@@ -192,6 +194,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
 			_ = c.CloseWithError(0, "")
 			return
 		}
-		go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
+		go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
 	}
 }

+ 238 - 354
client/service.go

@@ -16,32 +16,25 @@ package client
 
 import (
 	"context"
-	"crypto/tls"
+	"errors"
 	"fmt"
-	"io"
 	"net"
 	"runtime"
-	"strconv"
-	"strings"
 	"sync"
-	"sync/atomic"
 	"time"
 
 	"github.com/fatedier/golib/crypto"
-	libdial "github.com/fatedier/golib/net/dial"
-	fmux "github.com/hashicorp/yamux"
-	quic "github.com/quic-go/quic-go"
 	"github.com/samber/lo"
 
-	"github.com/fatedier/frp/assets"
+	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/pkg/auth"
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
-	"github.com/fatedier/frp/pkg/transport"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
-	"github.com/fatedier/frp/pkg/util/util"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/version"
+	"github.com/fatedier/frp/pkg/util/wait"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
@@ -49,212 +42,197 @@ func init() {
 	crypto.DefaultSalt = "frp"
 }
 
-// Service is a client service.
-type Service struct {
-	// uniq id got from frps, attach it in loginMsg
-	runID string
+type cancelErr struct {
+	Err error
+}
 
-	// manager control connection with server
-	ctl   *Control
+func (e cancelErr) Error() string {
+	return e.Err.Error()
+}
+
+// ServiceOptions contains options for creating a new client service.
+type ServiceOptions struct {
+	Common      *v1.ClientCommonConfig
+	ProxyCfgs   []v1.ProxyConfigurer
+	VisitorCfgs []v1.VisitorConfigurer
+
+	// ConfigFilePath is the path to the configuration file used to initialize.
+	// If it is empty, it means that the configuration file is not used for initialization.
+	// It may be initialized using command line parameters or called directly.
+	ConfigFilePath string
+
+	// ClientSpec is the client specification that control the client behavior.
+	ClientSpec *msg.ClientSpec
+
+	// ConnectorCreator is a function that creates a new connector to make connections to the server.
+	// The Connector shields the underlying connection details, whether it is through TCP or QUIC connection,
+	// and regardless of whether multiplexing is used.
+	//
+	// If it is not set, the default frpc connector will be used.
+	// By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps
+	// through a pipe instead of a real physical connection.
+	ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
+
+	// HandleWorkConnCb is a callback function that is called when a new work connection is created.
+	//
+	// If it is not set, the default frpc implementation will be used.
+	HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
+}
+
+// setServiceOptionsDefault sets the default values for ServiceOptions.
+func setServiceOptionsDefault(options *ServiceOptions) {
+	if options.Common != nil {
+		options.Common.Complete()
+	}
+	if options.ConnectorCreator == nil {
+		options.ConnectorCreator = NewConnector
+	}
+}
+
+// Service is the client service that connects to frps and provides proxy services.
+type Service struct {
 	ctlMu sync.RWMutex
+	// manager control connection with server
+	ctl *Control
+	// Uniq id got from frps, it will be attached to loginMsg.
+	runID string
 
 	// Sets authentication based on selected method
 	authSetter auth.Setter
 
-	cfg         *v1.ClientCommonConfig
-	pxyCfgs     []v1.ProxyConfigurer
-	visitorCfgs []v1.VisitorConfigurer
+	// web server for admin UI and apis
+	webServer *httppkg.Server
+
 	cfgMu       sync.RWMutex
+	common      *v1.ClientCommonConfig
+	proxyCfgs   []v1.ProxyConfigurer
+	visitorCfgs []v1.VisitorConfigurer
+	clientSpec  *msg.ClientSpec
 
 	// The configuration file used to initialize this client, or an empty
 	// string if no configuration file was used.
-	cfgFile string
-
-	exit uint32 // 0 means not exit
+	configFilePath string
 
 	// service context
 	ctx context.Context
 	// call cancel to stop service
-	cancel context.CancelFunc
-}
+	cancel                   context.CancelCauseFunc
+	gracefulShutdownDuration time.Duration
 
-func NewService(
-	cfg *v1.ClientCommonConfig,
-	pxyCfgs []v1.ProxyConfigurer,
-	visitorCfgs []v1.VisitorConfigurer,
-	cfgFile string,
-) (svr *Service, err error) {
-	svr = &Service{
-		authSetter:  auth.NewAuthSetter(cfg.Auth),
-		cfg:         cfg,
-		cfgFile:     cfgFile,
-		pxyCfgs:     pxyCfgs,
-		visitorCfgs: visitorCfgs,
-		ctx:         context.Background(),
-		exit:        0,
-	}
-	return
+	connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
+	handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
 }
 
-func (svr *Service) GetController() *Control {
-	svr.ctlMu.RLock()
-	defer svr.ctlMu.RUnlock()
-	return svr.ctl
+func NewService(options ServiceOptions) (*Service, error) {
+	setServiceOptionsDefault(&options)
+
+	var webServer *httppkg.Server
+	if options.Common.WebServer.Port > 0 {
+		ws, err := httppkg.NewServer(options.Common.WebServer)
+		if err != nil {
+			return nil, err
+		}
+		webServer = ws
+	}
+	s := &Service{
+		ctx:              context.Background(),
+		authSetter:       auth.NewAuthSetter(options.Common.Auth),
+		webServer:        webServer,
+		common:           options.Common,
+		configFilePath:   options.ConfigFilePath,
+		proxyCfgs:        options.ProxyCfgs,
+		visitorCfgs:      options.VisitorCfgs,
+		clientSpec:       options.ClientSpec,
+		connectorCreator: options.ConnectorCreator,
+		handleWorkConnCb: options.HandleWorkConnCb,
+	}
+	if webServer != nil {
+		webServer.RouteRegister(s.registerRouteHandlers)
+	}
+	return s, nil
 }
 
 func (svr *Service) Run(ctx context.Context) error {
-	ctx, cancel := context.WithCancel(ctx)
-	svr.ctx = xlog.NewContext(ctx, xlog.New())
+	ctx, cancel := context.WithCancelCause(ctx)
+	svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
 	svr.cancel = cancel
 
-	xl := xlog.FromContextSafe(svr.ctx)
-
 	// set custom DNSServer
-	if svr.cfg.DNSServer != "" {
-		dnsAddr := svr.cfg.DNSServer
-		if _, _, err := net.SplitHostPort(dnsAddr); err != nil {
-			dnsAddr = net.JoinHostPort(dnsAddr, "53")
-		}
-		// Change default dns server for frpc
-		net.DefaultResolver = &net.Resolver{
-			PreferGo: true,
-			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
-				return net.Dial("udp", dnsAddr)
-			},
-		}
+	if svr.common.DNSServer != "" {
+		netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
 	}
 
-	// login to frps
-	for {
-		conn, cm, err := svr.login()
-		if err != nil {
-			xl.Warn("login to server failed: %v", err)
-
-			// if login_fail_exit is true, just exit this program
-			// otherwise sleep a while and try again to connect to server
-			if lo.FromPtr(svr.cfg.LoginFailExit) {
-				return err
-			}
-			util.RandomSleep(5*time.Second, 0.9, 1.1)
-		} else {
-			// login success
-			ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
-			ctl.Run()
-			svr.ctlMu.Lock()
-			svr.ctl = ctl
-			svr.ctlMu.Unlock()
-			break
-		}
+	// first login to frps
+	svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
+	if svr.ctl == nil {
+		cancelCause := cancelErr{}
+		_ = errors.As(context.Cause(svr.ctx), &cancelCause)
+		return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
 	}
 
 	go svr.keepControllerWorking()
 
-	if svr.cfg.WebServer.Port != 0 {
-		// Init admin server assets
-		assets.Load(svr.cfg.WebServer.AssetsDir)
-
-		address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port))
-		err := svr.RunAdminServer(address)
-		if err != nil {
-			log.Warn("run admin server error: %v", err)
-		}
-		log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
+	if svr.webServer != nil {
+		go func() {
+			log.Info("admin server listen on %s", svr.webServer.Address())
+			if err := svr.webServer.Run(); err != nil {
+				log.Warn("admin server exit with error: %v", err)
+			}
+		}()
 	}
 	<-svr.ctx.Done()
-	// service context may not be canceled by svr.Close(), we should call it here to release resources
-	if atomic.LoadUint32(&svr.exit) == 0 {
-		svr.Close()
-	}
+	svr.stop()
 	return nil
 }
 
 func (svr *Service) keepControllerWorking() {
-	xl := xlog.FromContextSafe(svr.ctx)
-	maxDelayTime := 20 * time.Second
-	delayTime := time.Second
-
-	// if frpc reconnect frps, we need to limit retry times in 1min
-	// current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ...
-	// when exceed 1min, we will reset delay and counts
-	cutoffTime := time.Now().Add(time.Minute)
-	reconnectDelay := time.Second
-	reconnectCounts := 1
-
-	for {
-		<-svr.ctl.ClosedDoneCh()
-		if atomic.LoadUint32(&svr.exit) != 0 {
-			return
-		}
-
-		// the first three attempts with a low delay
-		if reconnectCounts > 3 {
-			util.RandomSleep(reconnectDelay, 0.9, 1.1)
-			xl.Info("wait %v to reconnect", reconnectDelay)
-			reconnectDelay *= 2
-		} else {
-			util.RandomSleep(time.Second, 0, 0.5)
-		}
-		reconnectCounts++
-
-		now := time.Now()
-		if now.After(cutoffTime) {
-			// reset
-			cutoffTime = now.Add(time.Minute)
-			reconnectDelay = time.Second
-			reconnectCounts = 1
-		}
-
-		for {
-			if atomic.LoadUint32(&svr.exit) != 0 {
-				return
-			}
-
-			xl.Info("try to reconnect to server...")
-			conn, cm, err := svr.login()
-			if err != nil {
-				xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
-				util.RandomSleep(delayTime, 0.9, 1.1)
-
-				delayTime *= 2
-				if delayTime > maxDelayTime {
-					delayTime = maxDelayTime
-				}
-				continue
-			}
-			// reconnect success, init delayTime
-			delayTime = time.Second
-
-			ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
-			ctl.Run()
-			svr.ctlMu.Lock()
-			if svr.ctl != nil {
-				svr.ctl.Close()
-			}
-			svr.ctl = ctl
-			svr.ctlMu.Unlock()
-			break
+	<-svr.ctl.Done()
+
+	// There is a situation where the login is successful but due to certain reasons,
+	// the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
+	// The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
+	// The maximum interval is 20 seconds.
+	wait.BackoffUntil(func() error {
+		// loopLoginUntilSuccess is another layer of loop that will continuously attempt to
+		// login to the server until successful.
+		svr.loopLoginUntilSuccess(20*time.Second, false)
+		if svr.ctl != nil {
+			<-svr.ctl.Done()
+			return errors.New("control is closed and try another loop")
 		}
-	}
+		// If the control is nil, it means that the login failed and the service is also closed.
+		return nil
+	}, wait.NewFastBackoffManager(
+		wait.FastBackoffOptions{
+			Duration:        time.Second,
+			Factor:          2,
+			Jitter:          0.1,
+			MaxDuration:     20 * time.Second,
+			FastRetryCount:  3,
+			FastRetryDelay:  200 * time.Millisecond,
+			FastRetryWindow: time.Minute,
+			FastRetryJitter: 0.5,
+		},
+	), true, svr.ctx.Done())
 }
 
 // login creates a connection to frps and registers it self as a client
 // conn: control connection
 // session: if it's not nil, using tcp mux
-func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
+func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
 	xl := xlog.FromContextSafe(svr.ctx)
-	cm = NewConnectionManager(svr.ctx, svr.cfg)
-
-	if err = cm.OpenConnection(); err != nil {
+	connector = svr.connectorCreator(svr.ctx, svr.common)
+	if err = connector.Open(); err != nil {
 		return nil, nil, err
 	}
 
 	defer func() {
 		if err != nil {
-			cm.Close()
+			connector.Close()
 		}
 	}()
 
-	conn, err = cm.Connect()
+	conn, err = connector.Connect()
 	if err != nil {
 		return
 	}
@@ -262,12 +240,15 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	loginMsg := &msg.Login{
 		Arch:      runtime.GOARCH,
 		Os:        runtime.GOOS,
-		PoolCount: svr.cfg.Transport.PoolCount,
-		User:      svr.cfg.User,
+		PoolCount: svr.common.Transport.PoolCount,
+		User:      svr.common.User,
 		Version:   version.Full(),
 		Timestamp: time.Now().Unix(),
 		RunID:     svr.runID,
-		Metas:     svr.cfg.Metadatas,
+		Metas:     svr.common.Metadatas,
+	}
+	if svr.clientSpec != nil {
+		loginMsg.ClientSpec = *svr.clientSpec
 	}
 
 	// Add auth
@@ -293,16 +274,79 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	}
 
 	svr.runID = loginRespMsg.RunID
-	xl.ResetPrefixes()
-	xl.AppendPrefix(svr.runID)
+	xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
 
 	xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
 	return
 }
 
-func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
+func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
+	xl := xlog.FromContextSafe(svr.ctx)
+	successCh := make(chan struct{})
+
+	loginFunc := func() error {
+		xl.Info("try to connect to server...")
+		conn, connector, err := svr.login()
+		if err != nil {
+			xl.Warn("connect to server error: %v", err)
+			if firstLoginExit {
+				svr.cancel(cancelErr{Err: err})
+			}
+			return err
+		}
+
+		svr.cfgMu.RLock()
+		proxyCfgs := svr.proxyCfgs
+		visitorCfgs := svr.visitorCfgs
+		svr.cfgMu.RUnlock()
+		connEncrypted := true
+		if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" {
+			connEncrypted = false
+		}
+		sessionCtx := &SessionContext{
+			Common:        svr.common,
+			RunID:         svr.runID,
+			Conn:          conn,
+			ConnEncrypted: connEncrypted,
+			AuthSetter:    svr.authSetter,
+			Connector:     connector,
+		}
+		ctl, err := NewControl(svr.ctx, sessionCtx)
+		if err != nil {
+			conn.Close()
+			xl.Error("NewControl error: %v", err)
+			return err
+		}
+		ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
+
+		ctl.Run(proxyCfgs, visitorCfgs)
+		// close and replace previous control
+		svr.ctlMu.Lock()
+		if svr.ctl != nil {
+			svr.ctl.Close()
+		}
+		svr.ctl = ctl
+		svr.ctlMu.Unlock()
+
+		close(successCh)
+		return nil
+	}
+
+	// try to reconnect to server until success
+	wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
+		wait.FastBackoffOptions{
+			Duration:    time.Second,
+			Factor:      2,
+			Jitter:      0.1,
+			MaxDuration: maxInterval,
+		}),
+		true,
+		wait.MergeAndCloseOnAnyStopChannel(svr.ctx.Done(), successCh))
+}
+
+func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
 	svr.cfgMu.Lock()
-	svr.pxyCfgs = pxyCfgs
+	svr.proxyCfgs = proxyCfgs
 	svr.visitorCfgs = visitorCfgs
 	svr.cfgMu.Unlock()
 
@@ -311,7 +355,7 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi
 	svr.ctlMu.RUnlock()
 
 	if ctl != nil {
-		return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
+		return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
 	}
 	return nil
 }
@@ -321,191 +365,31 @@ func (svr *Service) Close() {
 }
 
 func (svr *Service) GracefulClose(d time.Duration) {
-	atomic.StoreUint32(&svr.exit, 1)
+	svr.gracefulShutdownDuration = d
+	svr.cancel(nil)
+}
 
-	svr.ctlMu.RLock()
+func (svr *Service) stop() {
+	svr.ctlMu.Lock()
+	defer svr.ctlMu.Unlock()
 	if svr.ctl != nil {
-		svr.ctl.GracefulClose(d)
+		svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
 		svr.ctl = nil
 	}
-	svr.ctlMu.RUnlock()
-
-	if svr.cancel != nil {
-		svr.cancel()
-	}
-}
-
-type ConnectionManager struct {
-	ctx context.Context
-	cfg *v1.ClientCommonConfig
-
-	muxSession *fmux.Session
-	quicConn   quic.Connection
-}
-
-func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager {
-	return &ConnectionManager{
-		ctx: ctx,
-		cfg: cfg,
-	}
-}
-
-func (cm *ConnectionManager) OpenConnection() error {
-	xl := xlog.FromContextSafe(cm.ctx)
-
-	// special for quic
-	if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") {
-		var tlsConfig *tls.Config
-		var err error
-		sn := cm.cfg.Transport.TLS.ServerName
-		if sn == "" {
-			sn = cm.cfg.ServerAddr
-		}
-		if lo.FromPtr(cm.cfg.Transport.TLS.Enable) {
-			tlsConfig, err = transport.NewClientTLSConfig(
-				cm.cfg.Transport.TLS.CertFile,
-				cm.cfg.Transport.TLS.KeyFile,
-				cm.cfg.Transport.TLS.TrustedCaFile,
-				sn)
-		} else {
-			tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
-		}
-		if err != nil {
-			xl.Warn("fail to build tls configuration, err: %v", err)
-			return err
-		}
-		tlsConfig.NextProtos = []string{"frp"}
-
-		conn, err := quic.DialAddr(
-			cm.ctx,
-			net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
-			tlsConfig, &quic.Config{
-				MaxIdleTimeout:     time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
-				MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams),
-				KeepAlivePeriod:    time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
-			})
-		if err != nil {
-			return err
-		}
-		cm.quicConn = conn
-		return nil
-	}
-
-	if !lo.FromPtr(cm.cfg.Transport.TCPMux) {
-		return nil
-	}
-
-	conn, err := cm.realConnect()
-	if err != nil {
-		return err
-	}
-
-	fmuxCfg := fmux.DefaultConfig()
-	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
-	fmuxCfg.LogOutput = io.Discard
-	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
-	session, err := fmux.Client(conn, fmuxCfg)
-	if err != nil {
-		return err
-	}
-	cm.muxSession = session
-	return nil
-}
-
-func (cm *ConnectionManager) Connect() (net.Conn, error) {
-	if cm.quicConn != nil {
-		stream, err := cm.quicConn.OpenStreamSync(context.Background())
-		if err != nil {
-			return nil, err
-		}
-		return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil
-	} else if cm.muxSession != nil {
-		stream, err := cm.muxSession.OpenStream()
-		if err != nil {
-			return nil, err
-		}
-		return stream, nil
-	}
-
-	return cm.realConnect()
 }
 
-func (cm *ConnectionManager) realConnect() (net.Conn, error) {
-	xl := xlog.FromContextSafe(cm.ctx)
-	var tlsConfig *tls.Config
-	var err error
-	tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable)
-	if cm.cfg.Transport.Protocol == "wss" {
-		tlsEnable = true
-	}
-	if tlsEnable {
-		sn := cm.cfg.Transport.TLS.ServerName
-		if sn == "" {
-			sn = cm.cfg.ServerAddr
-		}
-
-		tlsConfig, err = transport.NewClientTLSConfig(
-			cm.cfg.Transport.TLS.CertFile,
-			cm.cfg.Transport.TLS.KeyFile,
-			cm.cfg.Transport.TLS.TrustedCaFile,
-			sn)
-		if err != nil {
-			xl.Warn("fail to build tls configuration, err: %v", err)
-			return nil, err
-		}
-	}
-
-	proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL)
-	if err != nil {
-		xl.Error("fail to parse proxy url")
-		return nil, err
-	}
-	dialOptions := []libdial.DialOption{}
-	protocol := cm.cfg.Transport.Protocol
-	switch protocol {
-	case "websocket":
-		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")}))
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
-			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
-		}))
-		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
-	case "wss":
-		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
-		// Make sure that if it is wss, the websocket hook is executed after the tls hook.
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
-	default:
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
-			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
-		}))
-		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
-	}
-
-	if cm.cfg.Transport.ConnectServerLocalIP != "" {
-		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP))
-	}
-	dialOptions = append(dialOptions,
-		libdial.WithProtocol(protocol),
-		libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second),
-		libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second),
-		libdial.WithProxy(proxyType, addr),
-		libdial.WithProxyAuth(auth),
-	)
-	conn, err := libdial.DialContext(
-		cm.ctx,
-		net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
-		dialOptions...,
-	)
-	return conn, err
-}
+// TODO(fatedier): Use StatusExporter to provide query interfaces instead of directly using methods from the Service.
+func (svr *Service) GetProxyStatus(name string) (*proxy.WorkingStatus, error) {
+	svr.ctlMu.RLock()
+	ctl := svr.ctl
+	svr.ctlMu.RUnlock()
 
-func (cm *ConnectionManager) Close() error {
-	if cm.quicConn != nil {
-		_ = cm.quicConn.CloseWithError(0, "")
+	if ctl == nil {
+		return nil, fmt.Errorf("control is not running")
 	}
-	if cm.muxSession != nil {
-		_ = cm.muxSession.Close()
+	ws, ok := ctl.pm.GetProxyStatus(name)
+	if !ok {
+		return nil, fmt.Errorf("proxy [%s] is not found", name)
 	}
-	return nil
+	return ws, nil
 }

+ 2 - 2
client/visitor/sudp.go

@@ -28,7 +28,7 @@ import (
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
@@ -242,7 +242,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
 	if sv.cfg.Transport.UseCompression {
 		remote = libio.WithCompression(remote)
 	}
-	return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
+	return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
 }
 
 func (sv *SUDPVisitor) Close() {

+ 4 - 4
client/visitor/visitor.go

@@ -21,11 +21,11 @@ import (
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
-// Helper wrapps some functions for visitor to use.
+// Helper wraps some functions for visitor to use.
 type Helper interface {
 	// ConnectServer directly connects to the frp server.
 	ConnectServer() (net.Conn, error)
@@ -56,7 +56,7 @@ func NewVisitor(
 		clientCfg:  clientCfg,
 		helper:     helper,
 		ctx:        xlog.NewContext(ctx, xl),
-		internalLn: utilnet.NewInternalListener(),
+		internalLn: netpkg.NewInternalListener(),
 	}
 	switch cfg := cfg.(type) {
 	case *v1.STCPVisitorConfig:
@@ -84,7 +84,7 @@ type BaseVisitor struct {
 	clientCfg  *v1.ClientCommonConfig
 	helper     Helper
 	l          net.Listener
-	internalLn *utilnet.InternalListener
+	internalLn *netpkg.InternalListener
 
 	mu  sync.RWMutex
 	ctx context.Context

+ 14 - 4
client/visitor/visitor_manager.go

@@ -35,7 +35,8 @@ type Manager struct {
 	visitors  map[string]Visitor
 	helper    Helper
 
-	checkInterval time.Duration
+	checkInterval           time.Duration
+	keepVisitorsRunningOnce sync.Once
 
 	mu  sync.RWMutex
 	ctx context.Context
@@ -67,7 +68,9 @@ func NewManager(
 	return m
 }
 
-func (vm *Manager) Run() {
+// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it.
+// It will only start after Reload is called and a new visitor is added.
+func (vm *Manager) keepVisitorsRunning() {
 	xl := xlog.FromContextSafe(vm.ctx)
 
 	ticker := time.NewTicker(vm.checkInterval)
@@ -76,7 +79,7 @@ func (vm *Manager) Run() {
 	for {
 		select {
 		case <-vm.stopCh:
-			xl.Info("gracefully shutdown visitor manager")
+			xl.Trace("gracefully shutdown visitor manager")
 			return
 		case <-ticker.C:
 			vm.mu.Lock()
@@ -120,7 +123,14 @@ func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
 	return
 }
 
-func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
+func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
+	if len(cfgs) > 0 {
+		// Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
+		vm.keepVisitorsRunningOnce.Do(func() {
+			go vm.keepVisitorsRunning()
+		})
+	}
+
 	xl := xlog.FromContextSafe(vm.ctx)
 	cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
 		return c.GetBaseConfig().Name

+ 3 - 3
client/visitor/xtcp.go

@@ -33,7 +33,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	"github.com/fatedier/frp/pkg/transport"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
@@ -349,7 +349,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
 	if err != nil {
 		return fmt.Errorf("dial udp error: %v", err)
 	}
-	remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
+	remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
 	if err != nil {
 		return fmt.Errorf("create kcp connection from udp connection error: %v", err)
 	}
@@ -440,7 +440,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
 	if err != nil {
 		return nil, err
 	}
-	return utilnet.QuicStreamToNetConn(stream, session), nil
+	return netpkg.QuicStreamToNetConn(stream, session), nil
 }
 
 func (qs *QUICTunnelSession) Close() {

+ 2 - 2
cmd/frpc/sub/admin.go

@@ -52,7 +52,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
 		Use:   name,
 		Short: short,
 		Run: func(cmd *cobra.Command, args []string) {
-			cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
+			cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
@@ -73,7 +73,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
 func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
 	client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
 	client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
-	if err := client.Reload(); err != nil {
+	if err := client.Reload(strictConfigMode); err != nil {
 		return err
 	}
 	fmt.Println("reload success")

+ 0 - 125
cmd/frpc/sub/flags.go

@@ -1,125 +0,0 @@
-// Copyright 2023 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config/types"
-	v1 "github.com/fatedier/frp/pkg/config/v1"
-	"github.com/fatedier/frp/pkg/config/v1/validation"
-)
-
-type BandwidthQuantityFlag struct {
-	V *types.BandwidthQuantity
-}
-
-func (f *BandwidthQuantityFlag) Set(s string) error {
-	return f.V.UnmarshalString(s)
-}
-
-func (f *BandwidthQuantityFlag) String() string {
-	return f.V.String()
-}
-
-func (f *BandwidthQuantityFlag) Type() string {
-	return "string"
-}
-
-func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
-	registerProxyBaseConfigFlags(cmd, c.GetBaseConfig())
-	switch cc := c.(type) {
-	case *v1.TCPProxyConfig:
-		cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
-	case *v1.UDPProxyConfig:
-		cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
-	case *v1.HTTPProxyConfig:
-		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
-		cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations")
-		cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
-		cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
-		cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
-	case *v1.HTTPSProxyConfig:
-		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
-	case *v1.TCPMuxProxyConfig:
-		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
-		cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer")
-	case *v1.STCPProxyConfig:
-		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
-	case *v1.SUDPProxyConfig:
-		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
-	case *v1.XTCPProxyConfig:
-		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
-	}
-}
-
-func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) {
-	if c == nil {
-		return
-	}
-	cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
-	cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
-	cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port")
-	cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
-	cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
-	cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
-	cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)")
-}
-
-func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
-	if c == nil {
-		return
-	}
-	cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains")
-	cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
-}
-
-func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) {
-	registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig())
-
-	// add visitor flags if exist
-}
-
-func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig) {
-	if c == nil {
-		return
-	}
-	cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name")
-	cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
-	cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
-	cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
-	cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
-	cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
-	cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
-}
-
-func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig) {
-	cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
-	cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
-	cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
-	cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
-		fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
-	cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
-	cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
-	cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
-	cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
-	cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
-	cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
-	cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
-
-	c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
-}

+ 2 - 1
cmd/frpc/sub/nathole.go

@@ -48,9 +48,10 @@ var natholeDiscoveryCmd = &cobra.Command{
 	Short: "Discover nathole information from stun server",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		// ignore error here, because we can use command line pameters
-		cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
+		cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
 		if err != nil {
 			cfg = &v1.ClientCommonConfig{}
+			cfg.Complete()
 		}
 		if natHoleSTUNServer != "" {
 			cfg.NatHoleSTUNServer = natHoleSTUNServer

+ 4 - 3
cmd/frpc/sub/proxy.go

@@ -21,6 +21,7 @@ import (
 	"github.com/samber/lo"
 	"github.com/spf13/cobra"
 
+	"github.com/fatedier/frp/pkg/config"
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/config/v1/validation"
 )
@@ -50,8 +51,8 @@ func init() {
 		}
 		clientCfg := v1.ClientCommonConfig{}
 		cmd := NewProxyCommand(string(typ), c, &clientCfg)
-		RegisterClientCommonConfigFlags(cmd, &clientCfg)
-		RegisterProxyFlags(cmd, c)
+		config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
+		config.RegisterProxyFlags(cmd, c)
 
 		// add sub command for visitor
 		if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
@@ -60,7 +61,7 @@ func init() {
 				panic("visitor type: " + typ + " not support")
 			}
 			visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
-			RegisterVisitorFlags(visitorCmd, vc)
+			config.RegisterVisitorFlags(visitorCmd, vc)
 			cmd.AddCommand(visitorCmd)
 		}
 		rootCmd.AddCommand(cmd)

+ 16 - 11
cmd/frpc/sub/root.go

@@ -36,15 +36,17 @@ import (
 )
 
 var (
-	cfgFile     string
-	cfgDir      string
-	showVersion bool
+	cfgFile          string
+	cfgDir           string
+	showVersion      bool
+	strictConfigMode bool
 )
 
 func init() {
 	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
 	rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
 	rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
+	rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fields will cause an error")
 }
 
 var rootCmd = &cobra.Command{
@@ -108,7 +110,7 @@ func handleTermSignal(svr *client.Service) {
 }
 
 func runClient(cfgFilePath string) error {
-	cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
+	cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
 	if err != nil {
 		return err
 	}
@@ -117,19 +119,19 @@ func runClient(cfgFilePath string) error {
 			"please use yaml/json/toml format instead!\n")
 	}
 
-	warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
+	warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
 	if warning != nil {
 		fmt.Printf("WARNING: %v\n", warning)
 	}
 	if err != nil {
 		return err
 	}
-	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
+	return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
 }
 
 func startService(
 	cfg *v1.ClientCommonConfig,
-	pxyCfgs []v1.ProxyConfigurer,
+	proxyCfgs []v1.ProxyConfigurer,
 	visitorCfgs []v1.VisitorConfigurer,
 	cfgFile string,
 ) error {
@@ -139,7 +141,12 @@ func startService(
 		log.Info("start frpc service for config file [%s]", cfgFile)
 		defer log.Info("frpc service for config file [%s] stopped", cfgFile)
 	}
-	svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
+	svr, err := client.NewService(client.ServiceOptions{
+		Common:         cfg,
+		ProxyCfgs:      proxyCfgs,
+		VisitorCfgs:    visitorCfgs,
+		ConfigFilePath: cfgFile,
+	})
 	if err != nil {
 		return err
 	}
@@ -149,7 +156,5 @@ func startService(
 	if shouldGracefulClose {
 		go handleTermSignal(svr)
 	}
-
-	_ = svr.Run(context.Background())
-	return nil
+	return svr.Run(context.Background())
 }

+ 2 - 2
cmd/frpc/sub/verify.go

@@ -37,12 +37,12 @@ var verifyCmd = &cobra.Command{
 			return nil
 		}
 
-		cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile)
+		cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs)
+		warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
 		if warning != nil {
 			fmt.Printf("WARNING: %v\n", warning)
 		}

+ 0 - 110
cmd/frps/flags.go

@@ -1,110 +0,0 @@
-// Copyright 2023 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"strconv"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config/types"
-	v1 "github.com/fatedier/frp/pkg/config/v1"
-)
-
-type PortsRangeSliceFlag struct {
-	V *[]types.PortsRange
-}
-
-func (f *PortsRangeSliceFlag) String() string {
-	if f.V == nil {
-		return ""
-	}
-	return types.PortsRangeSlice(*f.V).String()
-}
-
-func (f *PortsRangeSliceFlag) Set(s string) error {
-	slice, err := types.NewPortsRangeSliceFromString(s)
-	if err != nil {
-		return err
-	}
-	*f.V = slice
-	return nil
-}
-
-func (f *PortsRangeSliceFlag) Type() string {
-	return "string"
-}
-
-type BoolFuncFlag struct {
-	TrueFunc  func()
-	FalseFunc func()
-
-	v bool
-}
-
-func (f *BoolFuncFlag) String() string {
-	return strconv.FormatBool(f.v)
-}
-
-func (f *BoolFuncFlag) Set(s string) error {
-	f.v = strconv.FormatBool(f.v) == "true"
-
-	if !f.v {
-		if f.FalseFunc != nil {
-			f.FalseFunc()
-		}
-		return nil
-	}
-
-	if f.TrueFunc != nil {
-		f.TrueFunc()
-	}
-	return nil
-}
-
-func (f *BoolFuncFlag) Type() string {
-	return "bool"
-}
-
-func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
-	cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
-	cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
-	cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
-	cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
-	cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
-	cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
-	cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
-	cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
-	cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
-	cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
-	cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
-	cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
-	cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
-	cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
-	cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
-	cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
-	cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
-	cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
-	cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
-	cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
-	cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
-
-	webServerTLS := v1.TLSConfig{}
-	cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
-	cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
-	cmd.PersistentFlags().VarP(&BoolFuncFlag{
-		TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
-	}, "dashboard_tls_mode", "", "if enable dashboard tls mode")
-}

+ 6 - 4
cmd/frps/root.go

@@ -30,8 +30,9 @@ import (
 )
 
 var (
-	cfgFile     string
-	showVersion bool
+	cfgFile          string
+	showVersion      bool
+	strictConfigMode bool
 
 	serverCfg v1.ServerConfig
 )
@@ -39,8 +40,9 @@ var (
 func init() {
 	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
 	rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
+	rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fields will cause error")
 
-	RegisterServerConfigFlags(rootCmd, &serverCfg)
+	config.RegisterServerConfigFlags(rootCmd, &serverCfg)
 }
 
 var rootCmd = &cobra.Command{
@@ -58,7 +60,7 @@ var rootCmd = &cobra.Command{
 			err            error
 		)
 		if cfgFile != "" {
-			svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
+			svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)

+ 1 - 1
cmd/frps/verify.go

@@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{
 			fmt.Println("frps: the configuration file is not specified")
 			return nil
 		}
-		svrCfg, _, err := config.LoadServerConfig(cfgFile)
+		svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
conf/frpc_full_example.toml

@@ -38,7 +38,7 @@ auth.token = "12345678"
 # auth.oidc.clientSecret = ""
 # oidc.audience specifies the audience of the token in OIDC authentication.
 # auth.oidc.audience = ""
-# oidc.scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
+# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
 # auth.oidc.scope = ""
 # oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
 # It will be used to get an OIDC token.

+ 8 - 0
conf/frps_full_example.toml

@@ -143,6 +143,14 @@ udpPacketSize = 1500
 # Retention time for NAT hole punching strategy data.
 natholeAnalysisDataReserveHours = 168
 
+# ssh tunnel gateway
+# If you want to enable this feature, the bindPort parameter is required, while others are optional.
+# By default, this feature is disabled. It will be enabled if bindPort is greater than 0.
+# sshTunnelGateway.bindPort = 2200
+# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa"
+# sshTunnelGateway.autoGenPrivateKeyPath = ""
+# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys"
+
 [[httpPlugins]]
 name = "user-manager"
 addr = "127.0.0.1:9000"

+ 1 - 1
conf/legacy/frpc_legacy_full.ini

@@ -56,7 +56,7 @@ oidc_client_secret =
 # oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
 oidc_audience =
 
-# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
+# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
 oidc_scope =
 
 # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.

BIN
doc/pic/donate-alipay.png


BIN
doc/pic/sponsor_asocks.jpg


BIN
doc/pic/sponsor_nango.png


+ 160 - 0
doc/ssh_tunnel_gateway.md

@@ -0,0 +1,160 @@
+### SSH Tunnel Gateway
+
+*Added in v0.53.0*
+
+### Concept
+
+SSH supports reverse proxy capabilities [rfc](https://www.rfc-editor.org/rfc/rfc4254#page-16).
+
+frp supports listening on an SSH port on the frps side to achieve TCP protocol proxying using the SSH -R protocol. This mode does not rely on frpc.
+
+SSH reverse tunneling proxying and proxying SSH ports through frp are two different concepts. SSH reverse tunneling proxying is essentially a basic reverse proxying accomplished by connecting to frps via an SSH client when you don't want to use frpc.
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 0
+sshTunnelGateway.privateKeyFile = ""
+sshTunnelGateway.autoGenPrivateKeyPath = ""
+sshTunnelGateway.authorizedKeysFile = ""
+```
+
+| Field | Type | Description | Required |
+| :--- | :--- | :--- | :--- |
+| bindPort| int | The ssh server port that frps listens on.| Yes |
+| privateKeyFile | string | Default value is empty. The private key file used by the ssh server. If it is empty, frps will read the private key file under the autoGenPrivateKeyPath path. It can reuse the /home/user/.ssh/id_rsa file on the local machine, or a custom path can be specified.| No |
+| autoGenPrivateKeyPath  | string |Default value is ./.autogen_ssh_key. If the file does not exist or its content is empty, frps will automatically generate RSA private key file content and store it in this file.|No|
+| authorizedKeysFile  | string |Default value is empty. If it is empty, ssh client authentication is not authenticated. If it is not empty, it can implement ssh password-free login authentication. It can reuse the local /home/user/.ssh/authorized_keys file or a custom path can be specified.| No |
+
+### Basic Usage
+
+#### Server-side frps
+
+Minimal configuration:
+
+```toml
+sshTunnelGateway.bindPort = 2200
+```
+
+Place the above configuration in frps.toml and run `./frps -c frps.toml`. It will listen on port 2200 and accept SSH reverse proxy requests.
+
+Note:
+
+1. When using the minimal configuration, a `.autogen_ssh_key` private key file will be automatically created in the current working directory. The SSH server of frps will use this private key file for encryption and decryption. Alternatively, you can reuse an existing private key file on your local machine, such as `/home/user/.ssh/id_rsa`.
+
+2. When running frps in the minimal configuration mode, connecting to frps via SSH does not require authentication. It is strongly recommended to configure a token in frps and specify the token in the SSH command line.
+
+#### Client-side SSH
+
+The command format is:
+
+```bash
+ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token}
+```
+
+1. `--proxy_name` is optional, and if left empty, a random one will be generated.
+2. The username for logging in to frps is always "v0" and currently has no significance, i.e., `v0@{frps_address}`.
+3. The server-side proxy listens on the port determined by `--remote_port`.
+4. `{tcp|http|https|stcp|tcpmux}` supports the complete command parameters, which can be obtained by using `--help`. For example: `ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help`.
+5. The token is optional, but for security reasons, it is strongly recommended to configure the token in frps.
+
+#### TCP Proxy
+
+```bash
+ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090
+```
+
+This sets up a proxy on frps that listens on port 9090 and proxies local service on port 8080.
+
+```bash
+frp (via SSH) (Ctrl+C to quit)
+
+User: 
+ProxyName: test-tcp
+Type: tcp
+RemoteAddress: :9090
+```
+
+Equivalent to:
+
+```bash
+frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090
+```
+
+More parameters can be obtained by executing `--help`.
+
+#### HTTP Proxy
+
+```bash
+ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 http --proxy_name "test-http"  --custom_domain test-http.frps.com
+```
+
+Equivalent to:
+```bash
+frpc http --proxy_name "test-http" --custom_domain test-http.frps.com
+```
+
+You can access the HTTP service using the following command:
+
+curl 'http://test-http.frps.com'
+
+More parameters can be obtained by executing --help.
+
+#### HTTPS/STCP/TCPMUX Proxy
+
+To obtain the usage instructions, use the following command:
+
+```bash
+ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 {https|stcp|tcpmux} --help
+```
+
+### Advanced Usage
+
+#### Reusing the id_rsa File on the Local Machine
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 2200
+sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa"
+```
+
+During the SSH protocol handshake, public keys are exchanged for data encryption. Therefore, the SSH server on the frps side needs to specify a private key file, which can be reused from an existing file on the local machine. If the privateKeyFile field is empty, frps will automatically create an RSA private key file.
+
+#### Specifying the Auto-Generated Private Key File Path
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 2200
+sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file"
+```
+
+frps will automatically create a private key file and store it at the specified path.
+
+Note: Changing the private key file in frps can cause SSH client login failures. If you need to log in successfully, you can delete the old records from the `/home/user/.ssh/known_hosts` file.
+
+#### Using an Existing authorized_keys File for SSH Public Key Authentication
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 2200
+sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys"
+```
+
+The authorizedKeysFile is the file used for SSH public key authentication, which contains the public key information for users, with one key per line.
+
+If authorizedKeysFile is empty, frps won't perform any authentication for SSH clients. Frps does not support SSH username and password authentication.
+
+You can reuse an existing `authorized_keys` file on your local machine for client authentication.
+
+Note: authorizedKeysFile is for user authentication during the SSH login phase, while the token is for frps authentication. These two authentication methods are independent. SSH authentication comes first, followed by frps token authentication. It is strongly recommended to enable at least one of them. If authorizedKeysFile is empty, it is highly recommended to enable token authentication in frps to avoid security risks.
+
+#### Using a Custom authorized_keys File for SSH Public Key Authentication
+
+```toml
+# frps.toml
+sshTunnelGateway.bindPort = 2200
+sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file"
+```
+
+Specify the path to a custom `authorized_keys` file.
+
+Note that changes to the authorizedKeysFile file may result in SSH authentication failures. You may need to re-add the public key information to the authorizedKeysFile.

+ 6 - 6
go.mod

@@ -21,9 +21,11 @@ require (
 	github.com/quic-go/quic-go v0.37.4
 	github.com/rodaine/table v1.1.0
 	github.com/samber/lo v1.38.1
-	github.com/spf13/cobra v1.7.0
+	github.com/spf13/cobra v1.8.0
+	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.4
-	golang.org/x/net v0.12.0
+	golang.org/x/crypto v0.15.0
+	golang.org/x/net v0.17.0
 	golang.org/x/oauth2 v0.10.0
 	golang.org/x/sync v0.3.0
 	golang.org/x/time v0.3.0
@@ -60,15 +62,13 @@ require (
 	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
 	github.com/rogpeppe/go-internal v1.11.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
 	github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
-	golang.org/x/crypto v0.11.0 // indirect
 	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
 	golang.org/x/mod v0.10.0 // indirect
-	golang.org/x/sys v0.10.0 // indirect
-	golang.org/x/text v0.11.0 // indirect
+	golang.org/x/sys v0.14.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/tools v0.9.3 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect

+ 12 - 11
go.sum

@@ -16,7 +16,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
 github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -128,8 +128,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
 github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -157,8 +157,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
+golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
 golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -183,8 +183,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
 golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
@@ -210,20 +210,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
+golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

+ 31 - 0
pkg/auth/pass.go

@@ -0,0 +1,31 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth
+
+import (
+	"github.com/fatedier/frp/pkg/msg"
+)
+
+var AlwaysPassVerifier = &alwaysPass{}
+
+var _ Verifier = &alwaysPass{}
+
+type alwaysPass struct{}
+
+func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil }
+
+func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil }
+
+func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil }

+ 244 - 0
pkg/config/flags.go

@@ -0,0 +1,244 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/spf13/cobra"
+
+	"github.com/fatedier/frp/pkg/config/types"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
+)
+
+type RegisterFlagOption func(*registerFlagOptions)
+
+type registerFlagOptions struct {
+	sshMode bool
+}
+
+func WithSSHMode() RegisterFlagOption {
+	return func(o *registerFlagOptions) {
+		o.sshMode = true
+	}
+}
+
+type BandwidthQuantityFlag struct {
+	V *types.BandwidthQuantity
+}
+
+func (f *BandwidthQuantityFlag) Set(s string) error {
+	return f.V.UnmarshalString(s)
+}
+
+func (f *BandwidthQuantityFlag) String() string {
+	return f.V.String()
+}
+
+func (f *BandwidthQuantityFlag) Type() string {
+	return "string"
+}
+
+func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer, opts ...RegisterFlagOption) {
+	registerProxyBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
+
+	switch cc := c.(type) {
+	case *v1.TCPProxyConfig:
+		cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
+	case *v1.UDPProxyConfig:
+		cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port")
+	case *v1.HTTPProxyConfig:
+		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
+		cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations")
+		cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
+		cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
+		cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
+	case *v1.HTTPSProxyConfig:
+		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
+	case *v1.TCPMuxProxyConfig:
+		registerProxyDomainConfigFlags(cmd, &cc.DomainConfig)
+		cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer")
+		cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user")
+		cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password")
+	case *v1.STCPProxyConfig:
+		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
+		cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
+	case *v1.SUDPProxyConfig:
+		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
+		cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
+	case *v1.XTCPProxyConfig:
+		cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key")
+		cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users")
+	}
+}
+
+func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) {
+	if c == nil {
+		return
+	}
+	options := &registerFlagOptions{}
+	for _, opt := range opts {
+		opt(options)
+	}
+
+	cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
+
+	if !options.sshMode {
+		cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip")
+		cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port")
+		cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
+		cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
+		cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
+		cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)")
+	}
+}
+
+func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
+	if c == nil {
+		return
+	}
+	cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains")
+	cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
+}
+
+func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer, opts ...RegisterFlagOption) {
+	registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig(), opts...)
+
+	// add visitor flags if exist
+}
+
+func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) {
+	if c == nil {
+		return
+	}
+	cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name")
+	cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption")
+	cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression")
+	cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key")
+	cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name")
+	cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr")
+	cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port")
+}
+
+func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig, opts ...RegisterFlagOption) {
+	options := &registerFlagOptions{}
+	for _, opt := range opts {
+		opt(options)
+	}
+
+	if !options.sshMode {
+		cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
+		cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
+		cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
+			fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
+		cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
+		cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
+		cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
+		cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
+		cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
+		cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
+		c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
+	}
+	cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
+	cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
+}
+
+type PortsRangeSliceFlag struct {
+	V *[]types.PortsRange
+}
+
+func (f *PortsRangeSliceFlag) String() string {
+	if f.V == nil {
+		return ""
+	}
+	return types.PortsRangeSlice(*f.V).String()
+}
+
+func (f *PortsRangeSliceFlag) Set(s string) error {
+	slice, err := types.NewPortsRangeSliceFromString(s)
+	if err != nil {
+		return err
+	}
+	*f.V = slice
+	return nil
+}
+
+func (f *PortsRangeSliceFlag) Type() string {
+	return "string"
+}
+
+type BoolFuncFlag struct {
+	TrueFunc  func()
+	FalseFunc func()
+
+	v bool
+}
+
+func (f *BoolFuncFlag) String() string {
+	return strconv.FormatBool(f.v)
+}
+
+func (f *BoolFuncFlag) Set(s string) error {
+	f.v = strconv.FormatBool(f.v) == "true"
+
+	if !f.v {
+		if f.FalseFunc != nil {
+			f.FalseFunc()
+		}
+		return nil
+	}
+
+	if f.TrueFunc != nil {
+		f.TrueFunc()
+	}
+	return nil
+}
+
+func (f *BoolFuncFlag) Type() string {
+	return "bool"
+}
+
+func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...RegisterFlagOption) {
+	cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
+	cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
+	cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
+	cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
+	cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
+	cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
+	cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
+	cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
+	cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
+	cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
+	cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
+	cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
+	cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
+	cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
+	cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
+	cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
+	cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
+	cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
+	cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
+	cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
+	cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
+
+	webServerTLS := v1.TLSConfig{}
+	cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
+	cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
+	cmd.PersistentFlags().VarP(&BoolFuncFlag{
+		TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
+	}, "dashboard_tls_mode", "", "if enable dashboard tls mode")
+}

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

@@ -99,7 +99,7 @@ type ClientCommonConf struct {
 	// the server must have TCP multiplexing enabled as well. By default, this
 	// value is true.
 	TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
-	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
 	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
 	TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
 	// User specifies a prefix for proxy names to distinguish them from other

+ 2 - 2
pkg/config/legacy/parse.go

@@ -23,7 +23,7 @@ import (
 
 func ParseClientConfig(filePath string) (
 	cfg ClientCommonConf,
-	pxyCfgs map[string]ProxyConf,
+	proxyCfgs map[string]ProxyConf,
 	visitorCfgs map[string]VisitorConf,
 	err error,
 ) {
@@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) (
 	configBuffer.Write(buf)
 
 	// Parse all proxy and visitor configs.
-	pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
+	proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
 	if err != nil {
 		return
 	}

+ 1 - 1
pkg/config/legacy/server.go

@@ -139,7 +139,7 @@ type ServerCommonConf struct {
 	// from a client to share a single TCP connection. By default, this value
 	// is true.
 	TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
-	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
 	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
 	TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
 	// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.

+ 41 - 25
pkg/config/load.go

@@ -100,26 +100,42 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
 	return RenderWithTemplate(b, values)
 }
 
-func LoadConfigureFromFile(path string, c any) error {
+func LoadConfigureFromFile(path string, c any, strict bool) error {
 	content, err := LoadFileContentWithTemplate(path, GetValues())
 	if err != nil {
 		return err
 	}
-	return LoadConfigure(content, c)
+	return LoadConfigure(content, c, strict)
 }
 
 // LoadConfigure loads configuration from bytes and unmarshal into c.
 // Now it supports json, yaml and toml format.
-func LoadConfigure(b []byte, c any) error {
+func LoadConfigure(b []byte, c any, strict bool) error {
+	v1.DisallowUnknownFieldsMu.Lock()
+	defer v1.DisallowUnknownFieldsMu.Unlock()
+	v1.DisallowUnknownFields = strict
+
 	var tomlObj interface{}
+	// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
 	if err := toml.Unmarshal(b, &tomlObj); err == nil {
 		b, err = json.Marshal(&tomlObj)
 		if err != nil {
 			return err
 		}
 	}
-	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
-	return decoder.Decode(c)
+	// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
+	if yaml.IsJSONBuffer(b) {
+		decoder := json.NewDecoder(bytes.NewBuffer(b))
+		if strict {
+			decoder.DisallowUnknownFields()
+		}
+		return decoder.Decode(c)
+	}
+	// It wasn't JSON. Unmarshal as YAML.
+	if strict {
+		return yaml.UnmarshalStrict(b, c)
+	}
+	return yaml.Unmarshal(b, c)
 }
 
 func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
@@ -139,7 +155,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
 	return configurer, nil
 }
 
-func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
+func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) {
 	var (
 		svrCfg         *v1.ServerConfig
 		isLegacyFormat bool
@@ -158,7 +174,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
 		isLegacyFormat = true
 	} else {
 		svrCfg = &v1.ServerConfig{}
-		if err := LoadConfigureFromFile(path, svrCfg); err != nil {
+		if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil {
 			return nil, false, err
 		}
 	}
@@ -168,7 +184,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
 	return svrCfg, isLegacyFormat, nil
 }
 
-func LoadClientConfig(path string) (
+func LoadClientConfig(path string, strict bool) (
 	*v1.ClientCommonConfig,
 	[]v1.ProxyConfigurer,
 	[]v1.VisitorConfigurer,
@@ -176,19 +192,19 @@ func LoadClientConfig(path string) (
 ) {
 	var (
 		cliCfg         *v1.ClientCommonConfig
-		pxyCfgs        = make([]v1.ProxyConfigurer, 0)
+		proxyCfgs      = make([]v1.ProxyConfigurer, 0)
 		visitorCfgs    = make([]v1.VisitorConfigurer, 0)
 		isLegacyFormat bool
 	)
 
 	if DetectLegacyINIFormatFromFile(path) {
-		legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
+		legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
 		if err != nil {
 			return nil, nil, nil, true, err
 		}
 		cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
-		for _, c := range legacyPxyCfgs {
-			pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
+		for _, c := range legacyProxyCfgs {
+			proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
 		}
 		for _, c := range legacyVisitorCfgs {
 			visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
@@ -196,12 +212,12 @@ func LoadClientConfig(path string) (
 		isLegacyFormat = true
 	} else {
 		allCfg := v1.ClientConfig{}
-		if err := LoadConfigureFromFile(path, &allCfg); err != nil {
+		if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
 			return nil, nil, nil, false, err
 		}
 		cliCfg = &allCfg.ClientCommonConfig
 		for _, c := range allCfg.Proxies {
-			pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+			proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
 		}
 		for _, c := range allCfg.Visitors {
 			visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@@ -209,20 +225,20 @@ func LoadClientConfig(path string) (
 	}
 
 	// Load additional config from includes.
-	// legacy ini format alredy handle this in ParseClientConfig.
+	// legacy ini format already handle this in ParseClientConfig.
 	if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
-		extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat)
+		extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
 		if err != nil {
 			return nil, nil, nil, isLegacyFormat, err
 		}
-		pxyCfgs = append(pxyCfgs, extPxyCfgs...)
+		proxyCfgs = append(proxyCfgs, extProxyCfgs...)
 		visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
 	}
 
 	// Filter by start
 	if len(cliCfg.Start) > 0 {
 		startSet := sets.New(cliCfg.Start...)
-		pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
+		proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
 			return startSet.Has(c.GetBaseConfig().Name)
 		})
 		visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
@@ -233,17 +249,17 @@ func LoadClientConfig(path string) (
 	if cliCfg != nil {
 		cliCfg.Complete()
 	}
-	for _, c := range pxyCfgs {
+	for _, c := range proxyCfgs {
 		c.Complete(cliCfg.User)
 	}
 	for _, c := range visitorCfgs {
 		c.Complete(cliCfg)
 	}
-	return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil
+	return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
 }
 
-func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
-	pxyCfgs := make([]v1.ProxyConfigurer, 0)
+func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
+	proxyCfgs := make([]v1.ProxyConfigurer, 0)
 	visitorCfgs := make([]v1.VisitorConfigurer, 0)
 	for _, path := range paths {
 		absDir, err := filepath.Abs(filepath.Dir(path))
@@ -265,11 +281,11 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
 			if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
 				// support yaml/json/toml
 				cfg := v1.ClientConfig{}
-				if err := LoadConfigureFromFile(absFile, &cfg); err != nil {
+				if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil {
 					return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
 				}
 				for _, c := range cfg.Proxies {
-					pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+					proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
 				}
 				for _, c := range cfg.Visitors {
 					visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@@ -277,5 +293,5 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
 			}
 		}
 	}
-	return pxyCfgs, visitorCfgs, nil
+	return proxyCfgs, visitorCfgs, nil
 }

+ 132 - 11
pkg/config/load_test.go

@@ -15,6 +15,8 @@
 package config
 
 import (
+	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -22,9 +24,7 @@ import (
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
-func TestLoadConfigure(t *testing.T) {
-	require := require.New(t)
-	content := `
+const tomlServerContent = `
 bindAddr = "127.0.0.1"
 kcpBindPort = 7000
 quicBindPort = 7001
@@ -33,13 +33,134 @@ custom404Page = "/abc.html"
 transport.tcpKeepalive = 10
 `
 
-	svrCfg := v1.ServerConfig{}
-	err := LoadConfigure([]byte(content), &svrCfg)
+const yamlServerContent = `
+bindAddr: 127.0.0.1
+kcpBindPort: 7000
+quicBindPort: 7001
+tcpmuxHTTPConnectPort: 7005
+custom404Page: /abc.html
+transport:
+  tcpKeepalive: 10
+`
+
+const jsonServerContent = `
+{
+  "bindAddr": "127.0.0.1",
+  "kcpBindPort": 7000,
+  "quicBindPort": 7001,
+  "tcpmuxHTTPConnectPort": 7005,
+  "custom404Page": "/abc.html",
+  "transport": {
+    "tcpKeepalive": 10
+  }
+}
+`
+
+func TestLoadServerConfig(t *testing.T) {
+	tests := []struct {
+		name    string
+		content string
+	}{
+		{"toml", tomlServerContent},
+		{"yaml", yamlServerContent},
+		{"json", jsonServerContent},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			require := require.New(t)
+			svrCfg := v1.ServerConfig{}
+			err := LoadConfigure([]byte(test.content), &svrCfg, true)
+			require.NoError(err)
+			require.EqualValues("127.0.0.1", svrCfg.BindAddr)
+			require.EqualValues(7000, svrCfg.KCPBindPort)
+			require.EqualValues(7001, svrCfg.QUICBindPort)
+			require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
+			require.EqualValues("/abc.html", svrCfg.Custom404Page)
+			require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+		})
+	}
+}
+
+// Test that loading in strict mode fails when the config is invalid.
+func TestLoadServerConfigStrictMode(t *testing.T) {
+	tests := []struct {
+		name    string
+		content string
+	}{
+		{"toml", tomlServerContent},
+		{"yaml", yamlServerContent},
+		{"json", jsonServerContent},
+	}
+
+	for _, strict := range []bool{false, true} {
+		for _, test := range tests {
+			t.Run(fmt.Sprintf("%s-strict-%t", test.name, strict), func(t *testing.T) {
+				require := require.New(t)
+				// Break the content with an innocent typo
+				brokenContent := strings.Replace(test.content, "bindAddr", "bindAdur", 1)
+				svrCfg := v1.ServerConfig{}
+				err := LoadConfigure([]byte(brokenContent), &svrCfg, strict)
+				if strict {
+					require.ErrorContains(err, "bindAdur")
+				} else {
+					require.NoError(err)
+					// BindAddr didn't get parsed because of the typo.
+					require.EqualValues("", svrCfg.BindAddr)
+				}
+			})
+		}
+	}
+}
+
+func TestCustomStructStrictMode(t *testing.T) {
+	require := require.New(t)
+
+	proxyStr := `
+serverPort = 7000
+
+[[proxies]]
+name = "test"
+type = "tcp"
+remotePort = 6000
+`
+	clientCfg := v1.ClientConfig{}
+	err := LoadConfigure([]byte(proxyStr), &clientCfg, true)
+	require.NoError(err)
+
+	proxyStr += `unknown = "unknown"`
+	err = LoadConfigure([]byte(proxyStr), &clientCfg, true)
+	require.Error(err)
+
+	visitorStr := `
+serverPort = 7000
+
+[[visitors]]
+name = "test"
+type = "stcp"
+bindPort = 6000
+serverName = "server"
+`
+	err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
+	require.NoError(err)
+
+	visitorStr += `unknown = "unknown"`
+	err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
+	require.Error(err)
+
+	pluginStr := `
+serverPort = 7000
+
+[[proxies]]
+name = "test"
+type = "tcp"
+remotePort = 6000
+[proxies.plugin]
+type = "unix_domain_socket"
+unixPath = "/tmp/uds.sock"
+`
+	err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
 	require.NoError(err)
-	require.EqualValues("127.0.0.1", svrCfg.BindAddr)
-	require.EqualValues(7000, svrCfg.KCPBindPort)
-	require.EqualValues(7001, svrCfg.QUICBindPort)
-	require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
-	require.EqualValues("/abc.html", svrCfg.Custom404Page)
-	require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+	pluginStr += `unknown = "unknown"`
+	err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
+	require.Error(err)
 }

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

@@ -111,7 +111,7 @@ type ClientTransportConfig struct {
 	// the server must have TCP multiplexing enabled as well. By default, this
 	// value is true.
 	TCPMux *bool `json:"tcpMux,omitempty"`
-	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
 	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
 	TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
 	// QUIC protocol options.

+ 15 - 1
pkg/config/v1/common.go

@@ -15,9 +15,23 @@
 package v1
 
 import (
+	"sync"
+
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
+// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method
+// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder.
+// Here, a global variable is temporarily used to control whether unknown fields are allowed.
+// Once the v2 version is implemented by the community, we can switch to a standardized approach.
+//
+// https://github.com/golang/go/issues/41144
+// https://github.com/golang/go/discussions/63397
+var (
+	DisallowUnknownFields   = false
+	DisallowUnknownFieldsMu sync.Mutex
+)
+
 type AuthScope string
 
 const (
@@ -83,7 +97,7 @@ type TLSConfig struct {
 }
 
 type LogConfig struct {
-	// This is destination where frp should wirte the logs.
+	// This is destination where frp should write the logs.
 	// If "console" is used, logs will be printed to stdout, otherwise,
 	// logs will be written to the specified file.
 	// By default, this value is "console".

+ 15 - 1
pkg/config/v1/plugin.go

@@ -15,6 +15,7 @@
 package v1
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"reflect"
@@ -49,7 +50,13 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
 		return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
 	}
 	options := reflect.New(v).Interface().(ClientPluginOptions)
-	if err := json.Unmarshal(b, options); err != nil {
+
+	decoder := json.NewDecoder(bytes.NewBuffer(b))
+	if DisallowUnknownFields {
+		decoder.DisallowUnknownFields()
+	}
+
+	if err := decoder.Decode(options); err != nil {
 		return err
 	}
 	c.ClientPluginOptions = options
@@ -77,17 +84,20 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{
 }
 
 type HTTP2HTTPSPluginOptions struct {
+	Type              string           `json:"type,omitempty"`
 	LocalAddr         string           `json:"localAddr,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
 }
 
 type HTTPProxyPluginOptions struct {
+	Type         string `json:"type,omitempty"`
 	HTTPUser     string `json:"httpUser,omitempty"`
 	HTTPPassword string `json:"httpPassword,omitempty"`
 }
 
 type HTTPS2HTTPPluginOptions struct {
+	Type              string           `json:"type,omitempty"`
 	LocalAddr         string           `json:"localAddr,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
@@ -96,6 +106,7 @@ type HTTPS2HTTPPluginOptions struct {
 }
 
 type HTTPS2HTTPSPluginOptions struct {
+	Type              string           `json:"type,omitempty"`
 	LocalAddr         string           `json:"localAddr,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
@@ -104,11 +115,13 @@ type HTTPS2HTTPSPluginOptions struct {
 }
 
 type Socks5PluginOptions struct {
+	Type     string `json:"type,omitempty"`
 	Username string `json:"username,omitempty"`
 	Password string `json:"password,omitempty"`
 }
 
 type StaticFilePluginOptions struct {
+	Type         string `json:"type,omitempty"`
 	LocalPath    string `json:"localPath,omitempty"`
 	StripPrefix  string `json:"stripPrefix,omitempty"`
 	HTTPUser     string `json:"httpUser,omitempty"`
@@ -116,5 +129,6 @@ type StaticFilePluginOptions struct {
 }
 
 type UnixDomainSocketPluginOptions struct {
+	Type     string `json:"type,omitempty"`
 	UnixPath string `json:"unixPath,omitempty"`
 }

+ 9 - 2
pkg/config/v1/proxy.go

@@ -15,6 +15,7 @@
 package v1
 
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -177,7 +178,11 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
 	if configurer == nil {
 		return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
 	}
-	if err := json.Unmarshal(b, configurer); err != nil {
+	decoder := json.NewDecoder(bytes.NewBuffer(b))
+	if DisallowUnknownFields {
+		decoder.DisallowUnknownFields()
+	}
+	if err := decoder.Decode(configurer); err != nil {
 		return err
 	}
 	c.ProxyConfigurer = configurer
@@ -224,7 +229,9 @@ func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
 	if !ok {
 		return nil
 	}
-	return reflect.New(v).Interface().(ProxyConfigurer)
+	pc := reflect.New(v).Interface().(ProxyConfigurer)
+	pc.GetBaseConfig().Type = string(proxyType)
+	return pc
 }
 
 var _ ProxyConfigurer = &TCPProxyConfig{}

+ 15 - 1
pkg/config/v1/server.go

@@ -67,6 +67,8 @@ type ServerConfig struct {
 	// value is "", a default page will be displayed.
 	Custom404Page string `json:"custom404Page,omitempty"`
 
+	SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
+
 	WebServer WebServerConfig `json:"webServer,omitempty"`
 	// EnablePrometheus will export prometheus metrics on webserver address
 	// in /metrics api.
@@ -101,6 +103,7 @@ func (c *ServerConfig) Complete() {
 	c.Log.Complete()
 	c.Transport.Complete()
 	c.WebServer.Complete()
+	c.SSHTunnelGateway.Complete()
 
 	c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
 	c.BindPort = util.EmptyOr(c.BindPort, 7000)
@@ -152,7 +155,7 @@ type ServerTransportConfig struct {
 	// is true.
 	// $HideFromDoc
 	TCPMux *bool `json:"tcpMux,omitempty"`
-	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
 	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
 	TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
 	// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
@@ -189,3 +192,14 @@ type TLSServerConfig struct {
 
 	TLSConfig
 }
+
+type SSHTunnelGateway struct {
+	BindPort              int    `json:"bindPort,omitempty"`
+	PrivateKeyFile        string `json:"privateKeyFile,omitempty"`
+	AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"`
+	AuthorizedKeysFile    string `json:"authorizedKeysFile,omitempty"`
+}
+
+func (c *SSHTunnelGateway) Complete() {
+	c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
+}

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

@@ -80,7 +80,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
 	return warnings, errs
 }
 
-func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
+func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
 	var warnings Warning
 	if c != nil {
 		warning, err := ValidateClientCommonConfig(c)
@@ -90,7 +90,7 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigu
 		}
 	}
 
-	for _, c := range pxyCfgs {
+	for _, c := range proxyCfgs {
 		if err := ValidateProxyConfigurerForClient(c); err != nil {
 			return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err)
 		}

+ 9 - 2
pkg/config/v1/visitor.go

@@ -15,6 +15,7 @@
 package v1
 
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -108,7 +109,11 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
 	if configurer == nil {
 		return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
 	}
-	if err := json.Unmarshal(b, configurer); err != nil {
+	decoder := json.NewDecoder(bytes.NewBuffer(b))
+	if DisallowUnknownFields {
+		decoder.DisallowUnknownFields()
+	}
+	if err := decoder.Decode(configurer); err != nil {
 		return err
 	}
 	c.VisitorConfigurer = configurer
@@ -120,7 +125,9 @@ func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
 	if !ok {
 		return nil
 	}
-	return reflect.New(v).Interface().(VisitorConfigurer)
+	vc := reflect.New(v).Interface().(VisitorConfigurer)
+	vc.GetBaseConfig().Type = string(t)
+	return vc
 }
 
 var _ VisitorConfigurer = &STCPVisitorConfig{}

+ 14 - 0
pkg/metrics/metrics.go

@@ -1,3 +1,17 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package metrics
 
 import (

+ 103 - 0
pkg/msg/handler.go

@@ -0,0 +1,103 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package msg
+
+import (
+	"io"
+	"reflect"
+)
+
+func AsyncHandler(f func(Message)) func(Message) {
+	return func(m Message) {
+		go f(m)
+	}
+}
+
+// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn.
+type Dispatcher struct {
+	rw io.ReadWriter
+
+	sendCh         chan Message
+	doneCh         chan struct{}
+	msgHandlers    map[reflect.Type]func(Message)
+	defaultHandler func(Message)
+}
+
+func NewDispatcher(rw io.ReadWriter) *Dispatcher {
+	return &Dispatcher{
+		rw:          rw,
+		sendCh:      make(chan Message, 100),
+		doneCh:      make(chan struct{}),
+		msgHandlers: make(map[reflect.Type]func(Message)),
+	}
+}
+
+// Run will block until io.EOF or some error occurs.
+func (d *Dispatcher) Run() {
+	go d.sendLoop()
+	go d.readLoop()
+}
+
+func (d *Dispatcher) sendLoop() {
+	for {
+		select {
+		case <-d.doneCh:
+			return
+		case m := <-d.sendCh:
+			_ = WriteMsg(d.rw, m)
+		}
+	}
+}
+
+func (d *Dispatcher) readLoop() {
+	for {
+		m, err := ReadMsg(d.rw)
+		if err != nil {
+			close(d.doneCh)
+			return
+		}
+
+		if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok {
+			handler(m)
+		} else if d.defaultHandler != nil {
+			d.defaultHandler(m)
+		}
+	}
+}
+
+func (d *Dispatcher) Send(m Message) error {
+	select {
+	case <-d.doneCh:
+		return io.EOF
+	case d.sendCh <- m:
+		return nil
+	}
+}
+
+func (d *Dispatcher) SendChannel() chan Message {
+	return d.sendCh
+}
+
+func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
+	d.msgHandlers[reflect.TypeOf(msg)] = handler
+}
+
+func (d *Dispatcher) RegisterDefaultHandler(handler func(Message)) {
+	d.defaultHandler = handler
+}
+
+func (d *Dispatcher) Done() chan struct{} {
+	return d.doneCh
+}

+ 12 - 0
pkg/msg/msg.go

@@ -63,6 +63,15 @@ var msgTypeMap = map[byte]interface{}{
 
 var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
 
+type ClientSpec struct {
+	// Due to the support of VirtualClient, frps needs to know the client type in order to
+	// differentiate the processing logic.
+	// Optional values: ssh-tunnel
+	Type string `json:"type,omitempty"`
+	// If the value is true, the client will not require authentication.
+	AlwaysAuthPass bool `json:"always_auth_pass,omitempty"`
+}
+
 // When frpc start, client send this message to login to server.
 type Login struct {
 	Version      string            `json:"version,omitempty"`
@@ -75,6 +84,9 @@ type Login struct {
 	RunID        string            `json:"run_id,omitempty"`
 	Metas        map[string]string `json:"metas,omitempty"`
 
+	// Currently only effective for VirtualClient.
+	ClientSpec ClientSpec `json:"client_spec,omitempty"`
+
 	// Some global configures.
 	PoolCount int `json:"pool_count,omitempty"`
 }

+ 4 - 2
pkg/plugin/client/http2https.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -22,7 +24,7 @@ import (
 	"net/http/httputil"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -77,7 +79,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 }
 
 func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

+ 4 - 2
pkg/plugin/client/http_proxy.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -27,7 +29,7 @@ import (
 	libnet "github.com/fatedier/golib/net"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
@@ -66,7 +68,7 @@ func (hp *HTTPProxy) Name() string {
 }
 
 func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 
 	sc, rd := libnet.NewSharedConn(wrapConn)
 	firstBytes := make([]byte, 7)

+ 4 - 2
pkg/plugin/client/https2http.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -24,7 +26,7 @@ import (
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -96,7 +98,7 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
 }
 
 func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

+ 4 - 2
pkg/plugin/client/https2https.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -24,7 +26,7 @@ import (
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -102,7 +104,7 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
 }
 
 func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

+ 4 - 2
pkg/plugin/client/socks5.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -22,7 +24,7 @@ import (
 	gosocks5 "github.com/armon/go-socks5"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -50,7 +52,7 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
 
 func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	defer conn.Close()
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.Server.ServeConn(wrapConn)
 }
 

+ 6 - 4
pkg/plugin/client/static_file.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (
@@ -23,7 +25,7 @@ import (
 	"github.com/gorilla/mux"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
 func init() {
@@ -55,8 +57,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
 	}
 
 	router := mux.NewRouter()
-	router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-	router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
+	router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+	router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
 	sp.s = &http.Server{
 		Handler: router,
 	}
@@ -67,7 +69,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
 }
 
 func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
-	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.l.PutConn(wrapConn)
 }
 

+ 2 - 0
pkg/plugin/client/unix_domain_socket.go

@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//go:build !frps
+
 package plugin
 
 import (

+ 13 - 4
pkg/sdk/client/client.go

@@ -6,11 +6,12 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 
 	"github.com/fatedier/frp/client"
-	"github.com/fatedier/frp/pkg/util/util"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 )
 
 type Client struct {
@@ -69,8 +70,16 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) {
 	return allStatus, nil
 }
 
-func (c *Client) Reload() error {
-	req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil)
+func (c *Client) Reload(strictMode bool) error {
+	v := url.Values{}
+	if strictMode {
+		v.Set("strictConfig", "true")
+	}
+	queryStr := ""
+	if len(v) > 0 {
+		queryStr = "?" + v.Encode()
+	}
+	req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload"+queryStr, nil)
 	if err != nil {
 		return err
 	}
@@ -106,7 +115,7 @@ func (c *Client) UpdateConfig(content string) error {
 
 func (c *Client) setAuthHeader(req *http.Request) {
 	if c.authUser != "" || c.authPwd != "" {
-		req.Header.Set("Authorization", util.BasicAuth(c.authUser, c.authPwd))
+		req.Header.Set("Authorization", httppkg.BasicAuth(c.authUser, c.authPwd))
 	}
 }
 

+ 143 - 0
pkg/ssh/gateway.go

@@ -0,0 +1,143 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"strconv"
+	"strings"
+
+	"golang.org/x/crypto/ssh"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/pkg/util/log"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+)
+
+type Gateway struct {
+	bindPort int
+	ln       net.Listener
+
+	peerServerListener *netpkg.InternalListener
+
+	sshConfig *ssh.ServerConfig
+}
+
+func NewGateway(
+	cfg v1.SSHTunnelGateway, bindAddr string,
+	peerServerListener *netpkg.InternalListener,
+) (*Gateway, error) {
+	sshConfig := &ssh.ServerConfig{}
+
+	// privateKey
+	var (
+		privateKeyBytes []byte
+		err             error
+	)
+	if cfg.PrivateKeyFile != "" {
+		privateKeyBytes, err = os.ReadFile(cfg.PrivateKeyFile)
+	} else {
+		if cfg.AutoGenPrivateKeyPath != "" {
+			privateKeyBytes, _ = os.ReadFile(cfg.AutoGenPrivateKeyPath)
+		}
+		if len(privateKeyBytes) == 0 {
+			privateKeyBytes, err = transport.NewRandomPrivateKey()
+			if err == nil && cfg.AutoGenPrivateKeyPath != "" {
+				err = os.WriteFile(cfg.AutoGenPrivateKeyPath, privateKeyBytes, 0o600)
+			}
+		}
+	}
+	if err != nil {
+		return nil, err
+	}
+	privateKey, err := ssh.ParsePrivateKey(privateKeyBytes)
+	if err != nil {
+		return nil, err
+	}
+	sshConfig.AddHostKey(privateKey)
+
+	sshConfig.NoClientAuth = cfg.AuthorizedKeysFile == ""
+	sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+		authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile)
+		if err != nil {
+			log.Error("load authorized keys file error: %v", err)
+			return nil, fmt.Errorf("internal error")
+		}
+
+		user, ok := authorizedKeysMap[string(key.Marshal())]
+		if !ok {
+			return nil, fmt.Errorf("unknown public key for remoteAddr %q", conn.RemoteAddr())
+		}
+		return &ssh.Permissions{
+			Extensions: map[string]string{
+				"user": user,
+			},
+		}, nil
+	}
+
+	ln, err := net.Listen("tcp", net.JoinHostPort(bindAddr, strconv.Itoa(cfg.BindPort)))
+	if err != nil {
+		return nil, err
+	}
+	return &Gateway{
+		bindPort:           cfg.BindPort,
+		ln:                 ln,
+		peerServerListener: peerServerListener,
+		sshConfig:          sshConfig,
+	}, nil
+}
+
+func (g *Gateway) Run() {
+	for {
+		conn, err := g.ln.Accept()
+		if err != nil {
+			return
+		}
+		go g.handleConn(conn)
+	}
+}
+
+func (g *Gateway) handleConn(conn net.Conn) {
+	defer conn.Close()
+
+	ts, err := NewTunnelServer(conn, g.sshConfig, g.peerServerListener)
+	if err != nil {
+		return
+	}
+	if err := ts.Run(); err != nil {
+		log.Error("ssh tunnel server run error: %v", err)
+	}
+}
+
+func loadAuthorizedKeysFromFile(path string) (map[string]string, error) {
+	authorizedKeysMap := make(map[string]string) // value is username
+	authorizedKeysBytes, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	for len(authorizedKeysBytes) > 0 {
+		pubKey, comment, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
+		if err != nil {
+			return nil, err
+		}
+
+		authorizedKeysMap[string(pubKey.Marshal())] = strings.TrimSpace(comment)
+		authorizedKeysBytes = rest
+	}
+	return authorizedKeysMap, nil
+}

+ 383 - 0
pkg/ssh/server.go

@@ -0,0 +1,383 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+	"context"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"net"
+	"strings"
+	"sync"
+	"time"
+
+	libio "github.com/fatedier/golib/io"
+	"github.com/samber/lo"
+	"github.com/spf13/cobra"
+	flag "github.com/spf13/pflag"
+	"golang.org/x/crypto/ssh"
+
+	"github.com/fatedier/frp/client/proxy"
+	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/msg"
+	"github.com/fatedier/frp/pkg/util/log"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+	"github.com/fatedier/frp/pkg/util/util"
+	"github.com/fatedier/frp/pkg/util/xlog"
+	"github.com/fatedier/frp/pkg/virtual"
+)
+
+const (
+	// https://datatracker.ietf.org/doc/html/rfc4254#page-16
+	ChannelTypeServerOpenChannel = "forwarded-tcpip"
+	RequestTypeForward           = "tcpip-forward"
+)
+
+type tcpipForward struct {
+	Host string
+	Port uint32
+}
+
+// https://datatracker.ietf.org/doc/html/rfc4254#page-16
+type forwardedTCPPayload struct {
+	Addr string
+	Port uint32
+
+	OriginAddr string
+	OriginPort uint32
+}
+
+type TunnelServer struct {
+	underlyingConn net.Conn
+	sshConn        *ssh.ServerConn
+	sc             *ssh.ServerConfig
+	firstChannel   ssh.Channel
+
+	vc                 *virtual.Client
+	peerServerListener *netpkg.InternalListener
+	doneCh             chan struct{}
+	closeDoneChOnce    sync.Once
+}
+
+func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, peerServerListener *netpkg.InternalListener) (*TunnelServer, error) {
+	s := &TunnelServer{
+		underlyingConn:     conn,
+		sc:                 sc,
+		peerServerListener: peerServerListener,
+		doneCh:             make(chan struct{}),
+	}
+	return s, nil
+}
+
+func (s *TunnelServer) Run() error {
+	sshConn, channels, requests, err := ssh.NewServerConn(s.underlyingConn, s.sc)
+	if err != nil {
+		return err
+	}
+
+	s.sshConn = sshConn
+
+	addr, extraPayload, err := s.waitForwardAddrAndExtraPayload(channels, requests, 3*time.Second)
+	if err != nil {
+		return err
+	}
+
+	clientCfg, pc, helpMessage, err := s.parseClientAndProxyConfigurer(addr, extraPayload)
+	if err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			s.writeToClient(helpMessage)
+			return nil
+		}
+		s.writeToClient(err.Error())
+		return fmt.Errorf("parse flags from ssh client error: %v", err)
+	}
+	clientCfg.Complete()
+	if sshConn.Permissions != nil {
+		clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
+	}
+	pc.Complete(clientCfg.User)
+
+	vc, err := virtual.NewClient(virtual.ClientOptions{
+		Common: clientCfg,
+		Spec: &msg.ClientSpec{
+			Type: "ssh-tunnel",
+			// If ssh does not require authentication, then the virtual client needs to authenticate through a token.
+			// Otherwise, once ssh authentication is passed, the virtual client does not need to authenticate again.
+			AlwaysAuthPass: !s.sc.NoClientAuth,
+		},
+		HandleWorkConnCb: func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool {
+			// join workConn and ssh channel
+			c, err := s.openConn(addr)
+			if err != nil {
+				log.Trace("open conn error: %v", err)
+				workConn.Close()
+				return false
+			}
+			libio.Join(c, workConn)
+			return false
+		},
+	})
+	if err != nil {
+		return err
+	}
+	s.vc = vc
+
+	// transfer connection from virtual client to server peer listener
+	go func() {
+		l := s.vc.PeerListener()
+		for {
+			conn, err := l.Accept()
+			if err != nil {
+				return
+			}
+			_ = s.peerServerListener.PutConn(conn)
+		}
+	}()
+	xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100})
+	ctx := xlog.NewContext(context.Background(), xl)
+	go func() {
+		vcErr := s.vc.Run(ctx)
+		if vcErr != nil {
+			s.writeToClient(vcErr.Error())
+		}
+
+		// If vc.Run returns, it means that the virtual client has been closed, and the ssh tunnel connection should be closed.
+		// One scenario is that the virtual client exits due to login failure.
+		s.closeDoneChOnce.Do(func() {
+			_ = sshConn.Close()
+			close(s.doneCh)
+		})
+	}()
+
+	s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc})
+
+	if ps, err := s.waitProxyStatusReady(pc.GetBaseConfig().Name, time.Second); err != nil {
+		s.writeToClient(err.Error())
+		log.Warn("wait proxy status ready error: %v", err)
+	} else {
+		// success
+		s.writeToClient(createSuccessInfo(clientCfg.User, pc, ps))
+		_ = sshConn.Wait()
+	}
+
+	s.vc.Close()
+	log.Trace("ssh tunnel connection from %v closed", sshConn.RemoteAddr())
+	s.closeDoneChOnce.Do(func() {
+		_ = sshConn.Close()
+		close(s.doneCh)
+	})
+	return nil
+}
+
+func (s *TunnelServer) writeToClient(data string) {
+	if s.firstChannel == nil {
+		return
+	}
+	_, _ = s.firstChannel.Write([]byte(data + "\n"))
+}
+
+func (s *TunnelServer) waitForwardAddrAndExtraPayload(
+	channels <-chan ssh.NewChannel,
+	requests <-chan *ssh.Request,
+	timeout time.Duration,
+) (*tcpipForward, string, error) {
+	addrCh := make(chan *tcpipForward, 1)
+	extraPayloadCh := make(chan string, 1)
+
+	// get forward address
+	go func() {
+		addrGot := false
+		for req := range requests {
+			if req.Type == RequestTypeForward && !addrGot {
+				payload := tcpipForward{}
+				if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
+					return
+				}
+				addrGot = true
+				addrCh <- &payload
+			}
+			if req.WantReply {
+				_ = req.Reply(true, nil)
+			}
+		}
+	}()
+
+	// get extra payload
+	go func() {
+		for newChannel := range channels {
+			// extraPayload will send to extraPayloadCh
+			go s.handleNewChannel(newChannel, extraPayloadCh)
+		}
+	}()
+
+	var (
+		addr         *tcpipForward
+		extraPayload string
+	)
+
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+	for {
+		select {
+		case v := <-addrCh:
+			addr = v
+		case extra := <-extraPayloadCh:
+			extraPayload = extra
+		case <-timer.C:
+			return nil, "", fmt.Errorf("get addr and extra payload timeout")
+		}
+		if addr != nil && extraPayload != "" {
+			break
+		}
+	}
+	return addr, extraPayload, nil
+}
+
+func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPayload string) (*v1.ClientCommonConfig, v1.ProxyConfigurer, string, error) {
+	helpMessage := ""
+	cmd := &cobra.Command{
+		Use:   "ssh v0@{address} [command]",
+		Short: "ssh v0@{address} [command]",
+		Run:   func(*cobra.Command, []string) {},
+	}
+	args := strings.Split(extraPayload, " ")
+	if len(args) < 1 {
+		return nil, nil, helpMessage, fmt.Errorf("invalid extra payload")
+	}
+	proxyType := strings.TrimSpace(args[0])
+	supportTypes := []string{"tcp", "http", "https", "tcpmux", "stcp"}
+	if !lo.Contains(supportTypes, proxyType) {
+		return nil, nil, helpMessage, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes)
+	}
+	pc := v1.NewProxyConfigurerByType(v1.ProxyType(proxyType))
+	if pc == nil {
+		return nil, nil, helpMessage, fmt.Errorf("new proxy configurer error")
+	}
+	config.RegisterProxyFlags(cmd, pc, config.WithSSHMode())
+
+	clientCfg := v1.ClientCommonConfig{}
+	config.RegisterClientCommonConfigFlags(cmd, &clientCfg, config.WithSSHMode())
+
+	cmd.InitDefaultHelpCmd()
+	if err := cmd.ParseFlags(args); err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			helpMessage = cmd.UsageString()
+		}
+		return nil, nil, helpMessage, err
+	}
+	// if name is not set, generate a random one
+	if pc.GetBaseConfig().Name == "" {
+		id, err := util.RandIDWithLen(8)
+		if err != nil {
+			return nil, nil, helpMessage, fmt.Errorf("generate random id error: %v", err)
+		}
+		pc.GetBaseConfig().Name = fmt.Sprintf("sshtunnel-%s-%s", proxyType, id)
+	}
+	return &clientCfg, pc, helpMessage, nil
+}
+
+func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) {
+	ch, reqs, err := channel.Accept()
+	if err != nil {
+		return
+	}
+	if s.firstChannel == nil {
+		s.firstChannel = ch
+	}
+	go s.keepAlive(ch)
+
+	for req := range reqs {
+		if req.WantReply {
+			_ = req.Reply(true, nil)
+		}
+		if req.Type != "exec" || len(req.Payload) <= 4 {
+			continue
+		}
+		end := 4 + binary.BigEndian.Uint32(req.Payload[:4])
+		if len(req.Payload) < int(end) {
+			continue
+		}
+		extraPayload := string(req.Payload[4:end])
+		select {
+		case extraPayloadCh <- extraPayload:
+		default:
+		}
+	}
+}
+
+func (s *TunnelServer) keepAlive(ch ssh.Channel) {
+	tk := time.NewTicker(time.Second * 30)
+	defer tk.Stop()
+
+	for {
+		select {
+		case <-tk.C:
+			_, err := ch.SendRequest("heartbeat", false, nil)
+			if err != nil {
+				return
+			}
+		case <-s.doneCh:
+			return
+		}
+	}
+}
+
+func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) {
+	payload := forwardedTCPPayload{
+		Addr: addr.Host,
+		Port: addr.Port,
+		// Note: Here is just for compatibility, not the real source address.
+		OriginAddr: addr.Host,
+		OriginPort: addr.Port,
+	}
+	channel, reqs, err := s.sshConn.OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(&payload))
+	if err != nil {
+		return nil, fmt.Errorf("open ssh channel error: %v", err)
+	}
+	go ssh.DiscardRequests(reqs)
+
+	conn := netpkg.WrapReadWriteCloserToConn(channel, s.underlyingConn)
+	return conn, nil
+}
+
+func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) (*proxy.WorkingStatus, error) {
+	ticker := time.NewTicker(100 * time.Millisecond)
+	defer ticker.Stop()
+
+	timer := time.NewTimer(timeout)
+	defer timer.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			ps, err := s.vc.Service().GetProxyStatus(name)
+			if err != nil {
+				continue
+			}
+			switch ps.Phase {
+			case proxy.ProxyPhaseRunning:
+				return ps, nil
+			case proxy.ProxyPhaseStartErr, proxy.ProxyPhaseClosed:
+				return ps, errors.New(ps.Err)
+			}
+		case <-timer.C:
+			return nil, fmt.Errorf("wait proxy status ready timeout")
+		case <-s.doneCh:
+			return nil, fmt.Errorf("ssh tunnel server closed")
+		}
+	}
+}

+ 31 - 0
pkg/ssh/terminal.go

@@ -0,0 +1,31 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+	"github.com/fatedier/frp/client/proxy"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func createSuccessInfo(user string, pc v1.ProxyConfigurer, ps *proxy.WorkingStatus) string {
+	base := pc.GetBaseConfig()
+	out := "\n"
+	out += "frp (via SSH) (Ctrl+C to quit)\n\n"
+	out += "User: " + user + "\n"
+	out += "ProxyName: " + base.Name + "\n"
+	out += "Type: " + base.Type + "\n"
+	out += "RemoteAddress: " + ps.RemoteAddr + "\n"
+	return out
+}

+ 3 - 1
pkg/transport/message.go

@@ -29,7 +29,9 @@ type MessageTransporter interface {
 	// Recv(ctx context.Context, laneKey string, msgType string) (Message, error)
 	// Do will first send msg, then recv msg with the same laneKey and specified msgType.
 	Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error)
+	// Dispatch will dispatch message to related channel registered in Do function by its message type and laneKey.
 	Dispatch(m msg.Message, laneKey string) bool
+	// Same with Dispatch but with specified message type.
 	DispatchWithType(m msg.Message, msgType, laneKey string) bool
 }
 
@@ -44,7 +46,7 @@ type transporterImpl struct {
 	sendCh chan msg.Message
 
 	// First key is message type and second key is lane key.
-	// Dispatch will dispatch message to releated channel by its message type
+	// Dispatch will dispatch message to related channel by its message type
 	// and lane key.
 	registry map[string]map[string]chan msg.Message
 	mu       sync.RWMutex

+ 12 - 0
pkg/transport/tls.go

@@ -128,3 +128,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
 
 	return base, nil
 }
+
+func NewRandomPrivateKey() ([]byte, error) {
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, err
+	}
+	keyPEM := pem.EncodeToMemory(&pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
+	})
+	return keyPEM, nil
+}

+ 1 - 1
pkg/util/util/http.go → pkg/util/http/http.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package util
+package http
 
 import (
 	"encoding/base64"

+ 126 - 0
pkg/util/http/server.go

@@ -0,0 +1,126 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package http
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"net/http/pprof"
+	"strconv"
+	"time"
+
+	"github.com/gorilla/mux"
+
+	"github.com/fatedier/frp/assets"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+)
+
+var (
+	defaultReadTimeout  = 60 * time.Second
+	defaultWriteTimeout = 60 * time.Second
+)
+
+type Server struct {
+	addr   string
+	ln     net.Listener
+	tlsCfg *tls.Config
+
+	router *mux.Router
+	hs     *http.Server
+
+	authMiddleware mux.MiddlewareFunc
+}
+
+func NewServer(cfg v1.WebServerConfig) (*Server, error) {
+	assets.Load(cfg.AssetsDir)
+
+	addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port))
+	if addr == ":" {
+		addr = ":http"
+	}
+
+	ln, err := net.Listen("tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+
+	router := mux.NewRouter()
+	hs := &http.Server{
+		Addr:         addr,
+		Handler:      router,
+		ReadTimeout:  defaultReadTimeout,
+		WriteTimeout: defaultWriteTimeout,
+	}
+	s := &Server{
+		addr:   addr,
+		ln:     ln,
+		hs:     hs,
+		router: router,
+	}
+	if cfg.PprofEnable {
+		s.registerPprofHandlers()
+	}
+	if cfg.TLS != nil {
+		cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
+		if err != nil {
+			return nil, err
+		}
+		s.tlsCfg = &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		}
+	}
+	s.authMiddleware = netpkg.NewHTTPAuthMiddleware(cfg.User, cfg.Password).SetAuthFailDelay(200 * time.Millisecond).Middleware
+	return s, nil
+}
+
+func (s *Server) Address() string {
+	return s.addr
+}
+
+func (s *Server) Run() error {
+	ln := s.ln
+	if s.tlsCfg != nil {
+		ln = tls.NewListener(ln, s.tlsCfg)
+	}
+	return s.hs.Serve(ln)
+}
+
+func (s *Server) Close() error {
+	return s.hs.Close()
+}
+
+type RouterRegisterHelper struct {
+	Router         *mux.Router
+	AssetsFS       http.FileSystem
+	AuthMiddleware mux.MiddlewareFunc
+}
+
+func (s *Server) RouteRegister(register func(helper *RouterRegisterHelper)) {
+	register(&RouterRegisterHelper{
+		Router:         s.router,
+		AssetsFS:       assets.FileSystem,
+		AuthMiddleware: s.authMiddleware,
+	})
+}
+
+func (s *Server) registerPprofHandlers() {
+	s.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
+	s.router.HandleFunc("/debug/pprof/profile", pprof.Profile)
+	s.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
+	s.router.HandleFunc("/debug/pprof/trace", pprof.Trace)
+	s.router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
+}

+ 16 - 0
pkg/util/net/conn.go

@@ -22,6 +22,7 @@ import (
 	"sync/atomic"
 	"time"
 
+	"github.com/fatedier/golib/crypto"
 	quic "github.com/quic-go/quic-go"
 
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -216,3 +217,18 @@ func (conn *wrapQuicStream) Close() error {
 	conn.Stream.CancelRead(0)
 	return conn.Stream.Close()
 }
+
+func NewCryptoReadWriter(rw io.ReadWriter, key []byte) (io.ReadWriter, error) {
+	encReader := crypto.NewReader(rw, key)
+	encWriter, err := crypto.NewWriter(rw, key)
+	if err != nil {
+		return nil, err
+	}
+	return struct {
+		io.Reader
+		io.Writer
+	}{
+		Reader: encReader,
+		Writer: encWriter,
+	}, nil
+}

+ 33 - 0
pkg/util/net/dns.go

@@ -0,0 +1,33 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+	"context"
+	"net"
+)
+
+func SetDefaultDNSAddress(dnsAddress string) {
+	if _, _, err := net.SplitHostPort(dnsAddress); err != nil {
+		dnsAddress = net.JoinHostPort(dnsAddress, "53")
+	}
+	// Change default dns server
+	net.DefaultResolver = &net.Resolver{
+		PreferGo: true,
+		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
+			return net.Dial("udp", dnsAddress)
+		},
+	}
+}

+ 7 - 7
pkg/util/net/http.go

@@ -24,21 +24,21 @@ import (
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
-type HTTPAuthWraper struct {
+type HTTPAuthWrapper struct {
 	h      http.Handler
 	user   string
 	passwd string
 }
 
-func NewHTTPBasicAuthWraper(h http.Handler, user, passwd string) http.Handler {
-	return &HTTPAuthWraper{
+func NewHTTPBasicAuthWrapper(h http.Handler, user, passwd string) http.Handler {
+	return &HTTPAuthWrapper{
 		h:      h,
 		user:   user,
 		passwd: passwd,
 	}
 }
 
-func (aw *HTTPAuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (aw *HTTPAuthWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, passwd, hasAuth := r.BasicAuth()
 	if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
 		aw.h.ServeHTTP(w, r)
@@ -83,11 +83,11 @@ func (authMid *HTTPAuthMiddleware) Middleware(next http.Handler) http.Handler {
 	})
 }
 
-type HTTPGzipWraper struct {
+type HTTPGzipWrapper struct {
 	h http.Handler
 }
 
-func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (gw *HTTPGzipWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
 		gw.h.ServeHTTP(w, r)
 		return
@@ -100,7 +100,7 @@ func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 func MakeHTTPGzipHandler(h http.Handler) http.Handler {
-	return &HTTPGzipWraper{
+	return &HTTPGzipWrapper{
 		h: h,
 	}
 }

+ 4 - 1
pkg/util/net/listener.go

@@ -52,7 +52,10 @@ func (l *InternalListener) PutConn(conn net.Conn) error {
 			conn.Close()
 		}
 	})
-	return err
+	if err != nil {
+		return fmt.Errorf("put conn error: listener is closed")
+	}
+	return nil
 }
 
 func (l *InternalListener) Close() error {

+ 5 - 5
pkg/util/tcpmux/httpconnect.go

@@ -24,7 +24,7 @@ import (
 
 	libnet "github.com/fatedier/golib/net"
 
-	"github.com/fatedier/frp/pkg/util/util"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/vhost"
 )
 
@@ -59,10 +59,10 @@ func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host, ht
 		return
 	}
 
-	host, _ = util.CanonicalHost(req.Host)
+	host, _ = httppkg.CanonicalHost(req.Host)
 	proxyAuth := req.Header.Get("Proxy-Authorization")
 	if proxyAuth != "" {
-		httpUser, httpPwd, _ = util.ParseBasicAuth(proxyAuth)
+		httpUser, httpPwd, _ = httppkg.ParseBasicAuth(proxyAuth)
 	}
 	return
 }
@@ -71,7 +71,7 @@ func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, _ map[string]s
 	if muxer.passthrough {
 		return nil
 	}
-	res := util.OkResponse()
+	res := httppkg.OkResponse()
 	if res.Body != nil {
 		defer res.Body.Close()
 	}
@@ -85,7 +85,7 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re
 		return true, nil
 	}
 
-	resp := util.ProxyUnauthorizedResponse()
+	resp := httppkg.ProxyUnauthorizedResponse()
 	if resp.Body != nil {
 		defer resp.Body.Close()
 	}

+ 1 - 1
pkg/util/version/version.go

@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version = "0.52.3"
+var version = "0.53.0"
 
 func Full() string {
 	return version

+ 2 - 2
pkg/util/version/version_test.go

@@ -47,7 +47,7 @@ func TestVersion(t *testing.T) {
 	proto := Proto(Full())
 	major := Major(Full())
 	minor := Minor(Full())
-	parseVerion := fmt.Sprintf("%d.%d.%d", proto, major, minor)
+	parseVersion := fmt.Sprintf("%d.%d.%d", proto, major, minor)
 	version := Full()
-	assert.Equal(parseVerion, version)
+	assert.Equal(parseVersion, version)
 }

+ 10 - 10
pkg/util/vhost/http.go

@@ -31,8 +31,8 @@ import (
 	libio "github.com/fatedier/golib/io"
 	"github.com/fatedier/golib/pool"
 
-	frpLog "github.com/fatedier/frp/pkg/util/log"
-	"github.com/fatedier/frp/pkg/util/util"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
+	logpkg "github.com/fatedier/frp/pkg/util/log"
 )
 
 var ErrNoRouteFound = errors.New("no route found")
@@ -61,7 +61,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 		Director: func(req *http.Request) {
 			req.URL.Scheme = "http"
 			reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
-			oldHost, _ := util.CanonicalHost(reqRouteInfo.Host)
+			oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
 
 			rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
 			if rc != nil {
@@ -74,7 +74,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 					// ignore error here, it will use CreateConnFn instead later
 					endpoint, _ = rc.ChooseEndpointFn()
 					reqRouteInfo.Endpoint = endpoint
-					frpLog.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
+					logpkg.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
 						endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
 				}
 				// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
@@ -116,7 +116,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 		BufferPool: newWrapPool(),
 		ErrorLog:   log.New(newWrapLogger(), "", 0),
 		ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
-			frpLog.Warn("do http proxy request [host: %s] error: %v", req.Host, err)
+			logpkg.Warn("do http proxy request [host: %s] error: %v", req.Host, err)
 			rw.WriteHeader(http.StatusNotFound)
 			_, _ = rw.Write(getNotFoundPageContent())
 		},
@@ -143,7 +143,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
 func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
 	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
 	if ok {
-		frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
+		logpkg.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
 		return vr.payload.(*RouteConfig)
 	}
 	return nil
@@ -159,7 +159,7 @@ func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string)
 
 // CreateConnection create a new connection by route config
 func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
-	host, _ := util.CanonicalHost(reqRouteInfo.Host)
+	host, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
 	vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
 	if ok {
 		if byEndpoint {
@@ -188,7 +188,7 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, p
 	return true
 }
 
-// getVhost trys to get vhost router by route policy.
+// getVhost tries to get vhost router by route policy.
 func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
 	findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
 		vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser)
@@ -303,7 +303,7 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
 }
 
 func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
-	domain, _ := util.CanonicalHost(req.Host)
+	domain, _ := httppkg.CanonicalHost(req.Host)
 	location := req.URL.Path
 	user, passwd, _ := req.BasicAuth()
 	if !rp.CheckAuth(domain, location, user, user, passwd) {
@@ -333,6 +333,6 @@ type wrapLogger struct{}
 func newWrapLogger() *wrapLogger { return &wrapLogger{} }
 
 func (l *wrapLogger) Write(p []byte) (n int, err error) {
-	frpLog.Warn("%s", string(bytes.TrimRight(p, "\n")))
+	logpkg.Warn("%s", string(bytes.TrimRight(p, "\n")))
 	return len(p), nil
 }

+ 2 - 2
pkg/util/vhost/resource.go

@@ -20,7 +20,7 @@ import (
 	"net/http"
 	"os"
 
-	frpLog "github.com/fatedier/frp/pkg/util/log"
+	logpkg "github.com/fatedier/frp/pkg/util/log"
 	"github.com/fatedier/frp/pkg/util/version"
 )
 
@@ -58,7 +58,7 @@ func getNotFoundPageContent() []byte {
 	if NotFoundPagePath != "" {
 		buf, err = os.ReadFile(NotFoundPagePath)
 		if err != nil {
-			frpLog.Warn("read custom 404 page error: %v", err)
+			logpkg.Warn("read custom 404 page error: %v", err)
 			buf = []byte(NotFound)
 		}
 	} else {

+ 2 - 2
pkg/util/vhost/vhost.go

@@ -22,7 +22,7 @@ import (
 	"github.com/fatedier/golib/errors"
 
 	"github.com/fatedier/frp/pkg/util/log"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
@@ -284,7 +284,7 @@ func (l *Listener) Accept() (net.Conn, error) {
 		xl.Debug("rewrite host to [%s] success", l.rewriteHost)
 		conn = sConn
 	}
-	return utilnet.NewContextConn(l.ctx, conn), nil
+	return netpkg.NewContextConn(l.ctx, conn), nil
 }
 
 func (l *Listener) Close() error {

+ 197 - 0
pkg/util/wait/backoff.go

@@ -0,0 +1,197 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package wait
+
+import (
+	"math/rand"
+	"time"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type BackoffFunc func(previousDuration time.Duration, previousConditionError bool) time.Duration
+
+func (f BackoffFunc) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration {
+	return f(previousDuration, previousConditionError)
+}
+
+type BackoffManager interface {
+	Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration
+}
+
+type FastBackoffOptions struct {
+	Duration           time.Duration
+	Factor             float64
+	Jitter             float64
+	MaxDuration        time.Duration
+	InitDurationIfFail time.Duration
+
+	// If FastRetryCount > 0, then within the FastRetryWindow time window,
+	// the retry will be performed with a delay of FastRetryDelay for the first FastRetryCount calls.
+	FastRetryCount  int
+	FastRetryDelay  time.Duration
+	FastRetryJitter float64
+	FastRetryWindow time.Duration
+}
+
+type fastBackoffImpl struct {
+	options FastBackoffOptions
+
+	lastCalledTime      time.Time
+	consecutiveErrCount int
+
+	fastRetryCutoffTime     time.Time
+	countsInFastRetryWindow int
+}
+
+func NewFastBackoffManager(options FastBackoffOptions) BackoffManager {
+	return &fastBackoffImpl{
+		options:                 options,
+		countsInFastRetryWindow: 1,
+	}
+}
+
+func (f *fastBackoffImpl) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration {
+	if f.lastCalledTime.IsZero() {
+		f.lastCalledTime = time.Now()
+		return f.options.Duration
+	}
+	now := time.Now()
+	f.lastCalledTime = now
+
+	if previousConditionError {
+		f.consecutiveErrCount++
+	} else {
+		f.consecutiveErrCount = 0
+	}
+
+	if f.options.FastRetryCount > 0 && previousConditionError {
+		f.countsInFastRetryWindow++
+		if f.countsInFastRetryWindow <= f.options.FastRetryCount {
+			return Jitter(f.options.FastRetryDelay, f.options.FastRetryJitter)
+		}
+		if now.After(f.fastRetryCutoffTime) {
+			// reset
+			f.fastRetryCutoffTime = now.Add(f.options.FastRetryWindow)
+			f.countsInFastRetryWindow = 0
+		}
+	}
+
+	if previousConditionError {
+		var duration time.Duration
+		if f.consecutiveErrCount == 1 {
+			duration = util.EmptyOr(f.options.InitDurationIfFail, previousDuration)
+		} else {
+			duration = previousDuration
+		}
+
+		duration = util.EmptyOr(duration, time.Second)
+		if f.options.Factor != 0 {
+			duration = time.Duration(float64(duration) * f.options.Factor)
+		}
+		if f.options.Jitter > 0 {
+			duration = Jitter(duration, f.options.Jitter)
+		}
+		if f.options.MaxDuration > 0 && duration > f.options.MaxDuration {
+			duration = f.options.MaxDuration
+		}
+		return duration
+	}
+	return f.options.Duration
+}
+
+func BackoffUntil(f func() error, backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
+	var delay time.Duration
+	previousError := false
+
+	ticker := time.NewTicker(backoff.Backoff(delay, previousError))
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-stopCh:
+			return
+		default:
+		}
+
+		if !sliding {
+			delay = backoff.Backoff(delay, previousError)
+		}
+
+		if err := f(); err != nil {
+			previousError = true
+		} else {
+			previousError = false
+		}
+
+		if sliding {
+			delay = backoff.Backoff(delay, previousError)
+		}
+
+		ticker.Reset(delay)
+		select {
+		case <-stopCh:
+			return
+		default:
+		}
+
+		select {
+		case <-stopCh:
+			return
+		case <-ticker.C:
+		}
+	}
+}
+
+// Jitter returns a time.Duration between duration and duration + maxFactor *
+// duration.
+//
+// This allows clients to avoid converging on periodic behavior. If maxFactor
+// is 0.0, a suggested default value will be chosen.
+func Jitter(duration time.Duration, maxFactor float64) time.Duration {
+	if maxFactor <= 0.0 {
+		maxFactor = 1.0
+	}
+	wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
+	return wait
+}
+
+func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
+	ff := func() error {
+		f()
+		return nil
+	}
+	BackoffUntil(ff, BackoffFunc(func(time.Duration, bool) time.Duration {
+		return period
+	}), true, stopCh)
+}
+
+func MergeAndCloseOnAnyStopChannel[T any](upstreams ...<-chan T) <-chan T {
+	out := make(chan T)
+
+	for _, upstream := range upstreams {
+		ch := upstream
+		go lo.Try0(func() {
+			select {
+			case <-ch:
+				close(out)
+			case <-out:
+			}
+		})
+	}
+	return out
+}

+ 50 - 9
pkg/util/xlog/xlog.go

@@ -15,40 +15,81 @@
 package xlog
 
 import (
+	"sort"
+
 	"github.com/fatedier/frp/pkg/util/log"
 )
 
+type LogPrefix struct {
+	// Name is the name of the prefix, it won't be displayed in log but used to identify the prefix.
+	Name string
+	// Value is the value of the prefix, it will be displayed in log.
+	Value string
+	// The prefix with higher priority will be displayed first, default is 10.
+	Priority int
+}
+
 // Logger is not thread safety for operations on prefix
 type Logger struct {
-	prefixes []string
+	prefixes []LogPrefix
 
 	prefixString string
 }
 
 func New() *Logger {
 	return &Logger{
-		prefixes: make([]string, 0),
+		prefixes: make([]LogPrefix, 0),
 	}
 }
 
-func (l *Logger) ResetPrefixes() (old []string) {
+func (l *Logger) ResetPrefixes() (old []LogPrefix) {
 	old = l.prefixes
-	l.prefixes = make([]string, 0)
+	l.prefixes = make([]LogPrefix, 0)
 	l.prefixString = ""
 	return
 }
 
 func (l *Logger) AppendPrefix(prefix string) *Logger {
-	l.prefixes = append(l.prefixes, prefix)
-	l.prefixString += "[" + prefix + "] "
+	return l.AddPrefix(LogPrefix{
+		Name:     prefix,
+		Value:    prefix,
+		Priority: 10,
+	})
+}
+
+func (l *Logger) AddPrefix(prefix LogPrefix) *Logger {
+	found := false
+	if prefix.Priority <= 0 {
+		prefix.Priority = 10
+	}
+	for _, p := range l.prefixes {
+		if p.Name == prefix.Name {
+			found = true
+			p.Value = prefix.Value
+			p.Priority = prefix.Priority
+		}
+	}
+	if !found {
+		l.prefixes = append(l.prefixes, prefix)
+	}
+	l.renderPrefixString()
 	return l
 }
 
-func (l *Logger) Spawn() *Logger {
-	nl := New()
+func (l *Logger) renderPrefixString() {
+	sort.SliceStable(l.prefixes, func(i, j int) bool {
+		return l.prefixes[i].Priority < l.prefixes[j].Priority
+	})
+	l.prefixString = ""
 	for _, v := range l.prefixes {
-		nl.AppendPrefix(v)
+		l.prefixString += "[" + v.Value + "] "
 	}
+}
+
+func (l *Logger) Spawn() *Logger {
+	nl := New()
+	nl.prefixes = append(nl.prefixes, l.prefixes...)
+	nl.renderPrefixString()
 	return nl
 }
 

+ 107 - 0
pkg/virtual/client.go

@@ -0,0 +1,107 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package virtual
+
+import (
+	"context"
+	"net"
+
+	"github.com/fatedier/frp/client"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/msg"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
+)
+
+type ClientOptions struct {
+	Common           *v1.ClientCommonConfig
+	Spec             *msg.ClientSpec
+	HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
+}
+
+type Client struct {
+	l   *netpkg.InternalListener
+	svr *client.Service
+}
+
+func NewClient(options ClientOptions) (*Client, error) {
+	if options.Common != nil {
+		options.Common.Complete()
+	}
+
+	ln := netpkg.NewInternalListener()
+
+	serviceOptions := client.ServiceOptions{
+		Common:     options.Common,
+		ClientSpec: options.Spec,
+		ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) client.Connector {
+			return &pipeConnector{
+				peerListener: ln,
+			}
+		},
+		HandleWorkConnCb: options.HandleWorkConnCb,
+	}
+	svr, err := client.NewService(serviceOptions)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{
+		l:   ln,
+		svr: svr,
+	}, nil
+}
+
+func (c *Client) PeerListener() net.Listener {
+	return c.l
+}
+
+func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) {
+	_ = c.svr.UpdateAllConfigurer(proxyCfgs, nil)
+}
+
+func (c *Client) Run(ctx context.Context) error {
+	return c.svr.Run(ctx)
+}
+
+func (c *Client) Service() *client.Service {
+	return c.svr
+}
+
+func (c *Client) Close() {
+	c.svr.Close()
+	c.l.Close()
+}
+
+type pipeConnector struct {
+	peerListener *netpkg.InternalListener
+}
+
+func (pc *pipeConnector) Open() error {
+	return nil
+}
+
+func (pc *pipeConnector) Connect() (net.Conn, error) {
+	c1, c2 := net.Pipe()
+	if err := pc.peerListener.PutConn(c1); err != nil {
+		c1.Close()
+		c2.Close()
+		return nil, err
+	}
+	return c2, nil
+}
+
+func (pc *pipeConnector) Close() error {
+	pc.peerListener.Close()
+	return nil
+}

+ 148 - 245
server/control.go

@@ -17,15 +17,12 @@ package server
 import (
 	"context"
 	"fmt"
-	"io"
 	"net"
 	"runtime/debug"
 	"sync"
+	"sync/atomic"
 	"time"
 
-	"github.com/fatedier/golib/control/shutdown"
-	"github.com/fatedier/golib/crypto"
-	"github.com/fatedier/golib/errors"
 	"github.com/samber/lo"
 
 	"github.com/fatedier/frp/pkg/auth"
@@ -35,8 +32,10 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/pkg/transport"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/version"
+	"github.com/fatedier/frp/pkg/util/wait"
 	"github.com/fatedier/frp/pkg/util/xlog"
 	"github.com/fatedier/frp/server/controller"
 	"github.com/fatedier/frp/server/metrics"
@@ -111,18 +110,16 @@ type Control struct {
 	// other components can use this to communicate with client
 	msgTransporter transport.MessageTransporter
 
+	// msgDispatcher is a wrapper for control connection.
+	// It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
+	msgDispatcher *msg.Dispatcher
+
 	// login message
 	loginMsg *msg.Login
 
 	// control connection
 	conn net.Conn
 
-	// put a message in this channel to send it over control connection to client
-	sendCh chan (msg.Message)
-
-	// read from this channel to get the next message sent by client
-	readCh chan (msg.Message)
-
 	// work connections
 	workConnCh chan net.Conn
 
@@ -136,29 +133,24 @@ type Control struct {
 	portsUsedNum int
 
 	// last time got the Ping message
-	lastPing time.Time
+	lastPing atomic.Value
 
 	// A new run id will be generated when a new client login.
 	// If run id got from login message has same run id, it means it's the same client, so we can
 	// replace old controller instantly.
 	runID string
 
-	readerShutdown  *shutdown.Shutdown
-	writerShutdown  *shutdown.Shutdown
-	managerShutdown *shutdown.Shutdown
-	allShutdown     *shutdown.Shutdown
-
-	started bool
-
 	mu sync.RWMutex
 
 	// Server configuration information
 	serverCfg *v1.ServerConfig
 
-	xl  *xlog.Logger
-	ctx context.Context
+	xl     *xlog.Logger
+	ctx    context.Context
+	doneCh chan struct{}
 }
 
+// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext.
 func NewControl(
 	ctx context.Context,
 	rc *controller.ResourceController,
@@ -166,38 +158,45 @@ func NewControl(
 	pluginManager *plugin.Manager,
 	authVerifier auth.Verifier,
 	ctlConn net.Conn,
+	ctlConnEncrypted bool,
 	loginMsg *msg.Login,
 	serverCfg *v1.ServerConfig,
-) *Control {
+) (*Control, error) {
 	poolCount := loginMsg.PoolCount
 	if poolCount > int(serverCfg.Transport.MaxPoolCount) {
 		poolCount = int(serverCfg.Transport.MaxPoolCount)
 	}
 	ctl := &Control{
-		rc:              rc,
-		pxyManager:      pxyManager,
-		pluginManager:   pluginManager,
-		authVerifier:    authVerifier,
-		conn:            ctlConn,
-		loginMsg:        loginMsg,
-		sendCh:          make(chan msg.Message, 10),
-		readCh:          make(chan msg.Message, 10),
-		workConnCh:      make(chan net.Conn, poolCount+10),
-		proxies:         make(map[string]proxy.Proxy),
-		poolCount:       poolCount,
-		portsUsedNum:    0,
-		lastPing:        time.Now(),
-		runID:           loginMsg.RunID,
-		readerShutdown:  shutdown.New(),
-		writerShutdown:  shutdown.New(),
-		managerShutdown: shutdown.New(),
-		allShutdown:     shutdown.New(),
-		serverCfg:       serverCfg,
-		xl:              xlog.FromContextSafe(ctx),
-		ctx:             ctx,
+		rc:            rc,
+		pxyManager:    pxyManager,
+		pluginManager: pluginManager,
+		authVerifier:  authVerifier,
+		conn:          ctlConn,
+		loginMsg:      loginMsg,
+		workConnCh:    make(chan net.Conn, poolCount+10),
+		proxies:       make(map[string]proxy.Proxy),
+		poolCount:     poolCount,
+		portsUsedNum:  0,
+		runID:         loginMsg.RunID,
+		serverCfg:     serverCfg,
+		xl:            xlog.FromContextSafe(ctx),
+		ctx:           ctx,
+		doneCh:        make(chan struct{}),
+	}
+	ctl.lastPing.Store(time.Now())
+
+	if ctlConnEncrypted {
+		cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
+		if err != nil {
+			return nil, err
+		}
+		ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
+	} else {
+		ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
 	}
-	ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
-	return ctl
+	ctl.registerMsgHandlers()
+	ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
+	return ctl, nil
 }
 
 // Start send a login success message to client and start working.
@@ -208,27 +207,18 @@ func (ctl *Control) Start() {
 		Error:   "",
 	}
 	_ = msg.WriteMsg(ctl.conn, loginRespMsg)
-	ctl.mu.Lock()
-	ctl.started = true
-	ctl.mu.Unlock()
 
-	go ctl.writer()
 	go func() {
 		for i := 0; i < ctl.poolCount; i++ {
 			// ignore error here, that means that this control is closed
-			_ = errors.PanicToError(func() {
-				ctl.sendCh <- &msg.ReqWorkConn{}
-			})
+			_ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
 		}
 	}()
-
-	go ctl.manager()
-	go ctl.reader()
-	go ctl.stoper()
+	go ctl.worker()
 }
 
 func (ctl *Control) Close() error {
-	ctl.allShutdown.Start()
+	ctl.conn.Close()
 	return nil
 }
 
@@ -236,7 +226,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
 	xl := ctl.xl
 	xl.Info("Replaced by client [%s]", newCtl.runID)
 	ctl.runID = ""
-	ctl.allShutdown.Start()
+	ctl.conn.Close()
 }
 
 func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
@@ -282,9 +272,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 		xl.Debug("get work connection from pool")
 	default:
 		// no work connections available in the poll, send message to frpc to get more
-		if err = errors.PanicToError(func() {
-			ctl.sendCh <- &msg.ReqWorkConn{}
-		}); err != nil {
+		if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil {
 			return nil, fmt.Errorf("control is already closed")
 		}
 
@@ -304,92 +292,40 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 	}
 
 	// When we get a work connection from pool, replace it with a new one.
-	_ = errors.PanicToError(func() {
-		ctl.sendCh <- &msg.ReqWorkConn{}
-	})
+	_ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
 	return
 }
 
-func (ctl *Control) writer() {
-	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
-
-	defer ctl.allShutdown.Start()
-	defer ctl.writerShutdown.Done()
-
-	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
-	if err != nil {
-		xl.Error("crypto new writer error: %v", err)
-		ctl.allShutdown.Start()
-		return
-	}
-	for {
-		m, ok := <-ctl.sendCh
-		if !ok {
-			xl.Info("control writer is closing")
-			return
-		}
-
-		if err := msg.WriteMsg(encWriter, m); err != nil {
-			xl.Warn("write message to control connection error: %v", err)
-			return
-		}
-	}
-}
-
-func (ctl *Control) reader() {
+func (ctl *Control) heartbeatWorker() {
 	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
 
-	defer ctl.allShutdown.Start()
-	defer ctl.readerShutdown.Done()
-
-	encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
-	for {
-		m, err := msg.ReadMsg(encReader)
-		if err != nil {
-			if err == io.EOF {
-				xl.Debug("control connection closed")
+	// Don't need application heartbeat if TCPMux is enabled,
+	// yamux will do same thing.
+	// TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it.
+	if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
+		go wait.Until(func() {
+			if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
+				xl.Warn("heartbeat timeout")
+				ctl.conn.Close()
 				return
 			}
-			xl.Warn("read error: %v", err)
-			ctl.conn.Close()
-			return
-		}
-
-		ctl.readCh <- m
+		}, time.Second, ctl.doneCh)
 	}
 }
 
-func (ctl *Control) stoper() {
+// block until Control closed
+func (ctl *Control) WaitClosed() {
+	<-ctl.doneCh
+}
+
+func (ctl *Control) worker() {
 	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
 
-	ctl.allShutdown.WaitStart()
+	go ctl.heartbeatWorker()
+	go ctl.msgDispatcher.Run()
 
+	<-ctl.msgDispatcher.Done()
 	ctl.conn.Close()
-	ctl.readerShutdown.WaitDone()
-
-	close(ctl.readCh)
-	ctl.managerShutdown.WaitDone()
-
-	close(ctl.sendCh)
-	ctl.writerShutdown.WaitDone()
 
 	ctl.mu.Lock()
 	defer ctl.mu.Unlock()
@@ -419,136 +355,104 @@ func (ctl *Control) stoper() {
 		}()
 	}
 
-	ctl.allShutdown.Done()
-	xl.Info("client exit success")
 	metrics.Server.CloseClient()
+	xl.Info("client exit success")
+	close(ctl.doneCh)
 }
 
-// block until Control closed
-func (ctl *Control) WaitClosed() {
-	ctl.mu.RLock()
-	started := ctl.started
-	ctl.mu.RUnlock()
-
-	if !started {
-		ctl.allShutdown.Done()
-		return
-	}
-	ctl.allShutdown.WaitDone()
+func (ctl *Control) registerMsgHandlers() {
+	ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy)
+	ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing)
+	ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor))
+	ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient))
+	ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport))
+	ctl.msgDispatcher.RegisterHandler(&msg.CloseProxy{}, ctl.handleCloseProxy)
 }
 
-func (ctl *Control) manager() {
+func (ctl *Control) handleNewProxy(m msg.Message) {
 	xl := ctl.xl
-	defer func() {
-		if err := recover(); err != nil {
-			xl.Error("panic error: %v", err)
-			xl.Error(string(debug.Stack()))
-		}
-	}()
+	inMsg := m.(*msg.NewProxy)
 
-	defer ctl.allShutdown.Start()
-	defer ctl.managerShutdown.Done()
+	content := &plugin.NewProxyContent{
+		User: plugin.UserInfo{
+			User:  ctl.loginMsg.User,
+			Metas: ctl.loginMsg.Metas,
+			RunID: ctl.loginMsg.RunID,
+		},
+		NewProxy: *inMsg,
+	}
+	var remoteAddr string
+	retContent, err := ctl.pluginManager.NewProxy(content)
+	if err == nil {
+		inMsg = &retContent.NewProxy
+		remoteAddr, err = ctl.RegisterProxy(inMsg)
+	}
 
-	var heartbeatCh <-chan time.Time
-	// Don't need application heartbeat if TCPMux is enabled,
-	// yamux will do same thing.
-	if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
-		heartbeat := time.NewTicker(time.Second)
-		defer heartbeat.Stop()
-		heartbeatCh = heartbeat.C
+	// register proxy in this control
+	resp := &msg.NewProxyResp{
+		ProxyName: inMsg.ProxyName,
+	}
+	if err != nil {
+		xl.Warn("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
+		resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
+			err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
+	} else {
+		resp.RemoteAddr = remoteAddr
+		xl.Info("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
+		metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType)
 	}
+	_ = ctl.msgDispatcher.Send(resp)
+}
 
-	for {
-		select {
-		case <-heartbeatCh:
-			if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
-				xl.Warn("heartbeat timeout")
-				return
-			}
-		case rawMsg, ok := <-ctl.readCh:
-			if !ok {
-				return
-			}
+func (ctl *Control) handlePing(m msg.Message) {
+	xl := ctl.xl
+	inMsg := m.(*msg.Ping)
 
-			switch m := rawMsg.(type) {
-			case *msg.NewProxy:
-				content := &plugin.NewProxyContent{
-					User: plugin.UserInfo{
-						User:  ctl.loginMsg.User,
-						Metas: ctl.loginMsg.Metas,
-						RunID: ctl.loginMsg.RunID,
-					},
-					NewProxy: *m,
-				}
-				var remoteAddr string
-				retContent, err := ctl.pluginManager.NewProxy(content)
-				if err == nil {
-					m = &retContent.NewProxy
-					remoteAddr, err = ctl.RegisterProxy(m)
-				}
-
-				// register proxy in this control
-				resp := &msg.NewProxyResp{
-					ProxyName: m.ProxyName,
-				}
-				if err != nil {
-					xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
-					resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName),
-						err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
-				} else {
-					resp.RemoteAddr = remoteAddr
-					xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
-					metrics.Server.NewProxy(m.ProxyName, m.ProxyType)
-				}
-				ctl.sendCh <- resp
-			case *msg.NatHoleVisitor:
-				go ctl.HandleNatHoleVisitor(m)
-			case *msg.NatHoleClient:
-				go ctl.HandleNatHoleClient(m)
-			case *msg.NatHoleReport:
-				go ctl.HandleNatHoleReport(m)
-			case *msg.CloseProxy:
-				_ = ctl.CloseProxy(m)
-				xl.Info("close proxy [%s] success", m.ProxyName)
-			case *msg.Ping:
-				content := &plugin.PingContent{
-					User: plugin.UserInfo{
-						User:  ctl.loginMsg.User,
-						Metas: ctl.loginMsg.Metas,
-						RunID: ctl.loginMsg.RunID,
-					},
-					Ping: *m,
-				}
-				retContent, err := ctl.pluginManager.Ping(content)
-				if err == nil {
-					m = &retContent.Ping
-					err = ctl.authVerifier.VerifyPing(m)
-				}
-				if err != nil {
-					xl.Warn("received invalid ping: %v", err)
-					ctl.sendCh <- &msg.Pong{
-						Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
-					}
-					return
-				}
-				ctl.lastPing = time.Now()
-				xl.Debug("receive heartbeat")
-				ctl.sendCh <- &msg.Pong{}
-			}
-		}
+	content := &plugin.PingContent{
+		User: plugin.UserInfo{
+			User:  ctl.loginMsg.User,
+			Metas: ctl.loginMsg.Metas,
+			RunID: ctl.loginMsg.RunID,
+		},
+		Ping: *inMsg,
+	}
+	retContent, err := ctl.pluginManager.Ping(content)
+	if err == nil {
+		inMsg = &retContent.Ping
+		err = ctl.authVerifier.VerifyPing(inMsg)
 	}
+	if err != nil {
+		xl.Warn("received invalid ping: %v", err)
+		_ = ctl.msgDispatcher.Send(&msg.Pong{
+			Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
+		})
+		return
+	}
+	ctl.lastPing.Store(time.Now())
+	xl.Debug("receive heartbeat")
+	_ = ctl.msgDispatcher.Send(&msg.Pong{})
 }
 
-func (ctl *Control) HandleNatHoleVisitor(m *msg.NatHoleVisitor) {
-	ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter, ctl.loginMsg.User)
+func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
+	inMsg := m.(*msg.NatHoleVisitor)
+	ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User)
 }
 
-func (ctl *Control) HandleNatHoleClient(m *msg.NatHoleClient) {
-	ctl.rc.NatHoleController.HandleClient(m, ctl.msgTransporter)
+func (ctl *Control) handleNatHoleClient(m msg.Message) {
+	inMsg := m.(*msg.NatHoleClient)
+	ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
 }
 
-func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) {
-	ctl.rc.NatHoleController.HandleReport(m)
+func (ctl *Control) handleNatHoleReport(m msg.Message) {
+	inMsg := m.(*msg.NatHoleReport)
+	ctl.rc.NatHoleController.HandleReport(inMsg)
+}
+
+func (ctl *Control) handleCloseProxy(m msg.Message) {
+	xl := ctl.xl
+	inMsg := m.(*msg.CloseProxy)
+	_ = ctl.CloseProxy(inMsg)
+	xl.Info("close proxy [%s] success", inMsg.ProxyName)
 }
 
 func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
@@ -658,6 +562,5 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
 	go func() {
 		_ = ctl.pluginManager.CloseProxy(notifyContent)
 	}()
-
 	return
 }

+ 0 - 99
server/dashboard.go

@@ -1,99 +0,0 @@
-// Copyright 2017 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package server
-
-import (
-	"crypto/tls"
-	"net"
-	"net/http"
-	"net/http/pprof"
-	"time"
-
-	"github.com/gorilla/mux"
-	"github.com/prometheus/client_golang/prometheus/promhttp"
-
-	"github.com/fatedier/frp/assets"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
-)
-
-var (
-	httpServerReadTimeout  = 60 * time.Second
-	httpServerWriteTimeout = 60 * time.Second
-)
-
-func (svr *Service) RunDashboardServer(address string) (err error) {
-	// url router
-	router := mux.NewRouter()
-	router.HandleFunc("/healthz", svr.Healthz)
-
-	// debug
-	if svr.cfg.WebServer.PprofEnable {
-		router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
-		router.HandleFunc("/debug/pprof/profile", pprof.Profile)
-		router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
-		router.HandleFunc("/debug/pprof/trace", pprof.Trace)
-		router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
-	}
-
-	subRouter := router.NewRoute().Subrouter()
-
-	user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
-	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-
-	// metrics
-	if svr.cfg.EnablePrometheus {
-		subRouter.Handle("/metrics", promhttp.Handler())
-	}
-
-	// api, see dashboard_api.go
-	subRouter.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET")
-	subRouter.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET")
-	subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET")
-	subRouter.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET")
-
-	// view
-	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
-
-	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
-	})
-
-	server := &http.Server{
-		Addr:         address,
-		Handler:      router,
-		ReadTimeout:  httpServerReadTimeout,
-		WriteTimeout: httpServerWriteTimeout,
-	}
-	ln, err := net.Listen("tcp", address)
-	if err != nil {
-		return err
-	}
-
-	if svr.cfg.WebServer.TLS != nil {
-		cert, err := tls.LoadX509KeyPair(svr.cfg.WebServer.TLS.CertFile, svr.cfg.WebServer.TLS.KeyFile)
-		if err != nil {
-			return err
-		}
-		tlsCfg := &tls.Config{
-			Certificates: []tls.Certificate{cert},
-		}
-		ln = tls.NewListener(ln, tlsCfg)
-	}
-	go func() {
-		_ = server.Serve(ln)
-	}()
-	return
-}

+ 38 - 5
server/dashboard_api.go

@@ -19,19 +19,52 @@ import (
 	"net/http"
 
 	"github.com/gorilla/mux"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 
 	"github.com/fatedier/frp/pkg/config/types"
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/metrics/mem"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/version"
 )
 
+// TODO(fatedier): add an API to clean status of all offline proxies.
+
 type GeneralResponse struct {
 	Code int
 	Msg  string
 }
 
+func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
+	helper.Router.HandleFunc("/healthz", svr.healthz)
+	subRouter := helper.Router.NewRoute().Subrouter()
+
+	subRouter.Use(helper.AuthMiddleware.Middleware)
+
+	// metrics
+	if svr.cfg.EnablePrometheus {
+		subRouter.Handle("/metrics", promhttp.Handler())
+	}
+
+	// apis
+	subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
+	subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
+	subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
+	subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
+
+	// view
+	subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
+	subRouter.PathPrefix("/static/").Handler(
+		netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
+	).Methods("GET")
+
+	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
+	})
+}
+
 type serverInfoResp struct {
 	Version               string `json:"version"`
 	BindPort              int    `json:"bindPort"`
@@ -55,12 +88,12 @@ type serverInfoResp struct {
 }
 
 // /healthz
-func (svr *Service) Healthz(w http.ResponseWriter, _ *http.Request) {
+func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
 	w.WriteHeader(200)
 }
 
 // /api/serverinfo
-func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
 	defer func() {
 		log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
@@ -177,7 +210,7 @@ type GetProxyInfoResp struct {
 }
 
 // /api/proxy/:type
-func (svr *Service) APIProxyByType(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
 	params := mux.Vars(r)
 	proxyType := params["type"]
@@ -245,7 +278,7 @@ type GetProxyStatsResp struct {
 }
 
 // /api/proxy/:type/:name
-func (svr *Service) APIProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
 	params := mux.Vars(r)
 	proxyType := params["type"]
@@ -314,7 +347,7 @@ type GetProxyTrafficResp struct {
 	TrafficOut []int64 `json:"trafficOut"`
 }
 
-func (svr *Service) APIProxyTraffic(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
 	params := mux.Vars(r)
 	name := params["name"]

+ 3 - 3
server/proxy/http.go

@@ -24,7 +24,7 @@ import (
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/util/limit"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
 	"github.com/fatedier/frp/server/metrics"
@@ -180,8 +180,8 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
 		})
 	}
 
-	workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn)
-	workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
+	workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
+	workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
 	metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
 	return
 }

+ 2 - 2
server/proxy/proxy.go

@@ -32,7 +32,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/pkg/util/limit"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
 	"github.com/fatedier/frp/server/controller"
 	"github.com/fatedier/frp/server/metrics"
@@ -130,7 +130,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
 		}
 		xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
 		xl.Spawn().AppendPrefix(pxy.GetName())
-		workConn = utilnet.NewContextConn(pxy.ctx, workConn)
+		workConn = netpkg.NewContextConn(pxy.ctx, workConn)
 
 		var (
 			srcAddr    string

+ 2 - 2
server/proxy/udp.go

@@ -30,7 +30,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/server/metrics"
 )
 
@@ -222,7 +222,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 				})
 			}
 
-			pxy.workConn = utilnet.WrapReadWriteCloserToConn(rwc, workConn)
+			pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn)
 			ctx, cancel := context.WithCancel(context.Background())
 			go workConnReaderFn(pxy.workConn)
 			go workConnSenderFn(pxy.workConn, ctx)

+ 126 - 86
server/service.go

@@ -30,16 +30,17 @@ import (
 	quic "github.com/quic-go/quic-go"
 	"github.com/samber/lo"
 
-	"github.com/fatedier/frp/assets"
 	"github.com/fatedier/frp/pkg/auth"
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	modelmetrics "github.com/fatedier/frp/pkg/metrics"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
+	"github.com/fatedier/frp/pkg/ssh"
 	"github.com/fatedier/frp/pkg/transport"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/tcpmux"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/version"
@@ -78,6 +79,9 @@ type Service struct {
 	// Accept frp tls connections
 	tlsListener net.Listener
 
+	// Accept pipe connections from ssh tunnel gateway
+	sshTunnelListener *netpkg.InternalListener
+
 	// Manage all controllers
 	ctlManager *ControlManager
 
@@ -93,6 +97,11 @@ type Service struct {
 	// All resource managers and controllers
 	rc *controller.ResourceController
 
+	// web server for dashboard UI and apis
+	webServer *httppkg.Server
+
+	sshTunnelGateway *ssh.Gateway
+
 	// Verifies authentication based on selected method
 	authVerifier auth.Verifier
 
@@ -106,16 +115,30 @@ type Service struct {
 	cancel context.CancelFunc
 }
 
-func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
+func NewService(cfg *v1.ServerConfig) (*Service, error) {
 	tlsConfig, err := transport.NewServerTLSConfig(
 		cfg.Transport.TLS.CertFile,
 		cfg.Transport.TLS.KeyFile,
 		cfg.Transport.TLS.TrustedCaFile)
 	if err != nil {
-		return
+		return nil, err
+	}
+
+	var webServer *httppkg.Server
+	if cfg.WebServer.Port > 0 {
+		ws, err := httppkg.NewServer(cfg.WebServer)
+		if err != nil {
+			return nil, err
+		}
+		webServer = ws
+
+		modelmetrics.EnableMem()
+		if cfg.EnablePrometheus {
+			modelmetrics.EnablePrometheus()
+		}
 	}
 
-	svr = &Service{
+	svr := &Service{
 		ctlManager:    NewControlManager(),
 		pxyManager:    proxy.NewManager(),
 		pluginManager: plugin.NewManager(),
@@ -124,11 +147,16 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 			TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
 			UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
 		},
-		httpVhostRouter: vhost.NewRouters(),
-		authVerifier:    auth.NewAuthVerifier(cfg.Auth),
-		tlsConfig:       tlsConfig,
-		cfg:             cfg,
-		ctx:             context.Background(),
+		sshTunnelListener: netpkg.NewInternalListener(),
+		httpVhostRouter:   vhost.NewRouters(),
+		authVerifier:      auth.NewAuthVerifier(cfg.Auth),
+		webServer:         webServer,
+		tlsConfig:         tlsConfig,
+		cfg:               cfg,
+		ctx:               context.Background(),
+	}
+	if webServer != nil {
+		webServer.RouteRegister(svr.registerRouteHandlers)
 	}
 
 	// Create tcpmux httpconnect multiplexer.
@@ -137,14 +165,12 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 		address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort))
 		l, err = net.Listen("tcp", address)
 		if err != nil {
-			err = fmt.Errorf("create server listener error, %v", err)
-			return
+			return nil, fmt.Errorf("create server listener error, %v", err)
 		}
 
 		svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout)
 		if err != nil {
-			err = fmt.Errorf("create vhost tcpMuxer error, %v", err)
-			return
+			return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err)
 		}
 		log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough)
 	}
@@ -185,8 +211,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 	address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort))
 	ln, err := net.Listen("tcp", address)
 	if err != nil {
-		err = fmt.Errorf("create server listener error, %v", err)
-		return
+		return nil, fmt.Errorf("create server listener error, %v", err)
 	}
 
 	svr.muxer = mux.NewMux(ln)
@@ -202,10 +227,9 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 	// Listen for accepting connections from client using kcp protocol.
 	if cfg.KCPBindPort > 0 {
 		address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
-		svr.kcpListener, err = utilnet.ListenKcp(address)
+		svr.kcpListener, err = netpkg.ListenKcp(address)
 		if err != nil {
-			err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
-			return
+			return nil, fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
 		}
 		log.Info("frps kcp listen on udp %s", address)
 	}
@@ -220,18 +244,26 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 			KeepAlivePeriod:    time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
 		})
 		if err != nil {
-			err = fmt.Errorf("listen on quic udp address %s error: %v", address, err)
-			return
+			return nil, fmt.Errorf("listen on quic udp address %s error: %v", address, err)
+		}
+		log.Info("frps quic listen on %s", address)
+	}
+
+	if cfg.SSHTunnelGateway.BindPort > 0 {
+		sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.sshTunnelListener)
+		if err != nil {
+			return nil, fmt.Errorf("create ssh gateway error: %v", err)
 		}
-		log.Info("frps quic listen on quic %s", address)
+		svr.sshTunnelGateway = sshGateway
+		log.Info("frps sshTunnelGateway listen on port %d", cfg.SSHTunnelGateway.BindPort)
 	}
 
 	// Listen for accepting connections from client using websocket protocol.
-	websocketPrefix := []byte("GET " + utilnet.FrpWebsocketPath)
+	websocketPrefix := []byte("GET " + netpkg.FrpWebsocketPath)
 	websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool {
 		return bytes.Equal(data, websocketPrefix)
 	})
-	svr.websocketListener = utilnet.NewWebsocketListener(websocketLn)
+	svr.websocketListener = netpkg.NewWebsocketListener(websocketLn)
 
 	// Create http vhost muxer.
 	if cfg.VhostHTTPPort > 0 {
@@ -251,8 +283,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 		} else {
 			l, err = net.Listen("tcp", address)
 			if err != nil {
-				err = fmt.Errorf("create vhost http listener error, %v", err)
-				return
+				return nil, fmt.Errorf("create vhost http listener error, %v", err)
 			}
 		}
 		go func() {
@@ -270,55 +301,30 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 			address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
 			l, err = net.Listen("tcp", address)
 			if err != nil {
-				err = fmt.Errorf("create server listener error, %v", err)
-				return
+				return nil, fmt.Errorf("create server listener error, %v", err)
 			}
 			log.Info("https service listen on %s", address)
 		}
 
 		svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout)
 		if err != nil {
-			err = fmt.Errorf("create vhost httpsMuxer error, %v", err)
-			return
+			return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
 		}
 	}
 
 	// frp tls listener
 	svr.tlsListener = svr.muxer.Listen(2, 1, func(data []byte) bool {
 		// tls first byte can be 0x16 only when vhost https port is not same with bind port
-		return int(data[0]) == utilnet.FRPTLSHeadByte || int(data[0]) == 0x16
+		return int(data[0]) == netpkg.FRPTLSHeadByte || int(data[0]) == 0x16
 	})
 
 	// Create nat hole controller.
 	nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour)
 	if err != nil {
-		err = fmt.Errorf("create nat hole controller error, %v", err)
-		return
+		return nil, fmt.Errorf("create nat hole controller error, %v", err)
 	}
 	svr.rc.NatHoleController = nc
-
-	var statsEnable bool
-	// Create dashboard web server.
-	if cfg.WebServer.Port > 0 {
-		// Init dashboard assets
-		assets.Load(cfg.WebServer.AssetsDir)
-
-		address := net.JoinHostPort(cfg.WebServer.Addr, strconv.Itoa(cfg.WebServer.Port))
-		err = svr.RunDashboardServer(address)
-		if err != nil {
-			err = fmt.Errorf("create dashboard web server error, %v", err)
-			return
-		}
-		log.Info("Dashboard listen on %s", address)
-		statsEnable = true
-	}
-	if statsEnable {
-		modelmetrics.EnableMem()
-		if cfg.EnablePrometheus {
-			modelmetrics.EnablePrometheus()
-		}
-	}
-	return
+	return svr, nil
 }
 
 func (svr *Service) Run(ctx context.Context) {
@@ -326,19 +332,36 @@ func (svr *Service) Run(ctx context.Context) {
 	svr.ctx = ctx
 	svr.cancel = cancel
 
+	// run dashboard web server.
+	if svr.webServer != nil {
+		go func() {
+			log.Info("dashboard listen on %s", svr.webServer.Address())
+			if err := svr.webServer.Run(); err != nil {
+				log.Warn("dashboard server exit with error: %v", err)
+			}
+		}()
+	}
+
+	go svr.HandleListener(svr.sshTunnelListener, true)
+
 	if svr.kcpListener != nil {
-		go svr.HandleListener(svr.kcpListener)
+		go svr.HandleListener(svr.kcpListener, false)
 	}
 	if svr.quicListener != nil {
 		go svr.HandleQUICListener(svr.quicListener)
 	}
-	go svr.HandleListener(svr.websocketListener)
-	go svr.HandleListener(svr.tlsListener)
+	go svr.HandleListener(svr.websocketListener, false)
+	go svr.HandleListener(svr.tlsListener, false)
 
 	if svr.rc.NatHoleController != nil {
 		go svr.rc.NatHoleController.CleanWorker(svr.ctx)
 	}
-	svr.HandleListener(svr.listener)
+
+	if svr.sshTunnelGateway != nil {
+		go svr.sshTunnelGateway.Run()
+	}
+
+	svr.HandleListener(svr.listener, false)
 
 	<-svr.ctx.Done()
 	// service context may not be canceled by svr.Close(), we should call it here to release resources
@@ -375,7 +398,7 @@ func (svr *Service) Close() error {
 	return nil
 }
 
-func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
+func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) {
 	xl := xlog.FromContextSafe(ctx)
 
 	var (
@@ -401,7 +424,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
 		retContent, err := svr.pluginManager.Login(content)
 		if err == nil {
 			m = &retContent.Login
-			err = svr.RegisterControl(conn, m)
+			err = svr.RegisterControl(conn, m, internal)
 		}
 
 		// If login failed, send error message there.
@@ -438,7 +461,10 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
 	}
 }
 
-func (svr *Service) HandleListener(l net.Listener) {
+// HandleListener accepts connections from client and call handleConnection to handle them.
+// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway.
+// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters.
+func (svr *Service) HandleListener(l net.Listener, internal bool) {
 	// Listen for incoming connections from client.
 	for {
 		c, err := l.Accept()
@@ -450,22 +476,25 @@ func (svr *Service) HandleListener(l net.Listener) {
 		xl := xlog.New()
 		ctx := context.Background()
 
-		c = utilnet.NewContextConn(xlog.NewContext(ctx, xl), c)
+		c = netpkg.NewContextConn(xlog.NewContext(ctx, xl), c)
 
-		log.Trace("start check TLS connection...")
-		originConn := c
-		var isTLS, custom bool
-		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout)
-		if err != nil {
-			log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
-			originConn.Close()
-			continue
+		if !internal {
+			log.Trace("start check TLS connection...")
+			originConn := c
+			forceTLS := svr.cfg.Transport.TLS.Force
+			var isTLS, custom bool
+			c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
+			if err != nil {
+				log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
+				originConn.Close()
+				continue
+			}
+			log.Trace("check TLS connection success, isTLS: %v custom: %v internal: %v", isTLS, custom, internal)
 		}
-		log.Trace("check TLS connection success, isTLS: %v custom: %v", isTLS, custom)
 
 		// Start a new goroutine to handle connection.
 		go func(ctx context.Context, frpConn net.Conn) {
-			if lo.FromPtr(svr.cfg.Transport.TCPMux) {
+			if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal {
 				fmuxCfg := fmux.DefaultConfig()
 				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
 				fmuxCfg.LogOutput = io.Discard
@@ -484,10 +513,10 @@ func (svr *Service) HandleListener(l net.Listener) {
 						session.Close()
 						return
 					}
-					go svr.handleConnection(ctx, stream)
+					go svr.handleConnection(ctx, stream, internal)
 				}
 			} else {
-				svr.handleConnection(ctx, frpConn)
+				svr.handleConnection(ctx, frpConn, internal)
 			}
 		}(ctx, c)
 	}
@@ -510,23 +539,24 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
 					_ = frpConn.CloseWithError(0, "")
 					return
 				}
-				go svr.handleConnection(ctx, utilnet.QuicStreamToNetConn(stream, frpConn))
+				go svr.handleConnection(ctx, netpkg.QuicStreamToNetConn(stream, frpConn), false)
 			}
 		}(context.Background(), c)
 	}
 }
 
-func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) {
+func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, internal bool) error {
 	// If client's RunID is empty, it's a new client, we just create a new controller.
 	// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
+	var err error
 	if loginMsg.RunID == "" {
 		loginMsg.RunID, err = util.RandID()
 		if err != nil {
-			return
+			return err
 		}
 	}
 
-	ctx := utilnet.NewContextFromConn(ctlConn)
+	ctx := netpkg.NewContextFromConn(ctlConn)
 	xl := xlog.FromContextSafe(ctx)
 	xl.AppendPrefix(loginMsg.RunID)
 	ctx = xlog.NewContext(ctx, xl)
@@ -534,11 +564,21 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
 		ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
 
 	// Check auth.
-	if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil {
-		return
+	authVerifier := svr.authVerifier
+	if internal && loginMsg.ClientSpec.AlwaysAuthPass {
+		authVerifier = auth.AlwaysPassVerifier
+	}
+	if err := authVerifier.VerifyLogin(loginMsg); err != nil {
+		return err
 	}
 
-	ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg)
+	// TODO(fatedier): use SessionContext
+	ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg)
+	if err != nil {
+		xl.Warn("create new controller error: %v", err)
+		// don't return detailed errors to client
+		return fmt.Errorf("unexpected error when creating new controller")
+	}
 	if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
 		oldCtl.WaitClosed()
 	}
@@ -553,12 +593,12 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
 		ctl.WaitClosed()
 		svr.ctlManager.Del(loginMsg.RunID, ctl)
 	}()
-	return
+	return nil
 }
 
 // RegisterWorkConn register a new work connection to control and proxies need it.
 func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error {
-	xl := utilnet.NewLogFromConn(workConn)
+	xl := netpkg.NewLogFromConn(workConn)
 	ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
 	if !exist {
 		xl.Warn("No client control found for run id [%s]", newMsg.RunID)
@@ -577,7 +617,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
 	if err == nil {
 		newMsg = &retContent.NewWorkConn
 		// Check auth.
-		err = svr.authVerifier.VerifyNewWorkConn(newMsg)
+		err = ctl.authVerifier.VerifyNewWorkConn(newMsg)
 	}
 	if err != nil {
 		xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID)

+ 7 - 8
server/visitor/visitor.go

@@ -23,12 +23,12 @@ import (
 	libio "github.com/fatedier/golib/io"
 	"github.com/samber/lo"
 
-	utilnet "github.com/fatedier/frp/pkg/util/net"
+	netpkg "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
 type listenerBundle struct {
-	l          *utilnet.InternalListener
+	l          *netpkg.InternalListener
 	sk         string
 	allowUsers []string
 }
@@ -46,22 +46,21 @@ func NewManager() *Manager {
 	}
 }
 
-func (vm *Manager) Listen(name string, sk string, allowUsers []string) (l *utilnet.InternalListener, err error) {
+func (vm *Manager) Listen(name string, sk string, allowUsers []string) (*netpkg.InternalListener, error) {
 	vm.mu.Lock()
 	defer vm.mu.Unlock()
 
 	if _, ok := vm.listeners[name]; ok {
-		err = fmt.Errorf("custom listener for [%s] is repeated", name)
-		return
+		return nil, fmt.Errorf("custom listener for [%s] is repeated", name)
 	}
 
-	l = utilnet.NewInternalListener()
+	l := netpkg.NewInternalListener()
 	vm.listeners[name] = &listenerBundle{
 		l:          l,
 		sk:         sk,
 		allowUsers: allowUsers,
 	}
-	return
+	return l, nil
 }
 
 func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string,
@@ -91,7 +90,7 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey
 		if useCompression {
 			rwc = libio.WithCompression(rwc)
 		}
-		err = l.l.PutConn(utilnet.WrapReadWriteCloserToConn(rwc, conn))
+		err = l.l.PutConn(netpkg.WrapReadWriteCloserToConn(rwc, conn))
 	} else {
 		err = fmt.Errorf("custom listener for [%s] doesn't exist", name)
 		return

+ 6 - 6
test/e2e/framework/framework.go

@@ -29,8 +29,8 @@ type Framework struct {
 	// ports used in this framework indexed by port name.
 	usedPorts map[string]int
 
-	// record ports alloced by this framework and release them after each test
-	allocedPorts []int
+	// record ports allocated by this framework and release them after each test
+	allocatedPorts []int
 
 	// portAllocator to alloc port for this test case.
 	portAllocator *port.Allocator
@@ -153,11 +153,11 @@ func (f *Framework) AfterEach() {
 	}
 	f.usedPorts = make(map[string]int)
 
-	// release alloced ports
-	for _, port := range f.allocedPorts {
+	// release allocated ports
+	for _, port := range f.allocatedPorts {
 		f.portAllocator.Release(port)
 	}
-	f.allocedPorts = make([]int, 0)
+	f.allocatedPorts = make([]int, 0)
 
 	// clear os envs
 	f.osEnvs = make([]string, 0)
@@ -237,7 +237,7 @@ func (f *Framework) PortByName(name string) int {
 func (f *Framework) AllocPort() int {
 	port := f.portAllocator.Get()
 	ExpectTrue(port > 0, "alloc port failed")
-	f.allocedPorts = append(f.allocedPorts, port)
+	f.allocatedPorts = append(f.allocatedPorts, port)
 	return port
 }
 

+ 1 - 1
test/e2e/legacy/basic/client.go

@@ -69,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 		err = client.UpdateConfig(newClientConf)
 		framework.ExpectNoError(err)
 
-		err = client.Reload()
+		err = client.Reload(true)
 		framework.ExpectNoError(err)
 		time.Sleep(time.Second)
 

+ 2 - 2
test/e2e/legacy/basic/tcpmux.go

@@ -8,7 +8,7 @@ import (
 
 	"github.com/onsi/ginkgo/v2"
 
-	"github.com/fatedier/frp/pkg/util/util"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
@@ -176,7 +176,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
 					connectRequestHost = req.Host
 
 					// return ok response
-					res := util.OkResponse()
+					res := httppkg.OkResponse()
 					if res.Body != nil {
 						defer res.Body.Close()
 					}

+ 1 - 1
test/e2e/legacy/plugin/server.go

@@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
 			framework.NewRequestExpect(f).Port(remotePort).Ensure()
 		})
 
-		ginkgo.It("Mofify RemotePort", func() {
+		ginkgo.It("Modify RemotePort", func() {
 			localPort := f.AllocPort()
 			remotePort := f.AllocPort()
 			handler := func(req *plugin.Request) *plugin.Response {

+ 2 - 2
test/e2e/pkg/request/request.go

@@ -14,7 +14,7 @@ import (
 
 	libdial "github.com/fatedier/golib/net/dial"
 
-	"github.com/fatedier/frp/pkg/util/util"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/test/e2e/pkg/rpc"
 )
 
@@ -115,7 +115,7 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request {
 }
 
 func (r *Request) HTTPAuth(user, password string) *Request {
-	r.authValue = util.BasicAuth(user, password)
+	r.authValue = httppkg.BasicAuth(user, password)
 	return r
 }
 

+ 89 - 0
test/e2e/pkg/ssh/client.go

@@ -0,0 +1,89 @@
+package ssh
+
+import (
+	"net"
+
+	libio "github.com/fatedier/golib/io"
+	"golang.org/x/crypto/ssh"
+)
+
+type TunnelClient struct {
+	localAddr string
+	sshServer string
+	commands  string
+
+	sshConn *ssh.Client
+	ln      net.Listener
+}
+
+func NewTunnelClient(localAddr string, sshServer string, commands string) *TunnelClient {
+	return &TunnelClient{
+		localAddr: localAddr,
+		sshServer: sshServer,
+		commands:  commands,
+	}
+}
+
+func (c *TunnelClient) Start() error {
+	config := &ssh.ClientConfig{
+		User:            "v0",
+		HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { return nil },
+	}
+
+	conn, err := ssh.Dial("tcp", c.sshServer, config)
+	if err != nil {
+		return err
+	}
+	c.sshConn = conn
+
+	l, err := conn.Listen("tcp", "0.0.0.0:80")
+	if err != nil {
+		return err
+	}
+	c.ln = l
+	ch, req, err := conn.OpenChannel("session", []byte(""))
+	if err != nil {
+		return err
+	}
+	defer ch.Close()
+	go ssh.DiscardRequests(req)
+
+	type command struct {
+		Cmd string
+	}
+	_, err = ch.SendRequest("exec", false, ssh.Marshal(command{Cmd: c.commands}))
+	if err != nil {
+		return err
+	}
+
+	go c.serveListener()
+	return nil
+}
+
+func (c *TunnelClient) Close() {
+	if c.sshConn != nil {
+		_ = c.sshConn.Close()
+	}
+	if c.ln != nil {
+		_ = c.ln.Close()
+	}
+}
+
+func (c *TunnelClient) serveListener() {
+	for {
+		conn, err := c.ln.Accept()
+		if err != nil {
+			return
+		}
+		go c.hanldeConn(conn)
+	}
+}
+
+func (c *TunnelClient) hanldeConn(conn net.Conn) {
+	defer conn.Close()
+	local, err := net.Dial("tcp", c.localAddr)
+	if err != nil {
+		return
+	}
+	_, _, _ = libio.Join(local, conn)
+}

+ 1 - 1
test/e2e/v1/basic/client.go

@@ -72,7 +72,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
 		err = client.UpdateConfig(newClientConf)
 		framework.ExpectNoError(err)
 
-		err = client.Reload()
+		err = client.Reload(true)
 		framework.ExpectNoError(err)
 		time.Sleep(time.Second)
 

Vissa filer visades inte eftersom för många filer har ändrats