Browse Source

Merge pull request #3651 from fatedier/dev

bump version to v0.52.0
fatedier 1 year ago
parent
commit
2d3af8a108
100 changed files with 5332 additions and 5105 deletions
  1. 12 10
      .golangci.yml
  2. 1 3
      Makefile.cross-compiles
  3. 340 347
      README.md
  4. 0 4
      README_zh.md
  5. 7 1
      Release.md
  6. 2 2
      client/admin.go
  7. 13 58
      client/admin_api.go
  8. 15 13
      client/control.go
  9. 22 13
      client/health/health.go
  10. 8 8
      client/proxy/general_tcp.go
  11. 36 38
      client/proxy/proxy.go
  12. 12 6
      client/proxy/proxy_manager.go
  13. 16 15
      client/proxy/proxy_wrapper.go
  14. 8 8
      client/proxy/sudp.go
  15. 8 8
      client/proxy/udp.go
  16. 12 12
      client/proxy/xtcp.go
  17. 44 46
      client/service.go
  18. 8 8
      client/visitor/stcp.go
  19. 8 8
      client/visitor/sudp.go
  20. 8 8
      client/visitor/visitor.go
  21. 17 11
      client/visitor/visitor_manager.go
  22. 13 13
      client/visitor/xtcp.go
  23. 117 0
      cmd/frpc/sub/admin.go
  24. 125 0
      cmd/frpc/sub/flags.go
  25. 0 98
      cmd/frpc/sub/http.go
  26. 0 90
      cmd/frpc/sub/https.go
  27. 4 5
      cmd/frpc/sub/nathole.go
  28. 120 0
      cmd/frpc/sub/proxy.go
  29. 0 84
      cmd/frpc/sub/reload.go
  30. 22 104
      cmd/frpc/sub/root.go
  31. 0 107
      cmd/frpc/sub/status.go
  32. 0 116
      cmd/frpc/sub/stcp.go
  33. 0 84
      cmd/frpc/sub/stop.go
  34. 0 115
      cmd/frpc/sub/sudp.go
  35. 0 87
      cmd/frpc/sub/tcp.go
  36. 0 92
      cmd/frpc/sub/tcpmux.go
  37. 0 87
      cmd/frpc/sub/udp.go
  38. 15 1
      cmd/frpc/sub/verify.go
  39. 0 116
      cmd/frpc/sub/xtcp.go
  40. 110 0
      cmd/frps/flags.go
  41. 0 6
      cmd/frps/main.go
  42. 28 130
      cmd/frps/root.go
  43. 7 4
      cmd/frps/verify.go
  44. 0 9
      conf/frpc.ini
  45. 360 0
      conf/frpc.toml
  46. 0 2
      conf/frps.ini
  47. 154 0
      conf/frps.toml
  48. 0 0
      conf/legacy/frpc_legacy_full.ini
  49. 0 0
      conf/legacy/frps_legacy_full.ini
  50. 35 34
      doc/server_plugin.md
  51. 4 5
      go.mod
  52. 8 12
      go.sum
  53. 1 1
      hack/run-e2e.sh
  54. 1 0
      package.sh
  55. 14 66
      pkg/auth/auth.go
  56. 145 0
      pkg/auth/legacy/legacy.go
  57. 26 91
      pkg/auth/oidc.go
  58. 13 24
      pkg/auth/token.go
  59. 0 680
      pkg/config/client_test.go
  60. 0 0
      pkg/config/legacy/README.md
  61. 80 87
      pkg/config/legacy/client.go
  62. 350 0
      pkg/config/legacy/conversion.go
  63. 1 2
      pkg/config/legacy/parse.go
  64. 387 0
      pkg/config/legacy/proxy.go
  65. 28 68
      pkg/config/legacy/server.go
  66. 1 1
      pkg/config/legacy/utils.go
  67. 1 1
      pkg/config/legacy/value.go
  68. 53 124
      pkg/config/legacy/visitor.go
  69. 281 0
      pkg/config/load.go
  70. 45 0
      pkg/config/load_test.go
  71. 0 921
      pkg/config/proxy.go
  72. 0 478
      pkg/config/proxy_test.go
  73. 0 217
      pkg/config/server_test.go
  74. 64 1
      pkg/config/types/types.go
  75. 39 7
      pkg/config/types/types_test.go
  76. 19 0
      pkg/config/v1/api.go
  77. 199 0
      pkg/config/v1/client.go
  78. 34 0
      pkg/config/v1/client_test.go
  79. 117 0
      pkg/config/v1/common.go
  80. 118 0
      pkg/config/v1/plugin.go
  81. 438 0
      pkg/config/v1/proxy.go
  82. 49 0
      pkg/config/v1/proxy_test.go
  83. 190 0
      pkg/config/v1/server.go
  84. 15 24
      pkg/config/v1/server_test.go
  85. 105 0
      pkg/config/v1/validation/client.go
  86. 51 0
      pkg/config/v1/validation/common.go
  87. 72 0
      pkg/config/v1/validation/plugin.go
  88. 233 0
      pkg/config/v1/validation/proxy.go
  89. 58 0
      pkg/config/v1/validation/server.go
  90. 68 0
      pkg/config/v1/validation/validation.go
  91. 63 0
      pkg/config/v1/validation/visitor.go
  92. 162 0
      pkg/config/v1/visitor.go
  93. 0 112
      pkg/config/visitor_test.go
  94. 13 34
      pkg/plugin/client/http2https.go
  95. 15 18
      pkg/plugin/client/http_proxy.go
  96. 15 42
      pkg/plugin/client/https2http.go
  97. 15 41
      pkg/plugin/client/https2https.go
  98. 14 5
      pkg/plugin/client/plugin.go
  99. 8 10
      pkg/plugin/client/socks5.go
  100. 12 22
      pkg/plugin/client/static_file.go

+ 12 - 10
.golangci.yml

@@ -120,16 +120,18 @@ issues:
   #  - composite literal uses unkeyed fields
 
   exclude-rules:
-    # Exclude some linters from running on test files.
-    - path: _test\.go$|^tests/|^samples/
-      linters:
-        - errcheck
-        - maligned
-
-    # keep it until we only support go1.20
-    - linters:
-        - staticcheck
-      text: "SA1019: rand.Seed has been deprecated"
+  # Exclude some linters from running on test files.
+  - path: _test\.go$|^tests/|^samples/
+    linters:
+    - errcheck
+    - maligned
+  - linters:
+    - revive
+    - stylecheck
+    text: "use underscores in Go names"
+  - linters:
+    - revive
+    text: "unused-parameter"
 
   # Independently from option `exclude` we use default exclude patterns,
   # it can be disabled by this option. To list all

+ 1 - 3
Makefile.cross-compiles

@@ -2,7 +2,7 @@ export PATH := $(GOPATH)/bin:$(PATH)
 export GO111MODULE=on
 LDFLAGS := -s -w
 
-os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
+os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64
 
 all: build
 
@@ -19,8 +19,6 @@ app:
 		env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\
 		echo "Build $${os}-$${arch} done";\
 	)
-	@mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe
-	@mv ./release/frps_windows_386 ./release/frps_windows_386.exe
 	@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
 	@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe
 	@mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe

File diff suppressed because it is too large
+ 340 - 347
README.md


+ 0 - 4
README_zh.md

@@ -90,10 +90,6 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 
 ![zsxq](/doc/pic/zsxq.jpg)
 
-### 支付宝扫码捐赠
-
-![donate-alipay](/doc/pic/donate-alipay.png)
-
 ### 微信支付捐赠
 
 ![donate-wechatpay](/doc/pic/donate-wechatpay.png)

+ 7 - 1
Release.md

@@ -1,3 +1,9 @@
 ### Features
 
-* Support Go 1.21.
+* Configuration: We now support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. #2521
+
+### Breaking Changes
+
+* Change the way to start the visitor through the command line from `frpc stcp --role=visitor xxx` to `frpc stcp visitor xxx`.
+* Modified the semantics of the `server_addr` in the command line, no longer including the port. Added the `server_port` parameter to configure the port.
+* No longer support range ports mapping in TOML/YAML/JSON.

+ 2 - 2
client/admin.go

@@ -38,7 +38,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {
 	router.HandleFunc("/healthz", svr.healthz)
 
 	// debug
-	if svr.cfg.PprofEnable {
+	if svr.cfg.WebServer.PprofEnable {
 		router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
 		router.HandleFunc("/debug/pprof/profile", pprof.Profile)
 		router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
@@ -47,7 +47,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {
 	}
 
 	subRouter := router.NewRoute().Subrouter()
-	user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
+	user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
 	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
 
 	// api, see admin_api.go

+ 13 - 58
client/admin_api.go

@@ -30,6 +30,7 @@ import (
 
 	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/pkg/config"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
 	"github.com/fatedier/frp/pkg/util/log"
 )
 
@@ -56,15 +57,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
 		}
 	}()
 
-	_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)
+	cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile)
 	if err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)
 		return
 	}
+	if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil {
+		res.Code = 400
+		res.Msg = err.Error()
+		log.Warn("reload frpc proxy config error: %s", res.Msg)
+		return
+	}
 
-	if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
+	if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
 		res.Code = 500
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)
@@ -112,7 +119,7 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
 	if baseCfg.LocalPort != 0 {
 		psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
 	}
-	psr.Plugin = baseCfg.Plugin
+	psr.Plugin = baseCfg.Plugin.Type
 
 	if status.Err == "" {
 		psr.RemoteAddr = status.RemoteAddr
@@ -172,24 +179,14 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
 		return
 	}
 
-	content, err := config.GetRenderedConfFromFile(svr.cfgFile)
+	content, err := os.ReadFile(svr.cfgFile)
 	if err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
 		log.Warn("load frpc config file error: %s", res.Msg)
 		return
 	}
-
-	rows := strings.Split(string(content), "\n")
-	newRows := make([]string, 0, len(rows))
-	for _, row := range rows {
-		row = strings.TrimSpace(row)
-		if strings.HasPrefix(row, "token") {
-			continue
-		}
-		newRows = append(newRows, row)
-	}
-	res.Msg = strings.Join(newRows, "\n")
+	res.Msg = string(content)
 }
 
 // PUT /api/config
@@ -221,49 +218,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// get token from origin content
-	token := ""
-	b, err := os.ReadFile(svr.cfgFile)
-	if err != nil {
-		res.Code = 400
-		res.Msg = err.Error()
-		log.Warn("load frpc config file error: %s", res.Msg)
-		return
-	}
-	content := string(b)
-
-	for _, row := range strings.Split(content, "\n") {
-		row = strings.TrimSpace(row)
-		if strings.HasPrefix(row, "token") {
-			token = row
-			break
-		}
-	}
-
-	tmpRows := make([]string, 0)
-	for _, row := range strings.Split(string(body), "\n") {
-		row = strings.TrimSpace(row)
-		if strings.HasPrefix(row, "token") {
-			continue
-		}
-		tmpRows = append(tmpRows, row)
-	}
-
-	newRows := make([]string, 0)
-	if token != "" {
-		for _, row := range tmpRows {
-			newRows = append(newRows, row)
-			if strings.HasPrefix(row, "[common]") {
-				newRows = append(newRows, token)
-			}
-		}
-	} else {
-		newRows = tmpRows
-	}
-	content = strings.Join(newRows, "\n")
-
-	err = os.WriteFile(svr.cfgFile, []byte(content), 0o644)
-	if err != nil {
+	if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil {
 		res.Code = 500
 		res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
 		log.Warn("%s", res.Msg)

+ 15 - 13
client/control.go

@@ -23,11 +23,12 @@ import (
 
 	"github.com/fatedier/golib/control/shutdown"
 	"github.com/fatedier/golib/crypto"
+	"github.com/samber/lo"
 
 	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/client/visitor"
 	"github.com/fatedier/frp/pkg/auth"
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -43,7 +44,7 @@ type Control struct {
 	runID string
 
 	// manage all proxies
-	pxyCfgs map[string]config.ProxyConf
+	pxyCfgs []v1.ProxyConfigurer
 	pm      *proxy.Manager
 
 	// manage all visitors
@@ -69,7 +70,7 @@ type Control struct {
 	lastPong time.Time
 
 	// The client configuration
-	clientCfg config.ClientCommonConf
+	clientCfg *v1.ClientCommonConfig
 
 	readerShutdown     *shutdown.Shutdown
 	writerShutdown     *shutdown.Shutdown
@@ -83,9 +84,9 @@ type Control struct {
 
 func NewControl(
 	ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
-	clientCfg config.ClientCommonConf,
-	pxyCfgs map[string]config.ProxyConf,
-	visitorCfgs map[string]config.VisitorConf,
+	clientCfg *v1.ClientCommonConfig,
+	pxyCfgs []v1.ProxyConfigurer,
+	visitorCfgs []v1.VisitorConfigurer,
 	authSetter auth.Setter,
 ) *Control {
 	// new xlog instance
@@ -220,7 +221,7 @@ func (ctl *Control) reader() {
 	defer ctl.readerShutdown.Done()
 	defer close(ctl.closedCh)
 
-	encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token))
+	encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
 	for {
 		m, err := msg.ReadMsg(encReader)
 		if err != nil {
@@ -240,7 +241,7 @@ func (ctl *Control) reader() {
 func (ctl *Control) writer() {
 	xl := ctl.xl
 	defer ctl.writerShutdown.Done()
-	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Token))
+	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
 	if err != nil {
 		xl.Error("crypto new writer error: %v", err)
 		ctl.conn.Close()
@@ -274,15 +275,16 @@ func (ctl *Control) msgHandler() {
 	var hbSendCh <-chan time.Time
 	// TODO(fatedier): disable heartbeat if TCPMux is enabled.
 	// Just keep it here to keep compatible with old version frps.
-	if ctl.clientCfg.HeartbeatInterval > 0 {
-		hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
+	if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
+		hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second)
 		defer hbSend.Stop()
 		hbSendCh = hbSend.C
 	}
 
 	var hbCheckCh <-chan time.Time
 	// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
-	if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
+	if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 &&
+		!lo.FromPtr(ctl.clientCfg.Transport.TCPMux) {
 		hbCheck := time.NewTicker(time.Second)
 		defer hbCheck.Stop()
 		hbCheckCh = hbCheck.C
@@ -301,7 +303,7 @@ func (ctl *Control) msgHandler() {
 			}
 			ctl.sendCh <- pingMsg
 		case <-hbCheckCh:
-			if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
+			if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
 				xl.Warn("heartbeat timeout")
 				// let reader() stop
 				ctl.conn.Close()
@@ -354,7 +356,7 @@ func (ctl *Control) worker() {
 	ctl.cm.Close()
 }
 
-func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
+func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
 	ctl.vm.Reload(visitorCfgs)
 	ctl.pm.Reload(pxyCfgs)
 	return nil

+ 22 - 13
client/health/health.go

@@ -21,8 +21,10 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"strings"
 	"time"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
@@ -49,26 +51,33 @@ type Monitor struct {
 	cancel context.CancelFunc
 }
 
-func NewMonitor(ctx context.Context, checkType string,
-	intervalS int, timeoutS int, maxFailedTimes int,
-	addr string, url string,
+func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string,
 	statusNormalFn func(), statusFailedFn func(),
 ) *Monitor {
-	if intervalS <= 0 {
-		intervalS = 10
+	if cfg.IntervalSeconds <= 0 {
+		cfg.IntervalSeconds = 10
 	}
-	if timeoutS <= 0 {
-		timeoutS = 3
+	if cfg.TimeoutSeconds <= 0 {
+		cfg.TimeoutSeconds = 3
 	}
-	if maxFailedTimes <= 0 {
-		maxFailedTimes = 1
+	if cfg.MaxFailed <= 0 {
+		cfg.MaxFailed = 1
 	}
 	newctx, cancel := context.WithCancel(ctx)
+
+	var url string
+	if cfg.Type == "http" && cfg.Path != "" {
+		s := "http://" + addr
+		if !strings.HasPrefix(cfg.Path, "/") {
+			s += "/"
+		}
+		url = s + cfg.Path
+	}
 	return &Monitor{
-		checkType:      checkType,
-		interval:       time.Duration(intervalS) * time.Second,
-		timeout:        time.Duration(timeoutS) * time.Second,
-		maxFailedTimes: maxFailedTimes,
+		checkType:      cfg.Type,
+		interval:       time.Duration(cfg.IntervalSeconds) * time.Second,
+		timeout:        time.Duration(cfg.TimeoutSeconds) * time.Second,
+		maxFailedTimes: cfg.MaxFailed,
 		addr:           addr,
 		url:            url,
 		statusOK:       false,

+ 8 - 8
client/proxy/general_tcp.go

@@ -17,16 +17,16 @@ package proxy
 import (
 	"reflect"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
-	pxyConfs := []config.ProxyConf{
-		&config.TCPProxyConf{},
-		&config.HTTPProxyConf{},
-		&config.HTTPSProxyConf{},
-		&config.STCPProxyConf{},
-		&config.TCPMuxProxyConf{},
+	pxyConfs := []v1.ProxyConfigurer{
+		&v1.TCPProxyConfig{},
+		&v1.HTTPProxyConfig{},
+		&v1.HTTPSProxyConfig{},
+		&v1.STCPProxyConfig{},
+		&v1.TCPMuxProxyConfig{},
 	}
 	for _, cfg := range pxyConfs {
 		RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy)
@@ -40,7 +40,7 @@ type GeneralTCPProxy struct {
 	*BaseProxy
 }
 
-func NewGeneralTCPProxy(baseProxy *BaseProxy, _ config.ProxyConf) Proxy {
+func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy {
 	return &GeneralTCPProxy{
 		BaseProxy: baseProxy,
 	}

+ 36 - 38
client/proxy/proxy.go

@@ -15,7 +15,6 @@
 package proxy
 
 import (
-	"bytes"
 	"context"
 	"io"
 	"net"
@@ -30,7 +29,8 @@ import (
 	pp "github.com/pires/go-proxyproto"
 	"golang.org/x/time/rate"
 
-	"github.com/fatedier/frp/pkg/config"
+	"github.com/fatedier/frp/pkg/config/types"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/client"
 	"github.com/fatedier/frp/pkg/transport"
@@ -38,9 +38,9 @@ import (
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
-var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{}
+var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
 
-func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) {
+func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) {
 	proxyFactoryRegistry[proxyConfType] = factory
 }
 
@@ -56,23 +56,23 @@ type Proxy interface {
 
 func NewProxy(
 	ctx context.Context,
-	pxyConf config.ProxyConf,
-	clientCfg config.ClientCommonConf,
+	pxyConf v1.ProxyConfigurer,
+	clientCfg *v1.ClientCommonConfig,
 	msgTransporter transport.MessageTransporter,
 ) (pxy Proxy) {
 	var limiter *rate.Limiter
-	limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes()
-	if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeClient {
+	limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes()
+	if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient {
 		limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
 	}
 
 	baseProxy := BaseProxy{
-		baseProxyConfig: pxyConf.GetBaseConfig(),
-		clientCfg:       clientCfg,
-		limiter:         limiter,
-		msgTransporter:  msgTransporter,
-		xl:              xlog.FromContextSafe(ctx),
-		ctx:             ctx,
+		baseCfg:        pxyConf.GetBaseConfig(),
+		clientCfg:      clientCfg,
+		limiter:        limiter,
+		msgTransporter: msgTransporter,
+		xl:             xlog.FromContextSafe(ctx),
+		ctx:            ctx,
 	}
 
 	factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
@@ -83,10 +83,10 @@ func NewProxy(
 }
 
 type BaseProxy struct {
-	baseProxyConfig *config.BaseProxyConf
-	clientCfg       config.ClientCommonConf
-	msgTransporter  transport.MessageTransporter
-	limiter         *rate.Limiter
+	baseCfg        *v1.ProxyBaseConfig
+	clientCfg      *v1.ClientCommonConfig
+	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
@@ -97,8 +97,8 @@ type BaseProxy struct {
 }
 
 func (pxy *BaseProxy) Run() error {
-	if pxy.baseProxyConfig.Plugin != "" {
-		p, err := plugin.Create(pxy.baseProxyConfig.Plugin, pxy.baseProxyConfig.PluginParams)
+	if pxy.baseCfg.Plugin.Type != "" {
+		p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions)
 		if err != nil {
 			return err
 		}
@@ -114,13 +114,13 @@ func (pxy *BaseProxy) Close() {
 }
 
 func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
-	pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Token))
+	pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
 }
 
 // Common handler for tcp work connections.
 func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
 	xl := pxy.xl
-	baseConfig := pxy.baseProxyConfig
+	baseCfg := pxy.baseCfg
 	var (
 		remote io.ReadWriteCloser
 		err    error
@@ -133,8 +133,8 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 	}
 
 	xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
-		baseConfig.UseEncryption, baseConfig.UseCompression)
-	if baseConfig.UseEncryption {
+		baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
+	if baseCfg.Transport.UseEncryption {
 		remote, err = libio.WithEncryption(remote, encKey)
 		if err != nil {
 			workConn.Close()
@@ -143,13 +143,13 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 		}
 	}
 	var compressionResourceRecycleFn func()
-	if baseConfig.UseCompression {
+	if baseCfg.Transport.UseCompression {
 		remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
 	}
 
 	// check if we need to send proxy protocol info
-	var extraInfo []byte
-	if baseConfig.ProxyProtocolVersion != "" {
+	var extraInfo plugin.ExtraInfo
+	if baseCfg.Transport.ProxyProtocolVersion != "" {
 		if m.SrcAddr != "" && m.SrcPort != 0 {
 			if m.DstAddr == "" {
 				m.DstAddr = "127.0.0.1"
@@ -168,43 +168,41 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 				h.TransportProtocol = pp.TCPv6
 			}
 
-			if baseConfig.ProxyProtocolVersion == "v1" {
+			if baseCfg.Transport.ProxyProtocolVersion == "v1" {
 				h.Version = 1
-			} else if baseConfig.ProxyProtocolVersion == "v2" {
+			} else if baseCfg.Transport.ProxyProtocolVersion == "v2" {
 				h.Version = 2
 			}
 
-			buf := bytes.NewBuffer(nil)
-			_, _ = h.WriteTo(buf)
-			extraInfo = buf.Bytes()
+			extraInfo.ProxyProtocolHeader = h
 		}
 	}
 
 	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)
+		pxy.proxyPlugin.Handle(remote, workConn, &extraInfo)
 		xl.Debug("handle by plugin finished")
 		return
 	}
 
 	localConn, err := libdial.Dial(
-		net.JoinHostPort(baseConfig.LocalIP, strconv.Itoa(baseConfig.LocalPort)),
+		net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
 		libdial.WithTimeout(10*time.Second),
 	)
 	if err != nil {
 		workConn.Close()
-		xl.Error("connect to local service [%s:%d] error: %v", baseConfig.LocalIP, baseConfig.LocalPort, err)
+		xl.Error("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err)
 		return
 	}
 
 	xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(),
 		localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String())
 
-	if len(extraInfo) > 0 {
-		if _, err := localConn.Write(extraInfo); err != nil {
+	if extraInfo.ProxyProtocolHeader != nil {
+		if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
 			workConn.Close()
-			xl.Error("write extraInfo to local conn error: %v", err)
+			xl.Error("write proxy protocol header to local conn error: %v", err)
 			return
 		}
 	}

+ 12 - 6
client/proxy/proxy_manager.go

@@ -21,8 +21,10 @@ import (
 	"reflect"
 	"sync"
 
+	"github.com/samber/lo"
+
 	"github.com/fatedier/frp/client/event"
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -35,14 +37,14 @@ type Manager struct {
 	closed bool
 	mu     sync.RWMutex
 
-	clientCfg config.ClientCommonConf
+	clientCfg *v1.ClientCommonConfig
 
 	ctx context.Context
 }
 
 func NewManager(
 	ctx context.Context,
-	clientCfg config.ClientCommonConf,
+	clientCfg *v1.ClientCommonConfig,
 	msgTransporter transport.MessageTransporter,
 ) *Manager {
 	return &Manager{
@@ -113,15 +115,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
 	return ps
 }
 
-func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
+func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
 	xl := xlog.FromContextSafe(pm.ctx)
+	pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string {
+		return c.GetBaseConfig().Name
+	})
 	pm.mu.Lock()
 	defer pm.mu.Unlock()
 
 	delPxyNames := make([]string, 0)
 	for name, pxy := range pm.proxies {
 		del := false
-		cfg, ok := pxyCfgs[name]
+		cfg, ok := pxyCfgsMap[name]
 		if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
 			del = true
 		}
@@ -137,7 +142,8 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) {
 	}
 
 	addPxyNames := make([]string, 0)
-	for name, cfg := range pxyCfgs {
+	for _, cfg := range pxyCfgs {
+		name := cfg.GetBaseConfig().Name
 		if _, ok := pm.proxies[name]; !ok {
 			pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
 			pm.proxies[name] = pxy

+ 16 - 15
client/proxy/proxy_wrapper.go

@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -26,7 +27,7 @@ import (
 
 	"github.com/fatedier/frp/client/event"
 	"github.com/fatedier/frp/client/health"
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -48,11 +49,11 @@ var (
 )
 
 type WorkingStatus struct {
-	Name  string           `json:"name"`
-	Type  string           `json:"type"`
-	Phase string           `json:"status"`
-	Err   string           `json:"err"`
-	Cfg   config.ProxyConf `json:"cfg"`
+	Name  string             `json:"name"`
+	Type  string             `json:"type"`
+	Phase string             `json:"status"`
+	Err   string             `json:"err"`
+	Cfg   v1.ProxyConfigurer `json:"cfg"`
 
 	// Got from server.
 	RemoteAddr string `json:"remote_addr"`
@@ -86,17 +87,17 @@ type Wrapper struct {
 
 func NewWrapper(
 	ctx context.Context,
-	cfg config.ProxyConf,
-	clientCfg config.ClientCommonConf,
+	cfg v1.ProxyConfigurer,
+	clientCfg *v1.ClientCommonConfig,
 	eventHandler event.Handler,
 	msgTransporter transport.MessageTransporter,
 ) *Wrapper {
 	baseInfo := cfg.GetBaseConfig()
-	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName)
+	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
 	pw := &Wrapper{
 		WorkingStatus: WorkingStatus{
-			Name:  baseInfo.ProxyName,
-			Type:  baseInfo.ProxyType,
+			Name:  baseInfo.Name,
+			Type:  baseInfo.Type,
 			Phase: ProxyPhaseNew,
 			Cfg:   cfg,
 		},
@@ -108,11 +109,11 @@ func NewWrapper(
 		ctx:            xlog.NewContext(ctx, xl),
 	}
 
-	if baseInfo.HealthCheckType != "" {
+	if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
 		pw.health = 1 // means failed
-		pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheckType, baseInfo.HealthCheckIntervalS,
-			baseInfo.HealthCheckTimeoutS, baseInfo.HealthCheckMaxFailed, baseInfo.HealthCheckAddr,
-			baseInfo.HealthCheckURL, pw.statusNormalCallback, pw.statusFailedCallback)
+		addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
+		pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
+			pw.statusNormalCallback, pw.statusFailedCallback)
 		xl.Trace("enable health check monitor")
 	}
 

+ 8 - 8
client/proxy/sudp.go

@@ -25,7 +25,7 @@ import (
 	"github.com/fatedier/golib/errors"
 	libio "github.com/fatedier/golib/io"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
@@ -33,21 +33,21 @@ import (
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
 }
 
 type SUDPProxy struct {
 	*BaseProxy
 
-	cfg *config.SUDPProxyConf
+	cfg *v1.SUDPProxyConfig
 
 	localAddr *net.UDPAddr
 
 	closeCh chan struct{}
 }
 
-func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.SUDPProxyConf)
+func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
+	unwrapped, ok := cfg.(*v1.SUDPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -88,15 +88,15 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
 			return conn.Close()
 		})
 	}
-	if pxy.cfg.UseEncryption {
-		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
+	if pxy.cfg.Transport.UseEncryption {
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
 		if err != nil {
 			conn.Close()
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if pxy.cfg.UseCompression {
+	if pxy.cfg.Transport.UseCompression {
 		rwc = libio.WithCompression(rwc)
 	}
 	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)

+ 8 - 8
client/proxy/udp.go

@@ -24,7 +24,7 @@ import (
 	"github.com/fatedier/golib/errors"
 	libio "github.com/fatedier/golib/io"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	"github.com/fatedier/frp/pkg/util/limit"
@@ -32,13 +32,13 @@ import (
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
 }
 
 type UDPProxy struct {
 	*BaseProxy
 
-	cfg *config.UDPProxyConf
+	cfg *v1.UDPProxyConfig
 
 	localAddr *net.UDPAddr
 	readCh    chan *msg.UDPPacket
@@ -49,8 +49,8 @@ type UDPProxy struct {
 	closed   bool
 }
 
-func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.UDPProxyConf)
+func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
+	unwrapped, ok := cfg.(*v1.UDPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -99,15 +99,15 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
 			return conn.Close()
 		})
 	}
-	if pxy.cfg.UseEncryption {
-		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token))
+	if pxy.cfg.Transport.UseEncryption {
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
 		if err != nil {
 			conn.Close()
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if pxy.cfg.UseCompression {
+	if pxy.cfg.Transport.UseCompression {
 		rwc = libio.WithCompression(rwc)
 	}
 	conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)

+ 12 - 12
client/proxy/xtcp.go

@@ -23,7 +23,7 @@ import (
 	fmux "github.com/hashicorp/yamux"
 	"github.com/quic-go/quic-go"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	"github.com/fatedier/frp/pkg/transport"
@@ -31,17 +31,17 @@ import (
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
 }
 
 type XTCPProxy struct {
 	*BaseProxy
 
-	cfg *config.XTCPProxyConf
+	cfg *v1.XTCPProxyConfig
 }
 
-func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.XTCPProxyConf)
+func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
+	unwrapped, ok := cfg.(*v1.XTCPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -75,7 +75,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
 	transactionID := nathole.NewTransactionID()
 	natHoleClientMsg := &msg.NatHoleClient{
 		TransactionID: transactionID,
-		ProxyName:     pxy.cfg.ProxyName,
+		ProxyName:     pxy.cfg.Name,
 		Sid:           natHoleSidMsg.Sid,
 		MappedAddrs:   prepareResult.Addrs,
 		AssistedAddrs: prepareResult.AssistedAddrs,
@@ -93,7 +93,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
 		natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
 
 	listenConn := prepareResult.ListenConn
-	newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Sk))
+	newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey))
 	if err != nil {
 		listenConn.Close()
 		xl.Warn("make hole error: %v", err)
@@ -154,7 +154,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 			xl.Error("accept connection error: %v", err)
 			return
 		}
-		go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Sk))
+		go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey))
 	}
 }
 
@@ -170,9 +170,9 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
 	tlsConfig.NextProtos = []string{"frp"}
 	quicListener, err := quic.Listen(listenConn, tlsConfig,
 		&quic.Config{
-			MaxIdleTimeout:     time.Duration(pxy.clientCfg.QUICMaxIdleTimeout) * time.Second,
-			MaxIncomingStreams: int64(pxy.clientCfg.QUICMaxIncomingStreams),
-			KeepAlivePeriod:    time.Duration(pxy.clientCfg.QUICKeepalivePeriod) * time.Second,
+			MaxIdleTimeout:     time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
+			MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams),
+			KeepAlivePeriod:    time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
 		},
 	)
 	if err != nil {
@@ -192,6 +192,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
 			_ = c.CloseWithError(0, "")
 			return
 		}
-		go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Sk))
+		go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
 	}
 }

+ 44 - 46
client/service.go

@@ -19,7 +19,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
-	"math/rand"
 	"net"
 	"runtime"
 	"strconv"
@@ -32,10 +31,11 @@ import (
 	libdial "github.com/fatedier/golib/net/dial"
 	fmux "github.com/hashicorp/yamux"
 	quic "github.com/quic-go/quic-go"
+	"github.com/samber/lo"
 
 	"github.com/fatedier/frp/assets"
 	"github.com/fatedier/frp/pkg/auth"
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/log"
@@ -47,8 +47,6 @@ import (
 
 func init() {
 	crypto.DefaultSalt = "frp"
-	// TODO: remove this when we drop support for go1.19
-	rand.Seed(time.Now().UnixNano())
 }
 
 // Service is a client service.
@@ -63,9 +61,9 @@ type Service struct {
 	// Sets authentication based on selected method
 	authSetter auth.Setter
 
-	cfg         config.ClientCommonConf
-	pxyCfgs     map[string]config.ProxyConf
-	visitorCfgs map[string]config.VisitorConf
+	cfg         *v1.ClientCommonConfig
+	pxyCfgs     []v1.ProxyConfigurer
+	visitorCfgs []v1.VisitorConfigurer
 	cfgMu       sync.RWMutex
 
 	// The configuration file used to initialize this client, or an empty
@@ -81,13 +79,13 @@ type Service struct {
 }
 
 func NewService(
-	cfg config.ClientCommonConf,
-	pxyCfgs map[string]config.ProxyConf,
-	visitorCfgs map[string]config.VisitorConf,
+	cfg *v1.ClientCommonConfig,
+	pxyCfgs []v1.ProxyConfigurer,
+	visitorCfgs []v1.VisitorConfigurer,
 	cfgFile string,
 ) (svr *Service, err error) {
 	svr = &Service{
-		authSetter:  auth.NewAuthSetter(cfg.ClientConfig),
+		authSetter:  auth.NewAuthSetter(cfg.Auth),
 		cfg:         cfg,
 		cfgFile:     cfgFile,
 		pxyCfgs:     pxyCfgs,
@@ -134,7 +132,7 @@ func (svr *Service) Run(ctx context.Context) error {
 
 			// if login_fail_exit is true, just exit this program
 			// otherwise sleep a while and try again to connect to server
-			if svr.cfg.LoginFailExit {
+			if lo.FromPtr(svr.cfg.LoginFailExit) {
 				return err
 			}
 			util.RandomSleep(5*time.Second, 0.9, 1.1)
@@ -151,16 +149,16 @@ func (svr *Service) Run(ctx context.Context) error {
 
 	go svr.keepControllerWorking()
 
-	if svr.cfg.AdminPort != 0 {
+	if svr.cfg.WebServer.Port != 0 {
 		// Init admin server assets
-		assets.Load(svr.cfg.AssetsDir)
+		assets.Load(svr.cfg.WebServer.AssetsDir)
 
-		address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort))
+		address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port))
 		err := svr.RunAdminServer(address)
 		if err != nil {
 			log.Warn("run admin server error: %v", err)
 		}
-		log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort)
+		log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
 	}
 	<-svr.ctx.Done()
 	// service context may not be canceled by svr.Close(), we should call it here to release resources
@@ -244,7 +242,7 @@ func (svr *Service) keepControllerWorking() {
 // session: if it's not nil, using tcp mux
 func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	xl := xlog.FromContextSafe(svr.ctx)
-	cm = NewConnectionManager(svr.ctx, &svr.cfg)
+	cm = NewConnectionManager(svr.ctx, svr.cfg)
 
 	if err = cm.OpenConnection(); err != nil {
 		return nil, nil, err
@@ -264,12 +262,12 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	loginMsg := &msg.Login{
 		Arch:      runtime.GOARCH,
 		Os:        runtime.GOOS,
-		PoolCount: svr.cfg.PoolCount,
+		PoolCount: svr.cfg.Transport.PoolCount,
 		User:      svr.cfg.User,
 		Version:   version.Full(),
 		Timestamp: time.Now().Unix(),
 		RunID:     svr.runID,
-		Metas:     svr.cfg.Metas,
+		Metas:     svr.cfg.Metadatas,
 	}
 
 	// Add auth
@@ -302,7 +300,7 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	return
 }
 
-func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {
+func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
 	svr.cfgMu.Lock()
 	svr.pxyCfgs = pxyCfgs
 	svr.visitorCfgs = visitorCfgs
@@ -339,13 +337,13 @@ func (svr *Service) GracefulClose(d time.Duration) {
 
 type ConnectionManager struct {
 	ctx context.Context
-	cfg *config.ClientCommonConf
+	cfg *v1.ClientCommonConfig
 
 	muxSession *fmux.Session
 	quicConn   quic.Connection
 }
 
-func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager {
+func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager {
 	return &ConnectionManager{
 		ctx: ctx,
 		cfg: cfg,
@@ -356,18 +354,18 @@ func (cm *ConnectionManager) OpenConnection() error {
 	xl := xlog.FromContextSafe(cm.ctx)
 
 	// special for quic
-	if strings.EqualFold(cm.cfg.Protocol, "quic") {
+	if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") {
 		var tlsConfig *tls.Config
 		var err error
-		sn := cm.cfg.TLSServerName
+		sn := cm.cfg.Transport.TLS.ServerName
 		if sn == "" {
 			sn = cm.cfg.ServerAddr
 		}
-		if cm.cfg.TLSEnable {
+		if lo.FromPtr(cm.cfg.Transport.TLS.Enable) {
 			tlsConfig, err = transport.NewClientTLSConfig(
-				cm.cfg.TLSCertFile,
-				cm.cfg.TLSKeyFile,
-				cm.cfg.TLSTrustedCaFile,
+				cm.cfg.Transport.TLS.CertFile,
+				cm.cfg.Transport.TLS.KeyFile,
+				cm.cfg.Transport.TLS.TrustedCaFile,
 				sn)
 		} else {
 			tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
@@ -382,9 +380,9 @@ func (cm *ConnectionManager) OpenConnection() error {
 			cm.ctx,
 			net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
 			tlsConfig, &quic.Config{
-				MaxIdleTimeout:     time.Duration(cm.cfg.QUICMaxIdleTimeout) * time.Second,
-				MaxIncomingStreams: int64(cm.cfg.QUICMaxIncomingStreams),
-				KeepAlivePeriod:    time.Duration(cm.cfg.QUICKeepalivePeriod) * time.Second,
+				MaxIdleTimeout:     time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
+				MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams),
+				KeepAlivePeriod:    time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
 			})
 		if err != nil {
 			return err
@@ -393,7 +391,7 @@ func (cm *ConnectionManager) OpenConnection() error {
 		return nil
 	}
 
-	if !cm.cfg.TCPMux {
+	if !lo.FromPtr(cm.cfg.Transport.TCPMux) {
 		return nil
 	}
 
@@ -403,7 +401,7 @@ func (cm *ConnectionManager) OpenConnection() error {
 	}
 
 	fmuxCfg := fmux.DefaultConfig()
-	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second
+	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
 	fmuxCfg.LogOutput = io.Discard
 	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	session, err := fmux.Client(conn, fmuxCfg)
@@ -436,20 +434,20 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 	xl := xlog.FromContextSafe(cm.ctx)
 	var tlsConfig *tls.Config
 	var err error
-	tlsEnable := cm.cfg.TLSEnable
-	if cm.cfg.Protocol == "wss" {
+	tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable)
+	if cm.cfg.Transport.Protocol == "wss" {
 		tlsEnable = true
 	}
 	if tlsEnable {
-		sn := cm.cfg.TLSServerName
+		sn := cm.cfg.Transport.TLS.ServerName
 		if sn == "" {
 			sn = cm.cfg.ServerAddr
 		}
 
 		tlsConfig, err = transport.NewClientTLSConfig(
-			cm.cfg.TLSCertFile,
-			cm.cfg.TLSKeyFile,
-			cm.cfg.TLSTrustedCaFile,
+			cm.cfg.Transport.TLS.CertFile,
+			cm.cfg.Transport.TLS.KeyFile,
+			cm.cfg.Transport.TLS.TrustedCaFile,
 			sn)
 		if err != nil {
 			xl.Warn("fail to build tls configuration, err: %v", err)
@@ -457,19 +455,19 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 		}
 	}
 
-	proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.HTTPProxy)
+	proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL)
 	if err != nil {
 		xl.Error("fail to parse proxy url")
 		return nil, err
 	}
 	dialOptions := []libdial.DialOption{}
-	protocol := cm.cfg.Protocol
+	protocol := cm.cfg.Transport.Protocol
 	switch protocol {
 	case "websocket":
 		protocol = "tcp"
 		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")}))
 		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
-			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
+			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
 		}))
 		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
 	case "wss":
@@ -481,13 +479,13 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
 	}
 
-	if cm.cfg.ConnectServerLocalIP != "" {
-		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
+	if cm.cfg.Transport.ConnectServerLocalIP != "" {
+		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP))
 	}
 	dialOptions = append(dialOptions,
 		libdial.WithProtocol(protocol),
-		libdial.WithTimeout(time.Duration(cm.cfg.DialServerTimeout)*time.Second),
-		libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second),
+		libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second),
+		libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second),
 		libdial.WithProxy(proxyType, addr),
 		libdial.WithProxyAuth(auth),
 	)

+ 8 - 8
client/visitor/stcp.go

@@ -22,7 +22,7 @@ import (
 
 	libio "github.com/fatedier/golib/io"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -31,7 +31,7 @@ import (
 type STCPVisitor struct {
 	*BaseVisitor
 
-	cfg *config.STCPVisitorConf
+	cfg *v1.STCPVisitorConfig
 }
 
 func (sv *STCPVisitor) Run() (err error) {
@@ -90,10 +90,10 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 	newVisitorConnMsg := &msg.NewVisitorConn{
 		RunID:          sv.helper.RunID(),
 		ProxyName:      sv.cfg.ServerName,
-		SignKey:        util.GetAuthKey(sv.cfg.Sk, now),
+		SignKey:        util.GetAuthKey(sv.cfg.SecretKey, now),
 		Timestamp:      now,
-		UseEncryption:  sv.cfg.UseEncryption,
-		UseCompression: sv.cfg.UseCompression,
+		UseEncryption:  sv.cfg.Transport.UseEncryption,
+		UseCompression: sv.cfg.Transport.UseCompression,
 	}
 	err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
 	if err != nil {
@@ -117,15 +117,15 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
 
 	var remote io.ReadWriteCloser
 	remote = visitorConn
-	if sv.cfg.UseEncryption {
-		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
+	if sv.cfg.Transport.UseEncryption {
+		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
 
-	if sv.cfg.UseCompression {
+	if sv.cfg.Transport.UseCompression {
 		var recycleFn func()
 		remote, recycleFn = libio.WithCompressionFromPool(remote)
 		defer recycleFn()

+ 8 - 8
client/visitor/sudp.go

@@ -25,7 +25,7 @@ import (
 	"github.com/fatedier/golib/errors"
 	libio "github.com/fatedier/golib/io"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/proto/udp"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
@@ -42,7 +42,7 @@ type SUDPVisitor struct {
 	readCh  chan *msg.UDPPacket
 	sendCh  chan *msg.UDPPacket
 
-	cfg *config.SUDPVisitorConf
+	cfg *v1.SUDPVisitorConfig
 }
 
 // SUDP Run start listen a udp port
@@ -208,10 +208,10 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
 	newVisitorConnMsg := &msg.NewVisitorConn{
 		RunID:          sv.helper.RunID(),
 		ProxyName:      sv.cfg.ServerName,
-		SignKey:        util.GetAuthKey(sv.cfg.Sk, now),
+		SignKey:        util.GetAuthKey(sv.cfg.SecretKey, now),
 		Timestamp:      now,
-		UseEncryption:  sv.cfg.UseEncryption,
-		UseCompression: sv.cfg.UseCompression,
+		UseEncryption:  sv.cfg.Transport.UseEncryption,
+		UseCompression: sv.cfg.Transport.UseCompression,
 	}
 	err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
 	if err != nil {
@@ -232,14 +232,14 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
 
 	var remote io.ReadWriteCloser
 	remote = visitorConn
-	if sv.cfg.UseEncryption {
-		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk))
+	if sv.cfg.Transport.UseEncryption {
+		remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return nil, err
 		}
 	}
-	if sv.cfg.UseCompression {
+	if sv.cfg.Transport.UseCompression {
 		remote = libio.WithCompression(remote)
 	}
 	return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil

+ 8 - 8
client/visitor/visitor.go

@@ -19,7 +19,7 @@ import (
 	"net"
 	"sync"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
@@ -47,11 +47,11 @@ type Visitor interface {
 
 func NewVisitor(
 	ctx context.Context,
-	cfg config.VisitorConf,
-	clientCfg config.ClientCommonConf,
+	cfg v1.VisitorConfigurer,
+	clientCfg *v1.ClientCommonConfig,
 	helper Helper,
 ) (visitor Visitor) {
-	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().ProxyName)
+	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name)
 	baseVisitor := BaseVisitor{
 		clientCfg:  clientCfg,
 		helper:     helper,
@@ -59,18 +59,18 @@ func NewVisitor(
 		internalLn: utilnet.NewInternalListener(),
 	}
 	switch cfg := cfg.(type) {
-	case *config.STCPVisitorConf:
+	case *v1.STCPVisitorConfig:
 		visitor = &STCPVisitor{
 			BaseVisitor: &baseVisitor,
 			cfg:         cfg,
 		}
-	case *config.XTCPVisitorConf:
+	case *v1.XTCPVisitorConfig:
 		visitor = &XTCPVisitor{
 			BaseVisitor:   &baseVisitor,
 			cfg:           cfg,
 			startTunnelCh: make(chan struct{}),
 		}
-	case *config.SUDPVisitorConf:
+	case *v1.SUDPVisitorConfig:
 		visitor = &SUDPVisitor{
 			BaseVisitor:  &baseVisitor,
 			cfg:          cfg,
@@ -81,7 +81,7 @@ func NewVisitor(
 }
 
 type BaseVisitor struct {
-	clientCfg  config.ClientCommonConf
+	clientCfg  *v1.ClientCommonConfig
 	helper     Helper
 	l          net.Listener
 	internalLn *utilnet.InternalListener

+ 17 - 11
client/visitor/visitor_manager.go

@@ -22,14 +22,16 @@ import (
 	"sync"
 	"time"
 
-	"github.com/fatedier/frp/pkg/config"
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
 type Manager struct {
-	clientCfg config.ClientCommonConf
-	cfgs      map[string]config.VisitorConf
+	clientCfg *v1.ClientCommonConfig
+	cfgs      map[string]v1.VisitorConfigurer
 	visitors  map[string]Visitor
 	helper    Helper
 
@@ -44,13 +46,13 @@ type Manager struct {
 func NewManager(
 	ctx context.Context,
 	runID string,
-	clientCfg config.ClientCommonConf,
+	clientCfg *v1.ClientCommonConfig,
 	connectServer func() (net.Conn, error),
 	msgTransporter transport.MessageTransporter,
 ) *Manager {
 	m := &Manager{
 		clientCfg:     clientCfg,
-		cfgs:          make(map[string]config.VisitorConf),
+		cfgs:          make(map[string]v1.VisitorConfigurer),
 		visitors:      make(map[string]Visitor),
 		checkInterval: 10 * time.Second,
 		ctx:           ctx,
@@ -79,7 +81,7 @@ func (vm *Manager) Run() {
 		case <-ticker.C:
 			vm.mu.Lock()
 			for _, cfg := range vm.cfgs {
-				name := cfg.GetBaseConfig().ProxyName
+				name := cfg.GetBaseConfig().Name
 				if _, exist := vm.visitors[name]; !exist {
 					xl.Info("try to start visitor [%s]", name)
 					_ = vm.startVisitor(cfg)
@@ -104,9 +106,9 @@ func (vm *Manager) Close() {
 }
 
 // Hold lock before calling this function.
-func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) {
+func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
 	xl := xlog.FromContextSafe(vm.ctx)
-	name := cfg.GetBaseConfig().ProxyName
+	name := cfg.GetBaseConfig().Name
 	visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper)
 	err = visitor.Run()
 	if err != nil {
@@ -118,15 +120,18 @@ func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) {
 	return
 }
 
-func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
+func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
 	xl := xlog.FromContextSafe(vm.ctx)
+	cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
+		return c.GetBaseConfig().Name
+	})
 	vm.mu.Lock()
 	defer vm.mu.Unlock()
 
 	delNames := make([]string, 0)
 	for name, oldCfg := range vm.cfgs {
 		del := false
-		cfg, ok := cfgs[name]
+		cfg, ok := cfgsMap[name]
 		if !ok || !reflect.DeepEqual(oldCfg, cfg) {
 			del = true
 		}
@@ -145,7 +150,8 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) {
 	}
 
 	addNames := make([]string, 0)
-	for name, cfg := range cfgs {
+	for _, cfg := range cfgs {
+		name := cfg.GetBaseConfig().Name
 		if _, ok := vm.cfgs[name]; !ok {
 			vm.cfgs[name] = cfg
 			addNames = append(addNames, name)

+ 13 - 13
client/visitor/xtcp.go

@@ -29,7 +29,7 @@ import (
 	quic "github.com/quic-go/quic-go"
 	"golang.org/x/time/rate"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
 	"github.com/fatedier/frp/pkg/transport"
@@ -47,7 +47,7 @@ type XTCPVisitor struct {
 	retryLimiter  *rate.Limiter
 	cancel        context.CancelFunc
 
-	cfg *config.XTCPVisitorConf
+	cfg *v1.XTCPVisitorConfig
 }
 
 func (sv *XTCPVisitor) Run() (err error) {
@@ -56,7 +56,7 @@ func (sv *XTCPVisitor) Run() (err error) {
 	if sv.cfg.Protocol == "kcp" {
 		sv.session = NewKCPTunnelSession()
 	} else {
-		sv.session = NewQUICTunnelSession(&sv.clientCfg)
+		sv.session = NewQUICTunnelSession(sv.clientCfg)
 	}
 
 	if sv.cfg.BindPort > 0 {
@@ -192,14 +192,14 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
 	}
 
 	var muxConnRWCloser io.ReadWriteCloser = tunnelConn
-	if sv.cfg.UseEncryption {
-		muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk))
+	if sv.cfg.Transport.UseEncryption {
+		muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if sv.cfg.UseCompression {
+	if sv.cfg.Transport.UseCompression {
 		var recycleFn func()
 		muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
 		defer recycleFn()
@@ -292,7 +292,7 @@ func (sv *XTCPVisitor) makeNatHole() {
 		TransactionID: transactionID,
 		ProxyName:     sv.cfg.ServerName,
 		Protocol:      sv.cfg.Protocol,
-		SignKey:       util.GetAuthKey(sv.cfg.Sk, now),
+		SignKey:       util.GetAuthKey(sv.cfg.SecretKey, now),
 		Timestamp:     now,
 		MappedAddrs:   prepareResult.Addrs,
 		AssistedAddrs: prepareResult.AssistedAddrs,
@@ -310,7 +310,7 @@ func (sv *XTCPVisitor) makeNatHole() {
 		natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs,
 		natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior)
 
-	newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.Sk))
+	newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey))
 	if err != nil {
 		listenConn.Close()
 		xl.Warn("make hole error: %v", err)
@@ -398,10 +398,10 @@ type QUICTunnelSession struct {
 	listenConn *net.UDPConn
 	mu         sync.RWMutex
 
-	clientCfg *config.ClientCommonConf
+	clientCfg *v1.ClientCommonConfig
 }
 
-func NewQUICTunnelSession(clientCfg *config.ClientCommonConf) TunnelSession {
+func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession {
 	return &QUICTunnelSession{
 		clientCfg: clientCfg,
 	}
@@ -415,9 +415,9 @@ func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) e
 	tlsConfig.NextProtos = []string{"frp"}
 	quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,
 		&quic.Config{
-			MaxIdleTimeout:     time.Duration(qs.clientCfg.QUICMaxIdleTimeout) * time.Second,
-			MaxIncomingStreams: int64(qs.clientCfg.QUICMaxIncomingStreams),
-			KeepAlivePeriod:    time.Duration(qs.clientCfg.QUICKeepalivePeriod) * time.Second,
+			MaxIdleTimeout:     time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
+			MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),
+			KeepAlivePeriod:    time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
 		})
 	if err != nil {
 		return fmt.Errorf("dial quic error: %v", err)

+ 117 - 0
cmd/frpc/sub/admin.go

@@ -0,0 +1,117 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sub
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/rodaine/table"
+	"github.com/spf13/cobra"
+
+	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	clientsdk "github.com/fatedier/frp/pkg/sdk/client"
+)
+
+func init() {
+	rootCmd.AddCommand(NewAdminCommand(
+		"reload",
+		"Hot-Reload frpc configuration",
+		ReloadHandler,
+	))
+
+	rootCmd.AddCommand(NewAdminCommand(
+		"status",
+		"Overview of all proxies status",
+		StatusHandler,
+	))
+
+	rootCmd.AddCommand(NewAdminCommand(
+		"stop",
+		"Stop the running frpc",
+		StopHandler,
+	))
+}
+
+func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
+	return &cobra.Command{
+		Use:   name,
+		Short: short,
+		Run: func(cmd *cobra.Command, args []string) {
+			cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
+			if err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+			if cfg.WebServer.Port <= 0 {
+				fmt.Println("web server port should be set if you want to use this feature")
+				os.Exit(1)
+			}
+
+			if err := handler(cfg); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+		},
+	}
+}
+
+func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
+	client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
+	client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
+	if err := client.Reload(); err != nil {
+		return err
+	}
+	fmt.Println("reload success")
+	return nil
+}
+
+func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
+	client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
+	client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
+	res, err := client.GetAllProxyStatus()
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("Proxy Status...\n\n")
+	for _, typ := range proxyTypes {
+		arrs := res[string(typ)]
+		if len(arrs) == 0 {
+			continue
+		}
+
+		fmt.Println(strings.ToUpper(string(typ)))
+		tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
+		for _, ps := range arrs {
+			tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
+		}
+		tbl.Print()
+		fmt.Println("")
+	}
+	return nil
+}
+
+func StopHandler(clientCfg *v1.ClientCommonConfig) error {
+	client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
+	client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
+	if err := client.Stop(); err != nil {
+		return err
+	}
+	fmt.Println("stop success")
+	return nil
+}

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

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

+ 0 - 98
cmd/frpc/sub/http.go

@@ -1,98 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(httpCmd)
-
-	httpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	httpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	httpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	httpCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
-	httpCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
-	httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations")
-	httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user")
-	httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password")
-	httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
-	httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	httpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(httpCmd)
-}
-
-var httpCmd = &cobra.Command{
-	Use:   "http",
-	Short: "Run frpc with a single http proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		cfg := &config.HTTPProxyConf{}
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.HTTPProxy
-		cfg.LocalIP = localIP
-		cfg.LocalPort = localPort
-		cfg.CustomDomains = strings.Split(customDomains, ",")
-		cfg.SubDomain = subDomain
-		cfg.Locations = strings.Split(locations, ",")
-		cfg.HTTPUser = httpUser
-		cfg.HTTPPwd = httpPwd
-		cfg.HostHeaderRewrite = hostHeaderRewrite
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
-
-		err = cfg.ValidateForClient()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 0 - 90
cmd/frpc/sub/https.go

@@ -1,90 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(httpsCmd)
-
-	httpsCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	httpsCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	httpsCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	httpsCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
-	httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
-	httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	httpsCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(httpsCmd)
-}
-
-var httpsCmd = &cobra.Command{
-	Use:   "https",
-	Short: "Run frpc with a single https proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		cfg := &config.HTTPSProxyConf{}
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.HTTPSProxy
-		cfg.LocalIP = localIP
-		cfg.LocalPort = localPort
-		cfg.CustomDomains = strings.Split(customDomains, ",")
-		cfg.SubDomain = subDomain
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
-
-		err = cfg.ValidateForClient()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 4 - 5
cmd/frpc/sub/nathole.go

@@ -21,6 +21,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/nathole"
 )
 
@@ -30,8 +31,6 @@ var (
 )
 
 func init() {
-	RegisterCommonFlags(natholeCmd)
-
 	rootCmd.AddCommand(natholeCmd)
 	natholeCmd.AddCommand(natholeDiscoveryCmd)
 
@@ -49,9 +48,9 @@ var natholeDiscoveryCmd = &cobra.Command{
 	Short: "Discover nathole information from stun server",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		// ignore error here, because we can use command line pameters
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
+		cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
 		if err != nil {
-			cfg = config.GetDefaultClientConf()
+			cfg = &v1.ClientCommonConfig{}
 		}
 		if natHoleSTUNServer != "" {
 			cfg.NatHoleSTUNServer = natHoleSTUNServer
@@ -89,7 +88,7 @@ var natholeDiscoveryCmd = &cobra.Command{
 	},
 }
 
-func validateForNatHoleDiscovery(cfg config.ClientCommonConf) error {
+func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error {
 	if cfg.NatHoleSTUNServer == "" {
 		return fmt.Errorf("nat_hole_stun_server can not be empty")
 	}

+ 120 - 0
cmd/frpc/sub/proxy.go

@@ -0,0 +1,120 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sub
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/samber/lo"
+	"github.com/spf13/cobra"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
+)
+
+var proxyTypes = []v1.ProxyType{
+	v1.ProxyTypeTCP,
+	v1.ProxyTypeUDP,
+	v1.ProxyTypeTCPMUX,
+	v1.ProxyTypeHTTP,
+	v1.ProxyTypeHTTPS,
+	v1.ProxyTypeSTCP,
+	v1.ProxyTypeSUDP,
+	v1.ProxyTypeXTCP,
+}
+
+var visitorTypes = []v1.VisitorType{
+	v1.VisitorTypeSTCP,
+	v1.VisitorTypeSUDP,
+	v1.VisitorTypeXTCP,
+}
+
+func init() {
+	for _, typ := range proxyTypes {
+		c := v1.NewProxyConfigurerByType(typ)
+		if c == nil {
+			panic("proxy type: " + typ + " not support")
+		}
+		clientCfg := v1.ClientCommonConfig{}
+		cmd := NewProxyCommand(string(typ), c, &clientCfg)
+		RegisterClientCommonConfigFlags(cmd, &clientCfg)
+		RegisterProxyFlags(cmd, c)
+
+		// add sub command for visitor
+		if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
+			vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ))
+			if vc == nil {
+				panic("visitor type: " + typ + " not support")
+			}
+			visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
+			RegisterVisitorFlags(visitorCmd, vc)
+			cmd.AddCommand(visitorCmd)
+		}
+		rootCmd.AddCommand(cmd)
+	}
+}
+
+func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
+	return &cobra.Command{
+		Use:   name,
+		Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
+		Run: func(cmd *cobra.Command, args []string) {
+			clientCfg.Complete()
+			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+
+			c.Complete(clientCfg.User)
+			c.GetBaseConfig().Type = name
+			if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+			err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
+			if err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+		},
+	}
+}
+
+func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
+	return &cobra.Command{
+		Use:   "visitor",
+		Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
+		Run: func(cmd *cobra.Command, args []string) {
+			clientCfg.Complete()
+			if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+
+			c.Complete(clientCfg)
+			c.GetBaseConfig().Type = name
+			if err := validation.ValidateVisitorConfigurer(c); err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+			err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
+			if err != nil {
+				fmt.Println(err)
+				os.Exit(1)
+			}
+		},
+	}
+}

+ 0 - 84
cmd/frpc/sub/reload.go

@@ -1,84 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"encoding/base64"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-)
-
-func init() {
-	rootCmd.AddCommand(reloadCmd)
-}
-
-var reloadCmd = &cobra.Command{
-	Use:   "reload",
-	Short: "Hot-Reload frpc configuration",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		err = reload(cfg)
-		if err != nil {
-			fmt.Printf("frpc reload error: %v\n", err)
-			os.Exit(1)
-		}
-		fmt.Printf("reload success\n")
-		return nil
-	},
-}
-
-func reload(clientCfg config.ClientCommonConf) error {
-	if clientCfg.AdminPort == 0 {
-		return fmt.Errorf("admin_port shoud be set if you want to use reload feature")
-	}
-
-	req, err := http.NewRequest("GET", "http://"+
-		clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/reload", nil)
-	if err != nil {
-		return err
-	}
-
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
-
-	req.Header.Add("Authorization", authStr)
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode == 200 {
-		return nil
-	}
-
-	body, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return err
-	}
-	return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
-}

+ 22 - 104
cmd/frpc/sub/root.go

@@ -18,11 +18,9 @@ import (
 	"context"
 	"fmt"
 	"io/fs"
-	"net"
 	"os"
 	"os/signal"
 	"path/filepath"
-	"strconv"
 	"sync"
 	"syscall"
 	"time"
@@ -30,55 +28,17 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/client"
-	"github.com/fatedier/frp/pkg/auth"
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
 	"github.com/fatedier/frp/pkg/util/log"
 	"github.com/fatedier/frp/pkg/util/version"
 )
 
-const (
-	CfgFileTypeIni = iota
-	CfgFileTypeCmd
-)
-
 var (
 	cfgFile     string
 	cfgDir      string
 	showVersion bool
-
-	serverAddr      string
-	user            string
-	protocol        string
-	token           string
-	logLevel        string
-	logFile         string
-	logMaxDays      int
-	disableLogColor bool
-	dnsServer       string
-
-	proxyName          string
-	localIP            string
-	localPort          int
-	remotePort         int
-	useEncryption      bool
-	useCompression     bool
-	bandwidthLimit     string
-	bandwidthLimitMode string
-	customDomains      string
-	subDomain          string
-	httpUser           string
-	httpPwd            string
-	locations          string
-	hostHeaderRewrite  string
-	role               string
-	sk                 string
-	multiplexer        string
-	serverName         string
-	bindAddr           string
-	bindPort           int
-
-	tlsEnable     bool
-	tlsServerName string
 )
 
 func init() {
@@ -87,20 +47,6 @@ func init() {
 	rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
 }
 
-func RegisterCommonFlags(cmd *cobra.Command) {
-	cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address")
-	cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user")
-	cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp, kcp, quic, websocket, wss")
-	cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
-	cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
-	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", "", true, "enable frpc tls")
-	cmd.PersistentFlags().StringVarP(&tlsServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
-	cmd.PersistentFlags().StringVarP(&dnsServer, "dns_server", "", "", "specify dns server instead of using system default one")
-}
-
 var rootCmd = &cobra.Command{
 	Use:   "frpc",
 	Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
@@ -120,6 +66,7 @@ var rootCmd = &cobra.Command{
 		// Do not show command usage here.
 		err := runClient(cfgFile)
 		if err != nil {
+			fmt.Println(err)
 			os.Exit(1)
 		}
 		return nil
@@ -160,78 +107,49 @@ func handleTermSignal(svr *client.Service) {
 	svr.GracefulClose(500 * time.Millisecond)
 }
 
-func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
-	cfg = config.GetDefaultClientConf()
-
-	ipStr, portStr, err := net.SplitHostPort(serverAddr)
+func runClient(cfgFilePath string) error {
+	cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
 	if err != nil {
-		err = fmt.Errorf("invalid server_addr: %v", err)
-		return
+		return err
 	}
-
-	cfg.ServerAddr = ipStr
-	cfg.ServerPort, err = strconv.Atoi(portStr)
-	if err != nil {
-		err = fmt.Errorf("invalid server_addr: %v", err)
-		return
+	if isLegacyFormat {
+		fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
+			"please use yaml/json/toml format instead!\n")
 	}
 
-	cfg.User = user
-	cfg.Protocol = protocol
-	cfg.LogLevel = logLevel
-	cfg.LogFile = logFile
-	cfg.LogMaxDays = int64(logMaxDays)
-	cfg.DisableLogColor = disableLogColor
-	cfg.DNSServer = dnsServer
-
-	// Only token authentication is supported in cmd mode
-	cfg.ClientConfig = auth.GetDefaultClientConf()
-	cfg.Token = token
-	cfg.TLSEnable = tlsEnable
-	cfg.TLSServerName = tlsServerName
-
-	cfg.Complete()
-	if err = cfg.Validate(); err != nil {
-		err = fmt.Errorf("parse config error: %v", err)
-		return
+	warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
+	if warning != nil {
+		fmt.Printf("WARNING: %v\n", warning)
 	}
-	return
-}
-
-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)
 }
 
 func startService(
-	cfg config.ClientCommonConf,
-	pxyCfgs map[string]config.ProxyConf,
-	visitorCfgs map[string]config.VisitorConf,
+	cfg *v1.ClientCommonConfig,
+	pxyCfgs []v1.ProxyConfigurer,
+	visitorCfgs []v1.VisitorConfigurer,
 	cfgFile string,
-) (err error) {
-	log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
-		cfg.LogMaxDays, cfg.DisableLogColor)
+) error {
+	log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
 
 	if 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 {
-		err = errRet
-		return
+	svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
+	if err != nil {
+		return err
 	}
 
-	shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"
+	shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
 	// Capture the exit signal if we use kcp or quic.
 	if shouldGracefulClose {
 		go handleTermSignal(svr)
 	}
 
 	_ = svr.Run(context.Background())
-	return
+	return nil
 }

+ 0 - 107
cmd/frpc/sub/status.go

@@ -1,107 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"strings"
-
-	"github.com/rodaine/table"
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/client"
-	"github.com/fatedier/frp/pkg/config"
-)
-
-func init() {
-	rootCmd.AddCommand(statusCmd)
-}
-
-var statusCmd = &cobra.Command{
-	Use:   "status",
-	Short: "Overview of all proxies status",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		if err = status(cfg); err != nil {
-			fmt.Printf("frpc get status error: %v\n", err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}
-
-func status(clientCfg config.ClientCommonConf) error {
-	if clientCfg.AdminPort == 0 {
-		return fmt.Errorf("admin_port shoud be set if you want to get proxy status")
-	}
-
-	req, err := http.NewRequest("GET", "http://"+
-		clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/status", nil)
-	if err != nil {
-		return err
-	}
-
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
-
-	req.Header.Add("Authorization", authStr)
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != 200 {
-		return fmt.Errorf("admin api status code [%d]", resp.StatusCode)
-	}
-
-	body, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return err
-	}
-	res := make(client.StatusResp)
-	err = json.Unmarshal(body, &res)
-	if err != nil {
-		return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body)))
-	}
-
-	fmt.Println("Proxy Status...")
-	types := []string{"tcp", "udp", "tcpmux", "http", "https", "stcp", "sudp", "xtcp"}
-	for _, pxyType := range types {
-		arrs := res[pxyType]
-		if len(arrs) == 0 {
-			continue
-		}
-
-		fmt.Println(strings.ToUpper(pxyType))
-		tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error")
-		for _, ps := range arrs {
-			tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err)
-		}
-		tbl.Print()
-		fmt.Println("")
-	}
-	return nil
-}

+ 0 - 116
cmd/frpc/sub/stcp.go

@@ -1,116 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(stcpCmd)
-
-	stcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	stcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
-	stcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
-	stcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
-	stcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	stcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	stcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
-	stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
-	stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	stcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(stcpCmd)
-}
-
-var stcpCmd = &cobra.Command{
-	Use:   "stcp",
-	Short: "Run frpc with a single stcp proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
-
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-
-		switch role {
-		case "server":
-			cfg := &config.STCPProxyConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.STCPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.LocalIP = localIP
-			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			proxyConfs[cfg.ProxyName] = cfg
-		case "visitor":
-			cfg := &config.STCPVisitorConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.STCPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.ServerName = serverName
-			cfg.BindAddr = bindAddr
-			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			visitorConfs[cfg.ProxyName] = cfg
-		default:
-			fmt.Println("invalid role")
-			os.Exit(1)
-		}
-
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 0 - 84
cmd/frpc/sub/stop.go

@@ -1,84 +0,0 @@
-// Copyright 2023 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"encoding/base64"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-)
-
-func init() {
-	rootCmd.AddCommand(stopCmd)
-}
-
-var stopCmd = &cobra.Command{
-	Use:   "stop",
-	Short: "Stop the running frpc",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		err = stopClient(cfg)
-		if err != nil {
-			fmt.Printf("frpc stop error: %v\n", err)
-			os.Exit(1)
-		}
-		fmt.Printf("stop success\n")
-		return nil
-	},
-}
-
-func stopClient(clientCfg config.ClientCommonConf) error {
-	if clientCfg.AdminPort == 0 {
-		return fmt.Errorf("admin_port shoud be set if you want to use stop feature")
-	}
-
-	req, err := http.NewRequest("POST", "http://"+
-		clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/stop", nil)
-	if err != nil {
-		return err
-	}
-
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
-
-	req.Header.Add("Authorization", authStr)
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode == 200 {
-		return nil
-	}
-
-	body, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return err
-	}
-	return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
-}

+ 0 - 115
cmd/frpc/sub/sudp.go

@@ -1,115 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(sudpCmd)
-
-	sudpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	sudpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
-	sudpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
-	sudpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
-	sudpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	sudpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	sudpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
-	sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
-	sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	sudpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(sudpCmd)
-}
-
-var sudpCmd = &cobra.Command{
-	Use:   "sudp",
-	Short: "Run frpc with a single sudp proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
-
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-
-		switch role {
-		case "server":
-			cfg := &config.SUDPProxyConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.SUDPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.LocalIP = localIP
-			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			proxyConfs[cfg.ProxyName] = cfg
-		case "visitor":
-			cfg := &config.SUDPVisitorConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.SUDPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.ServerName = serverName
-			cfg.BindAddr = bindAddr
-			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			visitorConfs[cfg.ProxyName] = cfg
-		default:
-			fmt.Println("invalid role")
-			os.Exit(1)
-		}
-
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
-		if err != nil {
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 0 - 87
cmd/frpc/sub/tcp.go

@@ -1,87 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(tcpCmd)
-
-	tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
-	tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	tcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(tcpCmd)
-}
-
-var tcpCmd = &cobra.Command{
-	Use:   "tcp",
-	Short: "Run frpc with a single tcp proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		cfg := &config.TCPProxyConf{}
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.TCPProxy
-		cfg.LocalIP = localIP
-		cfg.LocalPort = localPort
-		cfg.RemotePort = remotePort
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
-
-		err = cfg.ValidateForClient()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 0 - 92
cmd/frpc/sub/tcpmux.go

@@ -1,92 +0,0 @@
-// Copyright 2020 guylewin, guy@lewin.co.il
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(tcpMuxCmd)
-
-	tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	tcpMuxCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain")
-	tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain")
-	tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer")
-	tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(tcpMuxCmd)
-}
-
-var tcpMuxCmd = &cobra.Command{
-	Use:   "tcpmux",
-	Short: "Run frpc with a single tcpmux proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		cfg := &config.TCPMuxProxyConf{}
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.TCPMuxProxy
-		cfg.LocalIP = localIP
-		cfg.LocalPort = localPort
-		cfg.CustomDomains = strings.Split(customDomains, ",")
-		cfg.SubDomain = subDomain
-		cfg.Multiplexer = multiplexer
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
-
-		err = cfg.ValidateForClient()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 0 - 87
cmd/frpc/sub/udp.go

@@ -1,87 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(udpCmd)
-
-	udpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	udpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	udpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
-	udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	udpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(udpCmd)
-}
-
-var udpCmd = &cobra.Command{
-	Use:   "udp",
-	Short: "Run frpc with a single udp proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		cfg := &config.UDPProxyConf{}
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.UDPProxy
-		cfg.LocalIP = localIP
-		cfg.LocalPort = localPort
-		cfg.RemotePort = remotePort
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
-
-		err = cfg.ValidateForClient()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 15 - 1
cmd/frpc/sub/verify.go

@@ -21,6 +21,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/pkg/config"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
 )
 
 func init() {
@@ -31,7 +32,20 @@ var verifyCmd = &cobra.Command{
 	Use:   "verify",
 	Short: "Verify that the configures is valid",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		_, _, _, err := config.ParseClientConfig(cfgFile)
+		if cfgFile == "" {
+			fmt.Println("frpc: the configuration file is not specified")
+			return nil
+		}
+
+		cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile)
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs)
+		if warning != nil {
+			fmt.Printf("WARNING: %v\n", warning)
+		}
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 0 - 116
cmd/frpc/sub/xtcp.go

@@ -1,116 +0,0 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sub
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/fatedier/frp/pkg/config"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-func init() {
-	RegisterCommonFlags(xtcpCmd)
-
-	xtcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
-	xtcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role")
-	xtcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key")
-	xtcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name")
-	xtcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
-	xtcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
-	xtcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr")
-	xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port")
-	xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
-	xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
-	xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit")
-	xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode")
-
-	rootCmd.AddCommand(xtcpCmd)
-}
-
-var xtcpCmd = &cobra.Command{
-	Use:   "xtcp",
-	Short: "Run frpc with a single xtcp proxy",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfgFromCmd()
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
-
-		var prefix string
-		if user != "" {
-			prefix = user + "."
-		}
-
-		switch role {
-		case "server":
-			cfg := &config.XTCPProxyConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.XTCPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.LocalIP = localIP
-			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			proxyConfs[cfg.ProxyName] = cfg
-		case "visitor":
-			cfg := &config.XTCPVisitorConf{}
-			cfg.ProxyName = prefix + proxyName
-			cfg.ProxyType = consts.XTCPProxy
-			cfg.UseEncryption = useEncryption
-			cfg.UseCompression = useCompression
-			cfg.Role = role
-			cfg.Sk = sk
-			cfg.ServerName = serverName
-			cfg.BindAddr = bindAddr
-			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
-				fmt.Println(err)
-				os.Exit(1)
-			}
-			visitorConfs[cfg.ProxyName] = cfg
-		default:
-			fmt.Println("invalid role")
-			os.Exit(1)
-		}
-
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-		return nil
-	},
-}

+ 110 - 0
cmd/frps/flags.go

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

+ 0 - 6
cmd/frps/main.go

@@ -15,9 +15,6 @@
 package main
 
 import (
-	"math/rand"
-	"time"
-
 	"github.com/fatedier/golib/crypto"
 
 	_ "github.com/fatedier/frp/assets/frps"
@@ -26,8 +23,5 @@ import (
 
 func main() {
 	crypto.DefaultSalt = "frp"
-	// TODO: remove this when we drop support for go1.19
-	rand.Seed(time.Now().UnixNano())
-
 	Execute()
 }

+ 28 - 130
cmd/frps/root.go

@@ -21,78 +21,26 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/fatedier/frp/pkg/auth"
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
 	"github.com/fatedier/frp/pkg/util/log"
-	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/version"
 	"github.com/fatedier/frp/server"
 )
 
-const (
-	CfgFileTypeIni = iota
-	CfgFileTypeCmd
-)
-
 var (
 	cfgFile     string
 	showVersion bool
 
-	bindAddr             string
-	bindPort             int
-	kcpBindPort          int
-	proxyBindAddr        string
-	vhostHTTPPort        int
-	vhostHTTPSPort       int
-	vhostHTTPTimeout     int64
-	dashboardAddr        string
-	dashboardPort        int
-	dashboardUser        string
-	dashboardPwd         string
-	enablePrometheus     bool
-	logFile              string
-	logLevel             string
-	logMaxDays           int64
-	disableLogColor      bool
-	token                string
-	subDomainHost        string
-	allowPorts           string
-	maxPortsPerClient    int64
-	tlsOnly              bool
-	dashboardTLSMode     bool
-	dashboardTLSCertFile string
-	dashboardTLSKeyFile  string
+	serverCfg v1.ServerConfig
 )
 
 func init() {
 	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
 	rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
 
-	rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address")
-	rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port")
-	rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
-	rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
-	rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
-	rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
-	rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
-	rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
-	rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port")
-	rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user")
-	rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password")
-	rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
-	rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file")
-	rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
-	rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days")
-	rootCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
-
-	rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
-	rootCmd.PersistentFlags().StringVarP(&subDomainHost, "subdomain_host", "", "", "subdomain host")
-	rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports")
-	rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
-	rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only")
-	rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode")
-	rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
-	rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
+	RegisterServerConfigFlags(rootCmd, &serverCfg)
 }
 
 var rootCmd = &cobra.Command{
@@ -104,24 +52,36 @@ var rootCmd = &cobra.Command{
 			return nil
 		}
 
-		var cfg config.ServerCommonConf
-		var err error
+		var (
+			svrCfg         *v1.ServerConfig
+			isLegacyFormat bool
+			err            error
+		)
 		if cfgFile != "" {
-			var content []byte
-			content, err = config.GetRenderedConfFromFile(cfgFile)
+			svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
 			if err != nil {
-				return err
+				fmt.Println(err)
+				os.Exit(1)
+			}
+			if isLegacyFormat {
+				fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
+					"please use yaml/json/toml format instead!\n")
 			}
-			cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
 		} else {
-			cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil)
+			serverCfg.Complete()
+			svrCfg = &serverCfg
+		}
+
+		warning, err := validation.ValidateServerConfig(svrCfg)
+		if warning != nil {
+			fmt.Printf("WARNING: %v\n", warning)
 		}
 		if err != nil {
-			return err
+			fmt.Println(err)
+			os.Exit(1)
 		}
 
-		err = runServer(cfg)
-		if err != nil {
+		if err := runServer(svrCfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
@@ -135,70 +95,8 @@ func Execute() {
 	}
 }
 
-func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) {
-	if fileType == CfgFileTypeIni {
-		cfg, err = config.UnmarshalServerConfFromIni(source)
-	} else if fileType == CfgFileTypeCmd {
-		cfg, err = parseServerCommonCfgFromCmd()
-	}
-	if err != nil {
-		return
-	}
-	cfg.Complete()
-	err = cfg.Validate()
-	if err != nil {
-		err = fmt.Errorf("parse config error: %v", err)
-		return
-	}
-	return
-}
-
-func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
-	cfg = config.GetDefaultServerConf()
-
-	cfg.BindAddr = bindAddr
-	cfg.BindPort = bindPort
-	cfg.KCPBindPort = kcpBindPort
-	cfg.ProxyBindAddr = proxyBindAddr
-	cfg.VhostHTTPPort = vhostHTTPPort
-	cfg.VhostHTTPSPort = vhostHTTPSPort
-	cfg.VhostHTTPTimeout = vhostHTTPTimeout
-	cfg.DashboardAddr = dashboardAddr
-	cfg.DashboardPort = dashboardPort
-	cfg.DashboardUser = dashboardUser
-	cfg.DashboardPwd = dashboardPwd
-	cfg.EnablePrometheus = enablePrometheus
-	cfg.DashboardTLSCertFile = dashboardTLSCertFile
-	cfg.DashboardTLSKeyFile = dashboardTLSKeyFile
-	cfg.DashboardTLSMode = dashboardTLSMode
-	cfg.LogFile = logFile
-	cfg.LogLevel = logLevel
-	cfg.LogMaxDays = logMaxDays
-	cfg.SubDomainHost = subDomainHost
-	cfg.TLSOnly = tlsOnly
-
-	// Only token authentication is supported in cmd mode
-	cfg.ServerConfig = auth.GetDefaultServerConf()
-	cfg.Token = token
-	if len(allowPorts) > 0 {
-		// e.g. 1000-2000,2001,2002,3000-4000
-		ports, errRet := util.ParseRangeNumbers(allowPorts)
-		if errRet != nil {
-			err = fmt.Errorf("parse conf error: allow_ports: %v", errRet)
-			return
-		}
-
-		for _, port := range ports {
-			cfg.AllowPorts[int(port)] = struct{}{}
-		}
-	}
-	cfg.MaxPortsPerClient = maxPortsPerClient
-	cfg.DisableLogColor = disableLogColor
-	return
-}
-
-func runServer(cfg config.ServerCommonConf) (err error) {
-	log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor)
+func runServer(cfg *v1.ServerConfig) (err error) {
+	log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
 
 	if cfgFile != "" {
 		log.Info("frps uses config file: %s", cfgFile)

+ 7 - 4
cmd/frps/verify.go

@@ -21,6 +21,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/pkg/config"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
 )
 
 func init() {
@@ -32,21 +33,23 @@ var verifyCmd = &cobra.Command{
 	Short: "Verify that the configures is valid",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		if cfgFile == "" {
-			fmt.Println("no config file is specified")
+			fmt.Println("frps: the configuration file is not specified")
 			return nil
 		}
-		iniContent, err := config.GetRenderedConfFromFile(cfgFile)
+		svrCfg, _, err := config.LoadServerConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		_, err = parseServerCommonCfg(CfgFileTypeIni, iniContent)
+		warning, err := validation.ValidateServerConfig(svrCfg)
+		if warning != nil {
+			fmt.Printf("WARNING: %v\n", warning)
+		}
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-
 		fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
 		return nil
 	},

+ 0 - 9
conf/frpc.ini

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

+ 360 - 0
conf/frpc.toml

@@ -0,0 +1,360 @@
+# your proxy name will be changed to {user}.{proxy}
+user = "your_name"
+
+# A literal address or host name for IPv6 must be enclosed
+# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
+# For single serverAddr field, no need square brackets, like serverAddr = "::".
+serverAddr = "0.0.0.0"
+serverPort = 7000
+
+# STUN server to help penetrate NAT hole.
+# natHoleStunServer = "stun.easyvoip.com:3478"
+
+# Decide if exit program when first login failed, otherwise continuous relogin to frps
+# default is true
+loginFailExit = true
+
+# console or real logFile path like ./frpc.log
+log.to = "./frpc.log"
+# trace, debug, info, warn, error
+log.level = "info"
+log.maxDays = 3
+# disable log colors when log.to is console, default is false
+log.disablePrintColor = false
+
+auth.method = "token"
+# auth.additionalScopes specifies additional scopes to include authentication information.
+# Optional values are HeartBeats, NewWorkConns.
+# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
+
+# auth token
+auth.token = "12345678"
+
+# oidc.clientID specifies the client ID to use to get a token in OIDC authentication.
+# auth.oidc.clientID = ""
+# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication.
+# auth.oidc.clientSecret = ""
+# oidc.audience specifies the audience of the token in OIDC authentication.
+# auth.oidc.audience = ""
+# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
+# auth.oidc.scope = ""
+# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
+# It will be used to get an OIDC token.
+# auth.oidc.tokenEndpointURL = ""
+
+# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint.
+# For example, if you want to specify the "audience" parameter, you can set as follow.
+# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
+# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/"
+# auth.oidc.additionalEndpointParams.var1 = "foobar"
+
+# Set admin address for control frpc's action by http api such as reload
+webServer.addr = "127.0.0.1"
+webServer.port = 7400
+webServer.user = "admin"
+webServer.password = "admin"
+# Admin assets directory. By default, these assets are bundled with frpc.
+# webServer.assetsDir = "./static"
+
+# Enable golang pprof handlers in admin listener.
+webServer.pprofEnable = false
+
+# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
+# transport.dialServerTimeout = 10
+
+# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
+# If negative, keep-alive probes are disabled.
+# transport.dialServerKeepalive = 7200
+
+# connections will be established in advance, default value is zero
+transport.poolCount = 5
+
+# If tcp stream multiplexing is used, default is true, it must be same with frps
+# transport.tcpMux = true
+
+# Specify keep alive interval for tcp mux.
+# only valid if tcpMux is enabled.
+# transport.tcpMuxKeepaliveInterval = 60
+
+# Communication protocol used to connect to server
+# supports tcp, kcp, quic, websocket and wss now, default is tcp
+transport.protocol = "tcp"
+
+# set client binding ip when connect server, default is empty.
+# only when protocol = tcp or websocket, the value will be used.
+transport.connectServerLocalIP = "0.0.0.0"
+
+# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
+# it only works when protocol is tcp
+# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
+# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080"
+# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080"
+
+# quic protocol options
+# transport.quic.keepalivePeriod = 10
+# transport.quic.maxIdleTimeout = 30
+# transport.quic.maxIncomingStreams = 100000
+
+# 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.
+transport.tls.enable = true
+
+# transport.tls.certFile = "client.crt"
+# transport.tls.keyFile = "client.key"
+# transport.tls.trustedCaFile = "ca.crt"
+# transport.tls.serverName = "example.com"
+
+# If the disableCustomTLSFirstByte 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.
+# transport.tls.disableCustomTLSFirstByte = true
+
+# Heartbeat configure, it's not recommended to modify the default value.
+# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
+# to disable it.
+# transport.heartbeatInterval = 30
+# transport.heartbeatTimeout = 90
+
+# Specify a dns server, so frpc will use this instead of default one
+# dnsServer = "8.8.8.8"
+
+# Proxy names you want to start.
+# Default is empty, means all proxies.
+# start = ["ssh", "dns"]
+
+# Specify udp packet size, unit is byte. If not set, the default value is 1500.
+# This parameter should be same between client and server.
+# It affects the udp and sudp proxy.
+udpPacketSize = 1500
+
+# Additional metadatas for client.
+metadatas.var1 = "abc"
+metadatas.var2 = "123"
+
+# Include other config files for proxies.
+# includes = ["./confd/*.ini"]
+
+[[proxies]]
+# 'ssh' is the unique proxy name
+# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
+name = "ssh"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 22
+# Limit bandwidth for this proxy, unit is KB and MB
+transport.bandwidthLimit = "1MB"
+# Where to limit bandwidth, can be 'client' or 'server', default is 'client'
+transport.bandwidthLimitMode = "client"
+# If true, traffic of this proxy will be encrypted, default is false
+transport.useEncryption = false
+# If true, traffic will be compressed
+transport.useCompression = false
+# Remote port listen by frps
+remotePort = 6001
+# frps will load balancing connections for proxies in same group
+loadBalancer.group = "test_group"
+# group should have same group key
+loadBalancer.groupKey = "123456"
+# Enable health check for the backend service, it supports 'tcp' and 'http' now.
+# frpc will connect local service's port to detect it's healthy status
+healthCheck.type = "tcp"
+# Health check connection timeout
+healthCheck.timeoutSeconds = 3
+# If continuous failed in 3 times, the proxy will be removed from frps
+healthCheck.maxFailed = 3
+# every 10 seconds will do a health check
+healthCheck.intervalSeconds = 10
+# additional meta info for each proxy
+metadatas.var1 = "abc"
+metadatas.var2 = "123"
+
+[[proxies]]
+name = "ssh_random"
+type = "tcp"
+localIP = "192.168.31.100"
+localPort = 22
+# If remote_port is 0, frps will assign a random port for you
+remotePort = 0
+
+[[proxies]]
+name = "dns"
+type = "udp"
+localIP = "114.114.114.114"
+localPort = 53
+remotePort = 6002
+
+# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
+[[proxies]]
+name = "web01"
+type = "http"
+localIP = "127.0.0.1"
+localPort = 80
+# http username and password are safety certification for http protocol
+# if not set, you can access this custom_domains without certification
+httpUser = "admin"
+httpPassword = "admin"
+# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com
+subdomain = "web01"
+customDomains = ["web01.yourdomain.com"]
+# locations is only available for http type
+locations = ["/", "/pic"]
+# route requests to this service if http basic auto user is abc
+# route_by_http_user = abc
+hostHeaderRewrite = "example.com"
+# params with prefix "header_" will be used to update http request headers
+requestHeaders.set.x-from-where = "frp"
+healthCheck.type = "http"
+# frpc will send a GET http request '/status' to local http service
+# http service is alive when it return 2xx http response code
+healthCheck.path = "/status"
+healthCheck.intervalSeconds = 10
+healthCheck.maxFailed = 3
+healthCheck.timeoutSeconds = 3
+
+[[proxies]]
+name = "web02"
+type = "https"
+localIP = "127.0.0.1"
+localPort = 8000
+subdomain = "web02"
+customDomains = ["web02.yourdomain.com"]
+# if not empty, frpc will use proxy protocol to transfer connection info to your local service
+# v1 or v2 or empty
+transport.proxyProtocolVersion = "v2"
+
+[[proxies]]
+name = "tcpmuxhttpconnect"
+type = "tcpmux"
+multiplexer = "httpconnect"
+localIP = "127.0.0.1"
+localPort = 10701
+customDomains = ["tunnel1"]
+# routeByHTTPUser = "user1"
+
+[[proxies]]
+name = "plugin_unix_domain_socket"
+type = "tcp"
+remotePort = 6003
+# if plugin is defined, local_ip and local_port is useless
+# plugin will handle connections got from frps
+[proxies.plugin]
+type = "unix_domain_socket"
+unixPath = "/var/run/docker.sock"
+
+[[proxies]]
+name = "plugin_http_proxy"
+type = "tcp"
+remotePort = 6004
+[proxies.plugin]
+type = "http_proxy"
+httpUser = "abc"
+httpPassword = "abc"
+
+[[proxies]]
+name = "plugin_socks5"
+type = "tcp"
+remotePort = 6005
+[proxies.plugin]
+type = "socks5"
+username = "abc"
+password = "abc"
+
+[[proxies]]
+name = "plugin_static_file"
+type = "tcp"
+remotePort = 6006
+[proxies.plugin]
+type = "static_file"
+localPath = "/var/www/blog"
+stripPrefix = "static"
+httpUser = "abc"
+httpPassword = "abc"
+
+[[proxies]]
+name = "plugin_https2http"
+type = "https"
+customDomains = ["test.yourdomain.com"]
+[proxies.plugin]
+type = "https2http"
+localAddr = "127.0.0.1:80"
+crtPath = "./server.crt"
+keyPath = "./server.key"
+hostHeaderRewrite = "127.0.0.1"
+requestHeaders.set.x-from-where = "frp"
+
+[[proxies]]
+name = "plugin_https2https"
+type = "https"
+customDomains = ["test.yourdomain.com"]
+[proxies.plugin]
+type = "https2https"
+localAddr = "127.0.0.1:443"
+crtPath = "./server.crt"
+keyPath = "./server.key"
+hostHeaderRewrite = "127.0.0.1"
+requestHeaders.set.x-from-where = "frp"
+
+[[proxies]]
+name = "plugin_http2https"
+type = "http"
+customDomains = ["test.yourdomain.com"]
+[proxies.plugin]
+type = "http2https"
+localAddr = "127.0.0.1:443"
+hostHeaderRewrite = "127.0.0.1"
+requestHeaders.set.x-from-where = "frp"
+
+[[proxies]]
+name = "secret_tcp"
+# If the type is secret tcp, remote_port is useless
+# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
+type = "stcp"
+# secretKey is used for authentication for visitors
+secretKey = "abcdefg"
+localIP = "127.0.0.1"
+localPort = 22
+# If not empty, only visitors from specified users can connect.
+# Otherwise, visitors from same user can connect. '*' means allow all users.
+allowUsers = ["*"]
+
+[[proxies]]
+name = "p2p_tcp"
+type = "xtcp"
+secretKey = "abcdefg"
+localIP = "127.0.0.1"
+localPort = 22
+# If not empty, only visitors from specified users can connect.
+# Otherwise, visitors from same user can connect. '*' means allow all users.
+allowUsers = ["user1", "user2"]
+
+# frpc role visitor -> frps -> frpc role server
+[[visitors]]
+name = "secret_tcp_visitor"
+type = "stcp"
+# the server name you want to visitor
+serverName = "secret_tcp"
+secretKey = "abcdefg"
+# connect this address to visitor stcp server
+bindAddr = "127.0.0.1"
+# bindPort 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 = 9000
+
+[[visitors]]
+name = "p2p_tcp_visitor"
+type = "xtcp"
+# if the server user is not set, it defaults to the current user
+serverUser = "user1"
+serverName = "p2p_tcp"
+secretKey = "abcdefg"
+bindAddr = "127.0.0.1"
+# bindPort 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 = 9001
+# when automatic tunnel persistence is required, set it to true
+keepTunnelOpen = false
+# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour
+maxRetriesAnHour = 8
+minRetryInterval = 90
+# fallbackTo = "stcp_visitor"
+# fallbackTimeoutMs = 500

+ 0 - 2
conf/frps.ini

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

+ 154 - 0
conf/frps.toml

@@ -0,0 +1,154 @@
+# A literal address or host name for IPv6 must be enclosed
+# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
+# For single "bind_addr" field, no need square brackets, like "bind_addr = ::".
+bindAddr = "0.0.0.0"
+bindPort = 7000
+
+# udp port used for kcp protocol, it can be same with 'bind_port'.
+# if not set, kcp is disabled in frps.
+kcpBindPort = 7000
+
+# udp port used for quic protocol.
+# if not set, quic is disabled in frps.
+# quicBindPort = 7002
+
+# Specify which address proxy will listen for, default value is same with bind_addr
+# proxy_bind_addr = "127.0.0.1"
+
+# quic protocol options
+# transport.quic.keepalivePeriod = 10
+# transport.quic.maxIdleTimeout = 30
+# transport.quic.maxIncomingStreams = 100000
+
+# Heartbeat configure, it's not recommended to modify the default value
+# The default value of heartbeat_timeout is 90. Set negative value to disable it.
+# transport.heartbeatTimeout = 90
+
+# Pool count in each proxy will keep no more than maxPoolCount.
+transport.maxPoolCount = 5
+
+# If tcp stream multiplexing is used, default is true
+# transport.tcpMux = true
+
+# Specify keep alive interval for tcp mux.
+# only valid if tcpMux is true.
+# transport.tcpMuxKeepaliveInterval = 60
+
+# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
+# If negative, keep-alive probes are disabled.
+# transport.tcpKeepalive = 7200
+
+# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
+tls.force = false
+
+# transport.tls.certFile = "server.crt"
+# transport.tls.keyFile = "server.key"
+# transport.tls.trustedCaFile = "ca.crt"
+
+# If you want to support virtual host, you must set the http port for listening (optional)
+# Note: http port and https port can be same with bind_port
+vhostHTTPPort = 80
+vhostHTTPSPort = 443
+
+# Response header timeout(seconds) for vhost http server, default is 60s
+# vhostHTTPTimeout = 60
+
+# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP
+# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
+# requests on one single port. If it's not - it will listen on this value for
+# HTTP CONNECT requests. By default, this value is 0.
+# tcpmuxHTTPConnectPort = 1337
+
+# If tcpmux_passthrough is true, frps won't do any update on traffic.
+# tcpmuxPassthrough = false
+
+# Configure the web server to enable the dashboard for frps.
+# dashboard is available only if webServer.port is set.
+webServer.addr = "127.0.0.1"
+webServer.port = 7500
+webServer.user = "admin"
+webServer.password = "admin"
+# webServer.tls.certFile = "server.crt"
+# webServer.tls.keyFile = "server.key"
+# dashboard assets directory(only for debug mode)
+# webServer.assetsDir = "./static"
+
+# Enable golang pprof handlers in dashboard listener.
+# Dashboard port must be set first
+webServer.pprofEnable = false
+
+# enablePrometheus will export prometheus metrics on webServer in /metrics api.
+enablePrometheus = true
+
+# console or real logFile path like ./frps.log
+log.to = "./frps.log"
+# trace, debug, info, warn, error
+log.level = info
+log.maxDays = 3
+# disable log colors when log.to is console, default is false
+log.disablePrintColor = false
+
+# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
+detailedErrorsToClient = true
+
+# auth.method specifies what authentication method to use authenticate frpc with frps.
+# If "token" is specified - token will be read into login message.
+# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
+auth.method = "token"
+
+# auth.additionalScopes specifies additional scopes to include authentication information.
+# Optional values are HeartBeats, NewWorkConns.
+# auth.additionalScopes = ["HeartBeats", "NewWorkConns"]
+
+# auth token
+auth.token = "12345678"
+
+# oidc issuer specifies the issuer to verify OIDC tokens with.
+auth.oidc.issuer = ""
+# oidc audience specifies the audience OIDC tokens should contain when validated.
+auth.oidc.audience = ""
+# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired.
+auth.oidc.skipExpiryCheck = false
+# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
+auth.oidc.skipIssuerCheck = false
+
+# userConnTimeout specifies the maximum time to wait for a work connection.
+# userConnTimeout = 10
+
+# Only allow frpc to bind ports you list. By default, there won't be any limit.
+allowPorts = [
+  { start = 2000, end = 3000 },
+  { single = 3001 },
+  { single = 3003 },
+  { start = 4000, end = 50000 }
+]
+
+# Max ports can be used for each client, default value is 0 means no limit
+maxPortsPerClient = 0
+
+# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
+# When subdomain is est, the host used by routing is test.frps.com
+subDomainHost = "frps.com"
+
+# custom 404 page for HTTP requests
+# custom404Page = "/path/to/404.html"
+
+# specify udp packet size, unit is byte. If not set, the default value is 1500.
+# This parameter should be same between client and server.
+# It affects the udp and sudp proxy.
+udpPacketSize = 1500
+
+# Retention time for NAT hole punching strategy data.
+natholeAnalysisDataReserveHours = 168
+
+[[httpPlugins]]
+name = "user-manager"
+addr = "127.0.0.1:9000"
+path = "/handler"
+ops = ["Login"]
+
+[[httpPlugins]]
+name = "port-manager"
+addr = "127.0.0.1:9001"
+path = "/handler"
+ops = ["NewProxy"]

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


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


+ 35 - 34
doc/server_plugin.md

@@ -216,49 +216,50 @@ New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcp
 
 ### Server Plugin Configuration
 
-```ini
-# frps.ini
-[common]
-bind_port = 7000
-
-[plugin.user-manager]
-addr = 127.0.0.1:9000
-path = /handler
-ops = Login
-
-[plugin.port-manager]
-addr = 127.0.0.1:9001
-path = /handler
-ops = NewProxy
+```toml
+# frps.toml
+bindPort = 7000
+
+[[httpPlugins]]
+name = "user-manager"
+addr = "127.0.0.1:9000"
+path = "/handler"
+ops = ["Login"]
+
+[[httpPlugins]]
+name = "port-manager"
+addr = "127.0.0.1:9001"
+path = "/handler"
+ops = ["NewProxy"]
 ```
 
-- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`.
+- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`.
 - path: http request url path for the POST request.
 - ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...).
-- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
+- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
 
 ### Metadata
 
 Metadata will be sent to the server plugin in each RPC request.
 
-There are 2 types of metadata entries - 1 under `[common]` and the other under each proxy configuration.
-Metadata entries under `[common]` will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
+There are 2 types of metadata entries - global one and the other under each proxy configuration.
+Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`.
 Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`.
 
-Metadata entries start with `meta_`. This is an example of metadata entries in `[common]` and under the proxy named `[ssh]`:
-
-```
-# frpc.ini
-[common]
-server_addr = 127.0.0.1
-server_port = 7000
-user = fake
-meta_token = fake
-meta_version = 1.0.0
-
-[ssh]
-type = tcp
-local_port = 22
-remote_port = 6000
-meta_id = 123
+This is an example of metadata entries:
+
+```toml
+# frpc.toml
+serverAddr = "127.0.0.1"
+serverPort = 7000
+user = "fake"
+metadatas.token = "fake"
+metadatas.version = "1.0.0"
+
+[[proxies]]
+name = "ssh"
+type = "tcp"
+localPort = 22
+remotePort = 6000
+metadatas.id = "123"
 ```

+ 4 - 5
go.mod

@@ -8,13 +8,13 @@ require (
 	github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
 	github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40
 	github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
-	github.com/go-playground/validator/v10 v10.14.1
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/hashicorp/yamux v0.1.1
 	github.com/onsi/ginkgo/v2 v2.11.0
 	github.com/onsi/gomega v1.27.8
+	github.com/pelletier/go-toml/v2 v2.1.0
 	github.com/pion/stun v0.6.1
 	github.com/pires/go-proxyproto v0.7.0
 	github.com/prometheus/client_golang v1.16.0
@@ -37,11 +37,8 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/go-logr/logr v1.2.4 // indirect
-	github.com/go-playground/locales v0.14.1 // indirect
-	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/golang/mock v1.6.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
@@ -52,7 +49,6 @@ require (
 	github.com/klauspost/cpuid/v2 v2.0.6 // indirect
 	github.com/klauspost/reedsolomon v1.9.15 // indirect
 	github.com/kr/text v0.2.0 // indirect
-	github.com/leodido/go-urn v1.2.4 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/pion/dtls/v2 v2.2.7 // indirect
 	github.com/pion/logging v0.2.2 // indirect
@@ -76,8 +72,11 @@ require (
 	golang.org/x/tools v0.9.3 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
+	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
 )
 
 // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.

+ 8 - 12
go.sum

@@ -32,19 +32,10 @@ github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
 github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
-github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
-github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
-github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
-github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -93,8 +84,6 @@ github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAK
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
-github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
@@ -103,6 +92,8 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU
 github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
 github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
 github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
 github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -147,7 +138,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -275,6 +265,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -286,3 +278,7 @@ k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk=
 k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc=
 k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
 k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

+ 1 - 1
hack/run-e2e.sh

@@ -6,7 +6,7 @@ ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
 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
+    go install github.com/onsi/ginkgo/v2/ginkgo@v2.11.0
 fi
 
 debug=false

+ 1 - 0
package.sh

@@ -47,6 +47,7 @@ for os in $os_all; do
         fi  
         cp ../LICENSE ${frp_path}
         cp -rf ../conf/* ${frp_path}
+        rm -rf ${frp_path}/legacy
 
         # packages
         cd ./packages

+ 14 - 66
pkg/auth/auth.go

@@ -17,76 +17,25 @@ package auth
 import (
 	"fmt"
 
-	"github.com/fatedier/frp/pkg/consts"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 )
 
-type BaseConfig struct {
-	// AuthenticationMethod specifies what authentication method to use to
-	// authenticate frpc with frps. If "token" is specified - token will be
-	// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
-	// token will be issued using OIDC settings. By default, this value is "token".
-	AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
-	// AuthenticateHeartBeats specifies whether to include authentication token in
-	// heartbeats sent to frps. By default, this value is false.
-	AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
-	// AuthenticateNewWorkConns specifies whether to include authentication token in
-	// new work connections sent to frps. By default, this value is false.
-	AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
-}
-
-func getDefaultBaseConf() BaseConfig {
-	return BaseConfig{
-		AuthenticationMethod:     "token",
-		AuthenticateHeartBeats:   false,
-		AuthenticateNewWorkConns: false,
-	}
-}
-
-type ClientConfig struct {
-	BaseConfig       `ini:",extends"`
-	OidcClientConfig `ini:",extends"`
-	TokenConfig      `ini:",extends"`
-}
-
-func GetDefaultClientConf() ClientConfig {
-	return ClientConfig{
-		BaseConfig:       getDefaultBaseConf(),
-		OidcClientConfig: getDefaultOidcClientConf(),
-		TokenConfig:      getDefaultTokenConf(),
-	}
-}
-
-type ServerConfig struct {
-	BaseConfig       `ini:",extends"`
-	OidcServerConfig `ini:",extends"`
-	TokenConfig      `ini:",extends"`
-}
-
-func GetDefaultServerConf() ServerConfig {
-	return ServerConfig{
-		BaseConfig:       getDefaultBaseConf(),
-		OidcServerConfig: getDefaultOidcServerConf(),
-		TokenConfig:      getDefaultTokenConf(),
-	}
-}
-
 type Setter interface {
 	SetLogin(*msg.Login) error
 	SetPing(*msg.Ping) error
 	SetNewWorkConn(*msg.NewWorkConn) error
 }
 
-func NewAuthSetter(cfg ClientConfig) (authProvider Setter) {
-	switch cfg.AuthenticationMethod {
-	case consts.TokenAuthMethod:
-		authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
-	case consts.OidcAuthMethod:
-		authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig)
+func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
+	switch cfg.Method {
+	case v1.AuthMethodToken:
+		authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
+	case v1.AuthMethodOIDC:
+		authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
 	default:
-		panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod))
+		panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
 	}
-
 	return authProvider
 }
 
@@ -96,13 +45,12 @@ type Verifier interface {
 	VerifyNewWorkConn(*msg.NewWorkConn) error
 }
 
-func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) {
-	switch cfg.AuthenticationMethod {
-	case consts.TokenAuthMethod:
-		authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
-	case consts.OidcAuthMethod:
-		authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig)
+func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
+	switch cfg.Method {
+	case v1.AuthMethodToken:
+		authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
+	case v1.AuthMethodOIDC:
+		authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC)
 	}
-
 	return authVerifier
 }

+ 145 - 0
pkg/auth/legacy/legacy.go

@@ -0,0 +1,145 @@
+// 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 legacy
+
+type BaseConfig struct {
+	// AuthenticationMethod specifies what authentication method to use to
+	// authenticate frpc with frps. If "token" is specified - token will be
+	// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
+	// token will be issued using OIDC settings. By default, this value is "token".
+	AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"`
+	// AuthenticateHeartBeats specifies whether to include authentication token in
+	// heartbeats sent to frps. By default, this value is false.
+	AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"`
+	// AuthenticateNewWorkConns specifies whether to include authentication token in
+	// new work connections sent to frps. By default, this value is false.
+	AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"`
+}
+
+func getDefaultBaseConf() BaseConfig {
+	return BaseConfig{
+		AuthenticationMethod:     "token",
+		AuthenticateHeartBeats:   false,
+		AuthenticateNewWorkConns: false,
+	}
+}
+
+type ClientConfig struct {
+	BaseConfig       `ini:",extends"`
+	OidcClientConfig `ini:",extends"`
+	TokenConfig      `ini:",extends"`
+}
+
+func GetDefaultClientConf() ClientConfig {
+	return ClientConfig{
+		BaseConfig:       getDefaultBaseConf(),
+		OidcClientConfig: getDefaultOidcClientConf(),
+		TokenConfig:      getDefaultTokenConf(),
+	}
+}
+
+type ServerConfig struct {
+	BaseConfig       `ini:",extends"`
+	OidcServerConfig `ini:",extends"`
+	TokenConfig      `ini:",extends"`
+}
+
+func GetDefaultServerConf() ServerConfig {
+	return ServerConfig{
+		BaseConfig:       getDefaultBaseConf(),
+		OidcServerConfig: getDefaultOidcServerConf(),
+		TokenConfig:      getDefaultTokenConf(),
+	}
+}
+
+type OidcClientConfig struct {
+	// OidcClientID specifies the client ID to use to get a token in OIDC
+	// authentication if AuthenticationMethod == "oidc". By default, this value
+	// is "".
+	OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
+	// OidcClientSecret specifies the client secret to use to get a token in OIDC
+	// authentication if AuthenticationMethod == "oidc". By default, this value
+	// is "".
+	OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
+	// OidcAudience specifies the audience of the token in OIDC authentication
+	// if AuthenticationMethod == "oidc". By default, this value is "".
+	OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
+	// OidcScope specifies the scope of the token in OIDC authentication
+	// if AuthenticationMethod == "oidc". By default, this value is "".
+	OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
+	// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
+	// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
+	// By default, this value is "".
+	OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
+
+	// OidcAdditionalEndpointParams specifies additional parameters to be sent
+	// this field will be transfer to map[string][]string in OIDC token generator
+	// The field will be set by prefix "oidc_additional_"
+	OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
+}
+
+func getDefaultOidcClientConf() OidcClientConfig {
+	return OidcClientConfig{
+		OidcClientID:                 "",
+		OidcClientSecret:             "",
+		OidcAudience:                 "",
+		OidcScope:                    "",
+		OidcTokenEndpointURL:         "",
+		OidcAdditionalEndpointParams: make(map[string]string),
+	}
+}
+
+type OidcServerConfig struct {
+	// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
+	// will be used to load public keys to verify signature and will be compared
+	// with the issuer claim in the OIDC token. It will be used if
+	// AuthenticationMethod == "oidc". By default, this value is "".
+	OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
+	// OidcAudience specifies the audience OIDC tokens should contain when validated.
+	// If this value is empty, audience ("client ID") verification will be skipped.
+	// It will be used when AuthenticationMethod == "oidc". By default, this
+	// value is "".
+	OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
+	// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
+	// expired. It will be used when AuthenticationMethod == "oidc". By default, this
+	// value is false.
+	OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
+	// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
+	// issuer claim matches the issuer specified in OidcIssuer. It will be used when
+	// AuthenticationMethod == "oidc". By default, this value is false.
+	OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
+}
+
+func getDefaultOidcServerConf() OidcServerConfig {
+	return OidcServerConfig{
+		OidcIssuer:          "",
+		OidcAudience:        "",
+		OidcSkipExpiryCheck: false,
+		OidcSkipIssuerCheck: false,
+	}
+}
+
+type TokenConfig struct {
+	// Token specifies the authorization token used to create keys to be sent
+	// to the server. The server must have a matching token for authorization
+	// to succeed.  By default, this value is "".
+	Token string `ini:"token" json:"token"`
+}
+
+func getDefaultTokenConf() TokenConfig {
+	return TokenConfig{
+		Token: "",
+	}
+}

+ 26 - 91
pkg/auth/oidc.go

@@ -19,105 +19,40 @@ import (
 	"fmt"
 
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/samber/lo"
 	"golang.org/x/oauth2/clientcredentials"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 )
 
-type OidcClientConfig struct {
-	// OidcClientID specifies the client ID to use to get a token in OIDC
-	// authentication if AuthenticationMethod == "oidc". By default, this value
-	// is "".
-	OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"`
-	// OidcClientSecret specifies the client secret to use to get a token in OIDC
-	// authentication if AuthenticationMethod == "oidc". By default, this value
-	// is "".
-	OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"`
-	// OidcAudience specifies the audience of the token in OIDC authentication
-	// if AuthenticationMethod == "oidc". By default, this value is "".
-	OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
-	// OidcScope specifies the scope of the token in OIDC authentication
-	// if AuthenticationMethod == "oidc". By default, this value is "".
-	OidcScope string `ini:"oidc_scope" json:"oidc_scope"`
-	// OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
-	// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
-	// By default, this value is "".
-	OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
-
-	// OidcAdditionalEndpointParams specifies additional parameters to be sent
-	// this field will be transfer to map[string][]string in OIDC token generator
-	// The field will be set by prefix "oidc_additional_"
-	OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
-}
-
-func getDefaultOidcClientConf() OidcClientConfig {
-	return OidcClientConfig{
-		OidcClientID:                 "",
-		OidcClientSecret:             "",
-		OidcAudience:                 "",
-		OidcScope:                    "",
-		OidcTokenEndpointURL:         "",
-		OidcAdditionalEndpointParams: make(map[string]string),
-	}
-}
-
-type OidcServerConfig struct {
-	// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
-	// will be used to load public keys to verify signature and will be compared
-	// with the issuer claim in the OIDC token. It will be used if
-	// AuthenticationMethod == "oidc". By default, this value is "".
-	OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"`
-	// OidcAudience specifies the audience OIDC tokens should contain when validated.
-	// If this value is empty, audience ("client ID") verification will be skipped.
-	// It will be used when AuthenticationMethod == "oidc". By default, this
-	// value is "".
-	OidcAudience string `ini:"oidc_audience" json:"oidc_audience"`
-	// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
-	// expired. It will be used when AuthenticationMethod == "oidc". By default, this
-	// value is false.
-	OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"`
-	// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
-	// issuer claim matches the issuer specified in OidcIssuer. It will be used when
-	// AuthenticationMethod == "oidc". By default, this value is false.
-	OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"`
-}
-
-func getDefaultOidcServerConf() OidcServerConfig {
-	return OidcServerConfig{
-		OidcIssuer:          "",
-		OidcAudience:        "",
-		OidcSkipExpiryCheck: false,
-		OidcSkipIssuerCheck: false,
-	}
-}
-
 type OidcAuthProvider struct {
-	BaseConfig
+	additionalAuthScopes []v1.AuthScope
 
 	tokenGenerator *clientcredentials.Config
 }
 
-func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider {
+func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
 	eps := make(map[string][]string)
-	for k, v := range cfg.OidcAdditionalEndpointParams {
+	for k, v := range cfg.AdditionalEndpointParams {
 		eps[k] = []string{v}
 	}
 
-	if cfg.OidcAudience != "" {
-		eps["audience"] = []string{cfg.OidcAudience}
+	if cfg.Audience != "" {
+		eps["audience"] = []string{cfg.Audience}
 	}
 
 	tokenGenerator := &clientcredentials.Config{
-		ClientID:       cfg.OidcClientID,
-		ClientSecret:   cfg.OidcClientSecret,
-		Scopes:         []string{cfg.OidcScope},
-		TokenURL:       cfg.OidcTokenEndpointURL,
+		ClientID:       cfg.ClientID,
+		ClientSecret:   cfg.ClientSecret,
+		Scopes:         []string{cfg.Scope},
+		TokenURL:       cfg.TokenEndpointURL,
 		EndpointParams: eps,
 	}
 
 	return &OidcAuthProvider{
-		BaseConfig:     baseCfg,
-		tokenGenerator: tokenGenerator,
+		additionalAuthScopes: additionalAuthScopes,
+		tokenGenerator:       tokenGenerator,
 	}
 }
 
@@ -135,7 +70,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
 }
 
 func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
-	if !auth.AuthenticateHeartBeats {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
 		return nil
 	}
 
@@ -144,7 +79,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
 }
 
 func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
-	if !auth.AuthenticateNewWorkConns {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
 		return nil
 	}
 
@@ -153,26 +88,26 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
 }
 
 type OidcAuthConsumer struct {
-	BaseConfig
+	additionalAuthScopes []v1.AuthScope
 
 	verifier         *oidc.IDTokenVerifier
 	subjectFromLogin string
 }
 
-func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer {
-	provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer)
+func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer {
+	provider, err := oidc.NewProvider(context.Background(), cfg.Issuer)
 	if err != nil {
 		panic(err)
 	}
 	verifierConf := oidc.Config{
-		ClientID:          cfg.OidcAudience,
-		SkipClientIDCheck: cfg.OidcAudience == "",
-		SkipExpiryCheck:   cfg.OidcSkipExpiryCheck,
-		SkipIssuerCheck:   cfg.OidcSkipIssuerCheck,
+		ClientID:          cfg.Audience,
+		SkipClientIDCheck: cfg.Audience == "",
+		SkipExpiryCheck:   cfg.SkipExpiryCheck,
+		SkipIssuerCheck:   cfg.SkipIssuerCheck,
 	}
 	return &OidcAuthConsumer{
-		BaseConfig: baseCfg,
-		verifier:   provider.Verifier(&verifierConf),
+		additionalAuthScopes: additionalAuthScopes,
+		verifier:             provider.Verifier(&verifierConf),
 	}
 }
 
@@ -200,7 +135,7 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
 }
 
 func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
-	if !auth.AuthenticateHeartBeats {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
 		return nil
 	}
 
@@ -208,7 +143,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
 }
 
 func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
-	if !auth.AuthenticateNewWorkConns {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
 		return nil
 	}
 

+ 13 - 24
pkg/auth/token.go

@@ -18,43 +18,32 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
-type TokenConfig struct {
-	// Token specifies the authorization token used to create keys to be sent
-	// to the server. The server must have a matching token for authorization
-	// to succeed.  By default, this value is "".
-	Token string `ini:"token" json:"token"`
-}
-
-func getDefaultTokenConf() TokenConfig {
-	return TokenConfig{
-		Token: "",
-	}
-}
-
 type TokenAuthSetterVerifier struct {
-	BaseConfig
-
-	token string
+	additionalAuthScopes []v1.AuthScope
+	token                string
 }
 
-func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier {
+func NewTokenAuth(additionalAuthScopes []v1.AuthScope, token string) *TokenAuthSetterVerifier {
 	return &TokenAuthSetterVerifier{
-		BaseConfig: baseCfg,
-		token:      cfg.Token,
+		additionalAuthScopes: additionalAuthScopes,
+		token:                token,
 	}
 }
 
-func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) {
+func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error {
 	loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp)
 	return nil
 }
 
 func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
-	if !auth.AuthenticateHeartBeats {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
 		return nil
 	}
 
@@ -64,7 +53,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error {
 }
 
 func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error {
-	if !auth.AuthenticateNewWorkConns {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
 		return nil
 	}
 
@@ -81,7 +70,7 @@ func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {
 }
 
 func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
-	if !auth.AuthenticateHeartBeats {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
 		return nil
 	}
 
@@ -92,7 +81,7 @@ func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {
 }
 
 func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {
-	if !auth.AuthenticateNewWorkConns {
+	if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
 		return nil
 	}
 

+ 0 - 680
pkg/config/client_test.go

@@ -1,680 +0,0 @@
-// Copyright 2020 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package config
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/fatedier/frp/pkg/auth"
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-const (
-	testUser = "test"
-)
-
-var testClientBytesWithFull = []byte(`
-		# [common] is integral section
-		[common]
-		server_addr = 0.0.0.9
-		server_port = 7009
-		http_proxy = http://user:passwd@192.168.1.128:8080
-		log_file = ./frpc.log9
-		log_way = file
-		log_level = info9
-		log_max_days = 39
-		disable_log_color = false
-		authenticate_heartbeats = false
-		authenticate_new_work_conns = false
-		token = 12345678
-		oidc_client_id = client-id
-		oidc_client_secret = client-secret
-		oidc_audience = audience
-		oidc_token_endpoint_url = endpoint_url
-		admin_addr = 127.0.0.9
-		admin_port = 7409
-		admin_user = admin9
-		admin_pwd = admin9
-		assets_dir = ./static9
-		pool_count = 59
-		tcp_mux
-		user = your_name
-		login_fail_exit
-		protocol = tcp
-		tls_enable = true
-		tls_cert_file = client.crt
-		tls_key_file = client.key
-		tls_trusted_ca_file = ca.crt
-		tls_server_name = example.com
-		dns_server = 8.8.8.9
-		start = ssh,dns
-		heartbeat_interval = 39
-		heartbeat_timeout = 99
-		meta_var1 = 123
-		meta_var2 = 234
-		udp_packet_size = 1509
-		
-		# all proxy
-		[ssh]
-		type = tcp
-		local_ip = 127.0.0.9
-		local_port = 29
-		bandwidth_limit = 19MB
-		bandwidth_limit_mode = server
-		use_encryption
-		use_compression
-		remote_port = 6009
-		group = test_group
-		group_key = 123456
-		health_check_type = tcp
-		health_check_timeout_s = 3
-		health_check_max_failed = 3
-		health_check_interval_s = 19
-		meta_var1 = 123
-		meta_var2 = 234
-		
-		[ssh_random]
-		type = tcp
-		local_ip = 127.0.0.9
-		local_port = 29
-		remote_port = 9
-		
-		[range:tcp_port]
-		type = tcp
-		local_ip = 127.0.0.9
-		local_port = 6010-6011,6019
-		remote_port = 6010-6011,6019
-		use_encryption = false
-		use_compression = false
-		
-		[dns]
-		type = udp
-		local_ip = 114.114.114.114
-		local_port = 59
-		remote_port = 6009
-		use_encryption
-		use_compression
-		
-		[range:udp_port]
-		type = udp
-		local_ip = 114.114.114.114
-		local_port = 6000,6010-6011
-		remote_port = 6000,6010-6011
-		use_encryption
-		use_compression
-		
-		[web01]
-		type = http
-		local_ip = 127.0.0.9
-		local_port = 89
-		use_encryption
-		use_compression
-		http_user = admin
-		http_pwd = admin
-		subdomain = web01
-		custom_domains = web02.yourdomain.com
-		locations = /,/pic
-		host_header_rewrite = example.com
-		header_X-From-Where = frp
-		health_check_type = http
-		health_check_url = /status
-		health_check_interval_s = 19
-		health_check_max_failed = 3
-		health_check_timeout_s = 3
-		
-		[web02]
-		type = https
-		local_ip = 127.0.0.9
-		local_port = 8009
-		use_encryption
-		use_compression
-		subdomain = web01
-		custom_domains = web02.yourdomain.com
-		proxy_protocol_version = v2
-		
-		[secret_tcp]
-		type = stcp
-		sk = abcdefg
-		local_ip = 127.0.0.1
-		local_port = 22
-		use_encryption = false
-		use_compression = false
-		
-		[p2p_tcp]
-		type = xtcp
-		sk = abcdefg
-		local_ip = 127.0.0.1
-		local_port = 22
-		use_encryption = false
-		use_compression = false
-		
-		[tcpmuxhttpconnect]
-		type = tcpmux
-		multiplexer = httpconnect
-		local_ip = 127.0.0.1
-		local_port = 10701
-		custom_domains = tunnel1
-		
-		[plugin_unix_domain_socket]
-		type = tcp
-		remote_port = 6003
-		plugin = unix_domain_socket
-		plugin_unix_path = /var/run/docker.sock
-		
-		[plugin_http_proxy]
-		type = tcp
-		remote_port = 6004
-		plugin = http_proxy
-		plugin_http_user = abc
-		plugin_http_passwd = abc
-		
-		[plugin_socks5]
-		type = tcp
-		remote_port = 6005
-		plugin = socks5
-		plugin_user = abc
-		plugin_passwd = abc
-		
-		[plugin_static_file]
-		type = tcp
-		remote_port = 6006
-		plugin = static_file
-		plugin_local_path = /var/www/blog
-		plugin_strip_prefix = static
-		plugin_http_user = abc
-		plugin_http_passwd = abc
-		
-		[plugin_https2http]
-		type = https
-		custom_domains = test.yourdomain.com
-		plugin = https2http
-		plugin_local_addr = 127.0.0.1:80
-		plugin_crt_path = ./server.crt
-		plugin_key_path = ./server.key
-		plugin_host_header_rewrite = 127.0.0.1
-		plugin_header_X-From-Where = frp
-		
-		[plugin_http2https]
-		type = http
-		custom_domains = test.yourdomain.com
-		plugin = http2https
-		plugin_local_addr = 127.0.0.1:443
-		plugin_host_header_rewrite = 127.0.0.1
-		plugin_header_X-From-Where = frp
-		
-		# visitor
-		[secret_tcp_visitor]
-		role = visitor
-		type = stcp
-		server_name = secret_tcp
-		sk = abcdefg
-		bind_addr = 127.0.0.1
-		bind_port = 9000
-		use_encryption = false
-		use_compression = false
-		
-		[p2p_tcp_visitor]
-		role = visitor
-		type = xtcp
-		server_name = p2p_tcp
-		sk = abcdefg
-		bind_addr = 127.0.0.1
-		bind_port = 9001
-		use_encryption = false
-		use_compression = false
-	`)
-
-func Test_LoadClientCommonConf(t *testing.T) {
-	assert := assert.New(t)
-
-	expected := ClientCommonConf{
-		ClientConfig: auth.ClientConfig{
-			BaseConfig: auth.BaseConfig{
-				AuthenticationMethod:     "token",
-				AuthenticateHeartBeats:   false,
-				AuthenticateNewWorkConns: false,
-			},
-			TokenConfig: auth.TokenConfig{
-				Token: "12345678",
-			},
-			OidcClientConfig: auth.OidcClientConfig{
-				OidcClientID:         "client-id",
-				OidcClientSecret:     "client-secret",
-				OidcAudience:         "audience",
-				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",
-		DisableCustomTLSFirstByte: true,
-		DNSServer:                 "8.8.8.9",
-		Start:                     []string{"ssh", "dns"},
-		HeartbeatInterval:         39,
-		HeartbeatTimeout:          99,
-		Metas: map[string]string{
-			"var1": "123",
-			"var2": "234",
-		},
-		UDPPacketSize:      1509,
-		IncludeConfigFiles: []string{},
-	}
-
-	common, err := UnmarshalClientConfFromIni(testClientBytesWithFull)
-	assert.NoError(err)
-	assert.EqualValues(expected, common)
-}
-
-func Test_LoadClientBasicConf(t *testing.T) {
-	assert := assert.New(t)
-
-	proxyExpected := map[string]ProxyConf{
-		testUser + ".ssh": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:          testUser + ".ssh",
-				ProxyType:          consts.TCPProxy,
-				UseCompression:     true,
-				UseEncryption:      true,
-				Group:              "test_group",
-				GroupKey:           "123456",
-				BandwidthLimit:     MustBandwidthQuantity("19MB"),
-				BandwidthLimitMode: BandwidthLimitModeServer,
-				Metas: map[string]string{
-					"var1": "123",
-					"var2": "234",
-				},
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 29,
-				},
-				HealthCheckConf: HealthCheckConf{
-					HealthCheckType:      consts.TCPProxy,
-					HealthCheckTimeoutS:  3,
-					HealthCheckMaxFailed: 3,
-					HealthCheckIntervalS: 19,
-					HealthCheckAddr:      "127.0.0.9:29",
-				},
-			},
-			RemotePort: 6009,
-		},
-		testUser + ".ssh_random": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".ssh_random",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 29,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 9,
-		},
-		testUser + ".tcp_port_0": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".tcp_port_0",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 6010,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6010,
-		},
-		testUser + ".tcp_port_1": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".tcp_port_1",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 6011,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6011,
-		},
-		testUser + ".tcp_port_2": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".tcp_port_2",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 6019,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6019,
-		},
-		testUser + ".dns": &UDPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".dns",
-				ProxyType:      consts.UDPProxy,
-				UseEncryption:  true,
-				UseCompression: true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "114.114.114.114",
-					LocalPort: 59,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6009,
-		},
-		testUser + ".udp_port_0": &UDPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".udp_port_0",
-				ProxyType:      consts.UDPProxy,
-				UseEncryption:  true,
-				UseCompression: true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "114.114.114.114",
-					LocalPort: 6000,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6000,
-		},
-		testUser + ".udp_port_1": &UDPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".udp_port_1",
-				ProxyType:      consts.UDPProxy,
-				UseEncryption:  true,
-				UseCompression: true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "114.114.114.114",
-					LocalPort: 6010,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6010,
-		},
-		testUser + ".udp_port_2": &UDPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".udp_port_2",
-				ProxyType:      consts.UDPProxy,
-				UseEncryption:  true,
-				UseCompression: true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "114.114.114.114",
-					LocalPort: 6011,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6011,
-		},
-		testUser + ".web01": &HTTPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".web01",
-				ProxyType:      consts.HTTPProxy,
-				UseCompression: true,
-				UseEncryption:  true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 89,
-				},
-				HealthCheckConf: HealthCheckConf{
-					HealthCheckType:      consts.HTTPProxy,
-					HealthCheckTimeoutS:  3,
-					HealthCheckMaxFailed: 3,
-					HealthCheckIntervalS: 19,
-					HealthCheckURL:       "http://127.0.0.9:89/status",
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			DomainConf: DomainConf{
-				CustomDomains: []string{"web02.yourdomain.com"},
-				SubDomain:     "web01",
-			},
-			Locations:         []string{"/", "/pic"},
-			HTTPUser:          "admin",
-			HTTPPwd:           "admin",
-			HostHeaderRewrite: "example.com",
-			Headers: map[string]string{
-				"X-From-Where": "frp",
-			},
-		},
-		testUser + ".web02": &HTTPSProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName:      testUser + ".web02",
-				ProxyType:      consts.HTTPSProxy,
-				UseCompression: true,
-				UseEncryption:  true,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.9",
-					LocalPort: 8009,
-				},
-				ProxyProtocolVersion: "v2",
-				BandwidthLimitMode:   BandwidthLimitModeClient,
-			},
-			DomainConf: DomainConf{
-				CustomDomains: []string{"web02.yourdomain.com"},
-				SubDomain:     "web01",
-			},
-		},
-		testUser + ".secret_tcp": &STCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".secret_tcp",
-				ProxyType: consts.STCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.1",
-					LocalPort: 22,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RoleServerCommonConf: RoleServerCommonConf{
-				Role: "server",
-				Sk:   "abcdefg",
-			},
-		},
-		testUser + ".p2p_tcp": &XTCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".p2p_tcp",
-				ProxyType: consts.XTCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.1",
-					LocalPort: 22,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RoleServerCommonConf: RoleServerCommonConf{
-				Role: "server",
-				Sk:   "abcdefg",
-			},
-		},
-		testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".tcpmuxhttpconnect",
-				ProxyType: consts.TCPMuxProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP:   "127.0.0.1",
-					LocalPort: 10701,
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			DomainConf: DomainConf{
-				CustomDomains: []string{"tunnel1"},
-				SubDomain:     "",
-			},
-			Multiplexer: "httpconnect",
-		},
-		testUser + ".plugin_unix_domain_socket": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_unix_domain_socket",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "unix_domain_socket",
-					PluginParams: map[string]string{
-						"plugin_unix_path": "/var/run/docker.sock",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6003,
-		},
-		testUser + ".plugin_http_proxy": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_http_proxy",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "http_proxy",
-					PluginParams: map[string]string{
-						"plugin_http_user":   "abc",
-						"plugin_http_passwd": "abc",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6004,
-		},
-		testUser + ".plugin_socks5": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_socks5",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "socks5",
-					PluginParams: map[string]string{
-						"plugin_user":   "abc",
-						"plugin_passwd": "abc",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6005,
-		},
-		testUser + ".plugin_static_file": &TCPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_static_file",
-				ProxyType: consts.TCPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "static_file",
-					PluginParams: map[string]string{
-						"plugin_local_path":   "/var/www/blog",
-						"plugin_strip_prefix": "static",
-						"plugin_http_user":    "abc",
-						"plugin_http_passwd":  "abc",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			RemotePort: 6006,
-		},
-		testUser + ".plugin_https2http": &HTTPSProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_https2http",
-				ProxyType: consts.HTTPSProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "https2http",
-					PluginParams: map[string]string{
-						"plugin_local_addr":          "127.0.0.1:80",
-						"plugin_crt_path":            "./server.crt",
-						"plugin_key_path":            "./server.key",
-						"plugin_host_header_rewrite": "127.0.0.1",
-						"plugin_header_X-From-Where": "frp",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			DomainConf: DomainConf{
-				CustomDomains: []string{"test.yourdomain.com"},
-			},
-		},
-		testUser + ".plugin_http2https": &HTTPProxyConf{
-			BaseProxyConf: BaseProxyConf{
-				ProxyName: testUser + ".plugin_http2https",
-				ProxyType: consts.HTTPProxy,
-				LocalSvrConf: LocalSvrConf{
-					LocalIP: "127.0.0.1",
-					Plugin:  "http2https",
-					PluginParams: map[string]string{
-						"plugin_local_addr":          "127.0.0.1:443",
-						"plugin_host_header_rewrite": "127.0.0.1",
-						"plugin_header_X-From-Where": "frp",
-					},
-				},
-				BandwidthLimitMode: BandwidthLimitModeClient,
-			},
-			DomainConf: DomainConf{
-				CustomDomains: []string{"test.yourdomain.com"},
-			},
-		},
-	}
-
-	visitorExpected := map[string]VisitorConf{
-		testUser + ".secret_tcp_visitor": &STCPVisitorConf{
-			BaseVisitorConf: BaseVisitorConf{
-				ProxyName:  testUser + ".secret_tcp_visitor",
-				ProxyType:  consts.STCPProxy,
-				Role:       "visitor",
-				Sk:         "abcdefg",
-				ServerName: testVisitorPrefix + "secret_tcp",
-				BindAddr:   "127.0.0.1",
-				BindPort:   9000,
-			},
-		},
-		testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{
-			BaseVisitorConf: BaseVisitorConf{
-				ProxyName:  testUser + ".p2p_tcp_visitor",
-				ProxyType:  consts.XTCPProxy,
-				Role:       "visitor",
-				Sk:         "abcdefg",
-				ServerName: testProxyPrefix + "p2p_tcp",
-				BindAddr:   "127.0.0.1",
-				BindPort:   9001,
-			},
-			Protocol:          "quic",
-			MaxRetriesAnHour:  8,
-			MinRetryInterval:  90,
-			FallbackTimeoutMs: 1000,
-		},
-	}
-
-	proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil)
-	assert.NoError(err)
-	assert.Equal(proxyExpected, proxyActual)
-	assert.Equal(visitorExpected, visitorActual)
-}

+ 0 - 0
pkg/config/README.md → pkg/config/legacy/README.md


+ 80 - 87
pkg/config/client.go → pkg/config/legacy/client.go

@@ -1,4 +1,4 @@
-// Copyright 2020 The frp Authors
+// 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.
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
 	"fmt"
@@ -23,15 +23,16 @@ import (
 	"github.com/samber/lo"
 	"gopkg.in/ini.v1"
 
-	"github.com/fatedier/frp/pkg/auth"
+	legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
-// ClientCommonConf contains information for a client service. It is
+// ClientCommonConf is the configuration parsed from ini.
+// It contains information for a client service. It is
 // recommended to use GetDefaultClientConf instead of creating this object
 // directly, so that all unspecified fields have reasonable default values.
 type ClientCommonConf struct {
-	auth.ClientConfig `ini:",extends"`
+	legacyauth.ClientConfig `ini:",extends"`
 
 	// ServerAddr specifies the address of the server to connect to. By
 	// default, this value is "0.0.0.0".
@@ -122,9 +123,9 @@ type ClientCommonConf struct {
 	// is "tcp".
 	Protocol string `ini:"protocol" json:"protocol"`
 	// QUIC protocol options
-	QUICKeepalivePeriod    int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
-	QUICMaxIdleTimeout     int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
-	QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
+	QUICKeepalivePeriod    int `ini:"quic_keepalive_period" json:"quic_keepalive_period"`
+	QUICMaxIdleTimeout     int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"`
+	QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"`
 	// 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.
@@ -168,85 +169,6 @@ type ClientCommonConf struct {
 	PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
 }
 
-// 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,
-		TLSEnable:                 true,
-		DisableCustomTLSFirstByte: true,
-		HeartbeatInterval:         30,
-		HeartbeatTimeout:          90,
-		Metas:                     make(map[string]string),
-		UDPPacketSize:             1500,
-		IncludeConfigFiles:        make([]string, 0),
-	}
-}
-
-func (cfg *ClientCommonConf) Complete() {
-	if cfg.LogFile == "console" {
-		cfg.LogWay = "console"
-	} else {
-		cfg.LogWay = "file"
-	}
-}
-
-func (cfg *ClientCommonConf) Validate() error {
-	if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
-		if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
-			return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
-		}
-	}
-
-	if !cfg.TLSEnable {
-		if cfg.TLSCertFile != "" {
-			fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
-		}
-
-		if cfg.TLSKeyFile != "" {
-			fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false")
-		}
-
-		if cfg.TLSTrustedCaFile != "" {
-			fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false")
-		}
-	}
-
-	if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
-		return fmt.Errorf("invalid protocol")
-	}
-
-	for _, f := range cfg.IncludeConfigFiles {
-		absDir, err := filepath.Abs(filepath.Dir(f))
-		if err != nil {
-			return fmt.Errorf("include: parse directory of %s failed: %v", f, absDir)
-		}
-		if _, err := os.Stat(absDir); os.IsNotExist(err) {
-			return fmt.Errorf("include: directory of %s not exist", f)
-		}
-	}
-	return nil
-}
-
 // Supported sources including: string(file path), []byte, Reader interface.
 func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
 	f, err := ini.LoadSources(ini.LoadOptions{
@@ -421,3 +343,74 @@ func copySection(source, target *ini.Section) {
 		_, _ = target.NewKey(key, value)
 	}
 }
+
+// GetDefaultClientConf returns a client configuration with default values.
+func GetDefaultClientConf() ClientCommonConf {
+	return ClientCommonConf{
+		ClientConfig:              legacyauth.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),
+	}
+}
+
+func (cfg *ClientCommonConf) Validate() error {
+	if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
+		if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
+			return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
+		}
+	}
+
+	if !cfg.TLSEnable {
+		if cfg.TLSCertFile != "" {
+			fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false")
+		}
+
+		if cfg.TLSKeyFile != "" {
+			fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false")
+		}
+
+		if cfg.TLSTrustedCaFile != "" {
+			fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false")
+		}
+	}
+
+	if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
+		return fmt.Errorf("invalid protocol")
+	}
+
+	for _, f := range cfg.IncludeConfigFiles {
+		absDir, err := filepath.Abs(filepath.Dir(f))
+		if err != nil {
+			return fmt.Errorf("include: parse directory of %s failed: %v", f, err)
+		}
+		if _, err := os.Stat(absDir); os.IsNotExist(err) {
+			return fmt.Errorf("include: directory of %s not exist", f)
+		}
+	}
+	return nil
+}

+ 350 - 0
pkg/config/legacy/conversion.go

@@ -0,0 +1,350 @@
+// 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 legacy
+
+import (
+	"strings"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/config/types"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
+	out := &v1.ClientCommonConfig{}
+	out.User = conf.User
+	out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod)
+	out.Auth.Token = conf.ClientConfig.Token
+	if conf.ClientConfig.AuthenticateHeartBeats {
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
+	}
+	if conf.ClientConfig.AuthenticateNewWorkConns {
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
+	}
+	out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID
+	out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret
+	out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience
+	out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope
+	out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL
+	out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams
+
+	out.ServerAddr = conf.ServerAddr
+	out.ServerPort = conf.ServerPort
+	out.NatHoleSTUNServer = conf.NatHoleSTUNServer
+	out.Transport.DialServerTimeout = conf.DialServerTimeout
+	out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive
+	out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP
+	out.Transport.ProxyURL = conf.HTTPProxy
+	out.Transport.PoolCount = conf.PoolCount
+	out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
+	out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval
+	out.Transport.Protocol = conf.Protocol
+	out.Transport.HeartbeatInterval = conf.HeartbeatInterval
+	out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
+	out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod
+	out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout
+	out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
+	out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable)
+	out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte)
+	out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile
+	out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile
+	out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile
+	out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName
+
+	out.Log.To = conf.LogFile
+	out.Log.Level = conf.LogLevel
+	out.Log.MaxDays = conf.LogMaxDays
+	out.Log.DisablePrintColor = conf.DisableLogColor
+
+	out.WebServer.Addr = conf.AdminAddr
+	out.WebServer.Port = conf.AdminPort
+	out.WebServer.Password = conf.AdminPwd
+	out.WebServer.AssetsDir = conf.AssetsDir
+	out.WebServer.PprofEnable = conf.PprofEnable
+
+	out.DNSServer = conf.DNSServer
+	out.LoginFailExit = lo.ToPtr(conf.LoginFailExit)
+	out.Start = conf.Start
+	out.UDPPacketSize = conf.UDPPacketSize
+	out.Metadatas = conf.Metas
+	out.IncludeConfigFiles = conf.IncludeConfigFiles
+	return out
+}
+
+func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
+	out := &v1.ServerConfig{}
+	out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod)
+	out.Auth.Token = conf.ServerConfig.Token
+	if conf.ServerConfig.AuthenticateHeartBeats {
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats)
+	}
+	if conf.ServerConfig.AuthenticateNewWorkConns {
+		out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns)
+	}
+	out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience
+	out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer
+	out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck
+	out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck
+
+	out.BindAddr = conf.BindAddr
+	out.BindPort = conf.BindPort
+	out.KCPBindPort = conf.KCPBindPort
+	out.QUICBindPort = conf.QUICBindPort
+	out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod
+	out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout
+	out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams
+
+	out.ProxyBindAddr = conf.ProxyBindAddr
+	out.VhostHTTPPort = conf.VhostHTTPPort
+	out.VhostHTTPSPort = conf.VhostHTTPSPort
+	out.TCPMuxHTTPConnectPort = conf.TCPMuxHTTPConnectPort
+	out.TCPMuxPassthrough = conf.TCPMuxPassthrough
+	out.VhostHTTPTimeout = conf.VhostHTTPTimeout
+
+	out.WebServer.Addr = conf.DashboardAddr
+	out.WebServer.Port = conf.DashboardPort
+	out.WebServer.User = conf.DashboardUser
+	out.WebServer.Password = conf.DashboardPwd
+	out.WebServer.AssetsDir = conf.AssetsDir
+	if conf.DashboardTLSMode {
+		out.WebServer.TLS = &v1.TLSConfig{}
+		out.WebServer.TLS.CertFile = conf.DashboardTLSCertFile
+		out.WebServer.TLS.KeyFile = conf.DashboardTLSKeyFile
+		out.WebServer.PprofEnable = conf.PprofEnable
+	}
+
+	out.EnablePrometheus = conf.EnablePrometheus
+
+	out.Log.To = conf.LogFile
+	out.Log.Level = conf.LogLevel
+	out.Log.MaxDays = conf.LogMaxDays
+	out.Log.DisablePrintColor = conf.DisableLogColor
+
+	out.DetailedErrorsToClient = lo.ToPtr(conf.DetailedErrorsToClient)
+	out.SubDomainHost = conf.SubDomainHost
+	out.Custom404Page = conf.Custom404Page
+	out.UserConnTimeout = conf.UserConnTimeout
+	out.UDPPacketSize = conf.UDPPacketSize
+	out.NatHoleAnalysisDataReserveHours = conf.NatHoleAnalysisDataReserveHours
+
+	out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
+	out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval
+	out.Transport.TCPKeepAlive = conf.TCPKeepAlive
+	out.Transport.MaxPoolCount = conf.MaxPoolCount
+	out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout
+
+	out.Transport.TLS.Force = conf.TLSOnly
+	out.Transport.TLS.CertFile = conf.TLSCertFile
+	out.Transport.TLS.KeyFile = conf.TLSKeyFile
+	out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile
+
+	out.MaxPortsPerClient = conf.MaxPortsPerClient
+
+	for _, v := range conf.HTTPPlugins {
+		out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{
+			Name:      v.Name,
+			Addr:      v.Addr,
+			Path:      v.Path,
+			Ops:       v.Ops,
+			TLSVerify: v.TLSVerify,
+		})
+	}
+
+	out.AllowPorts, _ = types.NewPortsRangeSliceFromString(conf.AllowPortsStr)
+	return out
+}
+
+func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
+	out := v1.HeaderOperations{}
+	for k, v := range params {
+		if !strings.HasPrefix(k, "plugin_header_") {
+			continue
+		}
+		if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
+			out.Set[k] = v
+		}
+	}
+	return out
+}
+
+func Convert_ProxyConf_To_v1_Base(conf ProxyConf) *v1.ProxyBaseConfig {
+	out := &v1.ProxyBaseConfig{}
+	base := conf.GetBaseConfig()
+
+	out.Name = base.ProxyName
+	out.Type = base.ProxyType
+	out.Metadatas = base.Metas
+
+	out.Transport.UseEncryption = base.UseEncryption
+	out.Transport.UseCompression = base.UseCompression
+	out.Transport.BandwidthLimit = base.BandwidthLimit
+	out.Transport.BandwidthLimitMode = base.BandwidthLimitMode
+	out.Transport.ProxyProtocolVersion = base.ProxyProtocolVersion
+
+	out.LoadBalancer.Group = base.Group
+	out.LoadBalancer.GroupKey = base.GroupKey
+
+	out.HealthCheck.Type = base.HealthCheckType
+	out.HealthCheck.TimeoutSeconds = base.HealthCheckTimeoutS
+	out.HealthCheck.MaxFailed = base.HealthCheckMaxFailed
+	out.HealthCheck.IntervalSeconds = base.HealthCheckIntervalS
+	out.HealthCheck.Path = base.HealthCheckURL
+
+	out.LocalIP = base.LocalIP
+	out.LocalPort = base.LocalPort
+
+	switch base.Plugin {
+	case "http2https":
+		out.Plugin.ClientPluginOptions = &v1.HTTP2HTTPSPluginOptions{
+			LocalAddr:         base.PluginParams["plugin_local_addr"],
+			HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
+			RequestHeaders:    transformHeadersFromPluginParams(base.PluginParams),
+		}
+	case "http_proxy":
+		out.Plugin.ClientPluginOptions = &v1.HTTPProxyPluginOptions{
+			HTTPUser:     base.PluginParams["plugin_http_user"],
+			HTTPPassword: base.PluginParams["plugin_http_passwd"],
+		}
+	case "https2http":
+		out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPPluginOptions{
+			LocalAddr:         base.PluginParams["plugin_local_addr"],
+			HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
+			RequestHeaders:    transformHeadersFromPluginParams(base.PluginParams),
+			CrtPath:           base.PluginParams["plugin_crt_path"],
+			KeyPath:           base.PluginParams["plugin_key_path"],
+		}
+	case "https2https":
+		out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPSPluginOptions{
+			LocalAddr:         base.PluginParams["plugin_local_addr"],
+			HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"],
+			RequestHeaders:    transformHeadersFromPluginParams(base.PluginParams),
+			CrtPath:           base.PluginParams["plugin_crt_path"],
+			KeyPath:           base.PluginParams["plugin_key_path"],
+		}
+	case "socks5":
+		out.Plugin.ClientPluginOptions = &v1.Socks5PluginOptions{
+			Username: base.PluginParams["plugin_user"],
+			Password: base.PluginParams["plugin_passwd"],
+		}
+	case "static_file":
+		out.Plugin.ClientPluginOptions = &v1.StaticFilePluginOptions{
+			LocalPath:    base.PluginParams["plugin_local_path"],
+			StripPrefix:  base.PluginParams["plugin_strip_prefix"],
+			HTTPUser:     base.PluginParams["plugin_http_user"],
+			HTTPPassword: base.PluginParams["plugin_http_passwd"],
+		}
+	case "unix_domain_socket":
+		out.Plugin.ClientPluginOptions = &v1.UnixDomainSocketPluginOptions{
+			UnixPath: base.PluginParams["plugin_unix_path"],
+		}
+	}
+	out.Plugin.Type = base.Plugin
+	return out
+}
+
+func Convert_ProxyConf_To_v1(conf ProxyConf) v1.ProxyConfigurer {
+	outBase := Convert_ProxyConf_To_v1_Base(conf)
+	var out v1.ProxyConfigurer
+	switch v := conf.(type) {
+	case *TCPProxyConf:
+		c := &v1.TCPProxyConfig{ProxyBaseConfig: *outBase}
+		c.RemotePort = v.RemotePort
+		out = c
+	case *UDPProxyConf:
+		c := &v1.UDPProxyConfig{ProxyBaseConfig: *outBase}
+		c.RemotePort = v.RemotePort
+		out = c
+	case *HTTPProxyConf:
+		c := &v1.HTTPProxyConfig{ProxyBaseConfig: *outBase}
+		c.CustomDomains = v.CustomDomains
+		c.SubDomain = v.SubDomain
+		c.Locations = v.Locations
+		c.HTTPUser = v.HTTPUser
+		c.HTTPPassword = v.HTTPPwd
+		c.HostHeaderRewrite = v.HostHeaderRewrite
+		c.RequestHeaders.Set = v.Headers
+		c.RouteByHTTPUser = v.RouteByHTTPUser
+		out = c
+	case *HTTPSProxyConf:
+		c := &v1.HTTPSProxyConfig{ProxyBaseConfig: *outBase}
+		c.CustomDomains = v.CustomDomains
+		c.SubDomain = v.SubDomain
+		out = c
+	case *TCPMuxProxyConf:
+		c := &v1.TCPMuxProxyConfig{ProxyBaseConfig: *outBase}
+		c.CustomDomains = v.CustomDomains
+		c.SubDomain = v.SubDomain
+		c.HTTPUser = v.HTTPUser
+		c.HTTPPassword = v.HTTPPwd
+		c.RouteByHTTPUser = v.RouteByHTTPUser
+		c.Multiplexer = v.Multiplexer
+		out = c
+	case *STCPProxyConf:
+		c := &v1.STCPProxyConfig{ProxyBaseConfig: *outBase}
+		c.Secretkey = v.Sk
+		c.AllowUsers = v.AllowUsers
+		out = c
+	case *SUDPProxyConf:
+		c := &v1.SUDPProxyConfig{ProxyBaseConfig: *outBase}
+		c.Secretkey = v.Sk
+		c.AllowUsers = v.AllowUsers
+		out = c
+	case *XTCPProxyConf:
+		c := &v1.XTCPProxyConfig{ProxyBaseConfig: *outBase}
+		c.Secretkey = v.Sk
+		c.AllowUsers = v.AllowUsers
+	}
+	return out
+}
+
+func Convert_VisitorConf_To_v1_Base(conf VisitorConf) *v1.VisitorBaseConfig {
+	out := &v1.VisitorBaseConfig{}
+	base := conf.GetBaseConfig()
+
+	out.Name = base.ProxyName
+	out.Type = base.ProxyType
+	out.Transport.UseEncryption = base.UseEncryption
+	out.Transport.UseCompression = base.UseCompression
+	out.SecretKey = base.Sk
+	out.ServerUser = base.ServerUser
+	out.ServerName = base.ServerName
+	out.BindAddr = base.BindAddr
+	out.BindPort = base.BindPort
+	return out
+}
+
+func Convert_VisitorConf_To_v1(conf VisitorConf) v1.VisitorConfigurer {
+	outBase := Convert_VisitorConf_To_v1_Base(conf)
+	var out v1.VisitorConfigurer
+	switch v := conf.(type) {
+	case *STCPVisitorConf:
+		c := &v1.STCPVisitorConfig{VisitorBaseConfig: *outBase}
+		out = c
+	case *SUDPVisitorConf:
+		c := &v1.SUDPVisitorConfig{VisitorBaseConfig: *outBase}
+		out = c
+	case *XTCPVisitorConf:
+		c := &v1.XTCPVisitorConfig{VisitorBaseConfig: *outBase}
+		c.Protocol = v.Protocol
+		c.KeepTunnelOpen = v.KeepTunnelOpen
+		c.MaxRetriesAnHour = v.MaxRetriesAnHour
+		c.MinRetryInterval = v.MinRetryInterval
+		c.FallbackTo = v.FallbackTo
+		c.FallbackTimeoutMs = v.FallbackTimeoutMs
+		out = c
+	}
+	return out
+}

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

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
 	"bytes"
@@ -40,7 +40,6 @@ func ParseClientConfig(filePath string) (
 	if err != nil {
 		return
 	}
-	cfg.Complete()
 	if err = cfg.Validate(); err != nil {
 		err = fmt.Errorf("parse config error: %v", err)
 		return

+ 387 - 0
pkg/config/legacy/proxy.go

@@ -0,0 +1,387 @@
+// 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 legacy
+
+import (
+	"fmt"
+	"reflect"
+
+	"gopkg.in/ini.v1"
+
+	"github.com/fatedier/frp/pkg/config/types"
+)
+
+type ProxyType string
+
+const (
+	ProxyTypeTCP    ProxyType = "tcp"
+	ProxyTypeUDP    ProxyType = "udp"
+	ProxyTypeTCPMUX ProxyType = "tcpmux"
+	ProxyTypeHTTP   ProxyType = "http"
+	ProxyTypeHTTPS  ProxyType = "https"
+	ProxyTypeSTCP   ProxyType = "stcp"
+	ProxyTypeXTCP   ProxyType = "xtcp"
+	ProxyTypeSUDP   ProxyType = "sudp"
+)
+
+// Proxy
+var (
+	proxyConfTypeMap = map[ProxyType]reflect.Type{
+		ProxyTypeTCP:    reflect.TypeOf(TCPProxyConf{}),
+		ProxyTypeUDP:    reflect.TypeOf(UDPProxyConf{}),
+		ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
+		ProxyTypeHTTP:   reflect.TypeOf(HTTPProxyConf{}),
+		ProxyTypeHTTPS:  reflect.TypeOf(HTTPSProxyConf{}),
+		ProxyTypeSTCP:   reflect.TypeOf(STCPProxyConf{}),
+		ProxyTypeXTCP:   reflect.TypeOf(XTCPProxyConf{}),
+		ProxyTypeSUDP:   reflect.TypeOf(SUDPProxyConf{}),
+	}
+)
+
+type ProxyConf interface {
+	// GetBaseConfig returns the BaseProxyConf for this config.
+	GetBaseConfig() *BaseProxyConf
+	// UnmarshalFromIni unmarshals a ini.Section into this config. This function
+	// will be called on the frpc side.
+	UnmarshalFromIni(string, string, *ini.Section) error
+}
+
+func NewConfByType(proxyType ProxyType) ProxyConf {
+	v, ok := proxyConfTypeMap[proxyType]
+	if !ok {
+		return nil
+	}
+	cfg := reflect.New(v).Interface().(ProxyConf)
+	return cfg
+}
+
+// Proxy Conf Loader
+// DefaultProxyConf creates a empty ProxyConf object by proxyType.
+// If proxyType doesn't exist, return nil.
+func DefaultProxyConf(proxyType ProxyType) ProxyConf {
+	return NewConfByType(proxyType)
+}
+
+// Proxy loaded from ini
+func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) {
+	// section.Key: if key not exists, section will set it with default value.
+	proxyType := ProxyType(section.Key("type").String())
+	if proxyType == "" {
+		proxyType = ProxyTypeTCP
+	}
+
+	conf := DefaultProxyConf(proxyType)
+	if conf == nil {
+		return nil, fmt.Errorf("invalid type [%s]", proxyType)
+	}
+
+	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
+		return nil, err
+	}
+	return conf, nil
+}
+
+// LocalSvrConf configures what location the client will to, or what
+// plugin will be used.
+type LocalSvrConf struct {
+	// LocalIP specifies the IP address or host name to to.
+	LocalIP string `ini:"local_ip" json:"local_ip"`
+	// LocalPort specifies the port to to.
+	LocalPort int `ini:"local_port" json:"local_port"`
+
+	// Plugin specifies what plugin should be used for ng. If this value
+	// is set, the LocalIp and LocalPort values will be ignored. By default,
+	// this value is "".
+	Plugin string `ini:"plugin" json:"plugin"`
+	// PluginParams specify parameters to be passed to the plugin, if one is
+	// being used. By default, this value is an empty map.
+	PluginParams map[string]string `ini:"-"`
+}
+
+// HealthCheckConf configures health checking. This can be useful for load
+// balancing purposes to detect and remove proxies to failing services.
+type HealthCheckConf struct {
+	// HealthCheckType specifies what protocol to use for health checking.
+	// Valid values include "tcp", "http", and "". If this value is "", health
+	// checking will not be performed. By default, this value is "".
+	//
+	// If the type is "tcp", a connection will be attempted to the target
+	// server. If a connection cannot be established, the health check fails.
+	//
+	// If the type is "http", a GET request will be made to the endpoint
+	// specified by HealthCheckURL. If the response is not a 200, the health
+	// check fails.
+	HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http
+	// HealthCheckTimeoutS specifies the number of seconds to wait for a health
+	// check attempt to connect. If the timeout is reached, this counts as a
+	// health check failure. By default, this value is 3.
+	HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"`
+	// HealthCheckMaxFailed specifies the number of allowed failures before the
+	// is stopped. By default, this value is 1.
+	HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"`
+	// HealthCheckIntervalS specifies the time in seconds between health
+	// checks. By default, this value is 10.
+	HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"`
+	// HealthCheckURL specifies the address to send health checks to if the
+	// health check type is "http".
+	HealthCheckURL string `ini:"health_check_url" json:"health_check_url"`
+	// HealthCheckAddr specifies the address to connect to if the health check
+	// type is "tcp".
+	HealthCheckAddr string `ini:"-"`
+}
+
+// BaseProxyConf provides configuration info that is common to all types.
+type BaseProxyConf struct {
+	// ProxyName is the name of this
+	ProxyName string `ini:"name" json:"name"`
+	// ProxyType specifies the type of this  Valid values include "tcp",
+	// "udp", "http", "https", "stcp", and "xtcp". By default, this value is
+	// "tcp".
+	ProxyType string `ini:"type" json:"type"`
+
+	// UseEncryption controls whether or not communication with the server will
+	// be encrypted. Encryption is done using the tokens supplied in the server
+	// and client configuration. By default, this value is false.
+	UseEncryption bool `ini:"use_encryption" json:"use_encryption"`
+	// UseCompression controls whether or not communication with the server
+	// will be compressed. By default, this value is false.
+	UseCompression bool `ini:"use_compression" json:"use_compression"`
+	// Group specifies which group the is a part of. The server will use
+	// this information to load balance proxies in the same group. If the value
+	// is "", this will not be in a group. By default, this value is "".
+	Group string `ini:"group" json:"group"`
+	// GroupKey specifies a group key, which should be the same among proxies
+	// of the same group. By default, this value is "".
+	GroupKey string `ini:"group_key" json:"group_key"`
+
+	// ProxyProtocolVersion specifies which protocol version to use. Valid
+	// values include "v1", "v2", and "". If the value is "", a protocol
+	// version will be automatically selected. By default, this value is "".
+	ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"`
+
+	// BandwidthLimit limit the bandwidth
+	// 0 means no limit
+	BandwidthLimit types.BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"`
+	// BandwidthLimitMode specifies whether to limit the bandwidth on the
+	// client or server side. Valid values include "client" and "server".
+	// By default, this value is "client".
+	BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"`
+
+	// meta info for each proxy
+	Metas map[string]string `ini:"-" json:"metas"`
+
+	LocalSvrConf    `ini:",extends"`
+	HealthCheckConf `ini:",extends"`
+}
+
+// Base
+func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf {
+	return cfg
+}
+
+// BaseProxyConf apply custom logic changes.
+func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section) error {
+	cfg.ProxyName = name
+	// metas_xxx
+	cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_")
+
+	// bandwidth_limit
+	if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil {
+		cfg.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidth.String())
+		if err != nil {
+			return err
+		}
+	}
+
+	// plugin_xxx
+	cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
+	return nil
+}
+
+type DomainConf struct {
+	CustomDomains []string `ini:"custom_domains" json:"custom_domains"`
+	SubDomain     string   `ini:"subdomain" json:"subdomain"`
+}
+
+type RoleServerCommonConf struct {
+	Role       string   `ini:"role" json:"role"`
+	Sk         string   `ini:"sk" json:"sk"`
+	AllowUsers []string `ini:"allow_users" json:"allow_users"`
+}
+
+// HTTP
+type HTTPProxyConf struct {
+	BaseProxyConf `ini:",extends"`
+	DomainConf    `ini:",extends"`
+
+	Locations         []string          `ini:"locations" json:"locations"`
+	HTTPUser          string            `ini:"http_user" json:"http_user"`
+	HTTPPwd           string            `ini:"http_pwd" json:"http_pwd"`
+	HostHeaderRewrite string            `ini:"host_header_rewrite" json:"host_header_rewrite"`
+	Headers           map[string]string `ini:"-" json:"headers"`
+	RouteByHTTPUser   string            `ini:"route_by_http_user" json:"route_by_http_user"`
+}
+
+func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+	cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_")
+	return nil
+}
+
+// HTTPS
+type HTTPSProxyConf struct {
+	BaseProxyConf `ini:",extends"`
+	DomainConf    `ini:",extends"`
+}
+
+func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+	return nil
+}
+
+// TCP
+type TCPProxyConf struct {
+	BaseProxyConf `ini:",extends"`
+	RemotePort    int `ini:"remote_port" json:"remote_port"`
+}
+
+func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+
+	return nil
+}
+
+// UDP
+type UDPProxyConf struct {
+	BaseProxyConf `ini:",extends"`
+
+	RemotePort int `ini:"remote_port" json:"remote_port"`
+}
+
+func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+
+	return nil
+}
+
+// TCPMux
+type TCPMuxProxyConf struct {
+	BaseProxyConf   `ini:",extends"`
+	DomainConf      `ini:",extends"`
+	HTTPUser        string `ini:"http_user" json:"http_user,omitempty"`
+	HTTPPwd         string `ini:"http_pwd" json:"http_pwd,omitempty"`
+	RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
+
+	Multiplexer string `ini:"multiplexer"`
+}
+
+func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+
+	return nil
+}
+
+// STCP
+type STCPProxyConf struct {
+	BaseProxyConf        `ini:",extends"`
+	RoleServerCommonConf `ini:",extends"`
+}
+
+func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+	if cfg.Role == "" {
+		cfg.Role = "server"
+	}
+	return nil
+}
+
+// XTCP
+type XTCPProxyConf struct {
+	BaseProxyConf        `ini:",extends"`
+	RoleServerCommonConf `ini:",extends"`
+}
+
+func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+	if cfg.Role == "" {
+		cfg.Role = "server"
+	}
+	return nil
+}
+
+// SUDP
+type SUDPProxyConf struct {
+	BaseProxyConf        `ini:",extends"`
+	RoleServerCommonConf `ini:",extends"`
+}
+
+func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
+	err := preUnmarshalFromIni(cfg, prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	// Add custom logic unmarshal if exists
+	return nil
+}
+
+func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error {
+	err := section.MapTo(cfg)
+	if err != nil {
+		return err
+	}
+
+	err = cfg.GetBaseConfig().decorate(prefix, name, section)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 28 - 68
pkg/config/server.go → pkg/config/legacy/server.go

@@ -1,4 +1,4 @@
-// Copyright 2020 The frp Authors
+// 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.
@@ -12,60 +12,64 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
-	"fmt"
 	"strings"
 
-	"github.com/go-playground/validator/v10"
 	"gopkg.in/ini.v1"
 
-	"github.com/fatedier/frp/pkg/auth"
-	plugin "github.com/fatedier/frp/pkg/plugin/server"
-	"github.com/fatedier/frp/pkg/util/util"
+	legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
 )
 
+type HTTPPluginOptions struct {
+	Name      string   `ini:"name"`
+	Addr      string   `ini:"addr"`
+	Path      string   `ini:"path"`
+	Ops       []string `ini:"ops"`
+	TLSVerify bool     `ini:"tls_verify"`
+}
+
 // ServerCommonConf contains information for a server service. It is
 // recommended to use GetDefaultServerConf instead of creating this object
 // directly, so that all unspecified fields have reasonable default values.
 type ServerCommonConf struct {
-	auth.ServerConfig `ini:",extends"`
+	legacyauth.ServerConfig `ini:",extends"`
 
 	// BindAddr specifies the address that the server binds to. By default,
 	// this value is "0.0.0.0".
 	BindAddr string `ini:"bind_addr" json:"bind_addr"`
 	// BindPort specifies the port that the server listens on. By default, this
 	// value is 7000.
-	BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"`
+	BindPort int `ini:"bind_port" json:"bind_port"`
 	// KCPBindPort specifies the KCP port that the server listens on. If this
 	// value is 0, the server will not listen for KCP connections. By default,
 	// this value is 0.
-	KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"`
+	KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port"`
 	// QUICBindPort specifies the QUIC port that the server listens on.
 	// Set this value to 0 will disable this feature.
 	// By default, the value is 0.
-	QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port" validate:"gte=0,lte=65535"`
+	QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port"`
 	// QUIC protocol options
-	QUICKeepalivePeriod    int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"`
-	QUICMaxIdleTimeout     int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"`
-	QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"`
+	QUICKeepalivePeriod    int `ini:"quic_keepalive_period" json:"quic_keepalive_period"`
+	QUICMaxIdleTimeout     int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"`
+	QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"`
 	// ProxyBindAddr specifies the address that the proxy binds to. This value
 	// may be the same as BindAddr.
 	ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"`
 	// VhostHTTPPort specifies the port that the server listens for HTTP Vhost
 	// requests. If this value is 0, the server will not listen for HTTP
 	// requests. By default, this value is 0.
-	VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port" validate:"gte=0,lte=65535"`
+	VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"`
 	// VhostHTTPSPort specifies the port that the server listens for HTTPS
 	// Vhost requests. If this value is 0, the server will not listen for HTTPS
 	// requests. By default, this value is 0.
-	VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port" validate:"gte=0,lte=65535"`
+	VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port"`
 	// TCPMuxHTTPConnectPort specifies the port that the server listens for TCP
 	// HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
 	// requests on one single port. If it's not - it will listen on this value for
 	// HTTP CONNECT requests. By default, this value is 0.
-	TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"`
+	TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port"`
 	// If TCPMuxPassthrough is true, frps won't do any update on traffic.
 	TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"`
 	// VhostHTTPTimeout specifies the response header timeout for the Vhost
@@ -77,7 +81,7 @@ type ServerCommonConf struct {
 	// DashboardPort specifies the port that the dashboard listens on. If this
 	// value is 0, the dashboard will not be started. By default, this value is
 	// 0.
-	DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"`
+	DashboardPort int `ini:"dashboard_port" json:"dashboard_port"`
 	// DashboardTLSCertFile specifies the path of the cert file that the server will
 	// load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this
 	// supplied tls configuration.
@@ -185,7 +189,7 @@ type ServerCommonConf struct {
 	// connection. By default, this value is 10.
 	UserConnTimeout int64 `ini:"user_conn_timeout" json:"user_conn_timeout"`
 	// HTTPPlugins specify the server plugins support HTTP protocol.
-	HTTPPlugins map[string]plugin.HTTPPluginOptions `ini:"-" json:"http_plugins"`
+	HTTPPlugins map[string]HTTPPluginOptions `ini:"-" json:"http_plugins"`
 	// UDPPacketSize specifies the UDP packet size
 	// By default, this value is 1500
 	UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
@@ -200,7 +204,7 @@ type ServerCommonConf struct {
 // defaults.
 func GetDefaultServerConf() ServerCommonConf {
 	return ServerCommonConf{
-		ServerConfig:                    auth.GetDefaultServerConf(),
+		ServerConfig:                    legacyauth.GetDefaultServerConf(),
 		BindAddr:                        "0.0.0.0",
 		BindPort:                        7000,
 		QUICKeepalivePeriod:             10,
@@ -221,7 +225,7 @@ func GetDefaultServerConf() ServerCommonConf {
 		MaxPortsPerClient:               0,
 		HeartbeatTimeout:                90,
 		UserConnTimeout:                 10,
-		HTTPPlugins:                     make(map[string]plugin.HTTPPluginOptions),
+		HTTPPlugins:                     make(map[string]HTTPPluginOptions),
 		UDPPacketSize:                   1500,
 		NatHoleAnalysisDataReserveHours: 7 * 24,
 	}
@@ -253,18 +257,11 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
 	// allow_ports
 	allowPortStr := s.Key("allow_ports").String()
 	if allowPortStr != "" {
-		allowPorts, err := util.ParseRangeNumbers(allowPortStr)
-		if err != nil {
-			return ServerCommonConf{}, fmt.Errorf("invalid allow_ports: %v", err)
-		}
-		for _, port := range allowPorts {
-			common.AllowPorts[int(port)] = struct{}{}
-		}
 		common.AllowPortsStr = allowPortStr
 	}
 
 	// plugin.xxx
-	pluginOpts := make(map[string]plugin.HTTPPluginOptions)
+	pluginOpts := make(map[string]HTTPPluginOptions)
 	for _, section := range f.Sections() {
 		name := section.Name()
 		if !strings.HasPrefix(name, "plugin.") {
@@ -283,47 +280,10 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
 	return common, nil
 }
 
-func (cfg *ServerCommonConf) Complete() {
-	if cfg.LogFile == "console" {
-		cfg.LogWay = "console"
-	} else {
-		cfg.LogWay = "file"
-	}
-
-	if cfg.ProxyBindAddr == "" {
-		cfg.ProxyBindAddr = cfg.BindAddr
-	}
-
-	if cfg.TLSTrustedCaFile != "" {
-		cfg.TLSOnly = true
-	}
-}
-
-func (cfg *ServerCommonConf) Validate() error {
-	if !cfg.DashboardTLSMode {
-		if cfg.DashboardTLSCertFile != "" {
-			fmt.Println("WARNING! dashboard_tls_cert_file is invalid when dashboard_tls_mode is false")
-		}
-
-		if cfg.DashboardTLSKeyFile != "" {
-			fmt.Println("WARNING! dashboard_tls_key_file is invalid when dashboard_tls_mode is false")
-		}
-	} else {
-		if cfg.DashboardTLSCertFile == "" {
-			return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
-		}
-
-		if cfg.DashboardTLSKeyFile == "" {
-			return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true")
-		}
-	}
-	return validator.New().Struct(cfg)
-}
-
-func loadHTTPPluginOpt(section *ini.Section) (*plugin.HTTPPluginOptions, error) {
+func loadHTTPPluginOpt(section *ini.Section) (*HTTPPluginOptions, error) {
 	name := strings.TrimSpace(strings.TrimPrefix(section.Name(), "plugin."))
 
-	opt := new(plugin.HTTPPluginOptions)
+	opt := &HTTPPluginOptions{}
 	err := section.MapTo(opt)
 	if err != nil {
 		return nil, err

+ 1 - 1
pkg/config/utils.go → pkg/config/legacy/utils.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
 	"strings"

+ 1 - 1
pkg/config/value.go → pkg/config/legacy/value.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
 	"bytes"

+ 53 - 124
pkg/config/visitor.go → pkg/config/legacy/visitor.go

@@ -1,4 +1,4 @@
-// Copyright 2018 fatedier, fatedier@gmail.com
+// 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.
@@ -12,24 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package legacy
 
 import (
 	"fmt"
 	"reflect"
 
-	"github.com/samber/lo"
 	"gopkg.in/ini.v1"
+)
+
+type VisitorType string
 
-	"github.com/fatedier/frp/pkg/consts"
+const (
+	VisitorTypeSTCP VisitorType = "stcp"
+	VisitorTypeXTCP VisitorType = "xtcp"
+	VisitorTypeSUDP VisitorType = "sudp"
 )
 
 // Visitor
 var (
-	visitorConfTypeMap = map[string]reflect.Type{
-		consts.STCPProxy: reflect.TypeOf(STCPVisitorConf{}),
-		consts.XTCPProxy: reflect.TypeOf(XTCPVisitorConf{}),
-		consts.SUDPProxy: reflect.TypeOf(SUDPVisitorConf{}),
+	visitorConfTypeMap = map[VisitorType]reflect.Type{
+		VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
+		VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
+		VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
 	}
 )
 
@@ -38,8 +43,16 @@ type VisitorConf interface {
 	GetBaseConfig() *BaseVisitorConf
 	// UnmarshalFromIni unmarshals config from ini.
 	UnmarshalFromIni(prefix string, name string, section *ini.Section) error
-	// Validate validates config.
-	Validate() error
+}
+
+// DefaultVisitorConf creates a empty VisitorConf object by visitorType.
+// If visitorType doesn't exist, return nil.
+func DefaultVisitorConf(visitorType VisitorType) VisitorConf {
+	v, ok := visitorConfTypeMap[visitorType]
+	if !ok {
+		return nil
+	}
+	return reflect.New(v).Interface().(VisitorConf)
 }
 
 type BaseVisitorConf struct {
@@ -59,96 +72,14 @@ type BaseVisitorConf struct {
 	BindPort int `ini:"bind_port" json:"bind_port"`
 }
 
-type SUDPVisitorConf struct {
-	BaseVisitorConf `ini:",extends"`
-}
-
-type STCPVisitorConf struct {
-	BaseVisitorConf `ini:",extends"`
-}
-
-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"`
-	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.
-// If visitorType doesn't exist, return nil.
-func DefaultVisitorConf(visitorType string) VisitorConf {
-	v, ok := visitorConfTypeMap[visitorType]
-	if !ok {
-		return nil
-	}
-	return reflect.New(v).Interface().(VisitorConf)
-}
-
-// Visitor loaded from ini
-func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) {
-	// section.Key: if key not exists, section will set it with default value.
-	visitorType := section.Key("type").String()
-
-	if visitorType == "" {
-		return nil, fmt.Errorf("type shouldn't be empty")
-	}
-
-	conf := DefaultVisitorConf(visitorType)
-	if conf == nil {
-		return nil, fmt.Errorf("type [%s] error", visitorType)
-	}
-
-	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
-		return nil, fmt.Errorf("type [%s] error", visitorType)
-	}
-
-	if err := conf.Validate(); err != nil {
-		return nil, err
-	}
-
-	return conf, nil
-}
-
 // Base
 func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf {
 	return cfg
 }
 
-func (cfg *BaseVisitorConf) validate() (err error) {
-	if cfg.Role != "visitor" {
-		err = fmt.Errorf("invalid role")
-		return
-	}
-	if cfg.BindAddr == "" {
-		err = fmt.Errorf("bind_addr shouldn't be empty")
-		return
-	}
-	// 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
-	}
-	return
-}
-
-func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	_ = section
-
+func (cfg *BaseVisitorConf) unmarshalFromIni(_ string, name string, _ *ini.Section) error {
 	// Custom decoration after basic unmarshal:
-	// proxy name
-	cfg.ProxyName = prefix + name
-
-	// server_name
-	if cfg.ServerUser == "" {
-		cfg.ServerName = prefix + cfg.ServerName
-	} else {
-		cfg.ServerName = cfg.ServerUser + "." + cfg.ServerName
-	}
+	cfg.ProxyName = name
 
 	// bind_addr
 	if cfg.BindAddr == "" {
@@ -170,8 +101,9 @@ func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, sec
 	return nil
 }
 
-// SUDP
-var _ VisitorConf = &SUDPVisitorConf{}
+type SUDPVisitorConf struct {
+	BaseVisitorConf `ini:",extends"`
+}
 
 func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
@@ -184,19 +116,10 @@ func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	return
 }
 
-func (cfg *SUDPVisitorConf) Validate() (err error) {
-	if err = cfg.BaseVisitorConf.validate(); err != nil {
-		return
-	}
-
-	// Add custom logic validate, if exists
-
-	return
+type STCPVisitorConf struct {
+	BaseVisitorConf `ini:",extends"`
 }
 
-// STCP
-var _ VisitorConf = &STCPVisitorConf{}
-
 func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
 	if err != nil {
@@ -208,19 +131,17 @@ func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	return
 }
 
-func (cfg *STCPVisitorConf) Validate() (err error) {
-	if err = cfg.BaseVisitorConf.validate(); err != nil {
-		return
-	}
-
-	// Add custom logic validate, if exists
+type XTCPVisitorConf struct {
+	BaseVisitorConf `ini:",extends"`
 
-	return
+	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"`
 }
 
-// XTCP
-var _ VisitorConf = &XTCPVisitorConf{}
-
 func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) {
 	err = preVisitorUnmarshalFromIni(cfg, prefix, name, section)
 	if err != nil {
@@ -243,14 +164,22 @@ func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section
 	return
 }
 
-func (cfg *XTCPVisitorConf) Validate() (err error) {
-	if err = cfg.BaseVisitorConf.validate(); err != nil {
-		return
+// Visitor loaded from ini
+func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) {
+	// section.Key: if key not exists, section will set it with default value.
+	visitorType := VisitorType(section.Key("type").String())
+
+	if visitorType == "" {
+		return nil, fmt.Errorf("type shouldn't be empty")
+	}
+
+	conf := DefaultVisitorConf(visitorType)
+	if conf == nil {
+		return nil, fmt.Errorf("type [%s] error", visitorType)
 	}
 
-	// Add custom logic validate, if exists
-	if !lo.Contains([]string{"", "kcp", "quic"}, cfg.Protocol) {
-		return fmt.Errorf("protocol should be 'kcp' or 'quic'")
+	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
+		return nil, fmt.Errorf("type [%s] error", visitorType)
 	}
-	return
+	return conf, nil
 }

+ 281 - 0
pkg/config/load.go

@@ -0,0 +1,281 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"os"
+	"path/filepath"
+	"strings"
+
+	toml "github.com/pelletier/go-toml/v2"
+	"github.com/samber/lo"
+	"gopkg.in/ini.v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/apimachinery/pkg/util/yaml"
+
+	"github.com/fatedier/frp/pkg/config/legacy"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/config/v1/validation"
+	"github.com/fatedier/frp/pkg/msg"
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+var glbEnvs map[string]string
+
+func init() {
+	glbEnvs = make(map[string]string)
+	envs := os.Environ()
+	for _, env := range envs {
+		pair := strings.SplitN(env, "=", 2)
+		if len(pair) != 2 {
+			continue
+		}
+		glbEnvs[pair[0]] = pair[1]
+	}
+}
+
+type Values struct {
+	Envs map[string]string // environment vars
+}
+
+func GetValues() *Values {
+	return &Values{
+		Envs: glbEnvs,
+	}
+}
+
+func DetectLegacyINIFormat(content []byte) bool {
+	f, err := ini.Load(content)
+	if err != nil {
+		return false
+	}
+	if _, err := f.GetSection("common"); err == nil {
+		return true
+	}
+	return false
+}
+
+func DetectLegacyINIFormatFromFile(path string) bool {
+	b, err := os.ReadFile(path)
+	if err != nil {
+		return false
+	}
+	return DetectLegacyINIFormat(b)
+}
+
+func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
+	tmpl, err := template.New("frp").Parse(string(in))
+	if err != nil {
+		return nil, err
+	}
+
+	buffer := bytes.NewBufferString("")
+	if err := tmpl.Execute(buffer, values); err != nil {
+		return nil, err
+	}
+	return buffer.Bytes(), nil
+}
+
+func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
+	b, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	return RenderWithTemplate(b, values)
+}
+
+func LoadConfigureFromFile(path string, c any) error {
+	content, err := LoadFileContentWithTemplate(path, GetValues())
+	if err != nil {
+		return err
+	}
+	return LoadConfigure(content, c)
+}
+
+// LoadConfigure loads configuration from bytes and unmarshal into c.
+// Now it supports json, yaml and toml format.
+func LoadConfigure(b []byte, c any) error {
+	var tomlObj interface{}
+	if err := toml.Unmarshal(b, &tomlObj); err == nil {
+		b, err = json.Marshal(&tomlObj)
+		if err != nil {
+			return err
+		}
+	}
+	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
+	return decoder.Decode(c)
+}
+
+func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
+	m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
+
+	configurer := v1.NewProxyConfigurerByType(v1.ProxyType(m.ProxyType))
+	if configurer == nil {
+		return nil, fmt.Errorf("unknown proxy type: %s", m.ProxyType)
+	}
+
+	configurer.UnmarshalFromMsg(m)
+	configurer.Complete("")
+
+	if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
+		return nil, err
+	}
+	return configurer, nil
+}
+
+func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
+	var (
+		svrCfg         *v1.ServerConfig
+		isLegacyFormat bool
+	)
+	// detect legacy ini format
+	if DetectLegacyINIFormatFromFile(path) {
+		content, err := legacy.GetRenderedConfFromFile(path)
+		if err != nil {
+			return nil, true, err
+		}
+		legacyCfg, err := legacy.UnmarshalServerConfFromIni(content)
+		if err != nil {
+			return nil, true, err
+		}
+		svrCfg = legacy.Convert_ServerCommonConf_To_v1(&legacyCfg)
+		isLegacyFormat = true
+	} else {
+		svrCfg = &v1.ServerConfig{}
+		if err := LoadConfigureFromFile(path, svrCfg); err != nil {
+			return nil, false, err
+		}
+	}
+	if svrCfg != nil {
+		svrCfg.Complete()
+	}
+	return svrCfg, isLegacyFormat, nil
+}
+
+func LoadClientConfig(path string) (
+	*v1.ClientCommonConfig,
+	[]v1.ProxyConfigurer,
+	[]v1.VisitorConfigurer,
+	bool, error,
+) {
+	var (
+		cliCfg         *v1.ClientCommonConfig
+		pxyCfgs        = make([]v1.ProxyConfigurer, 0)
+		visitorCfgs    = make([]v1.VisitorConfigurer, 0)
+		isLegacyFormat bool
+	)
+
+	if DetectLegacyINIFormatFromFile(path) {
+		legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
+		if err != nil {
+			return nil, nil, nil, true, err
+		}
+		cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
+		for _, c := range legacyPxyCfgs {
+			pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
+		}
+		for _, c := range legacyVisitorCfgs {
+			visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
+		}
+		isLegacyFormat = true
+	} else {
+		allCfg := v1.ClientConfig{}
+		if err := LoadConfigureFromFile(path, &allCfg); err != nil {
+			return nil, nil, nil, false, err
+		}
+		cliCfg = &allCfg.ClientCommonConfig
+		for _, c := range allCfg.Proxies {
+			pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+		}
+		for _, c := range allCfg.Visitors {
+			visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
+		}
+	}
+
+	// Load additional config from includes.
+	// legacy ini format alredy handle this in ParseClientConfig.
+	if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
+		extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat)
+		if err != nil {
+			return nil, nil, nil, isLegacyFormat, err
+		}
+		pxyCfgs = append(pxyCfgs, extPxyCfgs...)
+		visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
+	}
+
+	// Filter by start
+	if len(cliCfg.Start) > 0 {
+		startSet := sets.New(cliCfg.Start...)
+		pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
+			return startSet.Has(c.GetBaseConfig().Name)
+		})
+		visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
+			return startSet.Has(c.GetBaseConfig().Name)
+		})
+	}
+
+	if cliCfg != nil {
+		cliCfg.Complete()
+	}
+	for _, c := range pxyCfgs {
+		c.Complete(cliCfg.User)
+	}
+	for _, c := range visitorCfgs {
+		c.Complete(cliCfg)
+	}
+	return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil
+}
+
+func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
+	pxyCfgs := make([]v1.ProxyConfigurer, 0)
+	visitorCfgs := make([]v1.VisitorConfigurer, 0)
+	for _, path := range paths {
+		absDir, err := filepath.Abs(filepath.Dir(path))
+		if err != nil {
+			return nil, nil, err
+		}
+		if _, err := os.Stat(absDir); os.IsNotExist(err) {
+			return nil, nil, err
+		}
+		files, err := os.ReadDir(absDir)
+		if err != nil {
+			return nil, nil, err
+		}
+		for _, fi := range files {
+			if fi.IsDir() {
+				continue
+			}
+			absFile := filepath.Join(absDir, fi.Name())
+			if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
+				// support yaml/json/toml
+				cfg := v1.ClientConfig{}
+				if err := LoadConfigureFromFile(absFile, &cfg); err != nil {
+					return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
+				}
+				for _, c := range cfg.Proxies {
+					pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+				}
+				for _, c := range cfg.Visitors {
+					visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
+				}
+			}
+		}
+	}
+	return pxyCfgs, visitorCfgs, nil
+}

+ 45 - 0
pkg/config/load_test.go

@@ -0,0 +1,45 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func TestLoadConfigure(t *testing.T) {
+	require := require.New(t)
+	content := `
+bindAddr = "127.0.0.1"
+kcpBindPort = 7000
+quicBindPort = 7001
+tcpmuxHTTPConnectPort = 7005
+custom404Page = "/abc.html"
+transport.tcpKeepalive = 10
+`
+
+	svrCfg := v1.ServerConfig{}
+	err := LoadConfigure([]byte(content), &svrCfg)
+	require.NoError(err)
+	require.EqualValues("127.0.0.1", svrCfg.BindAddr)
+	require.EqualValues(7000, svrCfg.KCPBindPort)
+	require.EqualValues(7001, svrCfg.QUICBindPort)
+	require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
+	require.EqualValues("/abc.html", svrCfg.Custom404Page)
+	require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+}

+ 0 - 921
pkg/config/proxy.go

@@ -1,921 +0,0 @@
-// Copyright 2016 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package config
-
-import (
-	"fmt"
-	"net"
-	"reflect"
-	"strconv"
-	"strings"
-
-	"gopkg.in/ini.v1"
-
-	"github.com/fatedier/frp/pkg/consts"
-	"github.com/fatedier/frp/pkg/msg"
-)
-
-// Proxy
-var (
-	proxyConfTypeMap = map[string]reflect.Type{
-		consts.TCPProxy:    reflect.TypeOf(TCPProxyConf{}),
-		consts.TCPMuxProxy: reflect.TypeOf(TCPMuxProxyConf{}),
-		consts.UDPProxy:    reflect.TypeOf(UDPProxyConf{}),
-		consts.HTTPProxy:   reflect.TypeOf(HTTPProxyConf{}),
-		consts.HTTPSProxy:  reflect.TypeOf(HTTPSProxyConf{}),
-		consts.STCPProxy:   reflect.TypeOf(STCPProxyConf{}),
-		consts.XTCPProxy:   reflect.TypeOf(XTCPProxyConf{}),
-		consts.SUDPProxy:   reflect.TypeOf(SUDPProxyConf{}),
-	}
-)
-
-func NewConfByType(proxyType string) ProxyConf {
-	v, ok := proxyConfTypeMap[proxyType]
-	if !ok {
-		return nil
-	}
-	cfg := reflect.New(v).Interface().(ProxyConf)
-	return cfg
-}
-
-type ProxyConf interface {
-	// GetBaseConfig returns the BaseProxyConf for this config.
-	GetBaseConfig() *BaseProxyConf
-	// SetDefaultValues sets the default values for this config.
-	SetDefaultValues()
-	// UnmarshalFromMsg unmarshals a msg.NewProxy message into this config.
-	// This function will be called on the frps side.
-	UnmarshalFromMsg(*msg.NewProxy)
-	// UnmarshalFromIni unmarshals a ini.Section into this config. This function
-	// will be called on the frpc side.
-	UnmarshalFromIni(string, string, *ini.Section) error
-	// MarshalToMsg marshals this config into a msg.NewProxy message. This
-	// function will be called on the frpc side.
-	MarshalToMsg(*msg.NewProxy)
-	// ValidateForClient checks that the config is valid for the frpc side.
-	ValidateForClient() error
-	// ValidateForServer checks that the config is valid for the frps side.
-	ValidateForServer(ServerCommonConf) error
-}
-
-// LocalSvrConf configures what location the client will to, or what
-// plugin will be used.
-type LocalSvrConf struct {
-	// LocalIP specifies the IP address or host name to to.
-	LocalIP string `ini:"local_ip" json:"local_ip"`
-	// LocalPort specifies the port to to.
-	LocalPort int `ini:"local_port" json:"local_port"`
-
-	// Plugin specifies what plugin should be used for ng. If this value
-	// is set, the LocalIp and LocalPort values will be ignored. By default,
-	// this value is "".
-	Plugin string `ini:"plugin" json:"plugin"`
-	// PluginParams specify parameters to be passed to the plugin, if one is
-	// being used. By default, this value is an empty map.
-	PluginParams map[string]string `ini:"-"`
-}
-
-// HealthCheckConf configures health checking. This can be useful for load
-// balancing purposes to detect and remove proxies to failing services.
-type HealthCheckConf struct {
-	// HealthCheckType specifies what protocol to use for health checking.
-	// Valid values include "tcp", "http", and "". If this value is "", health
-	// checking will not be performed. By default, this value is "".
-	//
-	// If the type is "tcp", a connection will be attempted to the target
-	// server. If a connection cannot be established, the health check fails.
-	//
-	// If the type is "http", a GET request will be made to the endpoint
-	// specified by HealthCheckURL. If the response is not a 200, the health
-	// check fails.
-	HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http
-	// HealthCheckTimeoutS specifies the number of seconds to wait for a health
-	// check attempt to connect. If the timeout is reached, this counts as a
-	// health check failure. By default, this value is 3.
-	HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"`
-	// HealthCheckMaxFailed specifies the number of allowed failures before the
-	// is stopped. By default, this value is 1.
-	HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"`
-	// HealthCheckIntervalS specifies the time in seconds between health
-	// checks. By default, this value is 10.
-	HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"`
-	// HealthCheckURL specifies the address to send health checks to if the
-	// health check type is "http".
-	HealthCheckURL string `ini:"health_check_url" json:"health_check_url"`
-	// HealthCheckAddr specifies the address to connect to if the health check
-	// type is "tcp".
-	HealthCheckAddr string `ini:"-"`
-}
-
-// BaseProxyConf provides configuration info that is common to all types.
-type BaseProxyConf struct {
-	// ProxyName is the name of this
-	ProxyName string `ini:"name" json:"name"`
-	// ProxyType specifies the type of this  Valid values include "tcp",
-	// "udp", "http", "https", "stcp", and "xtcp". By default, this value is
-	// "tcp".
-	ProxyType string `ini:"type" json:"type"`
-
-	// UseEncryption controls whether or not communication with the server will
-	// be encrypted. Encryption is done using the tokens supplied in the server
-	// and client configuration. By default, this value is false.
-	UseEncryption bool `ini:"use_encryption" json:"use_encryption"`
-	// UseCompression controls whether or not communication with the server
-	// will be compressed. By default, this value is false.
-	UseCompression bool `ini:"use_compression" json:"use_compression"`
-	// Group specifies which group the is a part of. The server will use
-	// this information to load balance proxies in the same group. If the value
-	// is "", this will not be in a group. By default, this value is "".
-	Group string `ini:"group" json:"group"`
-	// GroupKey specifies a group key, which should be the same among proxies
-	// of the same group. By default, this value is "".
-	GroupKey string `ini:"group_key" json:"group_key"`
-
-	// ProxyProtocolVersion specifies which protocol version to use. Valid
-	// values include "v1", "v2", and "". If the value is "", a protocol
-	// version will be automatically selected. By default, this value is "".
-	ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"`
-
-	// BandwidthLimit limit the bandwidth
-	// 0 means no limit
-	BandwidthLimit BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"`
-	// BandwidthLimitMode specifies whether to limit the bandwidth on the
-	// client or server side. Valid values include "client" and "server".
-	// By default, this value is "client".
-	BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"`
-
-	// meta info for each proxy
-	Metas map[string]string `ini:"-" json:"metas"`
-
-	LocalSvrConf    `ini:",extends"`
-	HealthCheckConf `ini:",extends"`
-}
-
-type DomainConf struct {
-	CustomDomains []string `ini:"custom_domains" json:"custom_domains"`
-	SubDomain     string   `ini:"subdomain" json:"subdomain"`
-}
-
-type RoleServerCommonConf struct {
-	Role       string   `ini:"role" json:"role"`
-	Sk         string   `ini:"sk" json:"sk"`
-	AllowUsers []string `ini:"allow_users" json:"allow_users"`
-}
-
-func (cfg *RoleServerCommonConf) setDefaultValues() {
-	cfg.Role = "server"
-}
-
-func (cfg *RoleServerCommonConf) marshalToMsg(m *msg.NewProxy) {
-	m.Sk = cfg.Sk
-	m.AllowUsers = cfg.AllowUsers
-}
-
-func (cfg *RoleServerCommonConf) unmarshalFromMsg(m *msg.NewProxy) {
-	cfg.Sk = m.Sk
-	cfg.AllowUsers = m.AllowUsers
-}
-
-// HTTP
-type HTTPProxyConf struct {
-	BaseProxyConf `ini:",extends"`
-	DomainConf    `ini:",extends"`
-
-	Locations         []string          `ini:"locations" json:"locations"`
-	HTTPUser          string            `ini:"http_user" json:"http_user"`
-	HTTPPwd           string            `ini:"http_pwd" json:"http_pwd"`
-	HostHeaderRewrite string            `ini:"host_header_rewrite" json:"host_header_rewrite"`
-	Headers           map[string]string `ini:"-" json:"headers"`
-	RouteByHTTPUser   string            `ini:"route_by_http_user" json:"route_by_http_user"`
-}
-
-// HTTPS
-type HTTPSProxyConf struct {
-	BaseProxyConf `ini:",extends"`
-	DomainConf    `ini:",extends"`
-}
-
-// TCP
-type TCPProxyConf struct {
-	BaseProxyConf `ini:",extends"`
-	RemotePort    int `ini:"remote_port" json:"remote_port"`
-}
-
-// UDP
-type UDPProxyConf struct {
-	BaseProxyConf `ini:",extends"`
-
-	RemotePort int `ini:"remote_port" json:"remote_port"`
-}
-
-// TCPMux
-type TCPMuxProxyConf struct {
-	BaseProxyConf   `ini:",extends"`
-	DomainConf      `ini:",extends"`
-	HTTPUser        string `ini:"http_user" json:"http_user,omitempty"`
-	HTTPPwd         string `ini:"http_pwd" json:"http_pwd,omitempty"`
-	RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
-
-	Multiplexer string `ini:"multiplexer"`
-}
-
-// STCP
-type STCPProxyConf struct {
-	BaseProxyConf        `ini:",extends"`
-	RoleServerCommonConf `ini:",extends"`
-}
-
-// XTCP
-type XTCPProxyConf struct {
-	BaseProxyConf        `ini:",extends"`
-	RoleServerCommonConf `ini:",extends"`
-}
-
-// SUDP
-type SUDPProxyConf struct {
-	BaseProxyConf        `ini:",extends"`
-	RoleServerCommonConf `ini:",extends"`
-}
-
-// Proxy Conf Loader
-// DefaultProxyConf creates a empty ProxyConf object by proxyType.
-// If proxyType doesn't exist, return nil.
-func DefaultProxyConf(proxyType string) ProxyConf {
-	conf := NewConfByType(proxyType)
-	if conf != nil {
-		conf.SetDefaultValues()
-	}
-	return conf
-}
-
-// Proxy loaded from ini
-func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) {
-	// section.Key: if key not exists, section will set it with default value.
-	proxyType := section.Key("type").String()
-	if proxyType == "" {
-		proxyType = consts.TCPProxy
-	}
-
-	conf := DefaultProxyConf(proxyType)
-	if conf == nil {
-		return nil, fmt.Errorf("invalid type [%s]", proxyType)
-	}
-
-	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {
-		return nil, err
-	}
-
-	if err := conf.ValidateForClient(); err != nil {
-		return nil, err
-	}
-	return conf, nil
-}
-
-// Proxy loaded from msg
-func NewProxyConfFromMsg(m *msg.NewProxy, serverCfg ServerCommonConf) (ProxyConf, error) {
-	if m.ProxyType == "" {
-		m.ProxyType = consts.TCPProxy
-	}
-
-	conf := DefaultProxyConf(m.ProxyType)
-	if conf == nil {
-		return nil, fmt.Errorf("proxy [%s] type [%s] error", m.ProxyName, m.ProxyType)
-	}
-
-	conf.UnmarshalFromMsg(m)
-
-	err := conf.ValidateForServer(serverCfg)
-	if err != nil {
-		return nil, err
-	}
-
-	return conf, nil
-}
-
-// Base
-func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf {
-	return cfg
-}
-
-func (cfg *BaseProxyConf) SetDefaultValues() {
-	cfg.LocalSvrConf = LocalSvrConf{
-		LocalIP: "127.0.0.1",
-	}
-	cfg.BandwidthLimitMode = BandwidthLimitModeClient
-}
-
-// BaseProxyConf apply custom logic changes.
-func (cfg *BaseProxyConf) decorate(prefix string, name string, section *ini.Section) error {
-	// proxy_name
-	cfg.ProxyName = prefix + name
-
-	// metas_xxx
-	cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_")
-
-	// bandwidth_limit
-	if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil {
-		cfg.BandwidthLimit, err = NewBandwidthQuantity(bandwidth.String())
-		if err != nil {
-			return err
-		}
-	}
-
-	// plugin_xxx
-	cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_")
-
-	// custom logic code
-	if cfg.HealthCheckType == "tcp" && cfg.Plugin == "" {
-		cfg.HealthCheckAddr = cfg.LocalIP + fmt.Sprintf(":%d", cfg.LocalPort)
-	}
-
-	if cfg.HealthCheckType == "http" && cfg.Plugin == "" && cfg.HealthCheckURL != "" {
-		s := "http://" + net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort))
-		if !strings.HasPrefix(cfg.HealthCheckURL, "/") {
-			s += "/"
-		}
-		cfg.HealthCheckURL = s + cfg.HealthCheckURL
-	}
-
-	return nil
-}
-
-func (cfg *BaseProxyConf) marshalToMsg(m *msg.NewProxy) {
-	m.ProxyName = cfg.ProxyName
-	m.ProxyType = cfg.ProxyType
-	m.UseEncryption = cfg.UseEncryption
-	m.UseCompression = cfg.UseCompression
-	m.BandwidthLimit = cfg.BandwidthLimit.String()
-	// leave it empty for default value to reduce traffic
-	if cfg.BandwidthLimitMode != "client" {
-		m.BandwidthLimitMode = cfg.BandwidthLimitMode
-	}
-	m.Group = cfg.Group
-	m.GroupKey = cfg.GroupKey
-	m.Metas = cfg.Metas
-}
-
-func (cfg *BaseProxyConf) unmarshalFromMsg(m *msg.NewProxy) {
-	cfg.ProxyName = m.ProxyName
-	cfg.ProxyType = m.ProxyType
-	cfg.UseEncryption = m.UseEncryption
-	cfg.UseCompression = m.UseCompression
-	if m.BandwidthLimit != "" {
-		cfg.BandwidthLimit, _ = NewBandwidthQuantity(m.BandwidthLimit)
-	}
-	if m.BandwidthLimitMode != "" {
-		cfg.BandwidthLimitMode = m.BandwidthLimitMode
-	}
-	cfg.Group = m.Group
-	cfg.GroupKey = m.GroupKey
-	cfg.Metas = m.Metas
-}
-
-func (cfg *BaseProxyConf) validateForClient() (err error) {
-	if cfg.ProxyProtocolVersion != "" {
-		if cfg.ProxyProtocolVersion != "v1" && cfg.ProxyProtocolVersion != "v2" {
-			return fmt.Errorf("no support proxy protocol version: %s", cfg.ProxyProtocolVersion)
-		}
-	}
-
-	if cfg.BandwidthLimitMode != "client" && cfg.BandwidthLimitMode != "server" {
-		return fmt.Errorf("bandwidth_limit_mode should be client or server")
-	}
-
-	if err = cfg.LocalSvrConf.validateForClient(); err != nil {
-		return
-	}
-	if err = cfg.HealthCheckConf.validateForClient(); err != nil {
-		return
-	}
-	return nil
-}
-
-func (cfg *BaseProxyConf) validateForServer() (err error) {
-	if cfg.BandwidthLimitMode != "client" && cfg.BandwidthLimitMode != "server" {
-		return fmt.Errorf("bandwidth_limit_mode should be client or server")
-	}
-	return nil
-}
-
-// DomainConf
-func (cfg *DomainConf) check() (err error) {
-	if len(cfg.CustomDomains) == 0 && cfg.SubDomain == "" {
-		err = fmt.Errorf("custom_domains and subdomain should set at least one of them")
-		return
-	}
-	return
-}
-
-func (cfg *DomainConf) validateForClient() (err error) {
-	if err = cfg.check(); err != nil {
-		return
-	}
-	return
-}
-
-func (cfg *DomainConf) validateForServer(serverCfg ServerCommonConf) (err error) {
-	if err = cfg.check(); err != nil {
-		return
-	}
-
-	for _, domain := range cfg.CustomDomains {
-		if serverCfg.SubDomainHost != "" && len(strings.Split(serverCfg.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
-			if strings.Contains(domain, serverCfg.SubDomainHost) {
-				return fmt.Errorf("custom domain [%s] should not belong to subdomain_host [%s]", domain, serverCfg.SubDomainHost)
-			}
-		}
-	}
-
-	if cfg.SubDomain != "" {
-		if serverCfg.SubDomainHost == "" {
-			return fmt.Errorf("subdomain is not supported because this feature is not enabled in remote frps")
-		}
-		if strings.Contains(cfg.SubDomain, ".") || strings.Contains(cfg.SubDomain, "*") {
-			return fmt.Errorf("'.' and '*' is not supported in subdomain")
-		}
-	}
-	return nil
-}
-
-// LocalSvrConf
-func (cfg *LocalSvrConf) validateForClient() (err error) {
-	if cfg.Plugin == "" {
-		if cfg.LocalIP == "" {
-			err = fmt.Errorf("local ip or plugin is required")
-			return
-		}
-		if cfg.LocalPort <= 0 {
-			err = fmt.Errorf("error local_port")
-			return
-		}
-	}
-	return
-}
-
-// HealthCheckConf
-func (cfg *HealthCheckConf) validateForClient() error {
-	if cfg.HealthCheckType != "" && cfg.HealthCheckType != "tcp" && cfg.HealthCheckType != "http" {
-		return fmt.Errorf("unsupport health check type")
-	}
-	if cfg.HealthCheckType != "" {
-		if cfg.HealthCheckType == "http" && cfg.HealthCheckURL == "" {
-			return fmt.Errorf("health_check_url is required for health check type 'http'")
-		}
-	}
-	return nil
-}
-
-func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error {
-	err := section.MapTo(cfg)
-	if err != nil {
-		return err
-	}
-
-	err = cfg.GetBaseConfig().decorate(prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// TCP
-func (cfg *TCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.RemotePort = m.RemotePort
-}
-
-func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-
-	return nil
-}
-
-func (cfg *TCPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	m.RemotePort = cfg.RemotePort
-}
-
-func (cfg *TCPProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-
-	return
-}
-
-func (cfg *TCPProxyConf) ValidateForServer(_ ServerCommonConf) error {
-	return cfg.BaseProxyConf.validateForServer()
-}
-
-// TCPMux
-func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-
-	return nil
-}
-
-func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.CustomDomains = m.CustomDomains
-	cfg.SubDomain = m.SubDomain
-	cfg.Multiplexer = m.Multiplexer
-	cfg.HTTPUser = m.HTTPUser
-	cfg.HTTPPwd = m.HTTPPwd
-	cfg.RouteByHTTPUser = m.RouteByHTTPUser
-}
-
-func (cfg *TCPMuxProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	m.CustomDomains = cfg.CustomDomains
-	m.SubDomain = cfg.SubDomain
-	m.Multiplexer = cfg.Multiplexer
-	m.HTTPUser = cfg.HTTPUser
-	m.HTTPPwd = cfg.HTTPPwd
-	m.RouteByHTTPUser = cfg.RouteByHTTPUser
-}
-
-func (cfg *TCPMuxProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-	if err = cfg.DomainConf.validateForClient(); err != nil {
-		return
-	}
-
-	if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer {
-		return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer)
-	}
-
-	return
-}
-
-func (cfg *TCPMuxProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) {
-	if err := cfg.BaseProxyConf.validateForServer(); err != nil {
-		return err
-	}
-
-	if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer {
-		return fmt.Errorf("proxy [%s] incorrect multiplexer [%s]", cfg.ProxyName, cfg.Multiplexer)
-	}
-
-	if cfg.Multiplexer == consts.HTTPConnectTCPMultiplexer && serverCfg.TCPMuxHTTPConnectPort == 0 {
-		return fmt.Errorf("proxy [%s] type [tcpmux] with multiplexer [httpconnect] requires tcpmux_httpconnect_port configuration", cfg.ProxyName)
-	}
-
-	if err = cfg.DomainConf.validateForServer(serverCfg); err != nil {
-		err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err)
-		return
-	}
-
-	return
-}
-
-// UDP
-func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-
-	return nil
-}
-
-func (cfg *UDPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.RemotePort = m.RemotePort
-}
-
-func (cfg *UDPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	m.RemotePort = cfg.RemotePort
-}
-
-func (cfg *UDPProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-
-	return
-}
-
-func (cfg *UDPProxyConf) ValidateForServer(_ ServerCommonConf) error {
-	return cfg.BaseProxyConf.validateForServer()
-}
-
-// HTTP
-func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-	cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_")
-	return nil
-}
-
-func (cfg *HTTPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.CustomDomains = m.CustomDomains
-	cfg.SubDomain = m.SubDomain
-	cfg.Locations = m.Locations
-	cfg.HostHeaderRewrite = m.HostHeaderRewrite
-	cfg.HTTPUser = m.HTTPUser
-	cfg.HTTPPwd = m.HTTPPwd
-	cfg.Headers = m.Headers
-	cfg.RouteByHTTPUser = m.RouteByHTTPUser
-}
-
-func (cfg *HTTPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	m.CustomDomains = cfg.CustomDomains
-	m.SubDomain = cfg.SubDomain
-	m.Locations = cfg.Locations
-	m.HostHeaderRewrite = cfg.HostHeaderRewrite
-	m.HTTPUser = cfg.HTTPUser
-	m.HTTPPwd = cfg.HTTPPwd
-	m.Headers = cfg.Headers
-	m.RouteByHTTPUser = cfg.RouteByHTTPUser
-}
-
-func (cfg *HTTPProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-	if err = cfg.DomainConf.validateForClient(); err != nil {
-		return
-	}
-
-	return
-}
-
-func (cfg *HTTPProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) {
-	if err := cfg.BaseProxyConf.validateForServer(); err != nil {
-		return err
-	}
-
-	if serverCfg.VhostHTTPPort == 0 {
-		return fmt.Errorf("type [http] not support when vhost_http_port is not set")
-	}
-
-	if err = cfg.DomainConf.validateForServer(serverCfg); err != nil {
-		err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err)
-		return
-	}
-
-	return
-}
-
-// HTTPS
-func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-	return nil
-}
-
-func (cfg *HTTPSProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.CustomDomains = m.CustomDomains
-	cfg.SubDomain = m.SubDomain
-}
-
-func (cfg *HTTPSProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	m.CustomDomains = cfg.CustomDomains
-	m.SubDomain = cfg.SubDomain
-}
-
-func (cfg *HTTPSProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-	if err = cfg.DomainConf.validateForClient(); err != nil {
-		return
-	}
-	return
-}
-
-func (cfg *HTTPSProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) {
-	if err := cfg.BaseProxyConf.validateForServer(); err != nil {
-		return err
-	}
-
-	if serverCfg.VhostHTTPSPort == 0 {
-		return fmt.Errorf("type [https] not support when vhost_https_port is not set")
-	}
-
-	if err = cfg.DomainConf.validateForServer(serverCfg); err != nil {
-		err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err)
-		return
-	}
-
-	return
-}
-
-// SUDP
-func (cfg *SUDPProxyConf) SetDefaultValues() {
-	cfg.BaseProxyConf.SetDefaultValues()
-	cfg.RoleServerCommonConf.setDefaultValues()
-}
-
-func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-	return nil
-}
-
-// Only for role server.
-func (cfg *SUDPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.RoleServerCommonConf.unmarshalFromMsg(m)
-}
-
-func (cfg *SUDPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	cfg.RoleServerCommonConf.marshalToMsg(m)
-}
-
-func (cfg *SUDPProxyConf) ValidateForClient() (err error) {
-	if err := cfg.BaseProxyConf.validateForClient(); err != nil {
-		return err
-	}
-
-	// Add custom logic check if exists
-	if cfg.Role != "server" {
-		return fmt.Errorf("role should be 'server'")
-	}
-
-	return nil
-}
-
-func (cfg *SUDPProxyConf) ValidateForServer(_ ServerCommonConf) error {
-	return cfg.BaseProxyConf.validateForServer()
-}
-
-// STCP
-func (cfg *STCPProxyConf) SetDefaultValues() {
-	cfg.BaseProxyConf.SetDefaultValues()
-	cfg.RoleServerCommonConf.setDefaultValues()
-}
-
-func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-	if cfg.Role == "" {
-		cfg.Role = "server"
-	}
-	return nil
-}
-
-// Only for role server.
-func (cfg *STCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.RoleServerCommonConf.unmarshalFromMsg(m)
-}
-
-func (cfg *STCPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	cfg.RoleServerCommonConf.marshalToMsg(m)
-}
-
-func (cfg *STCPProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-	if cfg.Role != "server" {
-		return fmt.Errorf("role should be 'server'")
-	}
-
-	return
-}
-
-func (cfg *STCPProxyConf) ValidateForServer(_ ServerCommonConf) error {
-	return cfg.BaseProxyConf.validateForServer()
-}
-
-// XTCP
-func (cfg *XTCPProxyConf) SetDefaultValues() {
-	cfg.BaseProxyConf.SetDefaultValues()
-	cfg.RoleServerCommonConf.setDefaultValues()
-}
-
-func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error {
-	err := preUnmarshalFromIni(cfg, prefix, name, section)
-	if err != nil {
-		return err
-	}
-
-	// Add custom logic unmarshal if exists
-	if cfg.Role == "" {
-		cfg.Role = "server"
-	}
-	return nil
-}
-
-// Only for role server.
-func (cfg *XTCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.unmarshalFromMsg(m)
-
-	// Add custom logic unmarshal if exists
-	cfg.RoleServerCommonConf.unmarshalFromMsg(m)
-}
-
-func (cfg *XTCPProxyConf) MarshalToMsg(m *msg.NewProxy) {
-	cfg.BaseProxyConf.marshalToMsg(m)
-
-	// Add custom logic marshal if exists
-	cfg.RoleServerCommonConf.marshalToMsg(m)
-}
-
-func (cfg *XTCPProxyConf) ValidateForClient() (err error) {
-	if err = cfg.BaseProxyConf.validateForClient(); err != nil {
-		return
-	}
-
-	// Add custom logic check if exists
-	if cfg.Role != "server" {
-		return fmt.Errorf("role should be 'server'")
-	}
-	return
-}
-
-func (cfg *XTCPProxyConf) ValidateForServer(_ ServerCommonConf) error {
-	return cfg.BaseProxyConf.validateForServer()
-}

+ 0 - 478
pkg/config/proxy_test.go

@@ -1,478 +0,0 @@
-// Copyright 2020 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package config
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"gopkg.in/ini.v1"
-
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-var (
-	testLoadOptions = ini.LoadOptions{
-		Insensitive:         false,
-		InsensitiveSections: false,
-		InsensitiveKeys:     false,
-		IgnoreInlineComment: true,
-		AllowBooleanKeys:    true,
-	}
-
-	testProxyPrefix = "test."
-)
-
-func Test_Proxy_Interface(_ *testing.T) {
-	for name := range proxyConfTypeMap {
-		NewConfByType(name)
-	}
-}
-
-func Test_Proxy_UnmarshalFromIni(t *testing.T) {
-	assert := assert.New(t)
-
-	testcases := []struct {
-		sname    string
-		source   []byte
-		expected ProxyConf
-	}{
-		{
-			sname: "ssh",
-			source: []byte(`
-				[ssh]
-				# tcp | udp | http | https | stcp | xtcp, default is tcp
-				type = tcp
-				local_ip = 127.0.0.9
-				local_port = 29
-				bandwidth_limit = 19MB
-				bandwidth_limit_mode = server
-				use_encryption
-				use_compression
-				remote_port = 6009
-				group = test_group
-				group_key = 123456
-				health_check_type = tcp
-				health_check_timeout_s = 3
-				health_check_max_failed = 3
-				health_check_interval_s = 19
-				meta_var1 = 123
-				meta_var2 = 234`),
-			expected: &TCPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName:          testProxyPrefix + "ssh",
-					ProxyType:          consts.TCPProxy,
-					UseCompression:     true,
-					UseEncryption:      true,
-					Group:              "test_group",
-					GroupKey:           "123456",
-					BandwidthLimit:     MustBandwidthQuantity("19MB"),
-					BandwidthLimitMode: BandwidthLimitModeServer,
-					Metas: map[string]string{
-						"var1": "123",
-						"var2": "234",
-					},
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.9",
-						LocalPort: 29,
-					},
-					HealthCheckConf: HealthCheckConf{
-						HealthCheckType:      consts.TCPProxy,
-						HealthCheckTimeoutS:  3,
-						HealthCheckMaxFailed: 3,
-						HealthCheckIntervalS: 19,
-						HealthCheckAddr:      "127.0.0.9:29",
-					},
-				},
-				RemotePort: 6009,
-			},
-		},
-		{
-			sname: "ssh_random",
-			source: []byte(`
-				[ssh_random]
-				type = tcp
-				local_ip = 127.0.0.9
-				local_port = 29
-				remote_port = 9
-			`),
-			expected: &TCPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName: testProxyPrefix + "ssh_random",
-					ProxyType: consts.TCPProxy,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.9",
-						LocalPort: 29,
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				RemotePort: 9,
-			},
-		},
-		{
-			sname: "dns",
-			source: []byte(`
-				[dns]
-				type = udp
-				local_ip = 114.114.114.114
-				local_port = 59
-				remote_port = 6009
-				use_encryption
-				use_compression
-			`),
-			expected: &UDPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName:      testProxyPrefix + "dns",
-					ProxyType:      consts.UDPProxy,
-					UseEncryption:  true,
-					UseCompression: true,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "114.114.114.114",
-						LocalPort: 59,
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				RemotePort: 6009,
-			},
-		},
-		{
-			sname: "web01",
-			source: []byte(`
-				[web01]
-				type = http
-				local_ip = 127.0.0.9
-				local_port = 89
-				use_encryption
-				use_compression
-				http_user = admin
-				http_pwd = admin
-				subdomain = web01
-				custom_domains = web02.yourdomain.com
-				locations = /,/pic
-				host_header_rewrite = example.com
-				header_X-From-Where = frp
-				health_check_type = http
-				health_check_url = /status
-				health_check_interval_s = 19
-				health_check_max_failed = 3
-				health_check_timeout_s = 3
-			`),
-			expected: &HTTPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName:      testProxyPrefix + "web01",
-					ProxyType:      consts.HTTPProxy,
-					UseCompression: true,
-					UseEncryption:  true,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.9",
-						LocalPort: 89,
-					},
-					HealthCheckConf: HealthCheckConf{
-						HealthCheckType:      consts.HTTPProxy,
-						HealthCheckTimeoutS:  3,
-						HealthCheckMaxFailed: 3,
-						HealthCheckIntervalS: 19,
-						HealthCheckURL:       "http://127.0.0.9:89/status",
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				DomainConf: DomainConf{
-					CustomDomains: []string{"web02.yourdomain.com"},
-					SubDomain:     "web01",
-				},
-				Locations:         []string{"/", "/pic"},
-				HTTPUser:          "admin",
-				HTTPPwd:           "admin",
-				HostHeaderRewrite: "example.com",
-				Headers: map[string]string{
-					"X-From-Where": "frp",
-				},
-			},
-		},
-		{
-			sname: "web02",
-			source: []byte(`
-				[web02]
-				type = https
-				local_ip = 127.0.0.9
-				local_port = 8009
-				use_encryption
-				use_compression
-				subdomain = web01
-				custom_domains = web02.yourdomain.com
-				proxy_protocol_version = v2
-			`),
-			expected: &HTTPSProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName:      testProxyPrefix + "web02",
-					ProxyType:      consts.HTTPSProxy,
-					UseCompression: true,
-					UseEncryption:  true,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.9",
-						LocalPort: 8009,
-					},
-					ProxyProtocolVersion: "v2",
-					BandwidthLimitMode:   BandwidthLimitModeClient,
-				},
-				DomainConf: DomainConf{
-					CustomDomains: []string{"web02.yourdomain.com"},
-					SubDomain:     "web01",
-				},
-			},
-		},
-		{
-			sname: "secret_tcp",
-			source: []byte(`
-				[secret_tcp]
-				type = stcp
-				sk = abcdefg
-				local_ip = 127.0.0.1
-				local_port = 22
-				use_encryption = false
-				use_compression = false
-			`),
-			expected: &STCPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName: testProxyPrefix + "secret_tcp",
-					ProxyType: consts.STCPProxy,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.1",
-						LocalPort: 22,
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				RoleServerCommonConf: RoleServerCommonConf{
-					Role: "server",
-					Sk:   "abcdefg",
-				},
-			},
-		},
-		{
-			sname: "p2p_tcp",
-			source: []byte(`
-				[p2p_tcp]
-				type = xtcp
-				sk = abcdefg
-				local_ip = 127.0.0.1
-				local_port = 22
-				use_encryption = false
-				use_compression = false
-			`),
-			expected: &XTCPProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName: testProxyPrefix + "p2p_tcp",
-					ProxyType: consts.XTCPProxy,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.1",
-						LocalPort: 22,
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				RoleServerCommonConf: RoleServerCommonConf{
-					Role: "server",
-					Sk:   "abcdefg",
-				},
-			},
-		},
-		{
-			sname: "tcpmuxhttpconnect",
-			source: []byte(`
-				[tcpmuxhttpconnect]
-				type = tcpmux
-				multiplexer = httpconnect
-				local_ip = 127.0.0.1
-				local_port = 10701
-				custom_domains = tunnel1
-			`),
-			expected: &TCPMuxProxyConf{
-				BaseProxyConf: BaseProxyConf{
-					ProxyName: testProxyPrefix + "tcpmuxhttpconnect",
-					ProxyType: consts.TCPMuxProxy,
-					LocalSvrConf: LocalSvrConf{
-						LocalIP:   "127.0.0.1",
-						LocalPort: 10701,
-					},
-					BandwidthLimitMode: BandwidthLimitModeClient,
-				},
-				DomainConf: DomainConf{
-					CustomDomains: []string{"tunnel1"},
-					SubDomain:     "",
-				},
-				Multiplexer: "httpconnect",
-			},
-		},
-	}
-
-	for _, c := range testcases {
-		f, err := ini.LoadSources(testLoadOptions, c.source)
-		assert.NoError(err)
-
-		proxyType := f.Section(c.sname).Key("type").String()
-		assert.NotEmpty(proxyType)
-
-		actual := DefaultProxyConf(proxyType)
-		assert.NotNil(actual)
-
-		err = actual.UnmarshalFromIni(testProxyPrefix, c.sname, f.Section(c.sname))
-		assert.NoError(err)
-		assert.Equal(c.expected, actual)
-	}
-}
-
-func Test_RangeProxy_UnmarshalFromIni(t *testing.T) {
-	assert := assert.New(t)
-
-	testcases := []struct {
-		sname    string
-		source   []byte
-		expected map[string]ProxyConf
-	}{
-		{
-			sname: "range:tcp_port",
-			source: []byte(`
-				[range:tcp_port]
-				type = tcp
-				local_ip = 127.0.0.9
-				local_port = 6010-6011,6019
-				remote_port = 6010-6011,6019
-				use_encryption = false
-				use_compression = false
-			`),
-			expected: map[string]ProxyConf{
-				"tcp_port_0": &TCPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName: testProxyPrefix + "tcp_port_0",
-						ProxyType: consts.TCPProxy,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "127.0.0.9",
-							LocalPort: 6010,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6010,
-				},
-				"tcp_port_1": &TCPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName: testProxyPrefix + "tcp_port_1",
-						ProxyType: consts.TCPProxy,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "127.0.0.9",
-							LocalPort: 6011,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6011,
-				},
-				"tcp_port_2": &TCPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName: testProxyPrefix + "tcp_port_2",
-						ProxyType: consts.TCPProxy,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "127.0.0.9",
-							LocalPort: 6019,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6019,
-				},
-			},
-		},
-		{
-			sname: "range:udp_port",
-			source: []byte(`
-				[range:udp_port]
-				type = udp
-				local_ip = 114.114.114.114
-				local_port = 6000,6010-6011
-				remote_port = 6000,6010-6011
-				use_encryption
-				use_compression
-			`),
-			expected: map[string]ProxyConf{
-				"udp_port_0": &UDPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName:      testProxyPrefix + "udp_port_0",
-						ProxyType:      consts.UDPProxy,
-						UseEncryption:  true,
-						UseCompression: true,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "114.114.114.114",
-							LocalPort: 6000,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6000,
-				},
-				"udp_port_1": &UDPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName:      testProxyPrefix + "udp_port_1",
-						ProxyType:      consts.UDPProxy,
-						UseEncryption:  true,
-						UseCompression: true,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "114.114.114.114",
-							LocalPort: 6010,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6010,
-				},
-				"udp_port_2": &UDPProxyConf{
-					BaseProxyConf: BaseProxyConf{
-						ProxyName:      testProxyPrefix + "udp_port_2",
-						ProxyType:      consts.UDPProxy,
-						UseEncryption:  true,
-						UseCompression: true,
-						LocalSvrConf: LocalSvrConf{
-							LocalIP:   "114.114.114.114",
-							LocalPort: 6011,
-						},
-						BandwidthLimitMode: BandwidthLimitModeClient,
-					},
-					RemotePort: 6011,
-				},
-			},
-		},
-	}
-
-	for _, c := range testcases {
-
-		f, err := ini.LoadSources(testLoadOptions, c.source)
-		assert.NoError(err)
-
-		actual := make(map[string]ProxyConf)
-		s := f.Section(c.sname)
-
-		err = renderRangeProxyTemplates(f, s)
-		assert.NoError(err)
-
-		f.DeleteSection(ini.DefaultSection)
-		f.DeleteSection(c.sname)
-
-		for _, section := range f.Sections() {
-			proxyType := section.Key("type").String()
-			newsname := section.Name()
-
-			tmp := DefaultProxyConf(proxyType)
-			err = tmp.UnmarshalFromIni(testProxyPrefix, newsname, section)
-			assert.NoError(err)
-
-			actual[newsname] = tmp
-		}
-
-		assert.Equal(c.expected, actual)
-	}
-}

+ 0 - 217
pkg/config/server_test.go

@@ -1,217 +0,0 @@
-// Copyright 2020 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package config
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/fatedier/frp/pkg/auth"
-	plugin "github.com/fatedier/frp/pkg/plugin/server"
-)
-
-func Test_LoadServerCommonConf(t *testing.T) {
-	assert := assert.New(t)
-
-	testcases := []struct {
-		source   []byte
-		expected ServerCommonConf
-	}{
-		{
-			source: []byte(`
-				# [common] is integral section
-				[common]
-				bind_addr = 0.0.0.9
-				bind_port = 7009
-				kcp_bind_port = 7007
-				proxy_bind_addr = 127.0.0.9
-				vhost_http_port = 89
-				vhost_https_port = 449
-				vhost_http_timeout = 69
-				tcpmux_httpconnect_port = 1339
-				dashboard_addr = 0.0.0.9
-				dashboard_port = 7509
-				dashboard_user = admin9
-				dashboard_pwd = admin9
-				enable_prometheus
-				assets_dir = ./static9
-				log_file = ./frps.log9
-				log_way = file
-				log_level = info9
-				log_max_days = 39
-				disable_log_color = false
-				detailed_errors_to_client
-				authentication_method = token
-				authenticate_heartbeats = false
-				authenticate_new_work_conns = false
-				token = 123456789
-				oidc_issuer = test9
-				oidc_audience = test9
-				oidc_skip_expiry_check
-				oidc_skip_issuer_check
-				heartbeat_timeout = 99
-				user_conn_timeout = 9
-				allow_ports = 10-12,99
-				max_pool_count = 59
-				max_ports_per_client = 9
-				tls_only = false
-				tls_cert_file = server.crt
-				tls_key_file = server.key
-				tls_trusted_ca_file = ca.crt
-				subdomain_host = frps.com
-				tcp_mux
-				udp_packet_size = 1509
-				[plugin.user-manager]
-				addr = 127.0.0.1:9009
-				path = /handler
-				ops = Login
-				[plugin.port-manager]
-				addr = 127.0.0.1:9009
-				path = /handler
-				ops = NewProxy
-				tls_verify
-			`),
-			expected: ServerCommonConf{
-				ServerConfig: auth.ServerConfig{
-					BaseConfig: auth.BaseConfig{
-						AuthenticationMethod:     "token",
-						AuthenticateHeartBeats:   false,
-						AuthenticateNewWorkConns: false,
-					},
-					TokenConfig: auth.TokenConfig{
-						Token: "123456789",
-					},
-					OidcServerConfig: auth.OidcServerConfig{
-						OidcIssuer:          "test9",
-						OidcAudience:        "test9",
-						OidcSkipExpiryCheck: true,
-						OidcSkipIssuerCheck: true,
-					},
-				},
-				BindAddr:               "0.0.0.9",
-				BindPort:               7009,
-				KCPBindPort:            7007,
-				QUICKeepalivePeriod:    10,
-				QUICMaxIdleTimeout:     30,
-				QUICMaxIncomingStreams: 100000,
-				ProxyBindAddr:          "127.0.0.9",
-				VhostHTTPPort:          89,
-				VhostHTTPSPort:         449,
-				VhostHTTPTimeout:       69,
-				TCPMuxHTTPConnectPort:  1339,
-				DashboardAddr:          "0.0.0.9",
-				DashboardPort:          7509,
-				DashboardUser:          "admin9",
-				DashboardPwd:           "admin9",
-				EnablePrometheus:       true,
-				AssetsDir:              "./static9",
-				LogFile:                "./frps.log9",
-				LogWay:                 "file",
-				LogLevel:               "info9",
-				LogMaxDays:             39,
-				DisableLogColor:        false,
-				DetailedErrorsToClient: true,
-				HeartbeatTimeout:       99,
-				UserConnTimeout:        9,
-				AllowPorts: map[int]struct{}{
-					10: {},
-					11: {},
-					12: {},
-					99: {},
-				},
-				AllowPortsStr:                   "10-12,99",
-				MaxPoolCount:                    59,
-				MaxPortsPerClient:               9,
-				TLSOnly:                         true,
-				TLSCertFile:                     "server.crt",
-				TLSKeyFile:                      "server.key",
-				TLSTrustedCaFile:                "ca.crt",
-				SubDomainHost:                   "frps.com",
-				TCPMux:                          true,
-				TCPMuxKeepaliveInterval:         60,
-				TCPKeepAlive:                    7200,
-				UDPPacketSize:                   1509,
-				NatHoleAnalysisDataReserveHours: 7 * 24,
-
-				HTTPPlugins: map[string]plugin.HTTPPluginOptions{
-					"user-manager": {
-						Name: "user-manager",
-						Addr: "127.0.0.1:9009",
-						Path: "/handler",
-						Ops:  []string{"Login"},
-					},
-					"port-manager": {
-						Name:      "port-manager",
-						Addr:      "127.0.0.1:9009",
-						Path:      "/handler",
-						Ops:       []string{"NewProxy"},
-						TLSVerify: true,
-					},
-				},
-			},
-		},
-		{
-			source: []byte(`
-				# [common] is integral section
-				[common]
-				bind_addr = 0.0.0.9
-				bind_port = 7009
-			`),
-			expected: ServerCommonConf{
-				ServerConfig: auth.ServerConfig{
-					BaseConfig: auth.BaseConfig{
-						AuthenticationMethod:     "token",
-						AuthenticateHeartBeats:   false,
-						AuthenticateNewWorkConns: false,
-					},
-				},
-				BindAddr:                        "0.0.0.9",
-				BindPort:                        7009,
-				QUICKeepalivePeriod:             10,
-				QUICMaxIdleTimeout:              30,
-				QUICMaxIncomingStreams:          100000,
-				ProxyBindAddr:                   "0.0.0.9",
-				VhostHTTPTimeout:                60,
-				DashboardAddr:                   "0.0.0.0",
-				DashboardUser:                   "",
-				DashboardPwd:                    "",
-				EnablePrometheus:                false,
-				LogFile:                         "console",
-				LogWay:                          "console",
-				LogLevel:                        "info",
-				LogMaxDays:                      3,
-				DetailedErrorsToClient:          true,
-				TCPMux:                          true,
-				TCPMuxKeepaliveInterval:         60,
-				TCPKeepAlive:                    7200,
-				AllowPorts:                      make(map[int]struct{}),
-				MaxPoolCount:                    5,
-				HeartbeatTimeout:                90,
-				UserConnTimeout:                 10,
-				HTTPPlugins:                     make(map[string]plugin.HTTPPluginOptions),
-				UDPPacketSize:                   1500,
-				NatHoleAnalysisDataReserveHours: 7 * 24,
-			},
-		},
-	}
-
-	for _, c := range testcases {
-		actual, err := UnmarshalServerConfFromIni(c.source)
-		assert.NoError(err)
-		actual.Complete()
-		assert.Equal(c.expected, actual)
-	}
-}

+ 64 - 1
pkg/config/types.go → pkg/config/types/types.go

@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package types
 
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"strconv"
 	"strings"
 )
@@ -123,3 +124,65 @@ func (q *BandwidthQuantity) MarshalJSON() ([]byte, error) {
 func (q *BandwidthQuantity) Bytes() int64 {
 	return q.i
 }
+
+type PortsRange struct {
+	Start  int `json:"start,omitempty"`
+	End    int `json:"end,omitempty"`
+	Single int `json:"single,omitempty"`
+}
+
+type PortsRangeSlice []PortsRange
+
+func (p PortsRangeSlice) String() string {
+	if len(p) == 0 {
+		return ""
+	}
+	strs := []string{}
+	for _, v := range p {
+		if v.Single > 0 {
+			strs = append(strs, strconv.Itoa(v.Single))
+		} else {
+			strs = append(strs, strconv.Itoa(v.Start)+"-"+strconv.Itoa(v.End))
+		}
+	}
+	return strings.Join(strs, ",")
+}
+
+// the format of str is like "1000-2000,3000,4000-5000"
+func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
+	str = strings.TrimSpace(str)
+	out := []PortsRange{}
+	numRanges := strings.Split(str, ",")
+	for _, numRangeStr := range numRanges {
+		// 1000-2000 or 2001
+		numArray := strings.Split(numRangeStr, "-")
+		// length: only 1 or 2 is correct
+		rangeType := len(numArray)
+		switch rangeType {
+		case 1:
+			// single number
+			singleNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("range number is invalid, %v", err)
+			}
+			out = append(out, PortsRange{Single: int(singleNum)})
+		case 2:
+			// range numbers
+			min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("range number is invalid, %v", err)
+			}
+			max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("range number is invalid, %v", err)
+			}
+			if max < min {
+				return nil, fmt.Errorf("range number is invalid")
+			}
+			out = append(out, PortsRange{Start: int(min), End: int(max)})
+		default:
+			return nil, fmt.Errorf("range number is invalid")
+		}
+	}
+	return out, nil
+}

+ 39 - 7
pkg/config/types_test.go → pkg/config/types/types_test.go

@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package config
+package types
 
 import (
 	"encoding/json"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type Wrap struct {
@@ -27,14 +27,46 @@ type Wrap struct {
 }
 
 func TestBandwidthQuantity(t *testing.T) {
-	assert := assert.New(t)
+	require := require.New(t)
 
 	var w Wrap
 	err := json.Unmarshal([]byte(`{"b":"1KB","int":5}`), &w)
-	assert.NoError(err)
-	assert.EqualValues(1*KB, w.B.Bytes())
+	require.NoError(err)
+	require.EqualValues(1*KB, w.B.Bytes())
 
 	buf, err := json.Marshal(&w)
-	assert.NoError(err)
-	assert.Equal(`{"b":"1KB","int":5}`, string(buf))
+	require.NoError(err)
+	require.Equal(`{"b":"1KB","int":5}`, string(buf))
+}
+
+func TestPortsRangeSlice2String(t *testing.T) {
+	require := require.New(t)
+
+	ports := []PortsRange{
+		{
+			Start: 1000,
+			End:   2000,
+		},
+		{
+			Single: 3000,
+		},
+	}
+	str := PortsRangeSlice(ports).String()
+	require.Equal("1000-2000,3000", str)
+}
+
+func TestNewPortsRangeSliceFromString(t *testing.T) {
+	require := require.New(t)
+
+	ports, err := NewPortsRangeSliceFromString("1000-2000,3000")
+	require.NoError(err)
+	require.Equal([]PortsRange{
+		{
+			Start: 1000,
+			End:   2000,
+		},
+		{
+			Single: 3000,
+		},
+	}, ports)
 }

+ 19 - 0
pkg/config/v1/api.go

@@ -0,0 +1,19 @@
+// 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 v1
+
+type APIMetadata struct {
+	Version string `json:"version"`
+}

+ 199 - 0
pkg/config/v1/client.go

@@ -0,0 +1,199 @@
+// 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 v1
+
+import (
+	"os"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type ClientConfig struct {
+	ClientCommonConfig
+
+	Proxies  []TypedProxyConfig   `json:"proxies,omitempty"`
+	Visitors []TypedVisitorConfig `json:"visitors,omitempty"`
+}
+
+type ClientCommonConfig struct {
+	APIMetadata
+
+	Auth AuthClientConfig `json:"auth,omitempty"`
+	// User specifies a prefix for proxy names to distinguish them from other
+	// clients. If this value is not "", proxy names will automatically be
+	// changed to "{user}.{proxy_name}".
+	User string `json:"user,omitempty"`
+
+	// ServerAddr specifies the address of the server to connect to. By
+	// default, this value is "0.0.0.0".
+	ServerAddr string `json:"serverAddr,omitempty"`
+	// ServerPort specifies the port to connect to the server on. By default,
+	// this value is 7000.
+	ServerPort int `json:"serverPort,omitempty"`
+	// STUN server to help penetrate NAT hole.
+	NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"`
+	// DNSServer specifies a DNS server address for FRPC to use. If this value
+	// is "", the default DNS will be used.
+	DNSServer string `json:"dnsServer,omitempty"`
+	// LoginFailExit controls whether or not the client should exit after a
+	// failed login attempt. If false, the client will retry until a login
+	// attempt succeeds. By default, this value is true.
+	LoginFailExit *bool `json:"loginFailExit,omitempty"`
+	// Start specifies a set of enabled proxies by name. If this set is empty,
+	// all supplied proxies are enabled. By default, this value is an empty
+	// set.
+	Start []string `json:"start,omitempty"`
+
+	Log       LogConfig             `json:"log,omitempty"`
+	WebServer WebServerConfig       `json:"webServer,omitempty"`
+	Transport ClientTransportConfig `json:"transport,omitempty"`
+
+	// UDPPacketSize specifies the udp packet size
+	// By default, this value is 1500
+	UDPPacketSize int64 `json:"udpPacketSize,omitempty"`
+	// Client metadata info
+	Metadatas map[string]string `json:"metadatas,omitempty"`
+
+	// Include other config files for proxies.
+	IncludeConfigFiles []string `json:"includes,omitempty"`
+}
+
+func (c *ClientCommonConfig) Complete() {
+	c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0")
+	c.ServerPort = util.EmptyOr(c.ServerPort, 7000)
+	c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true))
+
+	c.Auth.Complete()
+	c.Log.Complete()
+	c.Transport.Complete()
+	c.WebServer.Complete()
+
+	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
+}
+
+type ClientTransportConfig struct {
+	// Protocol specifies the protocol to use when interacting with the server.
+	// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
+	// is "tcp".
+	Protocol string `json:"protocol,omitempty"`
+	// The maximum amount of time a dial to server will wait for a connect to complete.
+	DialServerTimeout int64 `json:"dialServerTimeout,omitempty"`
+	// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
+	// If negative, keep-alive probes are disabled.
+	DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"`
+	// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
+	// Note: This value only use in TCP/Websocket protocol. Not support in KCP protocol.
+	ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"`
+	// ProxyURL specifies a proxy address to connect to the server through. If
+	// this value is "", the server will be connected to directly. By default,
+	// this value is read from the "http_proxy" environment variable.
+	ProxyURL string `json:"proxyURL,omitempty"`
+	// PoolCount specifies the number of connections the client will make to
+	// the server in advance.
+	PoolCount int `json:"poolCount,omitempty"`
+	// TCPMux toggles TCP stream multiplexing. This allows multiple requests
+	// from a client to share a single TCP connection. If this value is true,
+	// the server must have TCP multiplexing enabled as well. By default, this
+	// value is true.
+	TCPMux *bool `json:"tcpMux,omitempty"`
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
+	TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
+	// QUIC protocol options.
+	QUIC QUICOptions `json:"quic,omitempty"`
+	// HeartBeatInterval specifies at what interval heartbeats are sent to the
+	// server, in seconds. It is not recommended to change this value. By
+	// default, this value is 30. Set negative value to disable it.
+	HeartbeatInterval int64 `json:"heartbeatInterval,omitempty"`
+	// HeartBeatTimeout specifies the maximum allowed heartbeat response delay
+	// before the connection is terminated, in seconds. It is not recommended
+	// to change this value. By default, this value is 90. Set negative value to disable it.
+	HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
+	// TLS specifies TLS settings for the connection to the server.
+	TLS TLSClientConfig `json:"tls,omitempty"`
+}
+
+func (c *ClientTransportConfig) Complete() {
+	c.Protocol = util.EmptyOr(c.Protocol, "tcp")
+	c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10)
+	c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200)
+	c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
+	c.PoolCount = util.EmptyOr(c.PoolCount, 1)
+	c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
+	c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
+	c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
+	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	c.QUIC.Complete()
+	c.TLS.Complete()
+}
+
+type TLSClientConfig struct {
+	// TLSEnable specifies whether or not TLS should be used when communicating
+	// with the server. If "tls.certFile" and "tls.keyFile" 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.
+	Enable *bool `json:"enable,omitempty"`
+	// If DisableCustomTLSFirstByte 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 `json:"disableCustomTLSFirstByte,omitempty"`
+
+	TLSConfig
+}
+
+func (c *TLSClientConfig) Complete() {
+	c.Enable = util.EmptyOr(c.Enable, lo.ToPtr(true))
+	c.DisableCustomTLSFirstByte = util.EmptyOr(c.DisableCustomTLSFirstByte, lo.ToPtr(true))
+}
+
+type AuthClientConfig struct {
+	// Method specifies what authentication method to use to
+	// authenticate frpc with frps. If "token" is specified - token will be
+	// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
+	// token will be issued using OIDC settings. By default, this value is "token".
+	Method AuthMethod `json:"method,omitempty"`
+	// Specify whether to include auth info in additional scope.
+	// Current supported scopes are: "HeartBeats", "NewWorkConns".
+	AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"`
+	// Token specifies the authorization token used to create keys to be sent
+	// to the server. The server must have a matching token for authorization
+	// to succeed.  By default, this value is "".
+	Token string               `json:"token,omitempty"`
+	OIDC  AuthOIDCClientConfig `json:"oidc,omitempty"`
+}
+
+func (c *AuthClientConfig) Complete() {
+	c.Method = util.EmptyOr(c.Method, "token")
+}
+
+type AuthOIDCClientConfig struct {
+	// ClientID specifies the client ID to use to get a token in OIDC authentication.
+	ClientID string `json:"clientID,omitempty"`
+	// ClientSecret specifies the client secret to use to get a token in OIDC
+	// authentication.
+	ClientSecret string `json:"clientSecret,omitempty"`
+	// Audience specifies the audience of the token in OIDC authentication.
+	Audience string `json:"audience,omitempty"`
+	// Scope specifies the scope of the token in OIDC authentication.
+	Scope string `json:"scope,omitempty"`
+	// TokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
+	// It will be used to get an OIDC token.
+	TokenEndpointURL string `json:"tokenEndpointURL,omitempty"`
+	// AdditionalEndpointParams specifies additional parameters to be sent
+	// this field will be transfer to map[string][]string in OIDC token generator.
+	AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
+}

+ 34 - 0
pkg/config/v1/client_test.go

@@ -0,0 +1,34 @@
+// 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 v1
+
+import (
+	"testing"
+
+	"github.com/samber/lo"
+	"github.com/stretchr/testify/require"
+)
+
+func TestClientConfigComplete(t *testing.T) {
+	require := require.New(t)
+	c := &ClientConfig{}
+	c.Complete()
+
+	require.EqualValues("token", c.Auth.Method)
+	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
+	require.Equal(true, lo.FromPtr(c.LoginFailExit))
+	require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable))
+	require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte))
+}

+ 117 - 0
pkg/config/v1/common.go

@@ -0,0 +1,117 @@
+// 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 v1
+
+import (
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type AuthScope string
+
+const (
+	AuthScopeHeartBeats   AuthScope = "HeartBeats"
+	AuthScopeNewWorkConns AuthScope = "NewWorkConns"
+)
+
+type AuthMethod string
+
+const (
+	AuthMethodToken AuthMethod = "token"
+	AuthMethodOIDC  AuthMethod = "oidc"
+)
+
+// QUIC protocol options
+type QUICOptions struct {
+	KeepalivePeriod    int `json:"keepalivePeriod,omitempty"`
+	MaxIdleTimeout     int `json:"maxIdleTimeout,omitempty"`
+	MaxIncomingStreams int `json:"maxIncomingStreams,omitempty"`
+}
+
+func (c *QUICOptions) Complete() {
+	c.KeepalivePeriod = util.EmptyOr(c.KeepalivePeriod, 10)
+	c.MaxIdleTimeout = util.EmptyOr(c.MaxIdleTimeout, 30)
+	c.MaxIncomingStreams = util.EmptyOr(c.MaxIncomingStreams, 100000)
+}
+
+type WebServerConfig struct {
+	// This is the network address to bind on for serving the web interface and API.
+	// By default, this value is "127.0.0.1".
+	Addr string `json:"addr,omitempty"`
+	// Port specifies the port for the web server to listen on. If this
+	// value is 0, the admin server will not be started.
+	Port int `json:"port,omitempty"`
+	// User specifies the username that the web server will use for login.
+	User string `json:"user,omitempty"`
+	// Password specifies the password that the admin server will use for login.
+	Password string `json:"password,omitempty"`
+	// AssetsDir specifies the local directory that the admin server will load
+	// resources from. If this value is "", assets will be loaded from the
+	// bundled executable using embed package.
+	AssetsDir string `json:"assetsDir,omitempty"`
+	// Enable golang pprof handlers.
+	PprofEnable bool `json:"pprofEnable,omitempty"`
+	// Enable TLS if TLSConfig is not nil.
+	TLS *TLSConfig `json:"tls,omitempty"`
+}
+
+func (c *WebServerConfig) Complete() {
+	c.Addr = util.EmptyOr(c.Addr, "127.0.0.1")
+}
+
+type TLSConfig struct {
+	// CertPath specifies the path of the cert file that client will load.
+	CertFile string `json:"certFile,omitempty"`
+	// KeyPath specifies the path of the secret key file that client will load.
+	KeyFile string `json:"keyFile,omitempty"`
+	// TrustedCaFile specifies the path of the trusted ca file that will load.
+	TrustedCaFile string `json:"trustedCaFile,omitempty"`
+	// ServerName specifies the custom server name of tls certificate. By
+	// default, server name if same to ServerAddr.
+	ServerName string `json:"serverName,omitempty"`
+}
+
+type LogConfig struct {
+	// This is destination where frp should wirte the logs.
+	// If "console" is used, logs will be printed to stdout, otherwise,
+	// logs will be written to the specified file.
+	// By default, this value is "console".
+	To string `json:"to,omitempty"`
+	// Level specifies the minimum log level. Valid values are "trace",
+	// "debug", "info", "warn", and "error". By default, this value is "info".
+	Level string `json:"level,omitempty"`
+	// MaxDays specifies the maximum number of days to store log information
+	// before deletion.
+	MaxDays int64 `json:"maxDays"`
+	// DisablePrintColor disables log colors when log.to is "console".
+	DisablePrintColor bool `json:"disablePrintColor,omitempty"`
+}
+
+func (c *LogConfig) Complete() {
+	c.To = util.EmptyOr(c.To, "console")
+	c.Level = util.EmptyOr(c.Level, "info")
+	c.MaxDays = util.EmptyOr(c.MaxDays, 3)
+}
+
+type HTTPPluginOptions struct {
+	Name      string   `json:"name"`
+	Addr      string   `json:"addr"`
+	Path      string   `json:"path"`
+	Ops       []string `json:"ops"`
+	TLSVerify bool     `json:"tls_verify,omitempty"`
+}
+
+type HeaderOperations struct {
+	Set map[string]string `json:"set,omitempty"`
+}

+ 118 - 0
pkg/config/v1/plugin.go

@@ -0,0 +1,118 @@
+// 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 v1
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+type ClientPluginOptions interface{}
+
+type TypedClientPluginOptions struct {
+	Type string `json:"type"`
+	ClientPluginOptions
+}
+
+func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
+	if len(b) == 4 && string(b) == "null" {
+		return errors.New("type is required")
+	}
+
+	typeStruct := struct {
+		Type string `json:"type"`
+	}{}
+	if err := json.Unmarshal(b, &typeStruct); err != nil {
+		return err
+	}
+
+	c.Type = typeStruct.Type
+
+	v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
+	if !ok {
+		return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
+	}
+	options := reflect.New(v).Interface().(ClientPluginOptions)
+	if err := json.Unmarshal(b, options); err != nil {
+		return err
+	}
+	c.ClientPluginOptions = options
+	return nil
+}
+
+const (
+	PluginHTTP2HTTPS       = "http2https"
+	PluginHTTPProxy        = "http_proxy"
+	PluginHTTPS2HTTP       = "https2http"
+	PluginHTTPS2HTTPS      = "https2https"
+	PluginSocks5           = "socks5"
+	PluginStaticFile       = "static_file"
+	PluginUnixDomainSocket = "unix_domain_socket"
+)
+
+var clientPluginOptionsTypeMap = map[string]reflect.Type{
+	PluginHTTP2HTTPS:       reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
+	PluginHTTPProxy:        reflect.TypeOf(HTTPProxyPluginOptions{}),
+	PluginHTTPS2HTTP:       reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
+	PluginHTTPS2HTTPS:      reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
+	PluginSocks5:           reflect.TypeOf(Socks5PluginOptions{}),
+	PluginStaticFile:       reflect.TypeOf(StaticFilePluginOptions{}),
+	PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
+}
+
+type HTTP2HTTPSPluginOptions struct {
+	LocalAddr         string           `json:"localAddr,omitempty"`
+	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
+	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+}
+
+type HTTPProxyPluginOptions struct {
+	HTTPUser     string `json:"httpUser,omitempty"`
+	HTTPPassword string `json:"httpPassword,omitempty"`
+}
+
+type HTTPS2HTTPPluginOptions struct {
+	LocalAddr         string           `json:"localAddr,omitempty"`
+	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
+	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	CrtPath           string           `json:"crtPath,omitempty"`
+	KeyPath           string           `json:"keyPath,omitempty"`
+}
+
+type HTTPS2HTTPSPluginOptions struct {
+	LocalAddr         string           `json:"localAddr,omitempty"`
+	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
+	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	CrtPath           string           `json:"crtPath,omitempty"`
+	KeyPath           string           `json:"keyPath,omitempty"`
+}
+
+type Socks5PluginOptions struct {
+	Username string `json:"username,omitempty"`
+	Password string `json:"password,omitempty"`
+}
+
+type StaticFilePluginOptions struct {
+	LocalPath    string `json:"localPath,omitempty"`
+	StripPrefix  string `json:"stripPrefix,omitempty"`
+	HTTPUser     string `json:"httpUser,omitempty"`
+	HTTPPassword string `json:"httpPassword,omitempty"`
+}
+
+type UnixDomainSocketPluginOptions struct {
+	UnixPath string `json:"unixPath,omitempty"`
+}

+ 438 - 0
pkg/config/v1/proxy.go

@@ -0,0 +1,438 @@
+// 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 v1
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/config/types"
+	"github.com/fatedier/frp/pkg/msg"
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type ProxyTransport struct {
+	// UseEncryption controls whether or not communication with the server will
+	// be encrypted. Encryption is done using the tokens supplied in the server
+	// and client configuration.
+	UseEncryption bool `json:"useEncryption,omitempty"`
+	// UseCompression controls whether or not communication with the server
+	// will be compressed.
+	UseCompression bool `json:"useCompression,omitempty"`
+	// BandwidthLimit limit the bandwidth
+	// 0 means no limit
+	BandwidthLimit types.BandwidthQuantity `json:"bandwidthLimit,omitempty"`
+	// BandwidthLimitMode specifies whether to limit the bandwidth on the
+	// client or server side. Valid values include "client" and "server".
+	// By default, this value is "client".
+	BandwidthLimitMode string `json:"bandwidthLimitMode,omitempty"`
+	// ProxyProtocolVersion specifies which protocol version to use. Valid
+	// values include "v1", "v2", and "". If the value is "", a protocol
+	// version will be automatically selected. By default, this value is "".
+	ProxyProtocolVersion string `json:"proxyProtocolVersion,omitempty"`
+}
+
+type LoadBalancerConfig struct {
+	// Group specifies which group the is a part of. The server will use
+	// this information to load balance proxies in the same group. If the value
+	// is "", this will not be in a group.
+	Group string `json:"group"`
+	// GroupKey specifies a group key, which should be the same among proxies
+	// of the same group.
+	GroupKey string `json:"groupKey,omitempty"`
+}
+
+type ProxyBackend struct {
+	// LocalIP specifies the IP address or host name of the backend.
+	LocalIP string `json:"localIP,omitempty"`
+	// LocalPort specifies the port of the backend.
+	LocalPort int `json:"localPort,omitempty"`
+
+	// Plugin specifies what plugin should be used for handling connections. If this value
+	// is set, the LocalIP and LocalPort values will be ignored.
+	Plugin TypedClientPluginOptions `json:"plugin,omitempty"`
+}
+
+// HealthCheckConfig configures health checking. This can be useful for load
+// balancing purposes to detect and remove proxies to failing services.
+type HealthCheckConfig struct {
+	// Type specifies what protocol to use for health checking.
+	// Valid values include "tcp", "http", and "". If this value is "", health
+	// checking will not be performed.
+	//
+	// If the type is "tcp", a connection will be attempted to the target
+	// server. If a connection cannot be established, the health check fails.
+	//
+	// If the type is "http", a GET request will be made to the endpoint
+	// specified by HealthCheckURL. If the response is not a 200, the health
+	// check fails.
+	Type string `json:"type"` // tcp | http
+	// TimeoutSeconds specifies the number of seconds to wait for a health
+	// check attempt to connect. If the timeout is reached, this counts as a
+	// health check failure. By default, this value is 3.
+	TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
+	// MaxFailed specifies the number of allowed failures before the
+	// is stopped. By default, this value is 1.
+	MaxFailed int `json:"maxFailed,omitempty"`
+	// IntervalSeconds specifies the time in seconds between health
+	// checks. By default, this value is 10.
+	IntervalSeconds int `json:"intervalSeconds"`
+	// Path specifies the path to send health checks to if the
+	// health check type is "http".
+	Path string `json:"path,omitempty"`
+}
+
+type DomainConfig struct {
+	CustomDomains []string `json:"customDomains,omitempty"`
+	SubDomain     string   `json:"subdomain,omitempty"`
+}
+
+type ProxyBaseConfig struct {
+	Name      string         `json:"name"`
+	Type      string         `json:"type"`
+	Transport ProxyTransport `json:"transport,omitempty"`
+	// metadata info for each proxy
+	Metadatas    map[string]string  `json:"metadatas,omitempty"`
+	LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"`
+	HealthCheck  HealthCheckConfig  `json:"healthCheck,omitempty"`
+	ProxyBackend
+}
+
+func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
+	return c
+}
+
+func (c *ProxyBaseConfig) Complete(namePrefix string) {
+	c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
+	c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
+	c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
+}
+
+func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
+	m.ProxyName = c.Name
+	m.ProxyType = c.Type
+	m.UseEncryption = c.Transport.UseEncryption
+	m.UseCompression = c.Transport.UseCompression
+	m.BandwidthLimit = c.Transport.BandwidthLimit.String()
+	// leave it empty for default value to reduce traffic
+	if c.Transport.BandwidthLimitMode != "client" {
+		m.BandwidthLimitMode = c.Transport.BandwidthLimitMode
+	}
+	m.Group = c.LoadBalancer.Group
+	m.GroupKey = c.LoadBalancer.GroupKey
+	m.Metas = c.Metadatas
+}
+
+func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.Name = m.ProxyName
+	c.Type = m.ProxyType
+	c.Transport.UseEncryption = m.UseEncryption
+	c.Transport.UseCompression = m.UseCompression
+	if m.BandwidthLimit != "" {
+		c.Transport.BandwidthLimit, _ = types.NewBandwidthQuantity(m.BandwidthLimit)
+	}
+	if m.BandwidthLimitMode != "" {
+		c.Transport.BandwidthLimitMode = m.BandwidthLimitMode
+	}
+	c.LoadBalancer.Group = m.Group
+	c.LoadBalancer.GroupKey = m.GroupKey
+	c.Metadatas = m.Metas
+}
+
+type TypedProxyConfig struct {
+	Type string `json:"type"`
+	ProxyConfigurer
+}
+
+func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
+	if len(b) == 4 && string(b) == "null" {
+		return errors.New("type is required")
+	}
+
+	typeStruct := struct {
+		Type string `json:"type"`
+	}{}
+	if err := json.Unmarshal(b, &typeStruct); err != nil {
+		return err
+	}
+
+	c.Type = typeStruct.Type
+	configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type))
+	if configurer == nil {
+		return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
+	}
+	if err := json.Unmarshal(b, configurer); err != nil {
+		return err
+	}
+	c.ProxyConfigurer = configurer
+	return nil
+}
+
+type ProxyConfigurer interface {
+	Complete(namePrefix string)
+	GetBaseConfig() *ProxyBaseConfig
+	// MarshalToMsg marshals this config into a msg.NewProxy message. This
+	// function will be called on the frpc side.
+	MarshalToMsg(*msg.NewProxy)
+	// UnmarshalFromMsg unmarshals a msg.NewProxy message into this config.
+	// This function will be called on the frps side.
+	UnmarshalFromMsg(*msg.NewProxy)
+}
+
+type ProxyType string
+
+const (
+	ProxyTypeTCP    ProxyType = "tcp"
+	ProxyTypeUDP    ProxyType = "udp"
+	ProxyTypeTCPMUX ProxyType = "tcpmux"
+	ProxyTypeHTTP   ProxyType = "http"
+	ProxyTypeHTTPS  ProxyType = "https"
+	ProxyTypeSTCP   ProxyType = "stcp"
+	ProxyTypeXTCP   ProxyType = "xtcp"
+	ProxyTypeSUDP   ProxyType = "sudp"
+)
+
+var proxyConfigTypeMap = map[ProxyType]reflect.Type{
+	ProxyTypeTCP:    reflect.TypeOf(TCPProxyConfig{}),
+	ProxyTypeUDP:    reflect.TypeOf(UDPProxyConfig{}),
+	ProxyTypeHTTP:   reflect.TypeOf(HTTPProxyConfig{}),
+	ProxyTypeHTTPS:  reflect.TypeOf(HTTPSProxyConfig{}),
+	ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
+	ProxyTypeSTCP:   reflect.TypeOf(STCPProxyConfig{}),
+	ProxyTypeXTCP:   reflect.TypeOf(XTCPProxyConfig{}),
+	ProxyTypeSUDP:   reflect.TypeOf(SUDPProxyConfig{}),
+}
+
+func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
+	v, ok := proxyConfigTypeMap[proxyType]
+	if !ok {
+		return nil
+	}
+	return reflect.New(v).Interface().(ProxyConfigurer)
+}
+
+var _ ProxyConfigurer = &TCPProxyConfig{}
+
+type TCPProxyConfig struct {
+	ProxyBaseConfig
+
+	RemotePort int `json:"remotePort,omitempty"`
+}
+
+func (c *TCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.RemotePort = c.RemotePort
+}
+
+func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.RemotePort = m.RemotePort
+}
+
+var _ ProxyConfigurer = &UDPProxyConfig{}
+
+type UDPProxyConfig struct {
+	ProxyBaseConfig
+
+	RemotePort int `json:"remotePort,omitempty"`
+}
+
+func (c *UDPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.RemotePort = c.RemotePort
+}
+
+func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.RemotePort = m.RemotePort
+}
+
+var _ ProxyConfigurer = &HTTPProxyConfig{}
+
+type HTTPProxyConfig struct {
+	ProxyBaseConfig
+	DomainConfig
+
+	Locations         []string         `json:"locations,omitempty"`
+	HTTPUser          string           `json:"httpUser,omitempty"`
+	HTTPPassword      string           `json:"httpPassword,omitempty"`
+	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
+	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	RouteByHTTPUser   string           `json:"routeByHTTPUser,omitempty"`
+}
+
+func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.CustomDomains = c.CustomDomains
+	m.SubDomain = c.SubDomain
+	m.Locations = c.Locations
+	m.HostHeaderRewrite = c.HostHeaderRewrite
+	m.HTTPUser = c.HTTPUser
+	m.HTTPPwd = c.HTTPPassword
+	m.Headers = c.RequestHeaders.Set
+	m.RouteByHTTPUser = c.RouteByHTTPUser
+}
+
+func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.CustomDomains = m.CustomDomains
+	c.SubDomain = m.SubDomain
+	c.Locations = m.Locations
+	c.HostHeaderRewrite = m.HostHeaderRewrite
+	c.HTTPUser = m.HTTPUser
+	c.HTTPPassword = m.HTTPPwd
+	c.RequestHeaders.Set = m.Headers
+	c.RouteByHTTPUser = m.RouteByHTTPUser
+}
+
+var _ ProxyConfigurer = &HTTPSProxyConfig{}
+
+type HTTPSProxyConfig struct {
+	ProxyBaseConfig
+	DomainConfig
+}
+
+func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.CustomDomains = c.CustomDomains
+	m.SubDomain = c.SubDomain
+}
+
+func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.CustomDomains = m.CustomDomains
+	c.SubDomain = m.SubDomain
+}
+
+type TCPMultiplexerType string
+
+const (
+	TCPMultiplexerHTTPConnect TCPMultiplexerType = "httpconnect"
+)
+
+var _ ProxyConfigurer = &TCPMuxProxyConfig{}
+
+type TCPMuxProxyConfig struct {
+	ProxyBaseConfig
+	DomainConfig
+
+	HTTPUser        string `json:"httpUser,omitempty"`
+	HTTPPassword    string `json:"httpPassword,omitempty"`
+	RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"`
+	Multiplexer     string `json:"multiplexer,omitempty"`
+}
+
+func (c *TCPMuxProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.CustomDomains = c.CustomDomains
+	m.SubDomain = c.SubDomain
+	m.Multiplexer = c.Multiplexer
+	m.HTTPUser = c.HTTPUser
+	m.HTTPPwd = c.HTTPPassword
+	m.RouteByHTTPUser = c.RouteByHTTPUser
+}
+
+func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.CustomDomains = m.CustomDomains
+	c.SubDomain = m.SubDomain
+	c.Multiplexer = m.Multiplexer
+	c.HTTPUser = m.HTTPUser
+	c.HTTPPassword = m.HTTPPwd
+	c.RouteByHTTPUser = m.RouteByHTTPUser
+}
+
+var _ ProxyConfigurer = &STCPProxyConfig{}
+
+type STCPProxyConfig struct {
+	ProxyBaseConfig
+
+	Secretkey  string   `json:"secretKey,omitempty"`
+	AllowUsers []string `json:"allowUsers,omitempty"`
+}
+
+func (c *STCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.Sk = c.Secretkey
+	m.AllowUsers = c.AllowUsers
+}
+
+func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.Secretkey = m.Sk
+	c.AllowUsers = m.AllowUsers
+}
+
+var _ ProxyConfigurer = &XTCPProxyConfig{}
+
+type XTCPProxyConfig struct {
+	ProxyBaseConfig
+
+	Secretkey  string   `json:"secretKey,omitempty"`
+	AllowUsers []string `json:"allowUsers,omitempty"`
+}
+
+func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.Sk = c.Secretkey
+	m.AllowUsers = c.AllowUsers
+}
+
+func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.Secretkey = m.Sk
+	c.AllowUsers = m.AllowUsers
+}
+
+var _ ProxyConfigurer = &SUDPProxyConfig{}
+
+type SUDPProxyConfig struct {
+	ProxyBaseConfig
+
+	Secretkey  string   `json:"secretKey,omitempty"`
+	AllowUsers []string `json:"allowUsers,omitempty"`
+}
+
+func (c *SUDPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.MarshalToMsg(m)
+
+	m.Sk = c.Secretkey
+	m.AllowUsers = c.AllowUsers
+}
+
+func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
+	c.ProxyBaseConfig.UnmarshalFromMsg(m)
+
+	c.Secretkey = m.Sk
+	c.AllowUsers = m.AllowUsers
+}

+ 49 - 0
pkg/config/v1/proxy_test.go

@@ -0,0 +1,49 @@
+// 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 v1
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestUnmarshalTypedProxyConfig(t *testing.T) {
+	require := require.New(t)
+	proxyConfigs := struct {
+		Proxies []TypedProxyConfig `json:"proxies,omitempty"`
+	}{}
+
+	strs := `{
+		"proxies": [
+			{
+				"type": "tcp",
+				"localPort": 22,
+				"remotePort": 6000
+			},
+			{
+				"type": "http",
+				"localPort": 80,
+				"customDomains": ["www.example.com"]
+			}
+		]
+	}`
+	err := json.Unmarshal([]byte(strs), &proxyConfigs)
+	require.NoError(err)
+
+	require.IsType(&TCPProxyConfig{}, proxyConfigs.Proxies[0].ProxyConfigurer)
+	require.IsType(&HTTPProxyConfig{}, proxyConfigs.Proxies[1].ProxyConfigurer)
+}

+ 190 - 0
pkg/config/v1/server.go

@@ -0,0 +1,190 @@
+// 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 v1
+
+import (
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/config/types"
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type ServerConfig struct {
+	APIMetadata
+
+	Auth AuthServerConfig `json:"auth,omitempty"`
+	// BindAddr specifies the address that the server binds to. By default,
+	// this value is "0.0.0.0".
+	BindAddr string `json:"bindAddr,omitempty"`
+	// BindPort specifies the port that the server listens on. By default, this
+	// value is 7000.
+	BindPort int `json:"bindPort,omitempty"`
+	// KCPBindPort specifies the KCP port that the server listens on. If this
+	// value is 0, the server will not listen for KCP connections.
+	KCPBindPort int `json:"kcpBindPort,omitempty"`
+	// QUICBindPort specifies the QUIC port that the server listens on.
+	// Set this value to 0 will disable this feature.
+	QUICBindPort int `json:"quicBindPort,omitempty"`
+	// ProxyBindAddr specifies the address that the proxy binds to. This value
+	// may be the same as BindAddr.
+	ProxyBindAddr string `json:"proxyBindAddr,omitempty"`
+	// VhostHTTPPort specifies the port that the server listens for HTTP Vhost
+	// requests. If this value is 0, the server will not listen for HTTP
+	// requests.
+	VhostHTTPPort int `json:"vhostHTTPPort,omitempty"`
+	// VhostHTTPTimeout specifies the response header timeout for the Vhost
+	// HTTP server, in seconds. By default, this value is 60.
+	VhostHTTPTimeout int64 `json:"vhostHTTPTimeout,omitempty"`
+	// VhostHTTPSPort specifies the port that the server listens for HTTPS
+	// Vhost requests. If this value is 0, the server will not listen for HTTPS
+	// requests.
+	VhostHTTPSPort int `json:"vhostHTTPSPort,omitempty"`
+	// TCPMuxHTTPConnectPort specifies the port that the server listens for TCP
+	// HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
+	// requests on one single port. If it's not - it will listen on this value for
+	// HTTP CONNECT requests.
+	TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort,omitempty"`
+	// If TCPMuxPassthrough is true, frps won't do any update on traffic.
+	TCPMuxPassthrough bool `json:"tcpmuxPassthrough,omitempty"`
+	// SubDomainHost specifies the domain that will be attached to sub-domains
+	// requested by the client when using Vhost proxying. For example, if this
+	// value is set to "frps.com" and the client requested the subdomain
+	// "test", the resulting URL would be "test.frps.com".
+	SubDomainHost string `json:"subDomainHost,omitempty"`
+	// Custom404Page specifies a path to a custom 404 page to display. If this
+	// value is "", a default page will be displayed.
+	Custom404Page string `json:"custom404Page,omitempty"`
+
+	WebServer WebServerConfig `json:"webServer,omitempty"`
+	// EnablePrometheus will export prometheus metrics on webserver address
+	// in /metrics api.
+	EnablePrometheus bool `json:"enablePrometheus,omitempty"`
+
+	Log LogConfig `json:"log,omitempty"`
+
+	Transport ServerTransportConfig `json:"transport,omitempty"`
+
+	// DetailedErrorsToClient defines whether to send the specific error (with
+	// debug info) to frpc. By default, this value is true.
+	DetailedErrorsToClient *bool `json:"detailedErrorsToClient,omitempty"`
+	// MaxPortsPerClient specifies the maximum number of ports a single client
+	// may proxy to. If this value is 0, no limit will be applied.
+	MaxPortsPerClient int64 `json:"maxPortsPerClient,omitempty"`
+	// UserConnTimeout specifies the maximum time to wait for a work
+	// connection. By default, this value is 10.
+	UserConnTimeout int64 `json:"userConnTimeout,omitempty"`
+	// UDPPacketSize specifies the UDP packet size
+	// By default, this value is 1500
+	UDPPacketSize int64 `json:"udpPacketSize,omitempty"`
+	// NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data.
+	NatHoleAnalysisDataReserveHours int64 `json:"natholeAnalysisDataReserveHours,omitempty"`
+
+	AllowPorts []types.PortsRange `json:"allowPorts,omitempty"`
+
+	HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
+}
+
+func (c *ServerConfig) Complete() {
+	c.Auth.Complete()
+	c.Log.Complete()
+	c.Transport.Complete()
+	c.WebServer.Complete()
+
+	c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
+	c.BindPort = util.EmptyOr(c.BindPort, 7000)
+	if c.ProxyBindAddr == "" {
+		c.ProxyBindAddr = c.BindAddr
+	}
+
+	if c.WebServer.Port > 0 {
+		c.WebServer.Addr = util.EmptyOr(c.WebServer.Addr, "0.0.0.0")
+	}
+
+	c.VhostHTTPTimeout = util.EmptyOr(c.VhostHTTPTimeout, 60)
+	c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true))
+	c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10)
+	c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
+	c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24)
+}
+
+type AuthServerConfig struct {
+	Method           AuthMethod           `json:"method,omitempty"`
+	AdditionalScopes []AuthScope          `json:"additionalScopes,omitempty"`
+	Token            string               `json:"token,omitempty"`
+	OIDC             AuthOIDCServerConfig `json:"oidc,omitempty"`
+}
+
+func (c *AuthServerConfig) Complete() {
+	c.Method = util.EmptyOr(c.Method, "token")
+}
+
+type AuthOIDCServerConfig struct {
+	// Issuer specifies the issuer to verify OIDC tokens with. This issuer
+	// will be used to load public keys to verify signature and will be compared
+	// with the issuer claim in the OIDC token.
+	Issuer string `json:"issuer,omitempty"`
+	// Audience specifies the audience OIDC tokens should contain when validated.
+	// If this value is empty, audience ("client ID") verification will be skipped.
+	Audience string `json:"audience,omitempty"`
+	// SkipExpiryCheck specifies whether to skip checking if the OIDC token is
+	// expired.
+	SkipExpiryCheck bool `json:"skipExpiryCheck,omitempty"`
+	// SkipIssuerCheck specifies whether to skip checking if the OIDC token's
+	// issuer claim matches the issuer specified in OidcIssuer.
+	SkipIssuerCheck bool `json:"skipIssuerCheck,omitempty"`
+}
+
+type ServerTransportConfig struct {
+	// TCPMux toggles TCP stream multiplexing. This allows multiple requests
+	// from a client to share a single TCP connection. By default, this value
+	// is true.
+	TCPMux *bool `json:"tcpMux,omitempty"`
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
+	TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
+	// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
+	// If negative, keep-alive probes are disabled.
+	TCPKeepAlive int64 `json:"tcpKeepalive,omitempty"`
+	// MaxPoolCount specifies the maximum pool size for each proxy. By default,
+	// this value is 5.
+	MaxPoolCount int64 `json:"maxPoolCount,omitempty"`
+	// HeartBeatTimeout specifies the maximum time to wait for a heartbeat
+	// before terminating the connection. It is not recommended to change this
+	// value. By default, this value is 90. Set negative value to disable it.
+	HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"`
+	// QUIC options.
+	QUIC QUICOptions `json:"quic,omitempty"`
+	// TLS specifies TLS settings for the connection from the client.
+	TLS TLSServerConfig `json:"tls,omitempty"`
+}
+
+func (c *ServerTransportConfig) Complete() {
+	c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
+	c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
+	c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
+	c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
+	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	c.QUIC.Complete()
+	if c.TLS.TrustedCaFile != "" {
+		c.TLS.Force = true
+	}
+}
+
+type TLSServerConfig struct {
+	// Force specifies whether to only accept TLS-encrypted connections.
+	Force bool `json:"force,omitempty"`
+
+	TLSConfig
+}

+ 15 - 24
pkg/consts/consts.go → pkg/config/v1/server_test.go

@@ -1,4 +1,4 @@
-// Copyright 2016 fatedier, fatedier@gmail.com
+// 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.
@@ -12,30 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package consts
+package v1
 
-var (
-	// proxy status
-	Idle    = "idle"
-	Working = "working"
-	Closed  = "closed"
-	Online  = "online"
-	Offline = "offline"
+import (
+	"testing"
 
-	// proxy type
-	TCPProxy    = "tcp"
-	UDPProxy    = "udp"
-	TCPMuxProxy = "tcpmux"
-	HTTPProxy   = "http"
-	HTTPSProxy  = "https"
-	STCPProxy   = "stcp"
-	XTCPProxy   = "xtcp"
-	SUDPProxy   = "sudp"
+	"github.com/samber/lo"
+	"github.com/stretchr/testify/require"
+)
 
-	// authentication method
-	TokenAuthMethod = "token"
-	OidcAuthMethod  = "oidc"
+func TestServerConfigComplete(t *testing.T) {
+	require := require.New(t)
+	c := &ServerConfig{}
+	c.Complete()
 
-	// TCP multiplexer
-	HTTPConnectTCPMultiplexer = "httpconnect"
-)
+	require.EqualValues("token", c.Auth.Method)
+	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
+	require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
+}

+ 105 - 0
pkg/config/v1/validation/client.go

@@ -0,0 +1,105 @@
+// 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 validation
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
+	var (
+		warnings Warning
+		errs     error
+	)
+	if !lo.Contains(SupportedAuthMethods, c.Auth.Method) {
+		errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
+	}
+	if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
+		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
+	}
+
+	if err := validateLogConfig(&c.Log); err != nil {
+		errs = AppendError(errs, err)
+	}
+
+	if err := validateWebServerConfig(&c.WebServer); err != nil {
+		errs = AppendError(errs, err)
+	}
+
+	if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
+		if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
+			errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
+		}
+	}
+
+	if !lo.FromPtr(c.Transport.TLS.Enable) {
+		checkTLSConfig := func(name string, value string) Warning {
+			if value != "" {
+				return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
+			}
+			return nil
+		}
+
+		warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
+		warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
+		warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
+	}
+
+	if !lo.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
+		errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
+	}
+
+	for _, f := range c.IncludeConfigFiles {
+		absDir, err := filepath.Abs(filepath.Dir(f))
+		if err != nil {
+			errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
+			continue
+		}
+		if _, err := os.Stat(absDir); os.IsNotExist(err) {
+			errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
+		}
+	}
+	return warnings, errs
+}
+
+func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
+	var warnings Warning
+	if c != nil {
+		warning, err := ValidateClientCommonConfig(c)
+		warnings = AppendError(warnings, warning)
+		if err != nil {
+			return warnings, err
+		}
+	}
+
+	for _, c := range pxyCfgs {
+		if err := ValidateProxyConfigurerForClient(c); err != nil {
+			return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err)
+		}
+	}
+
+	for _, c := range visitorCfgs {
+		if err := ValidateVisitorConfigurer(c); err != nil {
+			return warnings, fmt.Errorf("visitor %s: %v", c.GetBaseConfig().Name, err)
+		}
+	}
+	return warnings, nil
+}

+ 51 - 0
pkg/config/v1/validation/common.go

@@ -0,0 +1,51 @@
+// 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 validation
+
+import (
+	"fmt"
+
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func validateWebServerConfig(c *v1.WebServerConfig) error {
+	if c.TLS != nil {
+		if c.TLS.CertFile == "" {
+			return fmt.Errorf("tls.certFile must be specified when tls is enabled")
+		}
+		if c.TLS.KeyFile == "" {
+			return fmt.Errorf("tls.keyFile must be specified when tls is enabled")
+		}
+	}
+
+	return ValidatePort(c.Port, "webServer.port")
+}
+
+// ValidatePort checks that the network port is in range
+func ValidatePort(port int, fieldPath string) error {
+	if 0 <= port && port <= 65535 {
+		return nil
+	}
+	return fmt.Errorf("%s: port number %d must be in the range 0..65535", fieldPath, port)
+}
+
+func validateLogConfig(c *v1.LogConfig) error {
+	if !lo.Contains(SupportedLogLevels, c.Level) {
+		return fmt.Errorf("invalid log level, optional values are %v", SupportedLogLevels)
+	}
+	return nil
+}

+ 72 - 0
pkg/config/v1/validation/plugin.go

@@ -0,0 +1,72 @@
+// 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 validation
+
+import (
+	"errors"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
+	switch v := c.(type) {
+	case *v1.HTTP2HTTPSPluginOptions:
+		return validateHTTP2HTTPSPluginOptions(v)
+	case *v1.HTTPS2HTTPPluginOptions:
+		return validateHTTPS2HTTPPluginOptions(v)
+	case *v1.HTTPS2HTTPSPluginOptions:
+		return validateHTTPS2HTTPSPluginOptions(v)
+	case *v1.StaticFilePluginOptions:
+		return validateStaticFilePluginOptions(v)
+	case *v1.UnixDomainSocketPluginOptions:
+		return validateUnixDomainSocketPluginOptions(v)
+	}
+	return nil
+}
+
+func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error {
+	if c.LocalAddr == "" {
+		return errors.New("localAddr is required")
+	}
+	return nil
+}
+
+func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error {
+	if c.LocalAddr == "" {
+		return errors.New("localAddr is required")
+	}
+	return nil
+}
+
+func validateHTTPS2HTTPSPluginOptions(c *v1.HTTPS2HTTPSPluginOptions) error {
+	if c.LocalAddr == "" {
+		return errors.New("localAddr is required")
+	}
+	return nil
+}
+
+func validateStaticFilePluginOptions(c *v1.StaticFilePluginOptions) error {
+	if c.LocalPath == "" {
+		return errors.New("localPath is required")
+	}
+	return nil
+}
+
+func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions) error {
+	if c.UnixPath == "" {
+		return errors.New("unixPath is required")
+	}
+	return nil
+}

+ 233 - 0
pkg/config/v1/validation/proxy.go

@@ -0,0 +1,233 @@
+// 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 validation
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
+	if c.Name == "" {
+		return errors.New("name should not be empty")
+	}
+
+	if !lo.Contains([]string{"", "v1", "v2"}, c.Transport.ProxyProtocolVersion) {
+		return fmt.Errorf("not support proxy protocol version: %s", c.Transport.ProxyProtocolVersion)
+	}
+
+	if !lo.Contains([]string{"client", "server"}, c.Transport.BandwidthLimitMode) {
+		return fmt.Errorf("bandwidth limit mode should be client or server")
+	}
+
+	if c.Plugin.Type == "" {
+		if err := ValidatePort(c.LocalPort, "localPort"); err != nil {
+			return fmt.Errorf("localPort: %v", err)
+		}
+	}
+
+	if !lo.Contains([]string{"", "tcp", "http"}, c.HealthCheck.Type) {
+		return fmt.Errorf("not support health check type: %s", c.HealthCheck.Type)
+	}
+	if c.HealthCheck.Type != "" {
+		if c.HealthCheck.Type == "http" &&
+			c.HealthCheck.Path == "" {
+			return fmt.Errorf("health check path should not be empty")
+		}
+	}
+
+	if c.Plugin.Type != "" {
+		if err := ValidateClientPluginOptions(c.Plugin.ClientPluginOptions); err != nil {
+			return fmt.Errorf("plugin %s: %v", c.Plugin.Type, err)
+		}
+	}
+	return nil
+}
+
+func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig, s *v1.ServerConfig) error {
+	return nil
+}
+
+func validateDomainConfigForClient(c *v1.DomainConfig) error {
+	if c.SubDomain == "" && len(c.CustomDomains) == 0 {
+		return errors.New("subdomain and custom domains should not be both empty")
+	}
+	return nil
+}
+
+func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
+	for _, domain := range c.CustomDomains {
+		if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
+			if strings.Contains(domain, s.SubDomainHost) {
+				return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
+			}
+		}
+	}
+
+	if c.SubDomain != "" {
+		if s.SubDomainHost == "" {
+			return errors.New("subdomain is not supported because this feature is not enabled in server")
+		}
+
+		if strings.Contains(c.SubDomain, ".") || strings.Contains(c.SubDomain, "*") {
+			return errors.New("'.' and '*' are not supported in subdomain")
+		}
+	}
+	return nil
+}
+
+func ValidateProxyConfigurerForClient(c v1.ProxyConfigurer) error {
+	base := c.GetBaseConfig()
+	if err := validateProxyBaseConfigForClient(base); err != nil {
+		return err
+	}
+
+	switch v := c.(type) {
+	case *v1.TCPProxyConfig:
+		return validateTCPProxyConfigForClient(v)
+	case *v1.UDPProxyConfig:
+		return validateUDPProxyConfigForClient(v)
+	case *v1.TCPMuxProxyConfig:
+		return validateTCPMuxProxyConfigForClient(v)
+	case *v1.HTTPProxyConfig:
+		return validateHTTPProxyConfigForClient(v)
+	case *v1.HTTPSProxyConfig:
+		return validateHTTPSProxyConfigForClient(v)
+	case *v1.STCPProxyConfig:
+		return validateSTCPProxyConfigForClient(v)
+	case *v1.XTCPProxyConfig:
+		return validateXTCPProxyConfigForClient(v)
+	case *v1.SUDPProxyConfig:
+		return validateSUDPProxyConfigForClient(v)
+	}
+	return errors.New("unknown proxy config type")
+}
+
+func validateTCPProxyConfigForClient(c *v1.TCPProxyConfig) error {
+	return nil
+}
+
+func validateUDPProxyConfigForClient(c *v1.UDPProxyConfig) error {
+	return nil
+}
+
+func validateTCPMuxProxyConfigForClient(c *v1.TCPMuxProxyConfig) error {
+	if err := validateDomainConfigForClient(&c.DomainConfig); err != nil {
+		return err
+	}
+
+	if !lo.Contains([]string{string(v1.TCPMultiplexerHTTPConnect)}, c.Multiplexer) {
+		return fmt.Errorf("not support multiplexer: %s", c.Multiplexer)
+	}
+	return nil
+}
+
+func validateHTTPProxyConfigForClient(c *v1.HTTPProxyConfig) error {
+	return validateDomainConfigForClient(&c.DomainConfig)
+}
+
+func validateHTTPSProxyConfigForClient(c *v1.HTTPSProxyConfig) error {
+	return validateDomainConfigForClient(&c.DomainConfig)
+}
+
+func validateSTCPProxyConfigForClient(c *v1.STCPProxyConfig) error {
+	return nil
+}
+
+func validateXTCPProxyConfigForClient(c *v1.XTCPProxyConfig) error {
+	return nil
+}
+
+func validateSUDPProxyConfigForClient(c *v1.SUDPProxyConfig) error {
+	return nil
+}
+
+func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error {
+	base := c.GetBaseConfig()
+	if err := validateProxyBaseConfigForServer(base, s); err != nil {
+		return err
+	}
+
+	switch v := c.(type) {
+	case *v1.TCPProxyConfig:
+		return validateTCPProxyConfigForServer(v, s)
+	case *v1.UDPProxyConfig:
+		return validateUDPProxyConfigForServer(v, s)
+	case *v1.TCPMuxProxyConfig:
+		return validateTCPMuxProxyConfigForServer(v, s)
+	case *v1.HTTPProxyConfig:
+		return validateHTTPProxyConfigForServer(v, s)
+	case *v1.HTTPSProxyConfig:
+		return validateHTTPSProxyConfigForServer(v, s)
+	case *v1.STCPProxyConfig:
+		return validateSTCPProxyConfigForServer(v, s)
+	case *v1.XTCPProxyConfig:
+		return validateXTCPProxyConfigForServer(v, s)
+	case *v1.SUDPProxyConfig:
+		return validateSUDPProxyConfigForServer(v, s)
+	default:
+		return errors.New("unknown proxy config type")
+	}
+}
+
+func validateTCPProxyConfigForServer(c *v1.TCPProxyConfig, s *v1.ServerConfig) error {
+	return nil
+}
+
+func validateUDPProxyConfigForServer(c *v1.UDPProxyConfig, s *v1.ServerConfig) error {
+	return nil
+}
+
+func validateTCPMuxProxyConfigForServer(c *v1.TCPMuxProxyConfig, s *v1.ServerConfig) error {
+	if c.Multiplexer == string(v1.TCPMultiplexerHTTPConnect) &&
+		s.TCPMuxHTTPConnectPort == 0 {
+		return fmt.Errorf("tcpmux with multiplexer httpconnect not supported because this feature is not enabled in server")
+	}
+
+	return validateDomainConfigForServer(&c.DomainConfig, s)
+}
+
+func validateHTTPProxyConfigForServer(c *v1.HTTPProxyConfig, s *v1.ServerConfig) error {
+	if s.VhostHTTPPort == 0 {
+		return fmt.Errorf("type [http] not supported when vhost http port is not set")
+	}
+
+	return validateDomainConfigForServer(&c.DomainConfig, s)
+}
+
+func validateHTTPSProxyConfigForServer(c *v1.HTTPSProxyConfig, s *v1.ServerConfig) error {
+	if s.VhostHTTPSPort == 0 {
+		return fmt.Errorf("type [https] not supported when vhost https port is not set")
+	}
+
+	return validateDomainConfigForServer(&c.DomainConfig, s)
+}
+
+func validateSTCPProxyConfigForServer(c *v1.STCPProxyConfig, s *v1.ServerConfig) error {
+	return nil
+}
+
+func validateXTCPProxyConfigForServer(c *v1.XTCPProxyConfig, s *v1.ServerConfig) error {
+	return nil
+}
+
+func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error {
+	return nil
+}

+ 58 - 0
pkg/config/v1/validation/server.go

@@ -0,0 +1,58 @@
+// 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 validation
+
+import (
+	"fmt"
+
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
+	var (
+		warnings Warning
+		errs     error
+	)
+	if !lo.Contains(SupportedAuthMethods, c.Auth.Method) {
+		errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
+	}
+	if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
+		errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
+	}
+
+	if err := validateLogConfig(&c.Log); err != nil {
+		errs = AppendError(errs, err)
+	}
+
+	if err := validateWebServerConfig(&c.WebServer); err != nil {
+		errs = AppendError(errs, err)
+	}
+
+	errs = AppendError(errs, ValidatePort(c.BindPort, "bindPort"))
+	errs = AppendError(errs, ValidatePort(c.KCPBindPort, "kcpBindPort"))
+	errs = AppendError(errs, ValidatePort(c.QUICBindPort, "quicBindPort"))
+	errs = AppendError(errs, ValidatePort(c.VhostHTTPPort, "vhostHTTPPort"))
+	errs = AppendError(errs, ValidatePort(c.VhostHTTPSPort, "vhostHTTPSPort"))
+	errs = AppendError(errs, ValidatePort(c.TCPMuxHTTPConnectPort, "tcpMuxHTTPConnectPort"))
+
+	for _, p := range c.HTTPPlugins {
+		if !lo.Every(SupportedHTTPPluginOps, p.Ops) {
+			errs = AppendError(errs, fmt.Errorf("invalid http plugin ops, optional values are %v", SupportedHTTPPluginOps))
+		}
+	}
+	return warnings, errs
+}

+ 68 - 0
pkg/config/v1/validation/validation.go

@@ -0,0 +1,68 @@
+// 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 validation
+
+import (
+	"errors"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+	splugin "github.com/fatedier/frp/pkg/plugin/server"
+)
+
+var (
+	SupportedTransportProtocols = []string{
+		"tcp",
+		"kcp",
+		"quic",
+		"websocket",
+		"wss",
+	}
+
+	SupportedAuthMethods = []v1.AuthMethod{
+		"token",
+		"oidc",
+	}
+
+	SupportedAuthAdditionalScopes = []v1.AuthScope{
+		"HeartBeats",
+		"NewWorkConns",
+	}
+
+	SupportedLogLevels = []string{
+		"trace",
+		"debug",
+		"info",
+		"warn",
+		"error",
+	}
+
+	SupportedHTTPPluginOps = []string{
+		splugin.OpLogin,
+		splugin.OpNewProxy,
+		splugin.OpCloseProxy,
+		splugin.OpPing,
+		splugin.OpNewWorkConn,
+		splugin.OpNewUserConn,
+	}
+)
+
+type Warning error
+
+func AppendError(err error, errs ...error) error {
+	if len(errs) == 0 {
+		return err
+	}
+	return errors.Join(append([]error{err}, errs...)...)
+}

+ 63 - 0
pkg/config/v1/validation/visitor.go

@@ -0,0 +1,63 @@
+// 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 validation
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/samber/lo"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func ValidateVisitorConfigurer(c v1.VisitorConfigurer) error {
+	base := c.GetBaseConfig()
+	if err := validateVisitorBaseConfig(base); err != nil {
+		return err
+	}
+
+	switch v := c.(type) {
+	case *v1.STCPVisitorConfig:
+	case *v1.SUDPVisitorConfig:
+	case *v1.XTCPVisitorConfig:
+		return validateXTCPVisitorConfig(v)
+	default:
+		return errors.New("unknown visitor config type")
+	}
+	return nil
+}
+
+func validateVisitorBaseConfig(c *v1.VisitorBaseConfig) error {
+	if c.Name == "" {
+		return errors.New("name is required")
+	}
+
+	if c.ServerName == "" {
+		return errors.New("server name is required")
+	}
+
+	if c.BindPort == 0 {
+		return errors.New("bind port is required")
+	}
+	return nil
+}
+
+func validateXTCPVisitorConfig(c *v1.XTCPVisitorConfig) error {
+	if !lo.Contains([]string{"kcp", "quic"}, c.Protocol) {
+		return fmt.Errorf("protocol should be kcp or quic")
+	}
+	return nil
+}

+ 162 - 0
pkg/config/v1/visitor.go

@@ -0,0 +1,162 @@
+// 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 v1
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type VisitorTransport struct {
+	UseEncryption  bool `json:"useEncryption,omitempty"`
+	UseCompression bool `json:"useCompression,omitempty"`
+}
+
+type VisitorBaseConfig struct {
+	Name      string           `json:"name"`
+	Type      string           `json:"type"`
+	Transport VisitorTransport `json:"transport,omitempty"`
+	SecretKey string           `json:"secretKey,omitempty"`
+	// if the server user is not set, it defaults to the current user
+	ServerUser string `json:"serverUser,omitempty"`
+	ServerName string `json:"serverName,omitempty"`
+	BindAddr   string `json:"bindAddr,omitempty"`
+	// 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 `json:"bindPort,omitempty"`
+}
+
+func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
+	return c
+}
+
+func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) {
+	if c.BindAddr == "" {
+		c.BindAddr = "127.0.0.1"
+	}
+
+	namePrefix := ""
+	if g.User != "" {
+		namePrefix = g.User + "."
+	}
+	c.Name = namePrefix + c.Name
+
+	if c.ServerUser != "" {
+		c.ServerName = c.ServerUser + "." + c.ServerName
+	} else {
+		c.ServerName = namePrefix + c.ServerName
+	}
+}
+
+type VisitorConfigurer interface {
+	Complete(*ClientCommonConfig)
+	GetBaseConfig() *VisitorBaseConfig
+}
+
+type VisitorType string
+
+const (
+	VisitorTypeSTCP VisitorType = "stcp"
+	VisitorTypeXTCP VisitorType = "xtcp"
+	VisitorTypeSUDP VisitorType = "sudp"
+)
+
+var visitorConfigTypeMap = map[VisitorType]reflect.Type{
+	VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
+	VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
+	VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
+}
+
+type TypedVisitorConfig struct {
+	Type string `json:"type"`
+	VisitorConfigurer
+}
+
+func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
+	if len(b) == 4 && string(b) == "null" {
+		return errors.New("type is required")
+	}
+
+	typeStruct := struct {
+		Type string `json:"type"`
+	}{}
+	if err := json.Unmarshal(b, &typeStruct); err != nil {
+		return err
+	}
+
+	c.Type = typeStruct.Type
+	configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type))
+	if configurer == nil {
+		return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
+	}
+	if err := json.Unmarshal(b, configurer); err != nil {
+		return err
+	}
+	c.VisitorConfigurer = configurer
+	return nil
+}
+
+func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
+	v, ok := visitorConfigTypeMap[t]
+	if !ok {
+		return nil
+	}
+	return reflect.New(v).Interface().(VisitorConfigurer)
+}
+
+var _ VisitorConfigurer = &STCPVisitorConfig{}
+
+type STCPVisitorConfig struct {
+	VisitorBaseConfig
+}
+
+var _ VisitorConfigurer = &SUDPVisitorConfig{}
+
+type SUDPVisitorConfig struct {
+	VisitorBaseConfig
+}
+
+var _ VisitorConfigurer = &XTCPVisitorConfig{}
+
+type XTCPVisitorConfig struct {
+	VisitorBaseConfig
+
+	Protocol          string `json:"protocol,omitempty"`
+	KeepTunnelOpen    bool   `json:"keepTunnelOpen,omitempty"`
+	MaxRetriesAnHour  int    `json:"maxRetriesAnHour,omitempty"`
+	MinRetryInterval  int    `json:"minRetryInterval,omitempty"`
+	FallbackTo        string `json:"fallbackTo,omitempty"`
+	FallbackTimeoutMs int    `json:"fallbackTimeoutMs,omitempty"`
+}
+
+func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
+	c.VisitorBaseConfig.Complete(g)
+
+	c.Protocol = util.EmptyOr(c.Protocol, "quic")
+	c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
+	c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
+	c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
+
+	if c.FallbackTo != "" {
+		c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo
+	}
+}

+ 0 - 112
pkg/config/visitor_test.go

@@ -1,112 +0,0 @@
-// Copyright 2020 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package config
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"gopkg.in/ini.v1"
-
-	"github.com/fatedier/frp/pkg/consts"
-)
-
-const testVisitorPrefix = "test."
-
-func Test_Visitor_Interface(_ *testing.T) {
-	for name := range visitorConfTypeMap {
-		DefaultVisitorConf(name)
-	}
-}
-
-func Test_Visitor_UnmarshalFromIni(t *testing.T) {
-	assert := assert.New(t)
-
-	testcases := []struct {
-		sname    string
-		source   []byte
-		expected VisitorConf
-	}{
-		{
-			sname: "secret_tcp_visitor",
-			source: []byte(`
-				[secret_tcp_visitor]
-				role = visitor
-				type = stcp
-				server_name = secret_tcp
-				sk = abcdefg
-				bind_addr = 127.0.0.1
-				bind_port = 9000
-				use_encryption = false
-				use_compression = false
-			`),
-			expected: &STCPVisitorConf{
-				BaseVisitorConf: BaseVisitorConf{
-					ProxyName:  testVisitorPrefix + "secret_tcp_visitor",
-					ProxyType:  consts.STCPProxy,
-					Role:       "visitor",
-					Sk:         "abcdefg",
-					ServerName: testVisitorPrefix + "secret_tcp",
-					BindAddr:   "127.0.0.1",
-					BindPort:   9000,
-				},
-			},
-		},
-		{
-			sname: "p2p_tcp_visitor",
-			source: []byte(`
-				[p2p_tcp_visitor]
-				role = visitor
-				type = xtcp
-				server_name = p2p_tcp
-				sk = abcdefg
-				bind_addr = 127.0.0.1
-				bind_port = 9001
-				use_encryption = false
-				use_compression = false
-			`),
-			expected: &XTCPVisitorConf{
-				BaseVisitorConf: BaseVisitorConf{
-					ProxyName:  testVisitorPrefix + "p2p_tcp_visitor",
-					ProxyType:  consts.XTCPProxy,
-					Role:       "visitor",
-					Sk:         "abcdefg",
-					ServerName: testProxyPrefix + "p2p_tcp",
-					BindAddr:   "127.0.0.1",
-					BindPort:   9001,
-				},
-				Protocol:          "quic",
-				MaxRetriesAnHour:  8,
-				MinRetryInterval:  90,
-				FallbackTimeoutMs: 1000,
-			},
-		},
-	}
-
-	for _, c := range testcases {
-		f, err := ini.LoadSources(testLoadOptions, c.source)
-		assert.NoError(err)
-
-		visitorType := f.Section(c.sname).Key("type").String()
-		assert.NotEmpty(visitorType)
-
-		actual := DefaultVisitorConf(visitorType)
-		assert.NotNil(actual)
-
-		err = actual.UnmarshalFromIni(testVisitorPrefix, c.sname, f.Section(c.sname))
-		assert.NoError(err)
-		assert.Equal(c.expected, actual)
-	}
-}

+ 13 - 34
pkg/plugin/client/http2https.go

@@ -16,55 +16,34 @@ package plugin
 
 import (
 	"crypto/tls"
-	"fmt"
 	"io"
 	"net"
 	"net/http"
 	"net/http/httputil"
-	"strings"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-const PluginHTTP2HTTPS = "http2https"
-
 func init() {
-	Register(PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin)
+	Register(v1.PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin)
 }
 
 type HTTP2HTTPSPlugin struct {
-	hostHeaderRewrite string
-	localAddr         string
-	headers           map[string]string
+	opts *v1.HTTP2HTTPSPluginOptions
 
 	l *Listener
 	s *http.Server
 }
 
-func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) {
-	localAddr := params["plugin_local_addr"]
-	hostHeaderRewrite := params["plugin_host_header_rewrite"]
-	headers := make(map[string]string)
-	for k, v := range params {
-		if !strings.HasPrefix(k, "plugin_header_") {
-			continue
-		}
-		if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
-			headers[k] = v
-		}
-	}
-
-	if localAddr == "" {
-		return nil, fmt.Errorf("plugin_local_addr is required")
-	}
+func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+	opts := options.(*v1.HTTP2HTTPSPluginOptions)
 
 	listener := NewProxyListener()
 
 	p := &HTTP2HTTPSPlugin{
-		localAddr:         localAddr,
-		hostHeaderRewrite: hostHeaderRewrite,
-		headers:           headers,
-		l:                 listener,
+		opts: opts,
+		l:    listener,
 	}
 
 	tr := &http.Transport{
@@ -74,11 +53,11 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) {
 	rp := &httputil.ReverseProxy{
 		Director: func(req *http.Request) {
 			req.URL.Scheme = "https"
-			req.URL.Host = p.localAddr
-			if p.hostHeaderRewrite != "" {
-				req.Host = p.hostHeaderRewrite
+			req.URL.Host = p.opts.LocalAddr
+			if p.opts.HostHeaderRewrite != "" {
+				req.Host = p.opts.HostHeaderRewrite
 			}
-			for k, v := range p.headers {
+			for k, v := range p.opts.RequestHeaders.Set {
 				req.Header.Set(k, v)
 			}
 		},
@@ -97,13 +76,13 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) {
 	return p, nil
 }
 
-func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 
 func (p *HTTP2HTTPSPlugin) Name() string {
-	return PluginHTTP2HTTPS
+	return v1.PluginHTTP2HTTPS
 }
 
 func (p *HTTP2HTTPSPlugin) Close() error {

+ 15 - 18
pkg/plugin/client/http_proxy.go

@@ -26,32 +26,29 @@ import (
 	libio "github.com/fatedier/golib/io"
 	libnet "github.com/fatedier/golib/net"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
 )
 
-const PluginHTTPProxy = "http_proxy"
-
 func init() {
-	Register(PluginHTTPProxy, NewHTTPProxyPlugin)
+	Register(v1.PluginHTTPProxy, NewHTTPProxyPlugin)
 }
 
 type HTTPProxy struct {
-	l          *Listener
-	s          *http.Server
-	AuthUser   string
-	AuthPasswd string
+	opts *v1.HTTPProxyPluginOptions
+
+	l *Listener
+	s *http.Server
 }
 
-func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) {
-	user := params["plugin_http_user"]
-	passwd := params["plugin_http_passwd"]
+func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+	opts := options.(*v1.HTTPProxyPluginOptions)
 	listener := NewProxyListener()
 
 	hp := &HTTPProxy{
-		l:          listener,
-		AuthUser:   user,
-		AuthPasswd: passwd,
+		l:    listener,
+		opts: opts,
 	}
 
 	hp.s = &http.Server{
@@ -65,10 +62,10 @@ func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) {
 }
 
 func (hp *HTTPProxy) Name() string {
-	return PluginHTTPProxy
+	return v1.PluginHTTPProxy
 }
 
-func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 
 	sc, rd := libnet.NewSharedConn(wrapConn)
@@ -162,7 +159,7 @@ func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) {
 }
 
 func (hp *HTTPProxy) Auth(req *http.Request) bool {
-	if hp.AuthUser == "" && hp.AuthPasswd == "" {
+	if hp.opts.HTTPUser == "" && hp.opts.HTTPPassword == "" {
 		return true
 	}
 
@@ -181,8 +178,8 @@ func (hp *HTTPProxy) Auth(req *http.Request) bool {
 		return false
 	}
 
-	if !util.ConstantTimeEqString(pair[0], hp.AuthUser) ||
-		!util.ConstantTimeEqString(pair[1], hp.AuthPasswd) {
+	if !util.ConstantTimeEqString(pair[0], hp.opts.HTTPUser) ||
+		!util.ConstantTimeEqString(pair[1], hp.opts.HTTPPassword) {
 		time.Sleep(200 * time.Millisecond)
 		return false
 	}

+ 15 - 42
pkg/plugin/client/https2http.go

@@ -21,67 +21,40 @@ import (
 	"net"
 	"net/http"
 	"net/http/httputil"
-	"strings"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-const PluginHTTPS2HTTP = "https2http"
-
 func init() {
-	Register(PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin)
+	Register(v1.PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin)
 }
 
 type HTTPS2HTTPPlugin struct {
-	crtPath           string
-	keyPath           string
-	hostHeaderRewrite string
-	localAddr         string
-	headers           map[string]string
+	opts *v1.HTTPS2HTTPPluginOptions
 
 	l *Listener
 	s *http.Server
 }
 
-func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) {
-	crtPath := params["plugin_crt_path"]
-	keyPath := params["plugin_key_path"]
-	localAddr := params["plugin_local_addr"]
-	hostHeaderRewrite := params["plugin_host_header_rewrite"]
-	headers := make(map[string]string)
-	for k, v := range params {
-		if !strings.HasPrefix(k, "plugin_header_") {
-			continue
-		}
-		if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
-			headers[k] = v
-		}
-	}
-
-	if localAddr == "" {
-		return nil, fmt.Errorf("plugin_local_addr is required")
-	}
-
+func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+	opts := options.(*v1.HTTPS2HTTPPluginOptions)
 	listener := NewProxyListener()
 
 	p := &HTTPS2HTTPPlugin{
-		crtPath:           crtPath,
-		keyPath:           keyPath,
-		localAddr:         localAddr,
-		hostHeaderRewrite: hostHeaderRewrite,
-		headers:           headers,
-		l:                 listener,
+		opts: opts,
+		l:    listener,
 	}
 
 	rp := &httputil.ReverseProxy{
 		Director: func(req *http.Request) {
 			req.URL.Scheme = "http"
-			req.URL.Host = p.localAddr
-			if p.hostHeaderRewrite != "" {
-				req.Host = p.hostHeaderRewrite
+			req.URL.Host = p.opts.LocalAddr
+			if p.opts.HostHeaderRewrite != "" {
+				req.Host = p.opts.HostHeaderRewrite
 			}
-			for k, v := range p.headers {
+			for k, v := range p.opts.RequestHeaders.Set {
 				req.Header.Set(k, v)
 			}
 		},
@@ -95,7 +68,7 @@ func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) {
 		tlsConfig *tls.Config
 		err       error
 	)
-	if crtPath != "" || keyPath != "" {
+	if opts.CrtPath != "" || opts.KeyPath != "" {
 		tlsConfig, err = p.genTLSConfig()
 	} else {
 		tlsConfig, err = transport.NewServerTLSConfig("", "", "")
@@ -113,7 +86,7 @@ func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) {
 }
 
 func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
-	cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath)
+	cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath)
 	if err != nil {
 		return nil, err
 	}
@@ -122,13 +95,13 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
 	return config, nil
 }
 
-func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 
 func (p *HTTPS2HTTPPlugin) Name() string {
-	return PluginHTTPS2HTTP
+	return v1.PluginHTTPS2HTTP
 }
 
 func (p *HTTPS2HTTPPlugin) Close() error {

+ 15 - 41
pkg/plugin/client/https2https.go

@@ -21,57 +21,31 @@ import (
 	"net"
 	"net/http"
 	"net/http/httputil"
-	"strings"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-const PluginHTTPS2HTTPS = "https2https"
-
 func init() {
-	Register(PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin)
+	Register(v1.PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin)
 }
 
 type HTTPS2HTTPSPlugin struct {
-	crtPath           string
-	keyPath           string
-	hostHeaderRewrite string
-	localAddr         string
-	headers           map[string]string
+	opts *v1.HTTPS2HTTPSPluginOptions
 
 	l *Listener
 	s *http.Server
 }
 
-func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) {
-	crtPath := params["plugin_crt_path"]
-	keyPath := params["plugin_key_path"]
-	localAddr := params["plugin_local_addr"]
-	hostHeaderRewrite := params["plugin_host_header_rewrite"]
-	headers := make(map[string]string)
-	for k, v := range params {
-		if !strings.HasPrefix(k, "plugin_header_") {
-			continue
-		}
-		if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
-			headers[k] = v
-		}
-	}
-
-	if localAddr == "" {
-		return nil, fmt.Errorf("plugin_local_addr is required")
-	}
+func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
+	opts := options.(*v1.HTTPS2HTTPSPluginOptions)
 
 	listener := NewProxyListener()
 
 	p := &HTTPS2HTTPSPlugin{
-		crtPath:           crtPath,
-		keyPath:           keyPath,
-		localAddr:         localAddr,
-		hostHeaderRewrite: hostHeaderRewrite,
-		headers:           headers,
-		l:                 listener,
+		opts: opts,
+		l:    listener,
 	}
 
 	tr := &http.Transport{
@@ -81,11 +55,11 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) {
 	rp := &httputil.ReverseProxy{
 		Director: func(req *http.Request) {
 			req.URL.Scheme = "https"
-			req.URL.Host = p.localAddr
-			if p.hostHeaderRewrite != "" {
-				req.Host = p.hostHeaderRewrite
+			req.URL.Host = p.opts.LocalAddr
+			if p.opts.HostHeaderRewrite != "" {
+				req.Host = p.opts.HostHeaderRewrite
 			}
-			for k, v := range p.headers {
+			for k, v := range p.opts.RequestHeaders.Set {
 				req.Header.Set(k, v)
 			}
 		},
@@ -100,7 +74,7 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) {
 		tlsConfig *tls.Config
 		err       error
 	)
-	if crtPath != "" || keyPath != "" {
+	if opts.CrtPath != "" || opts.KeyPath != "" {
 		tlsConfig, err = p.genTLSConfig()
 	} else {
 		tlsConfig, err = transport.NewServerTLSConfig("", "", "")
@@ -118,7 +92,7 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) {
 }
 
 func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
-	cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath)
+	cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath)
 	if err != nil {
 		return nil, err
 	}
@@ -127,13 +101,13 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
 	return config, nil
 }
 
-func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = p.l.PutConn(wrapConn)
 }
 
 func (p *HTTPS2HTTPSPlugin) Name() string {
-	return PluginHTTPS2HTTPS
+	return v1.PluginHTTPS2HTTPS
 }
 
 func (p *HTTPS2HTTPSPlugin) Close() error {

+ 14 - 5
pkg/plugin/client/plugin.go

@@ -21,32 +21,41 @@ import (
 	"sync"
 
 	"github.com/fatedier/golib/errors"
+	pp "github.com/pires/go-proxyproto"
+
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 // Creators is used for create plugins to handle connections.
 var creators = make(map[string]CreatorFn)
 
 // params has prefix "plugin_"
-type CreatorFn func(params map[string]string) (Plugin, error)
+type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error)
 
 func Register(name string, fn CreatorFn) {
+	if _, exist := creators[name]; exist {
+		panic(fmt.Sprintf("plugin [%s] is already registered", name))
+	}
 	creators[name] = fn
 }
 
-func Create(name string, params map[string]string) (p Plugin, err error) {
+func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) {
 	if fn, ok := creators[name]; ok {
-		p, err = fn(params)
+		p, err = fn(options)
 	} else {
 		err = fmt.Errorf("plugin [%s] is not registered", name)
 	}
 	return
 }
 
+type ExtraInfo struct {
+	ProxyProtocolHeader *pp.Header
+}
+
 type Plugin interface {
 	Name() string
 
-	// extraBufToLocal will send to local connection first, then join conn with local connection
-	Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte)
+	Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo)
 	Close() error
 }
 

+ 8 - 10
pkg/plugin/client/socks5.go

@@ -21,28 +21,26 @@ import (
 
 	gosocks5 "github.com/armon/go-socks5"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-const PluginSocks5 = "socks5"
-
 func init() {
-	Register(PluginSocks5, NewSocks5Plugin)
+	Register(v1.PluginSocks5, NewSocks5Plugin)
 }
 
 type Socks5Plugin struct {
 	Server *gosocks5.Server
 }
 
-func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
-	user := params["plugin_user"]
-	passwd := params["plugin_passwd"]
+func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
+	opts := options.(*v1.Socks5PluginOptions)
 
 	cfg := &gosocks5.Config{
 		Logger: log.New(io.Discard, "", log.LstdFlags),
 	}
-	if user != "" || passwd != "" {
-		cfg.Credentials = gosocks5.StaticCredentials(map[string]string{user: passwd})
+	if opts.Username != "" || opts.Password != "" {
+		cfg.Credentials = gosocks5.StaticCredentials(map[string]string{opts.Username: opts.Password})
 	}
 	sp := &Socks5Plugin{}
 	sp.Server, err = gosocks5.New(cfg)
@@ -50,14 +48,14 @@ func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
 	return
 }
 
-func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	defer conn.Close()
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.Server.ServeConn(wrapConn)
 }
 
 func (sp *Socks5Plugin) Name() string {
-	return PluginSocks5
+	return v1.PluginSocks5
 }
 
 func (sp *Socks5Plugin) Close() error {

+ 12 - 22
pkg/plugin/client/static_file.go

@@ -22,51 +22,41 @@ import (
 
 	"github.com/gorilla/mux"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 )
 
-const PluginStaticFile = "static_file"
-
 func init() {
-	Register(PluginStaticFile, NewStaticFilePlugin)
+	Register(v1.PluginStaticFile, NewStaticFilePlugin)
 }
 
 type StaticFilePlugin struct {
-	localPath   string
-	stripPrefix string
-	httpUser    string
-	httpPasswd  string
+	opts *v1.StaticFilePluginOptions
 
 	l *Listener
 	s *http.Server
 }
 
-func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
-	localPath := params["plugin_local_path"]
-	stripPrefix := params["plugin_strip_prefix"]
-	httpUser := params["plugin_http_user"]
-	httpPasswd := params["plugin_http_passwd"]
+func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
+	opts := options.(*v1.StaticFilePluginOptions)
 
 	listener := NewProxyListener()
 
 	sp := &StaticFilePlugin{
-		localPath:   localPath,
-		stripPrefix: stripPrefix,
-		httpUser:    httpUser,
-		httpPasswd:  httpPasswd,
+		opts: opts,
 
 		l: listener,
 	}
 	var prefix string
-	if stripPrefix != "" {
-		prefix = "/" + stripPrefix + "/"
+	if opts.StripPrefix != "" {
+		prefix = "/" + opts.StripPrefix + "/"
 	} else {
 		prefix = "/"
 	}
 
 	router := mux.NewRouter()
-	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")
+	router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+	router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
 	sp.s = &http.Server{
 		Handler: router,
 	}
@@ -76,13 +66,13 @@ func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
 	return sp, nil
 }
 
-func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) {
+func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
 	wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
 	_ = sp.l.PutConn(wrapConn)
 }
 
 func (sp *StaticFilePlugin) Name() string {
-	return PluginStaticFile
+	return v1.PluginStaticFile
 }
 
 func (sp *StaticFilePlugin) Close() error {

Some files were not shown because too many files changed in this diff