Pārlūkot izejas kodu

Merge pull request #3499 from fatedier/dev

release v0.50.0
fatedier 1 gadu atpakaļ
vecāks
revīzija
4fd800bc48
79 mainītis faili ar 1435 papildinājumiem un 1296 dzēšanām
  1. 1 0
      .gitignore
  2. 15 0
      Makefile
  3. 2 5
      README.md
  4. 9 10
      Release.md
  5. 0 0
      assets/frps/static/index-ea3edf22.js
  6. 1 1
      assets/frps/static/index.html
  7. 3 3
      client/admin.go
  8. 1 1
      client/admin_api.go
  9. 1 1
      client/control.go
  10. 47 0
      client/proxy/general_tcp.go
  11. 57 209
      client/proxy/proxy.go
  12. 4 6
      client/proxy/proxy_manager.go
  13. 1 1
      client/proxy/proxy_wrapper.go
  14. 23 6
      client/proxy/sudp.go
  15. 23 7
      client/proxy/udp.go
  16. 20 23
      client/proxy/xtcp.go
  17. 9 6
      client/service.go
  18. 25 10
      client/visitor/stcp.go
  19. 8 6
      client/visitor/sudp.go
  20. 38 11
      client/visitor/visitor.go
  21. 70 31
      client/visitor/visitor_manager.go
  22. 64 19
      client/visitor/xtcp.go
  23. 1 1
      cmd/frpc/sub/http.go
  24. 1 1
      cmd/frpc/sub/https.go
  25. 4 4
      cmd/frpc/sub/root.go
  26. 2 2
      cmd/frpc/sub/stcp.go
  27. 2 2
      cmd/frpc/sub/sudp.go
  28. 1 1
      cmd/frpc/sub/tcp.go
  29. 1 1
      cmd/frpc/sub/tcpmux.go
  30. 1 1
      cmd/frpc/sub/udp.go
  31. 2 2
      cmd/frpc/sub/xtcp.go
  32. 20 4
      conf/frpc_full.ini
  33. 63 0
      hack/download.sh
  34. 18 8
      hack/run-e2e.sh
  35. 33 29
      pkg/config/client.go
  36. 47 41
      pkg/config/client_test.go
  37. 208 401
      pkg/config/proxy.go
  38. 8 4
      pkg/config/proxy_test.go
  39. 0 2
      pkg/config/server_test.go
  40. 43 93
      pkg/config/visitor.go
  41. 4 3
      pkg/config/visitor_test.go
  42. 4 2
      pkg/msg/msg.go
  43. 15 15
      pkg/nathole/analysis.go
  44. 6 5
      pkg/nathole/classify.go
  45. 23 14
      pkg/nathole/controller.go
  46. 1 1
      pkg/nathole/nathole.go
  47. 2 2
      pkg/plugin/client/http2https.go
  48. 8 8
      pkg/plugin/client/http_proxy.go
  49. 2 2
      pkg/plugin/client/https2http.go
  50. 2 2
      pkg/plugin/client/https2https.go
  51. 2 2
      pkg/plugin/client/socks5.go
  52. 4 4
      pkg/plugin/client/static_file.go
  53. 2 2
      pkg/plugin/client/unix_domain_socket.go
  54. 21 10
      pkg/util/net/listener.go
  55. 2 2
      pkg/util/net/tls.go
  56. 2 2
      pkg/util/tcpmux/httpconnect.go
  57. 1 1
      pkg/util/version/version.go
  58. 2 2
      pkg/util/vhost/http.go
  59. 2 2
      pkg/util/vhost/https.go
  60. 2 2
      pkg/util/vhost/vhost.go
  61. 14 9
      server/control.go
  62. 3 3
      server/dashboard.go
  63. 25 14
      server/proxy/http.go
  64. 17 7
      server/proxy/https.go
  65. 71 90
      server/proxy/proxy.go
  66. 23 7
      server/proxy/stcp.go
  67. 23 8
      server/proxy/sudp.go
  68. 27 16
      server/proxy/tcp.go
  69. 17 7
      server/proxy/tcpmux.go
  70. 32 20
      server/proxy/udp.go
  71. 25 7
      server/proxy/xtcp.go
  72. 21 11
      server/service.go
  73. 31 20
      server/visitor/visitor.go
  74. 59 20
      test/e2e/basic/basic.go
  75. 7 12
      test/e2e/basic/client_server.go
  76. 3 3
      test/e2e/basic/server.go
  77. 52 0
      test/e2e/basic/xtcp.go
  78. 1 1
      test/e2e/framework/process.go
  79. 0 5
      web/frps/src/components/ServerOverview.vue

+ 1 - 0
.gitignore

@@ -29,6 +29,7 @@ packages/
 release/
 test/bin/
 vendor/
+lastversion/
 dist/
 .idea/
 .vscode/

+ 15 - 0
Makefile

@@ -46,8 +46,23 @@ e2e:
 e2e-trace:
 	DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
 
+e2e-compatibility-last-frpc:
+	if [ ! -d "./lastversion" ]; then \
+		TARGET_DIRNAME=lastversion ./hack/download.sh; \
+	fi
+	FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh
+	rm -r ./lastversion
+
+e2e-compatibility-last-frps:
+	if [ ! -d "./lastversion" ]; then \
+		TARGET_DIRNAME=lastversion ./hack/download.sh; \
+	fi
+	FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh
+	rm -r ./lastversion
+
 alltest: vet gotest e2e
 	
 clean:
 	rm -f ./bin/frpc
 	rm -f ./bin/frps
+	rm -rf ./lastversion

+ 2 - 5
README.md

@@ -562,11 +562,9 @@ use_compression = true
 
 #### TLS
 
-frp supports the TLS protocol between `frpc` and `frps` since v0.25.0.
+Since v0.50.0, the default value of `tls_enable` and `disable_custom_tls_first_byte` has been changed to true, and tls is enabled by default.
 
-For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection.
-
-Configure `tls_enable = true` in the `[common]` section to `frpc.ini` to enable this feature.
+For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. This only takes effect when you set `disable_custom_tls_first_byte` to false.
 
 To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.**
 
@@ -581,7 +579,6 @@ tls_trusted_ca_file = ca.crt
 **`frps` TLS settings (under the `[common]` section):**
 ```ini
 tls_only = true
-tls_enable = true
 tls_cert_file = certificate.crt
 tls_key_file = certificate.key
 tls_trusted_ca_file = ca.crt

+ 9 - 10
Release.md

@@ -1,19 +1,18 @@
 ## Notes
 
-We have thoroughly refactored xtcp in this version to improve its penetration rate and stability.
+**For enhanced security, the default values for `tls_enable` and `disable_custom_tls_first_byte` have been set to true.**
 
-In this version, different penetration strategies can be attempted by retrying connections multiple times. Once a hole is successfully punched, the strategy will be recorded in the server cache for future reuse. When new users connect, the successfully penetrated tunnel can be reused instead of punching a new hole.
+If you wish to revert to the previous default values, you need to manually set the values of these two parameters to false.
 
-**Due to a significant refactor of xtcp, this version is not compatible with previous versions of xtcp.**
+### Features
 
-**To use features related to xtcp, both frpc and frps need to be updated to the latest version.**
+* Added support for `allow_users` in stcp, sudp, xtcp. By default, only the same user is allowed to access. Use `*` to allow access from any user. The visitor configuration now supports `server_user` to connect to proxies of other users.
+* Added fallback support to a specified alternative visitor when xtcp connection fails.
 
-### New
+### Improvements
 
-* The frpc has added the `nathole discover` command for testing the NAT type of the current network.
-* `XTCP` has been refactored, resulting in a significant improvement in the success rate of penetration.
-* When verifying passwords, use `subtle.ConstantTimeCompare` and introduce a certain delay when the password is incorrect.
+* Increased the default value of `MaxStreamWindowSize` for yamux to 6MB, improving traffic forwarding rate in high-latency scenarios.
 
-### Fix
+### Fixes
 
-* Fix the problem of lagging when opening multiple table entries in the frps dashboard.
+* Fixed an issue where having proxies with the same name would cause previously working proxies to become ineffective in `xtcp`.

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
assets/frps/static/index-ea3edf22.js


+ 1 - 1
assets/frps/static/index.html

@@ -4,7 +4,7 @@
 <head>
     <meta charset="utf-8">
     <title>frps dashboard</title>
-  <script type="module" crossorigin src="./index-93e38bbf.js"></script>
+  <script type="module" crossorigin src="./index-ea3edf22.js"></script>
   <link rel="stylesheet" href="./index-1e0c7400.css">
 </head>
 

+ 3 - 3
client/admin.go

@@ -23,7 +23,7 @@ import (
 	"github.com/gorilla/mux"
 
 	"github.com/fatedier/frp/assets"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 var (
@@ -48,7 +48,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {
 
 	subRouter := router.NewRoute().Subrouter()
 	user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
-	subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
 
 	// api, see admin_api.go
 	subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
@@ -58,7 +58,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {
 
 	// view
 	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", 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)
 	})

+ 1 - 1
client/admin_api.go

@@ -91,7 +91,7 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
 		Status: status.Phase,
 		Err:    status.Err,
 	}
-	baseCfg := status.Cfg.GetBaseInfo()
+	baseCfg := status.Cfg.GetBaseConfig()
 	if baseCfg.LocalPort != 0 {
 		psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
 	}

+ 1 - 1
client/control.go

@@ -109,7 +109,7 @@ func NewControl(
 	ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
 	ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
 
-	ctl.vm = visitor.NewManager(ctl.ctx, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
+	ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
 	ctl.vm.Reload(visitorCfgs)
 	return ctl
 }

+ 47 - 0
client/proxy/general_tcp.go

@@ -0,0 +1,47 @@
+// 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 proxy
+
+import (
+	"reflect"
+
+	"github.com/fatedier/frp/pkg/config"
+)
+
+func init() {
+	pxyConfs := []config.ProxyConf{
+		&config.TCPProxyConf{},
+		&config.HTTPProxyConf{},
+		&config.HTTPSProxyConf{},
+		&config.STCPProxyConf{},
+		&config.TCPMuxProxyConf{},
+	}
+	for _, cfg := range pxyConfs {
+		RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
+	}
+}
+
+// GeneralTCPProxy is a general implementation of Proxy interface for TCP protocol.
+// If the default GeneralTCPProxy cannot meet the requirements, you can customize
+// the implementation of the Proxy interface.
+type GeneralTCPProxy struct {
+	*BaseProxy
+}
+
+func NewGeneralTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	return &GeneralTCPProxy{
+		BaseProxy: baseProxy,
+	}
+}

+ 57 - 209
client/proxy/proxy.go

@@ -19,12 +19,13 @@ import (
 	"context"
 	"io"
 	"net"
+	"reflect"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 	libdial "github.com/fatedier/golib/net/dial"
 	pp "github.com/pires/go-proxyproto"
 	"golang.org/x/time/rate"
@@ -37,6 +38,12 @@ import (
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
+var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{}
+
+func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) {
+	proxyFactoryRegistry[proxyConfType] = factory
+}
+
 // Proxy defines how to handle work connections for different proxy type.
 type Proxy interface {
 	Run() error
@@ -54,253 +61,94 @@ func NewProxy(
 	msgTransporter transport.MessageTransporter,
 ) (pxy Proxy) {
 	var limiter *rate.Limiter
-	limitBytes := pxyConf.GetBaseInfo().BandwidthLimit.Bytes()
-	if limitBytes > 0 && pxyConf.GetBaseInfo().BandwidthLimitMode == config.BandwidthLimitModeClient {
+	limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes()
+	if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeClient {
 		limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
 	}
 
 	baseProxy := BaseProxy{
-		clientCfg:      clientCfg,
-		limiter:        limiter,
-		msgTransporter: msgTransporter,
-		xl:             xlog.FromContextSafe(ctx),
-		ctx:            ctx,
+		baseProxyConfig: pxyConf.GetBaseConfig(),
+		clientCfg:       clientCfg,
+		limiter:         limiter,
+		msgTransporter:  msgTransporter,
+		xl:              xlog.FromContextSafe(ctx),
+		ctx:             ctx,
 	}
-	switch cfg := pxyConf.(type) {
-	case *config.TCPProxyConf:
-		pxy = &TCPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.TCPMuxProxyConf:
-		pxy = &TCPMuxProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.UDPProxyConf:
-		pxy = &UDPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.HTTPProxyConf:
-		pxy = &HTTPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.HTTPSProxyConf:
-		pxy = &HTTPSProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.STCPProxyConf:
-		pxy = &STCPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.XTCPProxyConf:
-		pxy = &XTCPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-		}
-	case *config.SUDPProxyConf:
-		pxy = &SUDPProxy{
-			BaseProxy: &baseProxy,
-			cfg:       cfg,
-			closeCh:   make(chan struct{}),
-		}
+
+	factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
+	if factory == nil {
+		return nil
 	}
-	return
+	return factory(&baseProxy, pxyConf)
 }
 
 type BaseProxy struct {
-	closed         bool
-	clientCfg      config.ClientCommonConf
-	msgTransporter transport.MessageTransporter
-	limiter        *rate.Limiter
+	baseProxyConfig *config.BaseProxyConf
+	clientCfg       config.ClientCommonConf
+	msgTransporter  transport.MessageTransporter
+	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
 
 	mu  sync.RWMutex
 	xl  *xlog.Logger
 	ctx context.Context
 }
 
-// TCP
-type TCPProxy struct {
-	*BaseProxy
-
-	cfg         *config.TCPProxyConf
-	proxyPlugin plugin.Plugin
-}
-
-func (pxy *TCPProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
-		if err != nil {
-			return
-		}
-	}
-	return
-}
-
-func (pxy *TCPProxy) Close() {
-	if pxy.proxyPlugin != nil {
-		pxy.proxyPlugin.Close()
-	}
-}
-
-func (pxy *TCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-		conn, []byte(pxy.clientCfg.Token), m)
-}
-
-// TCP Multiplexer
-type TCPMuxProxy struct {
-	*BaseProxy
-
-	cfg         *config.TCPMuxProxyConf
-	proxyPlugin plugin.Plugin
-}
-
-func (pxy *TCPMuxProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
+func (pxy *BaseProxy) Run() error {
+	if pxy.baseProxyConfig.Plugin != "" {
+		p, err := plugin.Create(pxy.baseProxyConfig.Plugin, pxy.baseProxyConfig.PluginParams)
 		if err != nil {
-			return
+			return err
 		}
+		pxy.proxyPlugin = p
 	}
-	return
+	return nil
 }
 
-func (pxy *TCPMuxProxy) Close() {
+func (pxy *BaseProxy) Close() {
 	if pxy.proxyPlugin != nil {
 		pxy.proxyPlugin.Close()
 	}
 }
 
-func (pxy *TCPMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-		conn, []byte(pxy.clientCfg.Token), m)
-}
-
-// HTTP
-type HTTPProxy struct {
-	*BaseProxy
-
-	cfg         *config.HTTPProxyConf
-	proxyPlugin plugin.Plugin
-}
-
-func (pxy *HTTPProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
-		if err != nil {
-			return
-		}
-	}
-	return
-}
-
-func (pxy *HTTPProxy) Close() {
-	if pxy.proxyPlugin != nil {
-		pxy.proxyPlugin.Close()
-	}
-}
-
-func (pxy *HTTPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-		conn, []byte(pxy.clientCfg.Token), m)
-}
-
-// HTTPS
-type HTTPSProxy struct {
-	*BaseProxy
-
-	cfg         *config.HTTPSProxyConf
-	proxyPlugin plugin.Plugin
-}
-
-func (pxy *HTTPSProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
-		if err != nil {
-			return
-		}
-	}
-	return
-}
-
-func (pxy *HTTPSProxy) Close() {
-	if pxy.proxyPlugin != nil {
-		pxy.proxyPlugin.Close()
-	}
-}
-
-func (pxy *HTTPSProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-		conn, []byte(pxy.clientCfg.Token), m)
-}
-
-// STCP
-type STCPProxy struct {
-	*BaseProxy
-
-	cfg         *config.STCPProxyConf
-	proxyPlugin plugin.Plugin
-}
-
-func (pxy *STCPProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
-		if err != nil {
-			return
-		}
-	}
-	return
-}
-
-func (pxy *STCPProxy) Close() {
-	if pxy.proxyPlugin != nil {
-		pxy.proxyPlugin.Close()
-	}
-}
-
-func (pxy *STCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-		conn, []byte(pxy.clientCfg.Token), m)
+func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
+	pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Token))
 }
 
 // Common handler for tcp work connections.
-func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf, proxyPlugin plugin.Plugin,
-	baseInfo *config.BaseProxyConf, limiter *rate.Limiter, workConn net.Conn, encKey []byte, m *msg.StartWorkConn,
-) {
-	xl := xlog.FromContextSafe(ctx)
+func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
+	xl := pxy.xl
+	baseConfig := pxy.baseProxyConfig
 	var (
 		remote io.ReadWriteCloser
 		err    error
 	)
 	remote = workConn
-	if limiter != nil {
-		remote = frpIo.WrapReadWriteCloser(limit.NewReader(workConn, limiter), limit.NewWriter(workConn, limiter), func() error {
+	if pxy.limiter != nil {
+		remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
 			return workConn.Close()
 		})
 	}
 
 	xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
-		baseInfo.UseEncryption, baseInfo.UseCompression)
-	if baseInfo.UseEncryption {
-		remote, err = frpIo.WithEncryption(remote, encKey)
+		baseConfig.UseEncryption, baseConfig.UseCompression)
+	if baseConfig.UseEncryption {
+		remote, err = libio.WithEncryption(remote, encKey)
 		if err != nil {
 			workConn.Close()
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if baseInfo.UseCompression {
-		remote = frpIo.WithCompression(remote)
+	if baseConfig.UseCompression {
+		remote = libio.WithCompression(remote)
 	}
 
 	// check if we need to send proxy protocol info
 	var extraInfo []byte
-	if baseInfo.ProxyProtocolVersion != "" {
+	if baseConfig.ProxyProtocolVersion != "" {
 		if m.SrcAddr != "" && m.SrcPort != 0 {
 			if m.DstAddr == "" {
 				m.DstAddr = "127.0.0.1"
@@ -319,9 +167,9 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
 				h.TransportProtocol = pp.TCPv6
 			}
 
-			if baseInfo.ProxyProtocolVersion == "v1" {
+			if baseConfig.ProxyProtocolVersion == "v1" {
 				h.Version = 1
-			} else if baseInfo.ProxyProtocolVersion == "v2" {
+			} else if baseConfig.ProxyProtocolVersion == "v2" {
 				h.Version = 2
 			}
 
@@ -331,21 +179,21 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
 		}
 	}
 
-	if proxyPlugin != nil {
-		// if plugin is set, let plugin handle connections first
-		xl.Debug("handle by plugin: %s", proxyPlugin.Name())
-		proxyPlugin.Handle(remote, workConn, extraInfo)
+	if pxy.proxyPlugin != nil {
+		// if plugin is set, let plugin handle connection first
+		xl.Debug("handle by plugin: %s", pxy.proxyPlugin.Name())
+		pxy.proxyPlugin.Handle(remote, workConn, extraInfo)
 		xl.Debug("handle by plugin finished")
 		return
 	}
 
 	localConn, err := libdial.Dial(
-		net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)),
+		net.JoinHostPort(baseConfig.LocalIP, strconv.Itoa(baseConfig.LocalPort)),
 		libdial.WithTimeout(10*time.Second),
 	)
 	if err != nil {
 		workConn.Close()
-		xl.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIP, localInfo.LocalPort, err)
+		xl.Error("connect to local service [%s:%d] error: %v", baseConfig.LocalIP, baseConfig.LocalPort, err)
 		return
 	}
 
@@ -360,7 +208,7 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
 		}
 	}
 
-	_, _, errs := frpIo.Join(localConn, remote)
+	_, _, errs := libio.Join(localConn, remote)
 	xl.Debug("join connections closed")
 	if len(errs) > 0 {
 		xl.Trace("join connections errors: %v", errs)

+ 4 - 6
client/proxy/proxy_manager.go

@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"reflect"
 	"sync"
 
 	"github.com/fatedier/frp/client/event"
@@ -121,21 +122,18 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
 	for name, pxy := range pm.proxies {
 		del := false
 		cfg, ok := pxyCfgs[name]
-		if !ok {
-			del = true
-		} else if !pxy.Cfg.Compare(cfg) {
+		if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
 			del = true
 		}
 
 		if del {
 			delPxyNames = append(delPxyNames, name)
 			delete(pm.proxies, name)
-
 			pxy.Stop()
 		}
 	}
 	if len(delPxyNames) > 0 {
-		xl.Info("proxy removed: %v", delPxyNames)
+		xl.Info("proxy removed: %s", delPxyNames)
 	}
 
 	addPxyNames := make([]string, 0)
@@ -149,6 +147,6 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
 		}
 	}
 	if len(addPxyNames) > 0 {
-		xl.Info("proxy added: %v", addPxyNames)
+		xl.Info("proxy added: %s", addPxyNames)
 	}
 }

+ 1 - 1
client/proxy/proxy_wrapper.go

@@ -91,7 +91,7 @@ func NewWrapper(
 	eventHandler event.Handler,
 	msgTransporter transport.MessageTransporter,
 ) *Wrapper {
-	baseInfo := cfg.GetBaseInfo()
+	baseInfo := cfg.GetBaseConfig()
 	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName)
 	pw := &Wrapper{
 		WorkingStatus: WorkingStatus{

+ 23 - 6
client/proxy/sudp.go

@@ -17,20 +17,25 @@ package proxy
 import (
 	"io"
 	"net"
+	"reflect"
 	"strconv"
 	"sync"
 	"time"
 
 	"github.com/fatedier/golib/errors"
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
+}
+
 type SUDPProxy struct {
 	*BaseProxy
 
@@ -41,6 +46,18 @@ type SUDPProxy struct {
 	closeCh chan struct{}
 }
 
+func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.SUDPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &SUDPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+		closeCh:   make(chan struct{}),
+	}
+}
+
 func (pxy *SUDPProxy) Run() (err error) {
 	pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort)))
 	if err != nil {
@@ -67,12 +84,12 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
 	var rwc io.ReadWriteCloser = conn
 	var err error
 	if pxy.limiter != nil {
-		rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
+		rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
 			return conn.Close()
 		})
 	}
 	if pxy.cfg.UseEncryption {
-		rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
 		if err != nil {
 			conn.Close()
 			xl.Error("create encryption stream error: %v", err)
@@ -80,9 +97,9 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
 		}
 	}
 	if pxy.cfg.UseCompression {
-		rwc = frpIo.WithCompression(rwc)
+		rwc = libio.WithCompression(rwc)
 	}
-	conn = frpNet.WrapReadWriteCloserToConn(rwc, conn)
+	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
 
 	workConn := conn
 	readCh := make(chan *msg.UDPPacket, 1024)

+ 23 - 7
client/proxy/udp.go

@@ -17,20 +17,24 @@ package proxy
 import (
 	"io"
 	"net"
+	"reflect"
 	"strconv"
 	"time"
 
 	"github.com/fatedier/golib/errors"
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-// UDP
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy)
+}
+
 type UDPProxy struct {
 	*BaseProxy
 
@@ -42,6 +46,18 @@ type UDPProxy struct {
 	// include msg.UDPPacket and msg.Ping
 	sendCh   chan msg.Message
 	workConn net.Conn
+	closed   bool
+}
+
+func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.UDPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &UDPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
 }
 
 func (pxy *UDPProxy) Run() (err error) {
@@ -79,12 +95,12 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
 	var rwc io.ReadWriteCloser = conn
 	var err error
 	if pxy.limiter != nil {
-		rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
+		rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
 			return conn.Close()
 		})
 	}
 	if pxy.cfg.UseEncryption {
-		rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
 		if err != nil {
 			conn.Close()
 			xl.Error("create encryption stream error: %v", err)
@@ -92,9 +108,9 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
 		}
 	}
 	if pxy.cfg.UseCompression {
-		rwc = frpIo.WithCompression(rwc)
+		rwc = libio.WithCompression(rwc)
 	}
-	conn = frpNet.WrapReadWriteCloserToConn(rwc, conn)
+	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
 
 	pxy.mu.Lock()
 	pxy.workConn = conn

+ 20 - 23
client/proxy/xtcp.go

@@ -17,6 +17,7 @@ package proxy
 import (
 	"io"
 	"net"
+	"reflect"
 	"time"
 
 	fmux "github.com/hashicorp/yamux"
@@ -25,32 +26,28 @@ import (
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
-	plugin "github.com/fatedier/frp/pkg/plugin/client"
 	"github.com/fatedier/frp/pkg/transport"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-// XTCP
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
+}
+
 type XTCPProxy struct {
 	*BaseProxy
 
-	cfg         *config.XTCPProxyConf
-	proxyPlugin plugin.Plugin
+	cfg *config.XTCPProxyConf
 }
 
-func (pxy *XTCPProxy) Run() (err error) {
-	if pxy.cfg.Plugin != "" {
-		pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams)
-		if err != nil {
-			return
-		}
+func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.XTCPProxyConf)
+	if !ok {
+		return nil
 	}
-	return
-}
-
-func (pxy *XTCPProxy) Close() {
-	if pxy.proxyPlugin != nil {
-		pxy.proxyPlugin.Close()
+	return &XTCPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
 	}
 }
 
@@ -64,6 +61,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
 		return
 	}
 
+	xl.Trace("nathole prepare start")
 	prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer})
 	if err != nil {
 		xl.Warn("nathole prepare error: %v", err)
@@ -83,6 +81,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
 		AssistedAddrs: prepareResult.AssistedAddrs,
 	}
 
+	xl.Trace("nathole exchange info start")
 	natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second)
 	if err != nil {
 		xl.Warn("nathole exchange info error: %v", err)
@@ -132,7 +131,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 	}
 	defer lConn.Close()
 
-	remote, err := frpNet.NewKCPConnFromUDP(lConn, true, raddr.String())
+	remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
 	if err != nil {
 		xl.Warn("create kcp connection from udp connection error: %v", err)
 		return
@@ -140,7 +139,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = 10 * time.Second
-	fmuxCfg.MaxStreamWindowSize = 2 * 1024 * 1024
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	fmuxCfg.LogOutput = io.Discard
 	session, err := fmux.Server(remote, fmuxCfg)
 	if err != nil {
@@ -155,8 +154,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 			xl.Error("accept connection error: %v", err)
 			return
 		}
-		go HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-			muxConn, []byte(pxy.cfg.Sk), startWorkConnMsg)
+		go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Sk))
 	}
 }
 
@@ -194,7 +192,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
 			_ = c.CloseWithError(0, "")
 			return
 		}
-		go HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
-			frpNet.QuicStreamToNetConn(stream, c), []byte(pxy.cfg.Sk), startWorkConnMsg)
+		go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Sk))
 	}
 }

+ 9 - 6
client/service.go

@@ -39,7 +39,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/log"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "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/xlog"
@@ -369,7 +369,8 @@ func (cm *ConnectionManager) OpenConnection() error {
 		}
 		tlsConfig.NextProtos = []string{"frp"}
 
-		conn, err := quic.DialAddr(
+		conn, err := quic.DialAddrContext(
+			cm.ctx,
 			net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
 			tlsConfig, &quic.Config{
 				MaxIdleTimeout:     time.Duration(cm.cfg.QUICMaxIdleTimeout) * time.Second,
@@ -395,6 +396,7 @@ func (cm *ConnectionManager) OpenConnection() error {
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second
 	fmuxCfg.LogOutput = io.Discard
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	session, err := fmux.Client(conn, fmuxCfg)
 	if err != nil {
 		return err
@@ -409,7 +411,7 @@ func (cm *ConnectionManager) Connect() (net.Conn, error) {
 		if err != nil {
 			return nil, err
 		}
-		return frpNet.QuicStreamToNetConn(stream, cm.quicConn), nil
+		return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil
 	} else if cm.muxSession != nil {
 		stream, err := cm.muxSession.OpenStream()
 		if err != nil {
@@ -451,7 +453,7 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 	protocol := cm.cfg.Protocol
 	if protocol == "websocket" {
 		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket()}))
 	}
 	if cm.cfg.ConnectServerLocalIP != "" {
 		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
@@ -464,10 +466,11 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 		libdial.WithProxyAuth(auth),
 		libdial.WithTLSConfig(tlsConfig),
 		libdial.WithAfterHook(libdial.AfterHook{
-			Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
+			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
 		}),
 	)
-	conn, err := libdial.Dial(
+	conn, err := libdial.DialContext(
+		cm.ctx,
 		net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
 		dialOptions...,
 	)

+ 25 - 10
client/visitor/stcp.go

@@ -20,7 +20,7 @@ import (
 	"strconv"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
@@ -35,17 +35,20 @@ type STCPVisitor struct {
 }
 
 func (sv *STCPVisitor) Run() (err error) {
-	sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
-	if err != nil {
-		return
+	if sv.cfg.BindPort > 0 {
+		sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
+		if err != nil {
+			return
+		}
+		go sv.worker()
 	}
 
-	go sv.worker()
+	go sv.internalConnWorker()
 	return
 }
 
 func (sv *STCPVisitor) Close() {
-	sv.l.Close()
+	sv.BaseVisitor.Close()
 }
 
 func (sv *STCPVisitor) worker() {
@@ -56,7 +59,18 @@ func (sv *STCPVisitor) worker() {
 			xl.Warn("stcp local listener closed")
 			return
 		}
+		go sv.handleConn(conn)
+	}
+}
 
+func (sv *STCPVisitor) internalConnWorker() {
+	xl := xlog.FromContextSafe(sv.ctx)
+	for {
+		conn, err := sv.internalLn.Accept()
+		if err != nil {
+			xl.Warn("stcp internal listener closed")
+			return
+		}
 		go sv.handleConn(conn)
 	}
 }
@@ -66,7 +80,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 	defer userConn.Close()
 
 	xl.Debug("get a new stcp user connection")
-	visitorConn, err := sv.connectServer()
+	visitorConn, err := sv.helper.ConnectServer()
 	if err != nil {
 		return
 	}
@@ -74,6 +88,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 
 	now := time.Now().Unix()
 	newVisitorConnMsg := &msg.NewVisitorConn{
+		RunID:          sv.helper.RunID(),
 		ProxyName:      sv.cfg.ServerName,
 		SignKey:        util.GetAuthKey(sv.cfg.Sk, now),
 		Timestamp:      now,
@@ -103,7 +118,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 	var remote io.ReadWriteCloser
 	remote = visitorConn
 	if sv.cfg.UseEncryption {
-		remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
+		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
@@ -111,8 +126,8 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 	}
 
 	if sv.cfg.UseCompression {
-		remote = frpIo.WithCompression(remote)
+		remote = libio.WithCompression(remote)
 	}
 
-	frpIo.Join(userConn, remote)
+	libio.Join(userConn, remote)
 }

+ 8 - 6
client/visitor/sudp.go

@@ -23,12 +23,12 @@ import (
 	"time"
 
 	"github.com/fatedier/golib/errors"
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
@@ -199,13 +199,14 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
 
 func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
 	xl := xlog.FromContextSafe(sv.ctx)
-	visitorConn, err := sv.connectServer()
+	visitorConn, err := sv.helper.ConnectServer()
 	if err != nil {
 		return nil, fmt.Errorf("frpc connect frps error: %v", err)
 	}
 
 	now := time.Now().Unix()
 	newVisitorConnMsg := &msg.NewVisitorConn{
+		RunID:          sv.helper.RunID(),
 		ProxyName:      sv.cfg.ServerName,
 		SignKey:        util.GetAuthKey(sv.cfg.Sk, now),
 		Timestamp:      now,
@@ -232,16 +233,16 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
 	var remote io.ReadWriteCloser
 	remote = visitorConn
 	if sv.cfg.UseEncryption {
-		remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
+		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return nil, err
 		}
 	}
 	if sv.cfg.UseCompression {
-		remote = frpIo.WithCompression(remote)
+		remote = libio.WithCompression(remote)
 	}
-	return frpNet.WrapReadWriteCloserToConn(remote, visitorConn), nil
+	return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
 }
 
 func (sv *SUDPVisitor) Close() {
@@ -254,6 +255,7 @@ func (sv *SUDPVisitor) Close() {
 	default:
 		close(sv.checkCloseCh)
 	}
+	sv.BaseVisitor.Close()
 	if sv.udpConn != nil {
 		sv.udpConn.Close()
 	}

+ 38 - 11
client/visitor/visitor.go

@@ -21,12 +21,27 @@ import (
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/transport"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
+// Helper wrapps some functions for visitor to use.
+type Helper interface {
+	// ConnectServer directly connects to the frp server.
+	ConnectServer() (net.Conn, error)
+	// TransferConn transfers the connection to another visitor.
+	TransferConn(string, net.Conn) error
+	// MsgTransporter returns the message transporter that is used to send and receive messages
+	// to the frp server through the controller.
+	MsgTransporter() transport.MessageTransporter
+	// RunID returns the run id of current controller.
+	RunID() string
+}
+
 // Visitor is used for forward traffics from local port tot remote service.
 type Visitor interface {
 	Run() error
+	AcceptConn(conn net.Conn) error
 	Close()
 }
 
@@ -34,15 +49,14 @@ func NewVisitor(
 	ctx context.Context,
 	cfg config.VisitorConf,
 	clientCfg config.ClientCommonConf,
-	connectServer func() (net.Conn, error),
-	msgTransporter transport.MessageTransporter,
+	helper Helper,
 ) (visitor Visitor) {
-	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseInfo().ProxyName)
+	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().ProxyName)
 	baseVisitor := BaseVisitor{
-		clientCfg:      clientCfg,
-		connectServer:  connectServer,
-		msgTransporter: msgTransporter,
-		ctx:            xlog.NewContext(ctx, xl),
+		clientCfg:  clientCfg,
+		helper:     helper,
+		ctx:        xlog.NewContext(ctx, xl),
+		internalLn: utilnet.NewInternalListener(),
 	}
 	switch cfg := cfg.(type) {
 	case *config.STCPVisitorConf:
@@ -67,11 +81,24 @@ func NewVisitor(
 }
 
 type BaseVisitor struct {
-	clientCfg      config.ClientCommonConf
-	connectServer  func() (net.Conn, error)
-	msgTransporter transport.MessageTransporter
-	l              net.Listener
+	clientCfg  config.ClientCommonConf
+	helper     Helper
+	l          net.Listener
+	internalLn *utilnet.InternalListener
 
 	mu  sync.RWMutex
 	ctx context.Context
 }
+
+func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
+	return v.internalLn.PutConn(conn)
+}
+
+func (v *BaseVisitor) Close() {
+	if v.l != nil {
+		v.l.Close()
+	}
+	if v.internalLn != nil {
+		v.internalLn.Close()
+	}
+}

+ 70 - 31
client/visitor/visitor_manager.go

@@ -16,7 +16,9 @@ package visitor
 
 import (
 	"context"
+	"fmt"
 	"net"
+	"reflect"
 	"sync"
 	"time"
 
@@ -26,15 +28,14 @@ import (
 )
 
 type Manager struct {
-	clientCfg      config.ClientCommonConf
-	connectServer  func() (net.Conn, error)
-	msgTransporter transport.MessageTransporter
-	cfgs           map[string]config.VisitorConf
-	visitors       map[string]Visitor
+	clientCfg config.ClientCommonConf
+	cfgs      map[string]config.VisitorConf
+	visitors  map[string]Visitor
+	helper    Helper
 
 	checkInterval time.Duration
 
-	mu  sync.Mutex
+	mu  sync.RWMutex
 	ctx context.Context
 
 	stopCh chan struct{}
@@ -42,20 +43,26 @@ type Manager struct {
 
 func NewManager(
 	ctx context.Context,
+	runID string,
 	clientCfg config.ClientCommonConf,
 	connectServer func() (net.Conn, error),
 	msgTransporter transport.MessageTransporter,
 ) *Manager {
-	return &Manager{
-		clientCfg:      clientCfg,
-		connectServer:  connectServer,
-		msgTransporter: msgTransporter,
-		cfgs:           make(map[string]config.VisitorConf),
-		visitors:       make(map[string]Visitor),
-		checkInterval:  10 * time.Second,
-		ctx:            ctx,
-		stopCh:         make(chan struct{}),
+	m := &Manager{
+		clientCfg:     clientCfg,
+		cfgs:          make(map[string]config.VisitorConf),
+		visitors:      make(map[string]Visitor),
+		checkInterval: 10 * time.Second,
+		ctx:           ctx,
+		stopCh:        make(chan struct{}),
 	}
+	m.helper = &visitorHelperImpl{
+		connectServerFn: connectServer,
+		msgTransporter:  msgTransporter,
+		transferConnFn:  m.TransferConn,
+		runID:           runID,
+	}
+	return m
 }
 
 func (vm *Manager) Run() {
@@ -72,7 +79,7 @@ func (vm *Manager) Run() {
 		case <-ticker.C:
 			vm.mu.Lock()
 			for _, cfg := range vm.cfgs {
-				name := cfg.GetBaseInfo().ProxyName
+				name := cfg.GetBaseConfig().ProxyName
 				if _, exist := vm.visitors[name]; !exist {
 					xl.Info("try to start visitor [%s]", name)
 					_ = vm.startVisitor(cfg)
@@ -83,11 +90,24 @@ func (vm *Manager) Run() {
 	}
 }
 
+func (vm *Manager) Close() {
+	vm.mu.Lock()
+	defer vm.mu.Unlock()
+	for _, v := range vm.visitors {
+		v.Close()
+	}
+	select {
+	case <-vm.stopCh:
+	default:
+		close(vm.stopCh)
+	}
+}
+
 // Hold lock before calling this function.
 func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) {
 	xl := xlog.FromContextSafe(vm.ctx)
-	name := cfg.GetBaseInfo().ProxyName
-	visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.connectServer, vm.msgTransporter)
+	name := cfg.GetBaseConfig().ProxyName
+	visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
 	err = visitor.Run()
 	if err != nil {
 		xl.Warn("start error: %v", err)
@@ -107,9 +127,7 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
 	for name, oldCfg := range vm.cfgs {
 		del := false
 		cfg, ok := cfgs[name]
-		if !ok {
-			del = true
-		} else if !oldCfg.Compare(cfg) {
+		if !ok || !reflect.DeepEqual(oldCfg, cfg) {
 			del = true
 		}
 
@@ -139,15 +157,36 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
 	}
 }
 
-func (vm *Manager) Close() {
-	vm.mu.Lock()
-	defer vm.mu.Unlock()
-	for _, v := range vm.visitors {
-		v.Close()
-	}
-	select {
-	case <-vm.stopCh:
-	default:
-		close(vm.stopCh)
+// TransferConn transfers a connection to a visitor.
+func (vm *Manager) TransferConn(name string, conn net.Conn) error {
+	vm.mu.RLock()
+	defer vm.mu.RUnlock()
+	v, ok := vm.visitors[name]
+	if !ok {
+		return fmt.Errorf("visitor [%s] not found", name)
 	}
+	return v.AcceptConn(conn)
+}
+
+type visitorHelperImpl struct {
+	connectServerFn func() (net.Conn, error)
+	msgTransporter  transport.MessageTransporter
+	transferConnFn  func(name string, conn net.Conn) error
+	runID           string
+}
+
+func (v *visitorHelperImpl) ConnectServer() (net.Conn, error) {
+	return v.connectServerFn()
+}
+
+func (v *visitorHelperImpl) TransferConn(name string, conn net.Conn) error {
+	return v.transferConnFn(name, conn)
+}
+
+func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter {
+	return v.msgTransporter
+}
+
+func (v *visitorHelperImpl) RunID() string {
+	return v.runID
 }

+ 64 - 19
client/visitor/xtcp.go

@@ -24,7 +24,7 @@ import (
 	"sync"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 	fmux "github.com/hashicorp/yamux"
 	quic "github.com/quic-go/quic-go"
 	"golang.org/x/time/rate"
@@ -33,7 +33,7 @@ import (
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	"github.com/fatedier/frp/pkg/transport"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
@@ -59,12 +59,15 @@ func (sv *XTCPVisitor) Run() (err error) {
 		sv.session = NewQUICTunnelSession(&sv.clientCfg)
 	}
 
-	sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
-	if err != nil {
-		return
+	if sv.cfg.BindPort > 0 {
+		sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort)))
+		if err != nil {
+			return
+		}
+		go sv.worker()
 	}
 
-	go sv.worker()
+	go sv.internalConnWorker()
 	go sv.processTunnelStartEvents()
 	if sv.cfg.KeepTunnelOpen {
 		sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
@@ -74,8 +77,12 @@ func (sv *XTCPVisitor) Run() (err error) {
 }
 
 func (sv *XTCPVisitor) Close() {
-	sv.l.Close()
-	sv.cancel()
+	sv.mu.Lock()
+	defer sv.mu.Unlock()
+	sv.BaseVisitor.Close()
+	if sv.cancel != nil {
+		sv.cancel()
+	}
 	if sv.session != nil {
 		sv.session.Close()
 	}
@@ -89,7 +96,18 @@ func (sv *XTCPVisitor) worker() {
 			xl.Warn("xtcp local listener closed")
 			return
 		}
+		go sv.handleConn(conn)
+	}
+}
 
+func (sv *XTCPVisitor) internalConnWorker() {
+	xl := xlog.FromContextSafe(sv.ctx)
+	for {
+		conn, err := sv.internalLn.Accept()
+		if err != nil {
+			xl.Warn("xtcp internal listener closed")
+			return
+		}
 		go sv.handleConn(conn)
 	}
 }
@@ -139,31 +157,53 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
 
 func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
 	xl := xlog.FromContextSafe(sv.ctx)
-	defer userConn.Close()
+	isConnTrasfered := false
+	defer func() {
+		if !isConnTrasfered {
+			userConn.Close()
+		}
+	}()
 
 	xl.Debug("get a new xtcp user connection")
 
 	// Open a tunnel connection to the server. If there is already a successful hole-punching connection,
 	// it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout.
-	tunnelConn, err := sv.openTunnel()
+	ctx := context.Background()
+	if sv.cfg.FallbackTo != "" {
+		timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond)
+		defer cancel()
+		ctx = timeoutCtx
+	}
+	tunnelConn, err := sv.openTunnel(ctx)
 	if err != nil {
 		xl.Error("open tunnel error: %v", err)
+		// no fallback, just return
+		if sv.cfg.FallbackTo == "" {
+			return
+		}
+
+		xl.Debug("try to transfer connection to visitor: %s", sv.cfg.FallbackTo)
+		if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil {
+			xl.Error("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err)
+			return
+		}
+		isConnTrasfered = true
 		return
 	}
 
 	var muxConnRWCloser io.ReadWriteCloser = tunnelConn
 	if sv.cfg.UseEncryption {
-		muxConnRWCloser, err = frpIo.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
+		muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
 	if sv.cfg.UseCompression {
-		muxConnRWCloser = frpIo.WithCompression(muxConnRWCloser)
+		muxConnRWCloser = libio.WithCompression(muxConnRWCloser)
 	}
 
-	_, _, errs := frpIo.Join(userConn, muxConnRWCloser)
+	_, _, errs := libio.Join(userConn, muxConnRWCloser)
 	xl.Debug("join connections closed")
 	if len(errs) > 0 {
 		xl.Trace("join connections errors: %v", errs)
@@ -171,7 +211,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
 }
 
 // openTunnel will open a tunnel connection to the target server.
-func (sv *XTCPVisitor) openTunnel() (conn net.Conn, err error) {
+func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
 	xl := xlog.FromContextSafe(sv.ctx)
 	ticker := time.NewTicker(500 * time.Millisecond)
 	defer ticker.Stop()
@@ -185,6 +225,8 @@ func (sv *XTCPVisitor) openTunnel() (conn net.Conn, err error) {
 		select {
 		case <-sv.ctx.Done():
 			return nil, sv.ctx.Err()
+		case <-ctx.Done():
+			return nil, ctx.Err()
 		case <-immediateTrigger:
 			conn, err = sv.getTunnelConn()
 		case <-ticker.C:
@@ -224,11 +266,13 @@ func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) {
 // 4. Create a tunnel session using an underlying UDP connection.
 func (sv *XTCPVisitor) makeNatHole() {
 	xl := xlog.FromContextSafe(sv.ctx)
-	if err := nathole.PreCheck(sv.ctx, sv.msgTransporter, sv.cfg.ServerName, 5*time.Second); err != nil {
+	xl.Trace("makeNatHole start")
+	if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
 		xl.Warn("nathole precheck error: %v", err)
 		return
 	}
 
+	xl.Trace("nathole prepare start")
 	prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer})
 	if err != nil {
 		xl.Warn("nathole prepare error: %v", err)
@@ -252,7 +296,8 @@ func (sv *XTCPVisitor) makeNatHole() {
 		AssistedAddrs: prepareResult.AssistedAddrs,
 	}
 
-	natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.msgTransporter, transactionID, natHoleVisitorMsg, 5*time.Second)
+	xl.Trace("nathole exchange info start")
+	natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second)
 	if err != nil {
 		listenConn.Close()
 		xl.Warn("nathole exchange info error: %v", err)
@@ -302,14 +347,14 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
 	if err != nil {
 		return fmt.Errorf("dial udp error: %v", err)
 	}
-	remote, err := frpNet.NewKCPConnFromUDP(lConn, true, raddr.String())
+	remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
 	if err != nil {
 		return fmt.Errorf("create kcp connection from udp connection error: %v", err)
 	}
 
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = 10 * time.Second
-	fmuxCfg.MaxStreamWindowSize = 2 * 1024 * 1024
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	fmuxCfg.LogOutput = io.Discard
 	session, err := fmux.Client(remote, fmuxCfg)
 	if err != nil {
@@ -393,7 +438,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
 	if err != nil {
 		return nil, err
 	}
-	return frpNet.QuicStreamToNetConn(stream, session), nil
+	return utilnet.QuicStreamToNetConn(stream, session), nil
 }
 
 func (qs *QUICTunnelSession) Close() {

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

@@ -79,7 +79,7 @@ var httpCmd = &cobra.Command{
 		}
 		cfg.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.CheckForCli()
+		err = cfg.ValidateForClient()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

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

@@ -71,7 +71,7 @@ var httpsCmd = &cobra.Command{
 		}
 		cfg.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.CheckForCli()
+		err = cfg.ValidateForClient()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

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

@@ -94,7 +94,7 @@ func RegisterCommonFlags(cmd *cobra.Command) {
 	cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path")
 	cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days")
 	cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
-	cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", false, "enable frpc tls")
+	cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", true, "enable frpc tls")
 	cmd.PersistentFlags().StringVarP(&dnsServer, "dns_server", "", "", "specify dns server instead of using system default one")
 }
 
@@ -117,7 +117,6 @@ var rootCmd = &cobra.Command{
 		// Do not show command usage here.
 		err := runClient(cfgFile)
 		if err != nil {
-			fmt.Println(err)
 			os.Exit(1)
 		}
 		return nil
@@ -199,6 +198,7 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
 func runClient(cfgFilePath string) error {
 	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
 	if err != nil {
+		fmt.Println(err)
 		return err
 	}
 	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
@@ -214,8 +214,8 @@ func startService(
 		cfg.LogMaxDays, cfg.DisableLogColor)
 
 	if cfgFile != "" {
-		log.Trace("start frpc service for config file [%s]", cfgFile)
-		defer log.Trace("frpc service for config file [%s] stopped", cfgFile)
+		log.Info("start frpc service for config file [%s]", cfgFile)
+		defer log.Info("frpc service for config file [%s] stopped", cfgFile)
 	}
 	svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
 	if errRet != nil {

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

@@ -78,7 +78,7 @@ var stcpCmd = &cobra.Command{
 				os.Exit(1)
 			}
 			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.CheckForCli()
+			err = cfg.ValidateForClient()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
@@ -95,7 +95,7 @@ var stcpCmd = &cobra.Command{
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Check()
+			err = cfg.Validate()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)

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

@@ -78,7 +78,7 @@ var sudpCmd = &cobra.Command{
 				os.Exit(1)
 			}
 			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.CheckForCli()
+			err = cfg.ValidateForClient()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
@@ -95,7 +95,7 @@ var sudpCmd = &cobra.Command{
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Check()
+			err = cfg.Validate()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)

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

@@ -68,7 +68,7 @@ var tcpCmd = &cobra.Command{
 		}
 		cfg.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.CheckForCli()
+		err = cfg.ValidateForClient()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

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

@@ -73,7 +73,7 @@ var tcpMuxCmd = &cobra.Command{
 		}
 		cfg.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.CheckForCli()
+		err = cfg.ValidateForClient()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

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

@@ -68,7 +68,7 @@ var udpCmd = &cobra.Command{
 		}
 		cfg.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.CheckForCli()
+		err = cfg.ValidateForClient()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

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

@@ -78,7 +78,7 @@ var xtcpCmd = &cobra.Command{
 				os.Exit(1)
 			}
 			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.CheckForCli()
+			err = cfg.ValidateForClient()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
@@ -95,7 +95,7 @@ var xtcpCmd = &cobra.Command{
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Check()
+			err = cfg.Validate()
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)

+ 20 - 4
conf/frpc_full.ini

@@ -107,7 +107,8 @@ connect_server_local_ip = 0.0.0.0
 # quic_max_idle_timeout = 30
 # quic_max_incoming_streams = 100000
 
-# if tls_enable is true, frpc will connect frps by tls
+# If tls_enable is true, frpc will connect frps by tls.
+# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
 tls_enable = true
 
 # tls_cert_file = client.crt
@@ -140,9 +141,10 @@ udp_packet_size = 1500
 # include other config files for proxies.
 # includes = ./confd/*.ini
 
-# By default, frpc will connect frps with first custom byte if tls is enabled.
-# If DisableCustomTLSFirstByte is true, frpc will not send that custom byte.
-disable_custom_tls_first_byte = false
+# If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
+# first custom byte when tls is enabled.
+# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
+disable_custom_tls_first_byte = true
 
 # Enable golang pprof handlers in admin listener.
 # Admin port must be set first.
@@ -326,6 +328,9 @@ local_ip = 127.0.0.1
 local_port = 22
 use_encryption = false
 use_compression = false
+# If not empty, only visitors from specified users can connect.
+# Otherwise, visitors from same user can connect. '*' means allow all users.
+allow_users = *
 
 # user of frpc should be same in both stcp server and stcp visitor
 [secret_tcp_visitor]
@@ -337,6 +342,8 @@ server_name = secret_tcp
 sk = abcdefg
 # connect this address to visitor stcp server
 bind_addr = 127.0.0.1
+# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
+# other visitors. (This is not supported for SUDP now)
 bind_port = 9000
 use_encryption = false
 use_compression = false
@@ -348,13 +355,20 @@ local_ip = 127.0.0.1
 local_port = 22
 use_encryption = false
 use_compression = false
+# If not empty, only visitors from specified users can connect.
+# Otherwise, visitors from same user can connect. '*' means allow all users.
+allow_users = user1, user2
 
 [p2p_tcp_visitor]
 role = visitor
 type = xtcp
+# if the server user is not set, it defaults to the current user
+server_user = user1
 server_name = p2p_tcp
 sk = abcdefg
 bind_addr = 127.0.0.1
+# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from
+# other visitors. (This is not supported for SUDP now)
 bind_port = 9001
 use_encryption = false
 use_compression = false
@@ -363,6 +377,8 @@ keep_tunnel_open = false
 # effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
 max_retries_an_hour = 8
 min_retry_interval = 90
+# fallback_to = stcp_visitor
+# fallback_timeout_ms = 500
 
 [tcpmuxhttpconnect]
 type = tcpmux

+ 63 - 0
hack/download.sh

@@ -0,0 +1,63 @@
+#!/bin/sh
+
+OS="$(go env GOOS)"
+ARCH="$(go env GOARCH)"
+
+if [ "${TARGET_OS}" ]; then
+  OS="${TARGET_OS}"
+fi
+if [ "${TARGET_ARCH}" ]; then
+  ARCH="${TARGET_ARCH}"
+fi
+
+# Determine the latest version by version number ignoring alpha, beta, and rc versions.
+if [ "${FRP_VERSION}" = "" ] ; then
+  FRP_VERSION="$(curl -sL https://github.com/fatedier/frp/releases | \
+                  grep -o 'releases/tag/v[0-9]*.[0-9]*.[0-9]*"' | sort -V | \
+                  tail -1 | awk -F'/' '{ print $3}')"
+  FRP_VERSION="${FRP_VERSION%?}"
+  FRP_VERSION="${FRP_VERSION#?}"
+fi
+
+if [ "${FRP_VERSION}" = "" ] ; then
+  printf "Unable to get latest frp version. Set FRP_VERSION env var and re-run. For example: export FRP_VERSION=1.0.0"
+  exit 1;
+fi
+
+SUFFIX=".tar.gz"
+if [ "${OS}" = "windows" ] ; then
+  SUFFIX=".zip"
+fi
+NAME="frp_${FRP_VERSION}_${OS}_${ARCH}${SUFFIX}"
+DIR_NAME="frp_${FRP_VERSION}_${OS}_${ARCH}"
+URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${NAME}"
+
+download_and_extract() {
+  printf "Downloading %s from %s ...\n" "$NAME" "${URL}"
+  if ! curl -o /dev/null -sIf "${URL}"; then
+    printf "\n%s is not found, please specify a valid FRP_VERSION\n" "${URL}"
+    exit 1
+  fi
+  curl -fsLO "${URL}"
+  filename=$NAME
+
+  if [ "${OS}" = "windows" ]; then
+    unzip "${filename}"
+  else
+    tar -xzf "${filename}"
+  fi
+  rm "${filename}"
+
+  if [ "${TARGET_DIRNAME}" ]; then
+    mv "${DIR_NAME}" "${TARGET_DIRNAME}"
+    DIR_NAME="${TARGET_DIRNAME}"
+  fi
+}
+
+download_and_extract
+
+printf ""
+printf "\nfrp %s Download Complete!\n" "$FRP_VERSION"
+printf "\n"
+printf "frp has been successfully downloaded into the %s folder on your system.\n" "$DIR_NAME"
+printf "\n"

+ 18 - 8
hack/run-e2e.sh

@@ -1,20 +1,30 @@
-#!/usr/bin/env bash
+#!/bin/sh
 
-ROOT=$(unset CDPATH && cd $(dirname "${BASH_SOURCE[0]}")/.. && pwd)
+SCRIPT=$(readlink -f "$0")
+ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
 
-which ginkgo &> /dev/null
-if [ $? -ne 0 ]; then
+ginkgo_command=$(which ginkgo 2>/dev/null)
+if [ -z "$ginkgo_command" ]; then
     echo "ginkgo not found, try to install..."
     go install github.com/onsi/ginkgo/v2/ginkgo@v2.8.3
 fi
 
 debug=false
-if [ x${DEBUG} == x"true" ]; then
+if [ "x${DEBUG}" = "xtrue" ]; then
     debug=true
 fi
 logLevel=debug
-if [ x${LOG_LEVEL} != x"" ]; then
-    logLevel=${LOG_LEVEL}
+if [ "${LOG_LEVEL}" ]; then
+    logLevel="${LOG_LEVEL}"
 fi
 
-ginkgo -nodes=8 --poll-progress-after=30s ${ROOT}/test/e2e -- -frpc-path=${ROOT}/bin/frpc -frps-path=${ROOT}/bin/frps -log-level=${logLevel} -debug=${debug}
+frpcPath=${ROOT}/bin/frpc
+if [ "${FRPC_PATH}" ]; then
+    frpcPath="${FRPC_PATH}"
+fi
+frpsPath=${ROOT}/bin/frps
+if [ "${FRPS_PATH}" ]; then
+    frpsPath="${FRPS_PATH}"
+fi
+
+ginkgo -nodes=8 --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}

+ 33 - 29
pkg/config/client.go

@@ -127,6 +127,7 @@ type ClientCommonConf struct {
 	// TLSEnable specifies whether or not TLS should be used when communicating
 	// with the server. If "tls_cert_file" and "tls_key_file" are valid,
 	// client will load the supplied tls configuration.
+	// Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
 	TLSEnable bool `ini:"tls_enable" json:"tls_enable"`
 	// TLSCertPath specifies the path of the cert file that client will
 	// load. It only works when "tls_enable" is true and "tls_key_file" is valid.
@@ -142,8 +143,9 @@ type ClientCommonConf struct {
 	// TLSServerName specifies the custom server name of tls certificate. By
 	// default, server name if same to ServerAddr.
 	TLSServerName string `ini:"tls_server_name" json:"tls_server_name"`
-	// By default, frpc will connect frps with first custom byte if tls is enabled.
-	// If DisableCustomTLSFirstByte is true, frpc will not send that custom byte.
+	// If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the
+	// first custom byte when tls is enabled.
+	// Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
 	DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
 	// HeartBeatInterval specifies at what interval heartbeats are sent to the
 	// server, in seconds. It is not recommended to change this value. By
@@ -168,32 +170,34 @@ type ClientCommonConf struct {
 // GetDefaultClientConf returns a client configuration with default values.
 func GetDefaultClientConf() ClientCommonConf {
 	return ClientCommonConf{
-		ClientConfig:            auth.GetDefaultClientConf(),
-		ServerAddr:              "0.0.0.0",
-		ServerPort:              7000,
-		NatHoleSTUNServer:       "stun.easyvoip.com:3478",
-		DialServerTimeout:       10,
-		DialServerKeepAlive:     7200,
-		HTTPProxy:               os.Getenv("http_proxy"),
-		LogFile:                 "console",
-		LogWay:                  "console",
-		LogLevel:                "info",
-		LogMaxDays:              3,
-		AdminAddr:               "127.0.0.1",
-		PoolCount:               1,
-		TCPMux:                  true,
-		TCPMuxKeepaliveInterval: 60,
-		LoginFailExit:           true,
-		Start:                   make([]string, 0),
-		Protocol:                "tcp",
-		QUICKeepalivePeriod:     10,
-		QUICMaxIdleTimeout:      30,
-		QUICMaxIncomingStreams:  100000,
-		HeartbeatInterval:       30,
-		HeartbeatTimeout:        90,
-		Metas:                   make(map[string]string),
-		UDPPacketSize:           1500,
-		IncludeConfigFiles:      make([]string, 0),
+		ClientConfig:              auth.GetDefaultClientConf(),
+		ServerAddr:                "0.0.0.0",
+		ServerPort:                7000,
+		NatHoleSTUNServer:         "stun.easyvoip.com:3478",
+		DialServerTimeout:         10,
+		DialServerKeepAlive:       7200,
+		HTTPProxy:                 os.Getenv("http_proxy"),
+		LogFile:                   "console",
+		LogWay:                    "console",
+		LogLevel:                  "info",
+		LogMaxDays:                3,
+		AdminAddr:                 "127.0.0.1",
+		PoolCount:                 1,
+		TCPMux:                    true,
+		TCPMuxKeepaliveInterval:   60,
+		LoginFailExit:             true,
+		Start:                     make([]string, 0),
+		Protocol:                  "tcp",
+		QUICKeepalivePeriod:       10,
+		QUICMaxIdleTimeout:        30,
+		QUICMaxIncomingStreams:    100000,
+		TLSEnable:                 true,
+		DisableCustomTLSFirstByte: true,
+		HeartbeatInterval:         30,
+		HeartbeatTimeout:          90,
+		Metas:                     make(map[string]string),
+		UDPPacketSize:             1500,
+		IncludeConfigFiles:        make([]string, 0),
 	}
 }
 
@@ -352,7 +356,7 @@ func LoadAllProxyConfsFromIni(
 		case "visitor":
 			newConf, newErr := NewVisitorConfFromIni(prefix, name, section)
 			if newErr != nil {
-				return nil, nil, newErr
+				return nil, nil, fmt.Errorf("failed to parse visitor %s, err: %v", name, newErr)
 			}
 			visitorConfs[prefix+name] = newConf
 		default:

+ 47 - 41
pkg/config/client_test.go

@@ -258,40 +258,41 @@ func Test_LoadClientCommonConf(t *testing.T) {
 				OidcTokenEndpointURL: "endpoint_url",
 			},
 		},
-		ServerAddr:              "0.0.0.9",
-		ServerPort:              7009,
-		NatHoleSTUNServer:       "stun.easyvoip.com:3478",
-		DialServerTimeout:       10,
-		DialServerKeepAlive:     7200,
-		HTTPProxy:               "http://user:passwd@192.168.1.128:8080",
-		LogFile:                 "./frpc.log9",
-		LogWay:                  "file",
-		LogLevel:                "info9",
-		LogMaxDays:              39,
-		DisableLogColor:         false,
-		AdminAddr:               "127.0.0.9",
-		AdminPort:               7409,
-		AdminUser:               "admin9",
-		AdminPwd:                "admin9",
-		AssetsDir:               "./static9",
-		PoolCount:               59,
-		TCPMux:                  true,
-		TCPMuxKeepaliveInterval: 60,
-		User:                    "your_name",
-		LoginFailExit:           true,
-		Protocol:                "tcp",
-		QUICKeepalivePeriod:     10,
-		QUICMaxIdleTimeout:      30,
-		QUICMaxIncomingStreams:  100000,
-		TLSEnable:               true,
-		TLSCertFile:             "client.crt",
-		TLSKeyFile:              "client.key",
-		TLSTrustedCaFile:        "ca.crt",
-		TLSServerName:           "example.com",
-		DNSServer:               "8.8.8.9",
-		Start:                   []string{"ssh", "dns"},
-		HeartbeatInterval:       39,
-		HeartbeatTimeout:        99,
+		ServerAddr:                "0.0.0.9",
+		ServerPort:                7009,
+		NatHoleSTUNServer:         "stun.easyvoip.com:3478",
+		DialServerTimeout:         10,
+		DialServerKeepAlive:       7200,
+		HTTPProxy:                 "http://user:passwd@192.168.1.128:8080",
+		LogFile:                   "./frpc.log9",
+		LogWay:                    "file",
+		LogLevel:                  "info9",
+		LogMaxDays:                39,
+		DisableLogColor:           false,
+		AdminAddr:                 "127.0.0.9",
+		AdminPort:                 7409,
+		AdminUser:                 "admin9",
+		AdminPwd:                  "admin9",
+		AssetsDir:                 "./static9",
+		PoolCount:                 59,
+		TCPMux:                    true,
+		TCPMuxKeepaliveInterval:   60,
+		User:                      "your_name",
+		LoginFailExit:             true,
+		Protocol:                  "tcp",
+		QUICKeepalivePeriod:       10,
+		QUICMaxIdleTimeout:        30,
+		QUICMaxIncomingStreams:    100000,
+		TLSEnable:                 true,
+		TLSCertFile:               "client.crt",
+		TLSKeyFile:                "client.key",
+		TLSTrustedCaFile:          "ca.crt",
+		TLSServerName:             "example.com",
+		DisableCustomTLSFirstByte: true,
+		DNSServer:                 "8.8.8.9",
+		Start:                     []string{"ssh", "dns"},
+		HeartbeatInterval:         39,
+		HeartbeatTimeout:          99,
 		Metas: map[string]string{
 			"var1": "123",
 			"var2": "234",
@@ -500,8 +501,10 @@ func Test_LoadClientBasicConf(t *testing.T) {
 				},
 				BandwidthLimitMode: BandwidthLimitModeClient,
 			},
-			Role: "server",
-			Sk:   "abcdefg",
+			RoleServerCommonConf: RoleServerCommonConf{
+				Role: "server",
+				Sk:   "abcdefg",
+			},
 		},
 		testUser + ".p2p_tcp": &XTCPProxyConf{
 			BaseProxyConf: BaseProxyConf{
@@ -513,8 +516,10 @@ func Test_LoadClientBasicConf(t *testing.T) {
 				},
 				BandwidthLimitMode: BandwidthLimitModeClient,
 			},
-			Role: "server",
-			Sk:   "abcdefg",
+			RoleServerCommonConf: RoleServerCommonConf{
+				Role: "server",
+				Sk:   "abcdefg",
+			},
 		},
 		testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{
 			BaseProxyConf: BaseProxyConf{
@@ -661,9 +666,10 @@ func Test_LoadClientBasicConf(t *testing.T) {
 				BindAddr:   "127.0.0.1",
 				BindPort:   9001,
 			},
-			Protocol:         "quic",
-			MaxRetriesAnHour: 8,
-			MinRetryInterval: 90,
+			Protocol:          "quic",
+			MaxRetriesAnHour:  8,
+			MinRetryInterval:  90,
+			FallbackTimeoutMs: 1000,
 		},
 	}
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 208 - 401
pkg/config/proxy.go


+ 8 - 4
pkg/config/proxy_test.go

@@ -254,8 +254,10 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
 					},
 					BandwidthLimitMode: BandwidthLimitModeClient,
 				},
-				Role: "server",
-				Sk:   "abcdefg",
+				RoleServerCommonConf: RoleServerCommonConf{
+					Role: "server",
+					Sk:   "abcdefg",
+				},
 			},
 		},
 		{
@@ -279,8 +281,10 @@ func Test_Proxy_UnmarshalFromIni(t *testing.T) {
 					},
 					BandwidthLimitMode: BandwidthLimitModeClient,
 				},
-				Role: "server",
-				Sk:   "abcdefg",
+				RoleServerCommonConf: RoleServerCommonConf{
+					Role: "server",
+					Sk:   "abcdefg",
+				},
 			},
 		},
 		{

+ 0 - 2
pkg/config/server_test.go

@@ -36,7 +36,6 @@ func Test_LoadServerCommonConf(t *testing.T) {
 				[common]
 				bind_addr = 0.0.0.9
 				bind_port = 7009
-				bind_udp_port = 7008
 				kcp_bind_port = 7007
 				proxy_bind_addr = 127.0.0.9
 				vhost_http_port = 89
@@ -170,7 +169,6 @@ func Test_LoadServerCommonConf(t *testing.T) {
 				[common]
 				bind_addr = 0.0.0.9
 				bind_port = 7009
-				bind_udp_port = 7008
 			`),
 			expected: ServerCommonConf{
 				ServerConfig: auth.ServerConfig{

+ 43 - 93
pkg/config/visitor.go

@@ -34,10 +34,12 @@ var (
 )
 
 type VisitorConf interface {
-	GetBaseInfo() *BaseVisitorConf
-	Compare(cmp VisitorConf) bool
+	// GetBaseConfig returns the base config of visitor.
+	GetBaseConfig() *BaseVisitorConf
+	// UnmarshalFromIni unmarshals config from ini.
 	UnmarshalFromIni(prefix string, name string, section *ini.Section) error
-	Check() error
+	// Validate validates config.
+	Validate() error
 }
 
 type BaseVisitorConf struct {
@@ -47,9 +49,14 @@ type BaseVisitorConf struct {
 	UseCompression bool   `ini:"use_compression" json:"use_compression"`
 	Role           string `ini:"role" json:"role"`
 	Sk             string `ini:"sk" json:"sk"`
-	ServerName     string `ini:"server_name" json:"server_name"`
-	BindAddr       string `ini:"bind_addr" json:"bind_addr"`
-	BindPort       int    `ini:"bind_port" json:"bind_port"`
+	// if the server user is not set, it defaults to the current user
+	ServerUser string `ini:"server_user" json:"server_user"`
+	ServerName string `ini:"server_name" json:"server_name"`
+	BindAddr   string `ini:"bind_addr" json:"bind_addr"`
+	// BindPort is the port that visitor listens on.
+	// It can be less than 0, it means don't bind to the port and only receive connections redirected from
+	// other visitors. (This is not supported for SUDP now)
+	BindPort int `ini:"bind_port" json:"bind_port"`
 }
 
 type SUDPVisitorConf struct {
@@ -63,10 +70,12 @@ type STCPVisitorConf struct {
 type XTCPVisitorConf struct {
 	BaseVisitorConf `ini:",extends"`
 
-	Protocol         string `ini:"protocol" json:"protocol,omitempty"`
-	KeepTunnelOpen   bool   `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"`
-	MaxRetriesAnHour int    `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"`
-	MinRetryInterval int    `ini:"min_retry_interval" json:"min_retry_interval,omitempty"`
+	Protocol          string `ini:"protocol" json:"protocol,omitempty"`
+	KeepTunnelOpen    bool   `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"`
+	MaxRetriesAnHour  int    `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"`
+	MinRetryInterval  int    `ini:"min_retry_interval" json:"min_retry_interval,omitempty"`
+	FallbackTo        string `ini:"fallback_to" json:"fallback_to,omitempty"`
+	FallbackTimeoutMs int    `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"`
 }
 
 // DefaultVisitorConf creates a empty VisitorConf object by visitorType.
@@ -76,7 +85,6 @@ func DefaultVisitorConf(visitorType string) VisitorConf {
 	if !ok {
 		return nil
 	}
-
 	return reflect.New(v).Interface().(VisitorConf)
 }
 
@@ -86,19 +94,19 @@ func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (Vi
 	visitorType := section.Key("type").String()
 
 	if visitorType == "" {
-		return nil, fmt.Errorf("visitor [%s] type shouldn't be empty", name)
+		return nil, fmt.Errorf("type shouldn't be empty")
 	}
 
 	conf := DefaultVisitorConf(visitorType)
 	if conf == nil {
-		return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType)
+		return nil, fmt.Errorf("type [%s] error", visitorType)
 	}
 
 	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
-		return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType)
+		return nil, fmt.Errorf("type [%s] error", visitorType)
 	}
 
-	if err := conf.Check(); err != nil {
+	if err := conf.Validate(); err != nil {
 		return nil, err
 	}
 
@@ -106,26 +114,11 @@ func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (Vi
 }
 
 // Base
-func (cfg *BaseVisitorConf) GetBaseInfo() *BaseVisitorConf {
+func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf {
 	return cfg
 }
 
-func (cfg *BaseVisitorConf) compare(cmp *BaseVisitorConf) bool {
-	if cfg.ProxyName != cmp.ProxyName ||
-		cfg.ProxyType != cmp.ProxyType ||
-		cfg.UseEncryption != cmp.UseEncryption ||
-		cfg.UseCompression != cmp.UseCompression ||
-		cfg.Role != cmp.Role ||
-		cfg.Sk != cmp.Sk ||
-		cfg.ServerName != cmp.ServerName ||
-		cfg.BindAddr != cmp.BindAddr ||
-		cfg.BindPort != cmp.BindPort {
-		return false
-	}
-	return true
-}
-
-func (cfg *BaseVisitorConf) check() (err error) {
+func (cfg *BaseVisitorConf) validate() (err error) {
 	if cfg.Role != "visitor" {
 		err = fmt.Errorf("invalid role")
 		return
@@ -134,7 +127,9 @@ func (cfg *BaseVisitorConf) check() (err error) {
 		err = fmt.Errorf("bind_addr shouldn't be empty")
 		return
 	}
-	if cfg.BindPort <= 0 {
+	// BindPort can be less than 0, it means don't bind to the port and only receive connections redirected from
+	// other visitors
+	if cfg.BindPort == 0 {
 		err = fmt.Errorf("bind_port is required")
 		return
 	}
@@ -149,13 +144,16 @@ func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section
 	cfg.ProxyName = prefix + name
 
 	// server_name
-	cfg.ServerName = prefix + cfg.ServerName
+	if cfg.ServerUser == "" {
+		cfg.ServerName = prefix + cfg.ServerName
+	} else {
+		cfg.ServerName = cfg.ServerUser + "." + cfg.ServerName
+	}
 
 	// bind_addr
 	if cfg.BindAddr == "" {
 		cfg.BindAddr = "127.0.0.1"
 	}
-
 	return nil
 }
 
@@ -165,32 +163,16 @@ func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, sec
 		return err
 	}
 
-	err = cfg.GetBaseInfo().unmarshalFromIni(prefix, name, section)
+	err = cfg.GetBaseConfig().unmarshalFromIni(prefix, name, section)
 	if err != nil {
 		return err
 	}
-
 	return nil
 }
 
 // SUDP
 var _ VisitorConf = &SUDPVisitorConf{}
 
-func (cfg *SUDPVisitorConf) Compare(cmp VisitorConf) bool {
-	cmpConf, ok := cmp.(*SUDPVisitorConf)
-	if !ok {
-		return false
-	}
-
-	if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
-		return false
-	}
-
-	// Add custom login equal, if exists
-
-	return true
-}
-
 func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
 	if err != nil {
@@ -202,8 +184,8 @@ func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	return
 }
 
-func (cfg *SUDPVisitorConf) Check() (err error) {
-	if err = cfg.BaseVisitorConf.check(); err != nil {
+func (cfg *SUDPVisitorConf) Validate() (err error) {
+	if err = cfg.BaseVisitorConf.validate(); err != nil {
 		return
 	}
 
@@ -215,21 +197,6 @@ func (cfg *SUDPVisitorConf) Check() (err error) {
 // STCP
 var _ VisitorConf = &STCPVisitorConf{}
 
-func (cfg *STCPVisitorConf) Compare(cmp VisitorConf) bool {
-	cmpConf, ok := cmp.(*STCPVisitorConf)
-	if !ok {
-		return false
-	}
-
-	if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
-		return false
-	}
-
-	// Add custom login equal, if exists
-
-	return true
-}
-
 func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
 	if err != nil {
@@ -241,8 +208,8 @@ func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	return
 }
 
-func (cfg *STCPVisitorConf) Check() (err error) {
-	if err = cfg.BaseVisitorConf.check(); err != nil {
+func (cfg *STCPVisitorConf) Validate() (err error) {
+	if err = cfg.BaseVisitorConf.validate(); err != nil {
 		return
 	}
 
@@ -254,26 +221,6 @@ func (cfg *STCPVisitorConf) Check() (err error) {
 // XTCP
 var _ VisitorConf = &XTCPVisitorConf{}
 
-func (cfg *XTCPVisitorConf) Compare(cmp VisitorConf) bool {
-	cmpConf, ok := cmp.(*XTCPVisitorConf)
-	if !ok {
-		return false
-	}
-
-	if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) {
-		return false
-	}
-
-	// Add custom login equal, if exists
-	if cfg.Protocol != cmpConf.Protocol ||
-		cfg.KeepTunnelOpen != cmpConf.KeepTunnelOpen ||
-		cfg.MaxRetriesAnHour != cmpConf.MaxRetriesAnHour ||
-		cfg.MinRetryInterval != cmpConf.MinRetryInterval {
-		return false
-	}
-	return true
-}
-
 func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
 	if err != nil {
@@ -290,11 +237,14 @@ func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	if cfg.MinRetryInterval <= 0 {
 		cfg.MinRetryInterval = 90
 	}
+	if cfg.FallbackTimeoutMs <= 0 {
+		cfg.FallbackTimeoutMs = 1000
+	}
 	return
 }
 
-func (cfg *XTCPVisitorConf) Check() (err error) {
-	if err = cfg.BaseVisitorConf.check(); err != nil {
+func (cfg *XTCPVisitorConf) Validate() (err error) {
+	if err = cfg.BaseVisitorConf.validate(); err != nil {
 		return
 	}
 

+ 4 - 3
pkg/config/visitor_test.go

@@ -87,9 +87,10 @@ func Test_Visitor_UnmarshalFromIni(t *testing.T) {
 					BindAddr:   "127.0.0.1",
 					BindPort:   9001,
 				},
-				Protocol:         "quic",
-				MaxRetriesAnHour: 8,
-				MinRetryInterval: 90,
+				Protocol:          "quic",
+				MaxRetriesAnHour:  8,
+				MinRetryInterval:  90,
+				FallbackTimeoutMs: 1000,
 			},
 		},
 	}

+ 4 - 2
pkg/msg/msg.go

@@ -110,8 +110,9 @@ type NewProxy struct {
 	Headers           map[string]string `json:"headers,omitempty"`
 	RouteByHTTPUser   string            `json:"route_by_http_user,omitempty"`
 
-	// stcp
-	Sk string `json:"sk,omitempty"`
+	// stcp, sudp, xtcp
+	Sk         string   `json:"sk,omitempty"`
+	AllowUsers []string `json:"allow_users,omitempty"`
 
 	// tcpmux
 	Multiplexer string `json:"multiplexer,omitempty"`
@@ -145,6 +146,7 @@ type StartWorkConn struct {
 }
 
 type NewVisitorConn struct {
+	RunID          string `json:"run_id,omitempty"`
 	ProxyName      string `json:"proxy_name,omitempty"`
 	SignKey        string `json:"sign_key,omitempty"`
 	Timestamp      int64  `json:"timestamp,omitempty"`

+ 15 - 15
pkg/nathole/analysis.go

@@ -63,20 +63,20 @@ var (
 	}
 
 	// mode 2, HardNAT is receiver, EasyNAT is sender
-	// sender, portsRandomNumber 1000, sendDelayMs 2000 | receiver, listen 256 ports, ttl 7
-	// sender, portsRandomNumber 1000, sendDelayMs 2000 | receiver, listen 256 ports, ttl 4
-	// sender, portsRandomNumber 1000, sendDelayMs 2000 | receiver, listen 256 ports
+	// sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports, ttl 7
+	// sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports, ttl 4
+	// sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports
 	mode2Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
 			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 7},
 		),
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
 			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 4},
 		),
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
 			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256},
 		),
 	}
@@ -98,21 +98,21 @@ var (
 	}
 
 	// mode 4, Regular ports changes are usually the sender.
-	// sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 7, portsRangeNumber 10
-	// sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 4, portsRangeNumber 10
-	// sender, portsRandomNumber 1000, SendDelayMs: 2000 | receiver, listen 256 ports, portsRangeNumber 10
+	// sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 7, portsRangeNumber 2
+	// sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 4, portsRangeNumber 2
+	// sender, portsRandomNumber 1000, SendDelayMs: 2000 | receiver, listen 256 ports, portsRangeNumber 2
 	mode4Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
-			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 7, PortsRangeNumber: 10},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
+			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 7, PortsRangeNumber: 2},
 		),
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
-			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 4, PortsRangeNumber: 10},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
+			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 4, PortsRangeNumber: 2},
 		),
 		lo.T2(
-			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 2000},
-			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, PortsRangeNumber: 10},
+			RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000},
+			RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, PortsRangeNumber: 2},
 		),
 	}
 )

+ 6 - 5
pkg/nathole/classify.go

@@ -85,11 +85,6 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err
 		}
 	}
 
-	natFeature.PortsDifference = portMax - portMin
-	if natFeature.PortsDifference <= 10 && natFeature.PortsDifference >= 1 {
-		natFeature.RegularPortsChange = true
-	}
-
 	switch {
 	case ipChanged && portChanged:
 		natFeature.NatType = HardNAT
@@ -104,6 +99,12 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err
 		natFeature.NatType = EasyNAT
 		natFeature.Behavior = BehaviorNoChange
 	}
+	if natFeature.Behavior == BehaviorPortChanged {
+		natFeature.PortsDifference = portMax - portMin
+		if natFeature.PortsDifference <= 5 && natFeature.PortsDifference >= 1 {
+			natFeature.RegularPortsChange = true
+		}
+	}
 	return natFeature, nil
 }
 

+ 23 - 14
pkg/nathole/controller.go

@@ -43,9 +43,10 @@ func NewTransactionID() string {
 }
 
 type ClientCfg struct {
-	name  string
-	sk    string
-	sidCh chan string
+	name       string
+	sk         string
+	allowUsers []string
+	sidCh      chan string
 }
 
 type Session struct {
@@ -120,16 +121,20 @@ func (c *Controller) CleanWorker(ctx context.Context) {
 	}
 }
 
-func (c *Controller) ListenClient(name string, sk string) chan string {
+func (c *Controller) ListenClient(name string, sk string, allowUsers []string) (chan string, error) {
 	cfg := &ClientCfg{
-		name:  name,
-		sk:    sk,
-		sidCh: make(chan string),
+		name:       name,
+		sk:         sk,
+		allowUsers: allowUsers,
+		sidCh:      make(chan string),
 	}
 	c.mu.Lock()
 	defer c.mu.Unlock()
+	if _, ok := c.clientCfgs[name]; ok {
+		return nil, fmt.Errorf("proxy [%s] is repeated", name)
+	}
 	c.clientCfgs[name] = cfg
-	return cfg.sidCh
+	return cfg.sidCh, nil
 }
 
 func (c *Controller) CloseClient(name string) {
@@ -144,14 +149,18 @@ func (c *Controller) GenSid() string {
 	return fmt.Sprintf("%d%s", t, id)
 }
 
-func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter) {
+func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
 	if m.PreCheck {
-		_, ok := c.clientCfgs[m.ProxyName]
+		cfg, ok := c.clientCfgs[m.ProxyName]
 		if !ok {
 			_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
-		} else {
-			_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, ""))
+			return
+		}
+		if !lo.Contains(cfg.allowUsers, visitorUser) && !lo.Contains(cfg.allowUsers, "*") {
+			_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp visitor user [%s] not allowed for [%s]", visitorUser, m.ProxyName)))
+			return
 		}
+		_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, ""))
 		return
 	}
 
@@ -185,7 +194,7 @@ func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.
 		_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, err.Error()))
 		return
 	}
-	log.Trace("handle visitor message, sid [%s]", sid)
+	log.Trace("handle visitor message, sid [%s], server name: %s", sid, m.ProxyName)
 
 	defer func() {
 		c.mu.Lock()
@@ -247,7 +256,7 @@ func (c *Controller) HandleClient(m *msg.NatHoleClient, transporter transport.Me
 	if !ok {
 		return
 	}
-	log.Trace("handle client message, sid [%s]", session.sid)
+	log.Trace("handle client message, sid [%s], server name: %s", session.sid, m.ProxyName)
 	session.clientMsg = m
 	session.clientTransporter = transporter
 	select {

+ 1 - 1
pkg/nathole/nathole.go

@@ -384,7 +384,7 @@ func sendSidMessageToRangePorts(
 				if err := sendFunc(conn, detectAddr); err != nil {
 					xl.Trace("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err)
 				}
-				time.Sleep(5 * time.Millisecond)
+				time.Sleep(2 * time.Millisecond)
 			}
 		}
 	}

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

@@ -23,7 +23,7 @@ import (
 	"net/http/httputil"
 	"strings"
 
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 const PluginHTTP2HTTPS = "http2https"
@@ -98,7 +98,7 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) {
 }
 
 func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

+ 8 - 8
pkg/plugin/client/http_proxy.go

@@ -23,10 +23,10 @@ import (
 	"strings"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
-	gnet "github.com/fatedier/golib/net"
+	libio "github.com/fatedier/golib/io"
+	libnet "github.com/fatedier/golib/net"
 
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
@@ -69,9 +69,9 @@ func (hp *HTTPProxy) Name() string {
 }
 
 func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 
-	sc, rd := gnet.NewSharedConn(wrapConn)
+	sc, rd := libnet.NewSharedConn(wrapConn)
 	firstBytes := make([]byte, 7)
 	_, err := rd.Read(firstBytes)
 	if err != nil {
@@ -86,7 +86,7 @@ func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBuf
 			wrapConn.Close()
 			return
 		}
-		hp.handleConnectReq(request, frpIo.WrapReadWriteCloser(bufRd, wrapConn, wrapConn.Close))
+		hp.handleConnectReq(request, libio.WrapReadWriteCloser(bufRd, wrapConn, wrapConn.Close))
 		return
 	}
 
@@ -158,7 +158,7 @@ func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) {
 	}
 	_, _ = client.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
 
-	go frpIo.Join(remote, client)
+	go libio.Join(remote, client)
 }
 
 func (hp *HTTPProxy) Auth(req *http.Request) bool {
@@ -213,7 +213,7 @@ func (hp *HTTPProxy) handleConnectReq(req *http.Request, rwc io.ReadWriteCloser)
 	}
 	_, _ = rwc.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
 
-	frpIo.Join(remote, rwc)
+	libio.Join(remote, rwc)
 }
 
 func copyHeaders(dst, src http.Header) {

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

@@ -24,7 +24,7 @@ import (
 	"strings"
 
 	"github.com/fatedier/frp/pkg/transport"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 const PluginHTTPS2HTTP = "https2http"
@@ -123,7 +123,7 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
 }
 
 func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

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

@@ -24,7 +24,7 @@ import (
 	"strings"
 
 	"github.com/fatedier/frp/pkg/transport"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 const PluginHTTPS2HTTPS = "https2https"
@@ -128,7 +128,7 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
 }
 
 func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 

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

@@ -21,7 +21,7 @@ import (
 
 	gosocks5 "github.com/armon/go-socks5"
 
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 const PluginSocks5 = "socks5"
@@ -52,7 +52,7 @@ func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
 
 func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
 	defer conn.Close()
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.Server.ServeConn(wrapConn)
 }
 

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

@@ -22,7 +22,7 @@ import (
 
 	"github.com/gorilla/mux"
 
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 const PluginStaticFile = "static_file"
@@ -65,8 +65,8 @@ func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
 	}
 
 	router := mux.NewRouter()
-	router.Use(frpNet.NewHTTPAuthMiddleware(httpUser, httpPasswd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-	router.PathPrefix(prefix).Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))))).Methods("GET")
+	router.Use(utilnet.NewHTTPAuthMiddleware(httpUser, httpPasswd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+	router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))))).Methods("GET")
 	sp.s = &http.Server{
 		Handler: router,
 	}
@@ -77,7 +77,7 @@ func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
 }
 
 func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) {
-	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.l.PutConn(wrapConn)
 }
 

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

@@ -19,7 +19,7 @@ import (
 	"io"
 	"net"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 )
 
 const PluginUnixDomainSocket = "unix_domain_socket"
@@ -62,7 +62,7 @@ func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, realConn net.
 		}
 	}
 
-	frpIo.Join(localConn, conn)
+	libio.Join(localConn, conn)
 }
 
 func (uds *UnixDomainSocketPlugin) Name() string {

+ 21 - 10
pkg/util/net/listener.go

@@ -22,20 +22,21 @@ import (
 	"github.com/fatedier/golib/errors"
 )
 
-// Custom listener
-type CustomListener struct {
+// InternalListener is a listener that can be used to accept connections from
+// other goroutines.
+type InternalListener struct {
 	acceptCh chan net.Conn
 	closed   bool
 	mu       sync.Mutex
 }
 
-func NewCustomListener() *CustomListener {
-	return &CustomListener{
-		acceptCh: make(chan net.Conn, 64),
+func NewInternalListener() *InternalListener {
+	return &InternalListener{
+		acceptCh: make(chan net.Conn, 128),
 	}
 }
 
-func (l *CustomListener) Accept() (net.Conn, error) {
+func (l *InternalListener) Accept() (net.Conn, error) {
 	conn, ok := <-l.acceptCh
 	if !ok {
 		return nil, fmt.Errorf("listener closed")
@@ -43,7 +44,7 @@ func (l *CustomListener) Accept() (net.Conn, error) {
 	return conn, nil
 }
 
-func (l *CustomListener) PutConn(conn net.Conn) error {
+func (l *InternalListener) PutConn(conn net.Conn) error {
 	err := errors.PanicToError(func() {
 		select {
 		case l.acceptCh <- conn:
@@ -54,7 +55,7 @@ func (l *CustomListener) PutConn(conn net.Conn) error {
 	return err
 }
 
-func (l *CustomListener) Close() error {
+func (l *InternalListener) Close() error {
 	l.mu.Lock()
 	defer l.mu.Unlock()
 	if !l.closed {
@@ -64,6 +65,16 @@ func (l *CustomListener) Close() error {
 	return nil
 }
 
-func (l *CustomListener) Addr() net.Addr {
-	return (*net.TCPAddr)(nil)
+func (l *InternalListener) Addr() net.Addr {
+	return &InternalAddr{}
+}
+
+type InternalAddr struct{}
+
+func (ia *InternalAddr) Network() string {
+	return "internal"
+}
+
+func (ia *InternalAddr) String() string {
+	return "internal"
 }

+ 2 - 2
pkg/util/net/tls.go

@@ -20,7 +20,7 @@ import (
 	"net"
 	"time"
 
-	gnet "github.com/fatedier/golib/net"
+	libnet "github.com/fatedier/golib/net"
 )
 
 var FRPTLSHeadByte = 0x17
@@ -28,7 +28,7 @@ var FRPTLSHeadByte = 0x17
 func CheckAndEnableTLSServerConnWithTimeout(
 	c net.Conn, tlsConfig *tls.Config, tlsOnly bool, timeout time.Duration,
 ) (out net.Conn, isTLS bool, custom bool, err error) {
-	sc, r := gnet.NewSharedConnSize(c, 2)
+	sc, r := libnet.NewSharedConnSize(c, 2)
 	buf := make([]byte, 1)
 	var n int
 	_ = c.SetReadDeadline(time.Now().Add(timeout))

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

@@ -22,7 +22,7 @@ import (
 	"net/http"
 	"time"
 
-	gnet "github.com/fatedier/golib/net"
+	libnet "github.com/fatedier/golib/net"
 
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
@@ -94,7 +94,7 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re
 
 func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, map[string]string, error) {
 	reqInfoMap := make(map[string]string, 0)
-	sc, rd := gnet.NewSharedConn(c)
+	sc, rd := libnet.NewSharedConn(c)
 
 	host, httpUser, httpPwd, err := muxer.readHTTPConnectRequest(rd)
 	if err != nil {

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

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

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

@@ -28,7 +28,7 @@ import (
 	"strings"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 	"github.com/fatedier/golib/pool"
 
 	frpLog "github.com/fatedier/frp/pkg/util/log"
@@ -256,7 +256,7 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
 		return
 	}
 	_ = req.Write(remote)
-	go frpIo.Join(remote, client)
+	go libio.Join(remote, client)
 }
 
 func parseBasicAuth(auth string) (username, password string, ok bool) {

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

@@ -20,7 +20,7 @@ import (
 	"net"
 	"time"
 
-	gnet "github.com/fatedier/golib/net"
+	libnet "github.com/fatedier/golib/net"
 )
 
 type HTTPSMuxer struct {
@@ -37,7 +37,7 @@ func NewHTTPSMuxer(listener net.Listener, timeout time.Duration) (*HTTPSMuxer, e
 
 func GetHTTPSHostname(c net.Conn) (_ net.Conn, _ map[string]string, err error) {
 	reqInfoMap := make(map[string]string, 0)
-	sc, rd := gnet.NewSharedConn(c)
+	sc, rd := libnet.NewSharedConn(c)
 
 	clientHello, err := readClientHello(rd)
 	if err != nil {

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

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

+ 14 - 9
server/control.go

@@ -30,7 +30,7 @@ import (
 	"github.com/fatedier/frp/pkg/auth"
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/consts"
-	frpErr "github.com/fatedier/frp/pkg/errors"
+	pkgerr "github.com/fatedier/frp/pkg/errors"
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/pkg/transport"
@@ -268,7 +268,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 	select {
 	case workConn, ok = <-ctl.workConnCh:
 		if !ok {
-			err = frpErr.ErrCtlClosed
+			err = pkgerr.ErrCtlClosed
 			return
 		}
 		xl.Debug("get work connection from pool")
@@ -283,7 +283,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 		select {
 		case workConn, ok = <-ctl.workConnCh:
 			if !ok {
-				err = frpErr.ErrCtlClosed
+				err = pkgerr.ErrCtlClosed
 				xl.Warn("no work connections available, %v", err)
 				return
 			}
@@ -394,7 +394,7 @@ func (ctl *Control) stoper() {
 	for _, pxy := range ctl.proxies {
 		pxy.Close()
 		ctl.pxyManager.Del(pxy.GetName())
-		metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
+		metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
 
 		notifyContent := &plugin.CloseProxyContent{
 			User: plugin.UserInfo{
@@ -524,7 +524,7 @@ func (ctl *Control) manager() {
 }
 
 func (ctl *Control) HandleNatHoleVisitor(m *msg.NatHoleVisitor) {
-	ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter)
+	ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter, ctl.loginMsg.User)
 }
 
 func (ctl *Control) HandleNatHoleClient(m *msg.NatHoleClient) {
@@ -537,7 +537,7 @@ func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) {
 
 func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
 	var pxyConf config.ProxyConf
-	// Load configures from NewProxy message and check.
+	// Load configures from NewProxy message and validate.
 	pxyConf, err = config.NewProxyConfFromMsg(pxyMsg, ctl.serverCfg)
 	if err != nil {
 		return
@@ -550,8 +550,8 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
 		RunID: ctl.runID,
 	}
 
-	// NewProxy will return a interface Proxy.
-	// In fact it create different proxies by different proxy type, we just call run() here.
+	// NewProxy will return an interface Proxy.
+	// In fact, it creates different proxies based on the proxy type. We just call run() here.
 	pxy, err := proxy.NewProxy(ctl.ctx, userInfo, ctl.rc, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg, ctl.loginMsg)
 	if err != nil {
 		return remoteAddr, err
@@ -577,6 +577,11 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
 		}()
 	}
 
+	if ctl.pxyManager.Exist(pxyMsg.ProxyName) {
+		err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName)
+		return
+	}
+
 	remoteAddr, err = pxy.Run()
 	if err != nil {
 		return
@@ -614,7 +619,7 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
 	delete(ctl.proxies, closeMsg.ProxyName)
 	ctl.mu.Unlock()
 
-	metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
+	metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
 
 	notifyContent := &plugin.CloseProxyContent{
 		User: plugin.UserInfo{

+ 3 - 3
server/dashboard.go

@@ -25,7 +25,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 
 	"github.com/fatedier/frp/assets"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
 var (
@@ -50,7 +50,7 @@ func (svr *Service) RunDashboardServer(address string) (err error) {
 	subRouter := router.NewRoute().Subrouter()
 
 	user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd
-	subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
 
 	// metrics
 	if svr.cfg.EnablePrometheus {
@@ -65,7 +65,7 @@ func (svr *Service) RunDashboardServer(address string) (err error) {
 
 	// view
 	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", 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)

+ 25 - 14
server/proxy/http.go

@@ -17,19 +17,23 @@ package proxy
 import (
 	"io"
 	"net"
+	"reflect"
 	"strings"
 
-	frpIo "github.com/fatedier/golib/io"
-	"golang.org/x/time/rate"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/util/limit"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "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"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.HTTPProxyConf{}), NewHTTPProxy)
+}
+
 type HTTPProxy struct {
 	*BaseProxy
 	cfg *config.HTTPProxyConf
@@ -37,6 +41,17 @@ type HTTPProxy struct {
 	closeFuncs []func()
 }
 
+func NewHTTPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.HTTPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &HTTPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 	routeConfig := vhost.RouteConfig{
@@ -137,10 +152,6 @@ func (pxy *HTTPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *HTTPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) {
 	xl := pxy.xl
 	rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr)
@@ -157,31 +168,31 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
 
 	var rwc io.ReadWriteCloser = tmpConn
 	if pxy.cfg.UseEncryption {
-		rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
 	if pxy.cfg.UseCompression {
-		rwc = frpIo.WithCompression(rwc)
+		rwc = libio.WithCompression(rwc)
 	}
 
 	if pxy.GetLimiter() != nil {
-		rwc = frpIo.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error {
+		rwc = libio.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error {
 			return rwc.Close()
 		})
 	}
 
-	workConn = frpNet.WrapReadWriteCloserToConn(rwc, tmpConn)
-	workConn = frpNet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
-	metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
+	workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn)
+	workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
+	metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
 	return
 }
 
 func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) {
 	name := pxy.GetName()
-	proxyType := pxy.GetConf().GetBaseInfo().ProxyType
+	proxyType := pxy.GetConf().GetBaseConfig().ProxyType
 	metrics.Server.CloseConnection(name, proxyType)
 	metrics.Server.AddTrafficIn(name, proxyType, totalWrite)
 	metrics.Server.AddTrafficOut(name, proxyType, totalRead)

+ 17 - 7
server/proxy/https.go

@@ -15,20 +15,34 @@
 package proxy
 
 import (
+	"reflect"
 	"strings"
 
-	"golang.org/x/time/rate"
-
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.HTTPSProxyConf{}), NewHTTPSProxy)
+}
+
 type HTTPSProxy struct {
 	*BaseProxy
 	cfg *config.HTTPSProxyConf
 }
 
+func NewHTTPSProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.HTTPSProxyConf)
+	if !ok {
+		return nil
+	}
+	return &HTTPSProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 	routeConfig := &vhost.RouteConfig{}
@@ -67,7 +81,7 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
 		addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
 	}
 
-	pxy.startListenHandler(pxy, HandleUserTCPConnection)
+	pxy.startCommonTCPListenersHandler()
 	remoteAddr = strings.Join(addrs, ",")
 	return
 }
@@ -76,10 +90,6 @@ func (pxy *HTTPSProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *HTTPSProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *HTTPSProxy) Close() {
 	pxy.BaseProxy.Close()
 }

+ 71 - 90
server/proxy/proxy.go

@@ -19,23 +19,30 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"reflect"
 	"strconv"
 	"sync"
 	"time"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
 	"golang.org/x/time/rate"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/pkg/util/limit"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "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"
 )
 
+var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{}
+
+func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) {
+	proxyFactoryRegistry[proxyConfType] = factory
+}
+
 type GetWorkConnFn func() (net.Conn, error)
 
 type Proxy interface {
@@ -63,6 +70,7 @@ type BaseProxy struct {
 	limiter       *rate.Limiter
 	userInfo      plugin.UserInfo
 	loginMsg      *msg.Login
+	pxyConf       config.ProxyConf
 
 	mu  sync.RWMutex
 	xl  *xlog.Logger
@@ -93,6 +101,10 @@ func (pxy *BaseProxy) GetLoginMsg() *msg.Login {
 	return pxy.loginMsg
 }
 
+func (pxy *BaseProxy) GetLimiter() *rate.Limiter {
+	return pxy.limiter
+}
+
 func (pxy *BaseProxy) Close() {
 	xl := xlog.FromContextSafe(pxy.ctx)
 	xl.Info("proxy closing")
@@ -113,7 +125,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 = frpNet.NewContextConn(pxy.ctx, workConn)
+		workConn = utilnet.NewContextConn(pxy.ctx, workConn)
 
 		var (
 			srcAddr    string
@@ -155,10 +167,8 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
 	return
 }
 
-// startListenHandler start a goroutine handler for each listener.
-// p: p will just be passed to handler(Proxy, frpNet.Conn).
-// handler: each proxy type can set different handler function to deal with connections accepted from listeners.
-func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, config.ServerCommonConf)) {
+// startCommonTCPListenersHandler start a goroutine handler for each listener.
+func (pxy *BaseProxy) startCommonTCPListenersHandler() {
 	xl := xlog.FromContextSafe(pxy.ctx)
 	for _, listener := range pxy.listeners {
 		go func(l net.Listener) {
@@ -187,97 +197,25 @@ func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn,
 					return
 				}
 				xl.Info("get a user connection [%s]", c.RemoteAddr().String())
-				go handler(p, c, pxy.serverCfg)
+				go pxy.handleUserTCPConnection(c)
 			}
 		}(listener)
 	}
 }
 
-func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.ResourceController, poolCount int,
-	getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf, loginMsg *msg.Login,
-) (pxy Proxy, err error) {
-	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseInfo().ProxyName)
-
-	var limiter *rate.Limiter
-	limitBytes := pxyConf.GetBaseInfo().BandwidthLimit.Bytes()
-	if limitBytes > 0 && pxyConf.GetBaseInfo().BandwidthLimitMode == config.BandwidthLimitModeServer {
-		limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
-	}
-
-	basePxy := BaseProxy{
-		name:          pxyConf.GetBaseInfo().ProxyName,
-		rc:            rc,
-		listeners:     make([]net.Listener, 0),
-		poolCount:     poolCount,
-		getWorkConnFn: getWorkConnFn,
-		serverCfg:     serverCfg,
-		limiter:       limiter,
-		xl:            xl,
-		ctx:           xlog.NewContext(ctx, xl),
-		userInfo:      userInfo,
-		loginMsg:      loginMsg,
-	}
-	switch cfg := pxyConf.(type) {
-	case *config.TCPProxyConf:
-		basePxy.usedPortsNum = 1
-		pxy = &TCPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.TCPMuxProxyConf:
-		pxy = &TCPMuxProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.HTTPProxyConf:
-		pxy = &HTTPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.HTTPSProxyConf:
-		pxy = &HTTPSProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.UDPProxyConf:
-		basePxy.usedPortsNum = 1
-		pxy = &UDPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.STCPProxyConf:
-		pxy = &STCPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.XTCPProxyConf:
-		pxy = &XTCPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	case *config.SUDPProxyConf:
-		pxy = &SUDPProxy{
-			BaseProxy: &basePxy,
-			cfg:       cfg,
-		}
-	default:
-		return pxy, fmt.Errorf("proxy type not support")
-	}
-	return
-}
-
 // HandleUserTCPConnection is used for incoming user TCP connections.
-// It can be used for tcp, http, https type.
-func HandleUserTCPConnection(pxy Proxy, userConn net.Conn, serverCfg config.ServerCommonConf) {
+func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
 	xl := xlog.FromContextSafe(pxy.Context())
 	defer userConn.Close()
 
+	serverCfg := pxy.serverCfg
+	cfg := pxy.pxyConf.GetBaseConfig()
 	// server plugin hook
 	rc := pxy.GetResourceController()
 	content := &plugin.NewUserConnContent{
 		User:       pxy.GetUserInfo(),
 		ProxyName:  pxy.GetName(),
-		ProxyType:  pxy.GetConf().GetBaseInfo().ProxyType,
+		ProxyType:  cfg.ProxyType,
 		RemoteAddr: userConn.RemoteAddr().String(),
 	}
 	_, err := rc.PluginManager.NewUserConn(content)
@@ -294,21 +232,20 @@ func HandleUserTCPConnection(pxy Proxy, userConn net.Conn, serverCfg config.Serv
 	defer workConn.Close()
 
 	var local io.ReadWriteCloser = workConn
-	cfg := pxy.GetConf().GetBaseInfo()
 	xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t", cfg.UseEncryption, cfg.UseCompression)
 	if cfg.UseEncryption {
-		local, err = frpIo.WithEncryption(local, []byte(serverCfg.Token))
+		local, err = libio.WithEncryption(local, []byte(serverCfg.Token))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
 	if cfg.UseCompression {
-		local = frpIo.WithCompression(local)
+		local = libio.WithCompression(local)
 	}
 
 	if pxy.GetLimiter() != nil {
-		local = frpIo.WrapReadWriteCloser(limit.NewReader(local, pxy.GetLimiter()), limit.NewWriter(local, pxy.GetLimiter()), func() error {
+		local = libio.WrapReadWriteCloser(limit.NewReader(local, pxy.GetLimiter()), limit.NewWriter(local, pxy.GetLimiter()), func() error {
 			return local.Close()
 		})
 	}
@@ -317,15 +254,52 @@ func HandleUserTCPConnection(pxy Proxy, userConn net.Conn, serverCfg config.Serv
 		workConn.RemoteAddr().String(), userConn.LocalAddr().String(), userConn.RemoteAddr().String())
 
 	name := pxy.GetName()
-	proxyType := pxy.GetConf().GetBaseInfo().ProxyType
+	proxyType := cfg.ProxyType
 	metrics.Server.OpenConnection(name, proxyType)
-	inCount, outCount, _ := frpIo.Join(local, userConn)
+	inCount, outCount, _ := libio.Join(local, userConn)
 	metrics.Server.CloseConnection(name, proxyType)
 	metrics.Server.AddTrafficIn(name, proxyType, inCount)
 	metrics.Server.AddTrafficOut(name, proxyType, outCount)
 	xl.Debug("join connections closed")
 }
 
+func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.ResourceController, poolCount int,
+	getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf, loginMsg *msg.Login,
+) (pxy Proxy, err error) {
+	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseConfig().ProxyName)
+
+	var limiter *rate.Limiter
+	limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes()
+	if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeServer {
+		limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
+	}
+
+	basePxy := BaseProxy{
+		name:          pxyConf.GetBaseConfig().ProxyName,
+		rc:            rc,
+		listeners:     make([]net.Listener, 0),
+		poolCount:     poolCount,
+		getWorkConnFn: getWorkConnFn,
+		serverCfg:     serverCfg,
+		limiter:       limiter,
+		xl:            xl,
+		ctx:           xlog.NewContext(ctx, xl),
+		userInfo:      userInfo,
+		loginMsg:      loginMsg,
+		pxyConf:       pxyConf,
+	}
+
+	factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
+	if factory == nil {
+		return pxy, fmt.Errorf("proxy type not support")
+	}
+	pxy = factory(&basePxy, pxyConf)
+	if pxy == nil {
+		return nil, fmt.Errorf("proxy not created")
+	}
+	return pxy, nil
+}
+
 type Manager struct {
 	// proxies indexed by proxy name
 	pxys map[string]Proxy
@@ -350,6 +324,13 @@ func (pm *Manager) Add(name string, pxy Proxy) error {
 	return nil
 }
 
+func (pm *Manager) Exist(name string) bool {
+	pm.mu.RLock()
+	defer pm.mu.RUnlock()
+	_, ok := pm.pxys[name]
+	return ok
+}
+
 func (pm *Manager) Del(name string) {
 	pm.mu.Lock()
 	defer pm.mu.Unlock()

+ 23 - 7
server/proxy/stcp.go

@@ -15,19 +15,39 @@
 package proxy
 
 import (
-	"golang.org/x/time/rate"
+	"reflect"
 
 	"github.com/fatedier/frp/pkg/config"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.STCPProxyConf{}), NewSTCPProxy)
+}
+
 type STCPProxy struct {
 	*BaseProxy
 	cfg *config.STCPProxyConf
 }
 
+func NewSTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.STCPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &STCPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *STCPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
-	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk)
+	allowUsers := pxy.cfg.AllowUsers
+	// if allowUsers is empty, only allow same user from proxy
+	if len(allowUsers) == 0 {
+		allowUsers = []string{pxy.GetUserInfo().User}
+	}
+	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers)
 	if errRet != nil {
 		err = errRet
 		return
@@ -35,7 +55,7 @@ func (pxy *STCPProxy) Run() (remoteAddr string, err error) {
 	pxy.listeners = append(pxy.listeners, listener)
 	xl.Info("stcp proxy custom listen success")
 
-	pxy.startListenHandler(pxy, HandleUserTCPConnection)
+	pxy.startCommonTCPListenersHandler()
 	return
 }
 
@@ -43,10 +63,6 @@ func (pxy *STCPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *STCPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *STCPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.VisitorManager.CloseListener(pxy.GetName())

+ 23 - 8
server/proxy/sudp.go

@@ -15,20 +15,39 @@
 package proxy
 
 import (
-	"golang.org/x/time/rate"
+	"reflect"
 
 	"github.com/fatedier/frp/pkg/config"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
+}
+
 type SUDPProxy struct {
 	*BaseProxy
 	cfg *config.SUDPProxyConf
 }
 
+func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.SUDPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &SUDPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *SUDPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
-
-	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk)
+	allowUsers := pxy.cfg.AllowUsers
+	// if allowUsers is empty, only allow same user from proxy
+	if len(allowUsers) == 0 {
+		allowUsers = []string{pxy.GetUserInfo().User}
+	}
+	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers)
 	if errRet != nil {
 		err = errRet
 		return
@@ -36,7 +55,7 @@ func (pxy *SUDPProxy) Run() (remoteAddr string, err error) {
 	pxy.listeners = append(pxy.listeners, listener)
 	xl.Info("sudp proxy custom listen success")
 
-	pxy.startListenHandler(pxy, HandleUserTCPConnection)
+	pxy.startCommonTCPListenersHandler()
 	return
 }
 
@@ -44,10 +63,6 @@ func (pxy *SUDPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *SUDPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *SUDPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.VisitorManager.CloseListener(pxy.GetName())

+ 27 - 16
server/proxy/tcp.go

@@ -17,24 +17,39 @@ package proxy
 import (
 	"fmt"
 	"net"
+	"reflect"
 	"strconv"
 
-	"golang.org/x/time/rate"
-
 	"github.com/fatedier/frp/pkg/config"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.TCPProxyConf{}), NewTCPProxy)
+}
+
 type TCPProxy struct {
 	*BaseProxy
 	cfg *config.TCPProxyConf
 
-	realPort int
+	realBindPort int
+}
+
+func NewTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.TCPProxyConf)
+	if !ok {
+		return nil
+	}
+	baseProxy.usedPortsNum = 1
+	return &TCPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
 }
 
 func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 	if pxy.cfg.Group != "" {
-		l, realPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort)
+		l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort)
 		if errRet != nil {
 			err = errRet
 			return
@@ -44,20 +59,20 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 				l.Close()
 			}
 		}()
-		pxy.realPort = realPort
+		pxy.realBindPort = realBindPort
 		pxy.listeners = append(pxy.listeners, l)
 		xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.Group)
 	} else {
-		pxy.realPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
+		pxy.realBindPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
 		if err != nil {
 			return
 		}
 		defer func() {
 			if err != nil {
-				pxy.rc.TCPPortManager.Release(pxy.realPort)
+				pxy.rc.TCPPortManager.Release(pxy.realBindPort)
 			}
 		}()
-		listener, errRet := net.Listen("tcp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realPort)))
+		listener, errRet := net.Listen("tcp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort)))
 		if errRet != nil {
 			err = errRet
 			return
@@ -66,9 +81,9 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 		xl.Info("tcp proxy listen port [%d]", pxy.cfg.RemotePort)
 	}
 
-	pxy.cfg.RemotePort = pxy.realPort
-	remoteAddr = fmt.Sprintf(":%d", pxy.realPort)
-	pxy.startListenHandler(pxy, HandleUserTCPConnection)
+	pxy.cfg.RemotePort = pxy.realBindPort
+	remoteAddr = fmt.Sprintf(":%d", pxy.realBindPort)
+	pxy.startCommonTCPListenersHandler()
 	return
 }
 
@@ -76,13 +91,9 @@ func (pxy *TCPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *TCPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *TCPProxy) Close() {
 	pxy.BaseProxy.Close()
 	if pxy.cfg.Group == "" {
-		pxy.rc.TCPPortManager.Release(pxy.realPort)
+		pxy.rc.TCPPortManager.Release(pxy.realBindPort)
 	}
 }

+ 17 - 7
server/proxy/tcpmux.go

@@ -17,21 +17,35 @@ package proxy
 import (
 	"fmt"
 	"net"
+	"reflect"
 	"strings"
 
-	"golang.org/x/time/rate"
-
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/consts"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.TCPMuxProxyConf{}), NewTCPMuxProxy)
+}
+
 type TCPMuxProxy struct {
 	*BaseProxy
 	cfg *config.TCPMuxProxyConf
 }
 
+func NewTCPMuxProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.TCPMuxProxyConf)
+	if !ok {
+		return nil
+	}
+	return &TCPMuxProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *TCPMuxProxy) httpConnectListen(
 	domain, routeByHTTPUser, httpUser, httpPwd string, addrs []string) ([]string, error,
 ) {
@@ -78,7 +92,7 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
 		}
 	}
 
-	pxy.startListenHandler(pxy, HandleUserTCPConnection)
+	pxy.startCommonTCPListenersHandler()
 	remoteAddr = strings.Join(addrs, ",")
 	return remoteAddr, err
 }
@@ -101,10 +115,6 @@ func (pxy *TCPMuxProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *TCPMuxProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *TCPMuxProxy) Close() {
 	pxy.BaseProxy.Close()
 }

+ 32 - 20
server/proxy/udp.go

@@ -19,26 +19,30 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"reflect"
 	"strconv"
 	"time"
 
 	"github.com/fatedier/golib/errors"
-	frpIo "github.com/fatedier/golib/io"
-	"golang.org/x/time/rate"
+	libio "github.com/fatedier/golib/io"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/server/metrics"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy)
+}
+
 type UDPProxy struct {
 	*BaseProxy
 	cfg *config.UDPProxyConf
 
-	realPort int
+	realBindPort int
 
 	// udpConn is the listener of udp packages
 	udpConn *net.UDPConn
@@ -59,21 +63,33 @@ type UDPProxy struct {
 	isClosed bool
 }
 
+func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.UDPProxyConf)
+	if !ok {
+		return nil
+	}
+	baseProxy.usedPortsNum = 1
+	return &UDPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
-	pxy.realPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
+	pxy.realBindPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
 	if err != nil {
 		return "", fmt.Errorf("acquire port %d error: %v", pxy.cfg.RemotePort, err)
 	}
 	defer func() {
 		if err != nil {
-			pxy.rc.UDPPortManager.Release(pxy.realPort)
+			pxy.rc.UDPPortManager.Release(pxy.realBindPort)
 		}
 	}()
 
-	remoteAddr = fmt.Sprintf(":%d", pxy.realPort)
-	pxy.cfg.RemotePort = pxy.realPort
-	addr, errRet := net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realPort)))
+	remoteAddr = fmt.Sprintf(":%d", pxy.realBindPort)
+	pxy.cfg.RemotePort = pxy.realBindPort
+	addr, errRet := net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort)))
 	if errRet != nil {
 		err = errRet
 		return
@@ -124,7 +140,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 					pxy.readCh <- m
 					metrics.Server.AddTrafficOut(
 						pxy.GetName(),
-						pxy.GetConf().GetBaseInfo().ProxyType,
+						pxy.GetConf().GetBaseConfig().ProxyType,
 						int64(len(m.Content)),
 					)
 				}); errRet != nil {
@@ -154,7 +170,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 				xl.Trace("send message to udp workConn: %s", udpMsg.Content)
 				metrics.Server.AddTrafficIn(
 					pxy.GetName(),
-					pxy.GetConf().GetBaseInfo().ProxyType,
+					pxy.GetConf().GetBaseConfig().ProxyType,
 					int64(len(udpMsg.Content)),
 				)
 				continue
@@ -189,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 
 			var rwc io.ReadWriteCloser = workConn
 			if pxy.cfg.UseEncryption {
-				rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
+				rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
 				if err != nil {
 					xl.Error("create encryption stream error: %v", err)
 					workConn.Close()
@@ -197,16 +213,16 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 				}
 			}
 			if pxy.cfg.UseCompression {
-				rwc = frpIo.WithCompression(rwc)
+				rwc = libio.WithCompression(rwc)
 			}
 
 			if pxy.GetLimiter() != nil {
-				rwc = frpIo.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error {
+				rwc = libio.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error {
 					return rwc.Close()
 				})
 			}
 
-			pxy.workConn = frpNet.WrapReadWriteCloserToConn(rwc, workConn)
+			pxy.workConn = utilnet.WrapReadWriteCloserToConn(rwc, workConn)
 			ctx, cancel := context.WithCancel(context.Background())
 			go workConnReaderFn(pxy.workConn)
 			go workConnSenderFn(pxy.workConn, ctx)
@@ -233,10 +249,6 @@ func (pxy *UDPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *UDPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *UDPProxy) Close() {
 	pxy.mu.Lock()
 	defer pxy.mu.Unlock()
@@ -254,5 +266,5 @@ func (pxy *UDPProxy) Close() {
 		close(pxy.readCh)
 		close(pxy.sendCh)
 	}
-	pxy.rc.UDPPortManager.Release(pxy.realPort)
+	pxy.rc.UDPPortManager.Release(pxy.realBindPort)
 }

+ 25 - 7
server/proxy/xtcp.go

@@ -16,14 +16,18 @@ package proxy
 
 import (
 	"fmt"
+	"reflect"
 
 	"github.com/fatedier/golib/errors"
-	"golang.org/x/time/rate"
 
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
 )
 
+func init() {
+	RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
+}
+
 type XTCPProxy struct {
 	*BaseProxy
 	cfg *config.XTCPProxyConf
@@ -31,15 +35,33 @@ type XTCPProxy struct {
 	closeCh chan struct{}
 }
 
+func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
+	unwrapped, ok := cfg.(*config.XTCPProxyConf)
+	if !ok {
+		return nil
+	}
+	return &XTCPProxy{
+		BaseProxy: baseProxy,
+		cfg:       unwrapped,
+	}
+}
+
 func (pxy *XTCPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 
 	if pxy.rc.NatHoleController == nil {
-		xl.Error("udp port for xtcp is not specified.")
 		err = fmt.Errorf("xtcp is not supported in frps")
 		return
 	}
-	sidCh := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Sk)
+	allowUsers := pxy.cfg.AllowUsers
+	// if allowUsers is empty, only allow same user from proxy
+	if len(allowUsers) == 0 {
+		allowUsers = []string{pxy.GetUserInfo().User}
+	}
+	sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Sk, allowUsers)
+	if err != nil {
+		return "", err
+	}
 	go func() {
 		for {
 			select {
@@ -68,10 +90,6 @@ func (pxy *XTCPProxy) GetConf() config.ProxyConf {
 	return pxy.cfg
 }
 
-func (pxy *XTCPProxy) GetLimiter() *rate.Limiter {
-	return pxy.limiter
-}
-
 func (pxy *XTCPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.NatHoleController.CloseClient(pxy.GetName())

+ 21 - 11
server/service.go

@@ -39,7 +39,7 @@ import (
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/log"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "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"
@@ -210,7 +210,7 @@ func NewService(cfg config.ServerCommonConf) (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 = frpNet.ListenKcp(address)
+		svr.kcpListener, err = utilnet.ListenKcp(address)
 		if err != nil {
 			err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
 			return
@@ -235,11 +235,11 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 	}
 
 	// Listen for accepting connections from client using websocket protocol.
-	websocketPrefix := []byte("GET " + frpNet.FrpWebsocketPath)
+	websocketPrefix := []byte("GET " + utilnet.FrpWebsocketPath)
 	websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool {
 		return bytes.Equal(data, websocketPrefix)
 	})
-	svr.websocketListener = frpNet.NewWebsocketListener(websocketLn)
+	svr.websocketListener = utilnet.NewWebsocketListener(websocketLn)
 
 	// Create http vhost muxer.
 	if cfg.VhostHTTPPort > 0 {
@@ -294,7 +294,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 	// 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]) == frpNet.FRPTLSHeadByte || int(data[0]) == 0x16
+		return int(data[0]) == utilnet.FRPTLSHeadByte || int(data[0]) == 0x16
 	})
 
 	// Create nat hole controller.
@@ -442,12 +442,12 @@ func (svr *Service) HandleListener(l net.Listener) {
 		xl := xlog.New()
 		ctx := context.Background()
 
-		c = frpNet.NewContextConn(xlog.NewContext(ctx, xl), c)
+		c = utilnet.NewContextConn(xlog.NewContext(ctx, xl), c)
 
 		log.Trace("start check TLS connection...")
 		originConn := c
 		var isTLS, custom bool
-		c, isTLS, custom, err = frpNet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLSOnly, connReadTimeout)
+		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLSOnly, connReadTimeout)
 		if err != nil {
 			log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
 			originConn.Close()
@@ -461,6 +461,7 @@ func (svr *Service) HandleListener(l net.Listener) {
 				fmuxCfg := fmux.DefaultConfig()
 				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
 				fmuxCfg.LogOutput = io.Discard
+				fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 				session, err := fmux.Server(frpConn, fmuxCfg)
 				if err != nil {
 					log.Warn("Failed to create mux connection: %v", err)
@@ -501,7 +502,7 @@ func (svr *Service) HandleQUICListener(l quic.Listener) {
 					_ = frpConn.CloseWithError(0, "")
 					return
 				}
-				go svr.handleConnection(ctx, frpNet.QuicStreamToNetConn(stream, frpConn))
+				go svr.handleConnection(ctx, utilnet.QuicStreamToNetConn(stream, frpConn))
 			}
 		}(context.Background(), c)
 	}
@@ -517,7 +518,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
 		}
 	}
 
-	ctx := frpNet.NewContextFromConn(ctlConn)
+	ctx := utilnet.NewContextFromConn(ctlConn)
 	xl := xlog.FromContextSafe(ctx)
 	xl.AppendPrefix(loginMsg.RunID)
 	ctx = xlog.NewContext(ctx, xl)
@@ -555,7 +556,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
 
 // RegisterWorkConn register a new work connection to control and proxies need it.
 func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error {
-	xl := frpNet.NewLogFromConn(workConn)
+	xl := utilnet.NewLogFromConn(workConn)
 	ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
 	if !exist {
 		xl.Warn("No client control found for run id [%s]", newMsg.RunID)
@@ -587,6 +588,15 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
 }
 
 func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error {
+	visitorUser := ""
+	// TODO: Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID.
+	if newMsg.RunID != "" {
+		ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
+		if !exist {
+			return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID)
+		}
+		visitorUser = ctl.loginMsg.User
+	}
 	return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
-		newMsg.UseEncryption, newMsg.UseCompression)
+		newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
 }

+ 31 - 20
server/visitor/visitor.go

@@ -20,66 +20,78 @@ import (
 	"net"
 	"sync"
 
-	frpIo "github.com/fatedier/golib/io"
+	libio "github.com/fatedier/golib/io"
+	"github.com/samber/lo"
 
-	frpNet "github.com/fatedier/frp/pkg/util/net"
+	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
+type listenerBundle struct {
+	l          *utilnet.InternalListener
+	sk         string
+	allowUsers []string
+}
+
 // Manager for visitor listeners.
 type Manager struct {
-	visitorListeners map[string]*frpNet.CustomListener
-	skMap            map[string]string
+	listeners map[string]*listenerBundle
 
 	mu sync.RWMutex
 }
 
 func NewManager() *Manager {
 	return &Manager{
-		visitorListeners: make(map[string]*frpNet.CustomListener),
-		skMap:            make(map[string]string),
+		listeners: make(map[string]*listenerBundle),
 	}
 }
 
-func (vm *Manager) Listen(name string, sk string) (l *frpNet.CustomListener, err error) {
+func (vm *Manager) Listen(name string, sk string, allowUsers []string) (l *utilnet.InternalListener, err error) {
 	vm.mu.Lock()
 	defer vm.mu.Unlock()
 
-	if _, ok := vm.visitorListeners[name]; ok {
+	if _, ok := vm.listeners[name]; ok {
 		err = fmt.Errorf("custom listener for [%s] is repeated", name)
 		return
 	}
 
-	l = frpNet.NewCustomListener()
-	vm.visitorListeners[name] = l
-	vm.skMap[name] = sk
+	l = utilnet.NewInternalListener()
+	vm.listeners[name] = &listenerBundle{
+		l:          l,
+		sk:         sk,
+		allowUsers: allowUsers,
+	}
 	return
 }
 
 func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string,
-	useEncryption bool, useCompression bool,
+	useEncryption bool, useCompression bool, visitorUser string,
 ) (err error) {
 	vm.mu.RLock()
 	defer vm.mu.RUnlock()
 
-	if l, ok := vm.visitorListeners[name]; ok {
-		var sk string
-		if sk = vm.skMap[name]; util.GetAuthKey(sk, timestamp) != signKey {
+	if l, ok := vm.listeners[name]; ok {
+		if util.GetAuthKey(l.sk, timestamp) != signKey {
 			err = fmt.Errorf("visitor connection of [%s] auth failed", name)
 			return
 		}
 
+		if !lo.Contains(l.allowUsers, visitorUser) && !lo.Contains(l.allowUsers, "*") {
+			err = fmt.Errorf("visitor connection of [%s] user [%s] not allowed", name, visitorUser)
+			return
+		}
+
 		var rwc io.ReadWriteCloser = conn
 		if useEncryption {
-			if rwc, err = frpIo.WithEncryption(rwc, []byte(sk)); err != nil {
+			if rwc, err = libio.WithEncryption(rwc, []byte(l.sk)); err != nil {
 				err = fmt.Errorf("create encryption connection failed: %v", err)
 				return
 			}
 		}
 		if useCompression {
-			rwc = frpIo.WithCompression(rwc)
+			rwc = libio.WithCompression(rwc)
 		}
-		err = l.PutConn(frpNet.WrapReadWriteCloserToConn(rwc, conn))
+		err = l.l.PutConn(utilnet.WrapReadWriteCloserToConn(rwc, conn))
 	} else {
 		err = fmt.Errorf("custom listener for [%s] doesn't exist", name)
 		return
@@ -91,6 +103,5 @@ func (vm *Manager) CloseListener(name string) {
 	vm.mu.Lock()
 	defer vm.mu.Unlock()
 
-	delete(vm.visitorListeners, name)
-	delete(vm.skMap, name)
+	delete(vm.listeners, name)
 }

+ 59 - 20
test/e2e/basic/basic.go

@@ -282,8 +282,9 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 			proxyType := t
 			ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() {
 				serverConf := consts.DefaultServerConfig
-				clientServerConf := consts.DefaultClientConfig
-				clientVisitorConf := consts.DefaultClientConfig
+				clientServerConf := consts.DefaultClientConfig + "\nuser = user1"
+				clientVisitorConf := consts.DefaultClientConfig + "\nuser = user1"
+				clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = user2"
 
 				localPortName := ""
 				protocol := "tcp"
@@ -323,11 +324,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 				}
 
 				tests := []struct {
-					proxyName    string
-					bindPortName string
-					visitorSK    string
-					extraConfig  string
-					expectError  bool
+					proxyName          string
+					bindPortName       string
+					visitorSK          string
+					commonExtraConfig  string
+					proxyExtraConfig   string
+					visitorExtraConfig string
+					expectError        bool
+					user2              bool
 				}{
 					{
 						proxyName:    "normal",
@@ -335,22 +339,22 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 						visitorSK:    correctSK,
 					},
 					{
-						proxyName:    "with-encryption",
-						bindPortName: port.GenName("WithEncryption"),
-						visitorSK:    correctSK,
-						extraConfig:  "use_encryption = true",
+						proxyName:         "with-encryption",
+						bindPortName:      port.GenName("WithEncryption"),
+						visitorSK:         correctSK,
+						commonExtraConfig: "use_encryption = true",
 					},
 					{
-						proxyName:    "with-compression",
-						bindPortName: port.GenName("WithCompression"),
-						visitorSK:    correctSK,
-						extraConfig:  "use_compression = true",
+						proxyName:         "with-compression",
+						bindPortName:      port.GenName("WithCompression"),
+						visitorSK:         correctSK,
+						commonExtraConfig: "use_compression = true",
 					},
 					{
 						proxyName:    "with-encryption-and-compression",
 						bindPortName: port.GenName("WithEncryptionAndCompression"),
 						visitorSK:    correctSK,
-						extraConfig: `
+						commonExtraConfig: `
 						use_encryption = true
 						use_compression = true
 						`,
@@ -361,22 +365,57 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 						visitorSK:    wrongSK,
 						expectError:  true,
 					},
+					{
+						proxyName:          "allowed-user",
+						bindPortName:       port.GenName("AllowedUser"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   "allow_users = another, user2",
+						visitorExtraConfig: "server_user = user1",
+						user2:              true,
+					},
+					{
+						proxyName:          "not-allowed-user",
+						bindPortName:       port.GenName("NotAllowedUser"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   "allow_users = invalid",
+						visitorExtraConfig: "server_user = user1",
+						expectError:        true,
+					},
+					{
+						proxyName:          "allow-all",
+						bindPortName:       port.GenName("AllowAll"),
+						visitorSK:          correctSK,
+						proxyExtraConfig:   "allow_users = *",
+						visitorExtraConfig: "server_user = user1",
+						user2:              true,
+					},
 				}
 
 				// build all client config
 				for _, test := range tests {
-					clientServerConf += getProxyServerConf(test.proxyName, test.extraConfig) + "\n"
+					clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n"
 				}
 				for _, test := range tests {
-					clientVisitorConf += getProxyVisitorConf(test.proxyName, test.bindPortName, test.visitorSK, test.extraConfig) + "\n"
+					config := getProxyVisitorConf(
+						test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig,
+					) + "\n"
+					if test.user2 {
+						clientUser2VisitorConf += config
+					} else {
+						clientVisitorConf += config
+					}
 				}
 				// run frps and frpc
-				f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf})
+				f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf})
 
 				for _, test := range tests {
+					timeout := time.Second
+					if t == "xtcp" {
+						timeout = 4 * time.Second
+					}
 					framework.NewRequestExpect(f).
 						RequestModify(func(r *request.Request) {
-							r.Timeout(5 * time.Second)
+							r.Timeout(timeout)
 						}).
 						Protocol(protocol).
 						PortName(test.bindPortName).

+ 7 - 12
test/e2e/basic/client_server.go

@@ -101,11 +101,13 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			tmp := protocol
-			defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
+			// Since v0.50.0, the default value of tls_enable has been changed to true.
+			// Therefore, here it needs to be set as false to test the scenario of turning it off.
+			defineClientServerTest("Disable TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
 				server: fmt.Sprintf(`
 				%s
 				`, renderBindPortConfig(protocol)),
-				client: fmt.Sprintf(`tls_enable = true
+				client: fmt.Sprintf(`tls_enable = false
 				protocol = %s
 				`, protocol),
 			})
@@ -113,10 +115,10 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 
 		defineClientServerTest("enable tls_only, client with TLS", f, &generalTestConfigures{
 			server: "tls_only = true",
-			client: "tls_enable = true",
 		})
 		defineClientServerTest("enable tls_only, client without TLS", f, &generalTestConfigures{
 			server:      "tls_only = true",
+			client:      "tls_enable = false",
 			expectError: true,
 		})
 	})
@@ -155,7 +157,6 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 					`, renderBindPortConfig(tmp), caCrtPath),
 					client: fmt.Sprintf(`
 						protocol = %s
-						tls_enable = true
 						tls_cert_file = %s
 						tls_key_file = %s
 					`, tmp, clientCrtPath, clientKeyPath),
@@ -172,7 +173,6 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 					`, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath),
 					client: fmt.Sprintf(`
 						protocol = %s
-						tls_enable = true
 						tls_cert_file = %s
 						tls_key_file = %s
 						tls_trusted_ca_file = %s
@@ -211,7 +211,6 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 				tls_trusted_ca_file = %s
 				`, serverCrtPath, serverKeyPath, caCrtPath),
 				client: fmt.Sprintf(`
-				tls_enable = true
 				tls_server_name = example.com
 				tls_cert_file = %s
 				tls_key_file = %s
@@ -228,7 +227,6 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 				tls_trusted_ca_file = %s
 				`, serverCrtPath, serverKeyPath, caCrtPath),
 				client: fmt.Sprintf(`
-				tls_enable = true
 				tls_server_name = invalid.com
 				tls_cert_file = %s
 				tls_key_file = %s
@@ -239,7 +237,7 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 		})
 	})
 
-	ginkgo.Describe("TLS with disable_custom_tls_first_byte", func() {
+	ginkgo.Describe("TLS with disable_custom_tls_first_byte set to false", func() {
 		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			tmp := protocol
@@ -248,9 +246,8 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 					%s
 					`, renderBindPortConfig(protocol)),
 				client: fmt.Sprintf(`
-					tls_enable = true
 					protocol = %s
-					disable_custom_tls_first_byte = true
+					disable_custom_tls_first_byte = false
 					`, protocol),
 			})
 		}
@@ -266,9 +263,7 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 					%s
 					`, renderBindPortConfig(protocol)),
 				client: fmt.Sprintf(`
-					tls_enable = true
 					protocol = %s
-					disable_custom_tls_first_byte = true
 					`, protocol),
 			})
 		}

+ 3 - 3
test/e2e/basic/server.go

@@ -37,7 +37,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			[tcp-port-not-allowed]
 			type = tcp
 			local_port = {{ .%s }}
-			remote_port = 20001
+			remote_port = 25001
 			`, framework.TCPEchoServerPort)
 		clientConf += fmt.Sprintf(`
 			[tcp-port-unavailable]
@@ -55,7 +55,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			[udp-port-not-allowed]
 			type = udp
 			local_port = {{ .%s }}
-			remote_port = 20003
+			remote_port = 25003
 			`, framework.UDPEchoServerPort)
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
@@ -65,7 +65,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 		framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
 
 		// Not Allowed
-		framework.NewRequestExpect(f).Port(25003).ExpectError(true).Ensure()
+		framework.NewRequestExpect(f).Port(25001).ExpectError(true).Ensure()
 
 		// Unavailable, already bind by frps
 		framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()

+ 52 - 0
test/e2e/basic/xtcp.go

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

+ 1 - 1
test/e2e/framework/process.go

@@ -56,7 +56,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		ExpectNoError(err)
 		time.Sleep(500 * time.Millisecond)
 	}
-	time.Sleep(2 * time.Second)
+	time.Sleep(3 * time.Second)
 
 	return currentServerProcesses, currentClientProcesses
 }

+ 0 - 5
web/frps/src/components/ServerOverview.vue

@@ -14,9 +14,6 @@
             <el-form-item label="BindPort">
               <span>{{ data.bind_port }}</span>
             </el-form-item>
-            <el-form-item label="Bind UDP Port" v-if="data.bind_udp_port != 0">
-              <span>{{ data.bind_udp_port }}</span>
-            </el-form-item>
             <el-form-item label="KCP Bind Port" v-if="data.kcp_bind_port != 0">
               <span>{{ data.kcp_bind_port }}</span>
             </el-form-item>
@@ -91,7 +88,6 @@ import LongSpan from './LongSpan.vue'
 let data = ref({
   version: '',
   bind_port: 0,
-  bind_udp_port: 0,
   kcp_bind_port: 0,
   quic_bind_port: 0,
   vhost_http_port: 0,
@@ -114,7 +110,6 @@ const fetchData = () => {
     .then((json) => {
       data.value.version = json.version
       data.value.bind_port = json.bind_port
-      data.value.bind_udp_port = json.bind_udp_port
       data.value.kcp_bind_port = json.kcp_bind_port
       data.value.quic_bind_port = json.quic_bind_port
       data.value.vhost_http_port = json.vhost_http_port

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels