Procházet zdrojové kódy

support yaml/json/toml configuration format, make ini deprecated (#3599)

fatedier před 1 rokem
rodič
revize
c95311d1a0
100 změnil soubory, kde provedl 4142 přidání a 3784 odebrání
  1. 12 10
      .golangci.yml
  2. 0 2
      Release.md
  3. 2 2
      client/admin.go
  4. 13 58
      client/admin_api.go
  5. 15 13
      client/control.go
  6. 22 13
      client/health/health.go
  7. 8 8
      client/proxy/general_tcp.go
  8. 30 29
      client/proxy/proxy.go
  9. 12 6
      client/proxy/proxy_manager.go
  10. 16 15
      client/proxy/proxy_wrapper.go
  11. 8 8
      client/proxy/sudp.go
  12. 8 8
      client/proxy/udp.go
  13. 12 12
      client/proxy/xtcp.go
  14. 44 46
      client/service.go
  15. 8 8
      client/visitor/stcp.go
  16. 8 8
      client/visitor/sudp.go
  17. 8 8
      client/visitor/visitor.go
  18. 17 11
      client/visitor/visitor_manager.go
  19. 13 13
      client/visitor/xtcp.go
  20. 14 16
      cmd/frpc/sub/http.go
  21. 13 15
      cmd/frpc/sub/https.go
  22. 4 3
      cmd/frpc/sub/nathole.go
  23. 9 7
      cmd/frpc/sub/reload.go
  24. 46 38
      cmd/frpc/sub/root.go
  25. 8 7
      cmd/frpc/sub/status.go
  26. 25 27
      cmd/frpc/sub/stcp.go
  27. 9 7
      cmd/frpc/sub/stop.go
  28. 25 27
      cmd/frpc/sub/sudp.go
  29. 13 16
      cmd/frpc/sub/tcp.go
  30. 13 15
      cmd/frpc/sub/tcpmux.go
  31. 13 15
      cmd/frpc/sub/udp.go
  32. 15 1
      cmd/frpc/sub/verify.go
  33. 27 27
      cmd/frpc/sub/xtcp.go
  34. 0 6
      cmd/frps/main.go
  35. 51 62
      cmd/frps/root.go
  36. 7 4
      cmd/frps/verify.go
  37. 4 5
      go.mod
  38. 7 12
      go.sum
  39. 10 61
      pkg/auth/auth.go
  40. 145 0
      pkg/auth/legacy/legacy.go
  41. 26 91
      pkg/auth/oidc.go
  42. 13 24
      pkg/auth/token.go
  43. 0 680
      pkg/config/client_test.go
  44. 0 0
      pkg/config/legacy/README.md
  45. 77 84
      pkg/config/legacy/client.go
  46. 350 0
      pkg/config/legacy/conversion.go
  47. 1 2
      pkg/config/legacy/parse.go
  48. 375 0
      pkg/config/legacy/proxy.go
  49. 18 58
      pkg/config/legacy/server.go
  50. 1 1
      pkg/config/legacy/utils.go
  51. 1 1
      pkg/config/legacy/value.go
  52. 42 119
      pkg/config/legacy/visitor.go
  53. 283 0
      pkg/config/load.go
  54. 0 921
      pkg/config/proxy.go
  55. 0 478
      pkg/config/proxy_test.go
  56. 0 217
      pkg/config/server_test.go
  57. 61 1
      pkg/config/types/types.go
  58. 39 7
      pkg/config/types/types_test.go
  59. 19 0
      pkg/config/v1/api.go
  60. 199 0
      pkg/config/v1/client.go
  61. 34 0
      pkg/config/v1/client_test.go
  62. 110 0
      pkg/config/v1/common.go
  63. 117 0
      pkg/config/v1/plugin.go
  64. 420 0
      pkg/config/v1/proxy.go
  65. 49 0
      pkg/config/v1/proxy_test.go
  66. 190 0
      pkg/config/v1/server.go
  67. 32 0
      pkg/config/v1/server_test.go
  68. 90 0
      pkg/config/v1/validation/client.go
  69. 41 0
      pkg/config/v1/validation/common.go
  70. 72 0
      pkg/config/v1/validation/plugin.go
  71. 234 0
      pkg/config/v1/validation/proxy.go
  72. 37 0
      pkg/config/v1/validation/server.go
  73. 28 0
      pkg/config/v1/validation/validation.go
  74. 59 0
      pkg/config/v1/validation/visitor.go
  75. 155 0
      pkg/config/v1/visitor.go
  76. 0 112
      pkg/config/visitor_test.go
  77. 12 33
      pkg/plugin/client/http2https.go
  78. 14 17
      pkg/plugin/client/http_proxy.go
  79. 14 41
      pkg/plugin/client/https2http.go
  80. 14 40
      pkg/plugin/client/https2https.go
  81. 5 3
      pkg/plugin/client/plugin.go
  82. 7 9
      pkg/plugin/client/socks5.go
  83. 11 21
      pkg/plugin/client/static_file.go
  84. 7 12
      pkg/plugin/client/unix_domain_socket.go
  85. 4 10
      pkg/plugin/server/http.go
  86. 4 5
      pkg/util/log/log.go
  87. 1 1
      pkg/util/util/http.go
  88. 23 0
      pkg/util/util/types.go
  89. 17 14
      server/control.go
  90. 4 4
      server/dashboard.go
  91. 12 11
      server/dashboard_api.go
  92. 11 3
      server/ports/ports.go
  93. 20 24
      server/proxy/http.go
  94. 5 9
      server/proxy/https.go
  95. 27 21
      server/proxy/proxy.go
  96. 6 10
      server/proxy/stcp.go
  97. 6 10
      server/proxy/sudp.go
  98. 10 13
      server/proxy/tcp.go
  99. 11 14
      server/proxy/tcpmux.go
  100. 10 14
      server/proxy/udp.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

+ 0 - 2
Release.md

@@ -1,3 +1 @@
 ### Features
-
-* Support Go 1.21.

+ 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,
 	}

+ 30 - 29
client/proxy/proxy.go

@@ -30,7 +30,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 +39,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 +57,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 +84,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 +98,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 +115,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 +134,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 +144,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 != "" {
+	if baseCfg.Transport.ProxyProtocolVersion != "" {
 		if m.SrcAddr != "" && m.SrcPort != 0 {
 			if m.DstAddr == "" {
 				m.DstAddr = "127.0.0.1"
@@ -168,9 +169,9 @@ 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
 			}
 
@@ -189,12 +190,12 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 	}
 
 	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
 	}
 

+ 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)

+ 14 - 16
cmd/frpc/sub/http.go

@@ -21,7 +21,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -40,7 +42,7 @@ func init() {
 	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")
+	httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(httpCmd)
 }
@@ -55,40 +57,36 @@ var httpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		cfg := &config.HTTPProxyConf{}
+		cfg := &v1.HTTPProxyConfig{}
 		var prefix string
 		if user != "" {
 			prefix = user + "."
 		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.HTTPProxy
+		cfg.Name = prefix + proxyName
+		cfg.Type = 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.HTTPPassword = httpPwd
 		cfg.HostHeaderRewrite = hostHeaderRewrite
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+		cfg.Transport.UseEncryption = useEncryption
+		cfg.Transport.UseCompression = useCompression
+		cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
+		cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.ValidateForClient()
-		if err != nil {
+		if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
+		err = startService(clientCfg, []v1.ProxyConfigurer{cfg}, nil, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 13 - 15
cmd/frpc/sub/https.go

@@ -21,7 +21,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -36,7 +38,7 @@ func init() {
 	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")
+	httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(httpsCmd)
 }
@@ -51,36 +53,32 @@ var httpsCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		cfg := &config.HTTPSProxyConf{}
+		cfg := &v1.HTTPSProxyConfig{}
 		var prefix string
 		if user != "" {
 			prefix = user + "."
 		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.HTTPSProxy
+		cfg.Name = prefix + proxyName
+		cfg.Type = 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)
+		cfg.Transport.UseEncryption = useEncryption
+		cfg.Transport.UseCompression = useCompression
+		cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
+		cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.ValidateForClient()
-		if err != nil {
+		if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
+		err = startService(clientCfg, []v1.ProxyConfigurer{cfg}, nil, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 4 - 3
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"
 )
 
@@ -49,9 +50,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 +90,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")
 	}

+ 9 - 7
cmd/frpc/sub/reload.go

@@ -25,6 +25,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
@@ -35,7 +36,7 @@ var reloadCmd = &cobra.Command{
 	Use:   "reload",
 	Short: "Hot-Reload frpc configuration",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
+		cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
@@ -51,19 +52,20 @@ var reloadCmd = &cobra.Command{
 	},
 }
 
-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")
+func reload(clientCfg *v1.ClientCommonConfig) error {
+	if clientCfg.WebServer.Port == 0 {
+		return fmt.Errorf("the port of web server 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)
+		clientCfg.WebServer.Addr+":"+
+		fmt.Sprintf("%d", clientCfg.WebServer.Port)+"/api/reload", nil)
 	if err != nil {
 		return err
 	}
 
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
+	authStr := "Basic " + base64.StdEncoding.EncodeToString(
+		[]byte(clientCfg.WebServer.User+":"+clientCfg.WebServer.Password))
 
 	req.Header.Add("Authorization", authStr)
 	resp, err := http.DefaultClient.Do(req)

+ 46 - 38
cmd/frpc/sub/root.go

@@ -27,20 +27,17 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/samber/lo"
 	"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
@@ -160,78 +157,89 @@ func handleTermSignal(svr *client.Service) {
 	svr.GracefulClose(500 * time.Millisecond)
 }
 
-func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
-	cfg = config.GetDefaultClientConf()
+func parseClientCommonCfgFromCmd() (*v1.ClientCommonConfig, error) {
+	cfg := &v1.ClientCommonConfig{}
 
 	ipStr, portStr, err := net.SplitHostPort(serverAddr)
 	if err != nil {
-		err = fmt.Errorf("invalid server_addr: %v", err)
-		return
+		return nil, fmt.Errorf("invalid server_addr: %v", err)
 	}
 
 	cfg.ServerAddr = ipStr
 	cfg.ServerPort, err = strconv.Atoi(portStr)
 	if err != nil {
-		err = fmt.Errorf("invalid server_addr: %v", err)
-		return
+		return nil, fmt.Errorf("invalid server_addr: %v", err)
 	}
 
 	cfg.User = user
-	cfg.Protocol = protocol
-	cfg.LogLevel = logLevel
-	cfg.LogFile = logFile
-	cfg.LogMaxDays = int64(logMaxDays)
-	cfg.DisableLogColor = disableLogColor
+	cfg.Transport.Protocol = protocol
+	cfg.Log.Level = logLevel
+	cfg.Log.To = logFile
+	cfg.Log.MaxDays = int64(logMaxDays)
+	cfg.Log.DisablePrintColor = 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.Auth.Token = token
+	cfg.Transport.TLS.Enable = lo.ToPtr(tlsEnable)
+	cfg.Transport.TLS.ServerName = tlsServerName
 
 	cfg.Complete()
-	if err = cfg.Validate(); err != nil {
-		err = fmt.Errorf("parse config error: %v", err)
-		return
+
+	err, warning := validation.ValidateClientCommonConfig(cfg)
+	if warning != nil {
+		fmt.Printf("WARNING: %v\n", warning)
 	}
-	return
+	if err != nil {
+		return nil, fmt.Errorf("parse config error: %v", err)
+	}
+	return cfg, nil
 }
 
 func runClient(cfgFilePath string) error {
-	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
+	cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
 	if err != nil {
 		fmt.Println(err)
 		return err
 	}
+	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")
+	}
+
+	warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
+	if warning != nil {
+		fmt.Printf("WARNING: %v\n", warning)
+	}
+	if err != nil {
+		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
 }

+ 8 - 7
cmd/frpc/sub/status.go

@@ -28,6 +28,7 @@ import (
 
 	"github.com/fatedier/frp/client"
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
@@ -38,7 +39,7 @@ 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)
+		cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
@@ -52,19 +53,19 @@ var statusCmd = &cobra.Command{
 	},
 }
 
-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")
+func status(clientCfg *v1.ClientCommonConfig) error {
+	if clientCfg.WebServer.Port == 0 {
+		return fmt.Errorf("the port of web server 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)
+		clientCfg.WebServer.Addr+":"+fmt.Sprintf("%d", clientCfg.WebServer.Port)+"/api/status", nil)
 	if err != nil {
 		return err
 	}
 
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
+	authStr := "Basic " + base64.StdEncoding.EncodeToString(
+		[]byte(clientCfg.WebServer.User+":"+clientCfg.WebServer.Password))
 
 	req.Header.Add("Authorization", authStr)
 	resp, err := http.DefaultClient.Do(req)

+ 25 - 27
cmd/frpc/sub/stcp.go

@@ -20,7 +20,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -38,7 +40,7 @@ func init() {
 	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")
+	stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(stcpCmd)
 }
@@ -53,8 +55,8 @@ var stcpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
+		pxyCfgs := make([]v1.ProxyConfigurer, 0)
+		visitorCfgs := make([]v1.VisitorConfigurer, 0)
 
 		var prefix string
 		if user != "" {
@@ -63,50 +65,46 @@ var stcpCmd = &cobra.Command{
 
 		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 := &v1.STCPProxyConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.STCPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.Secretkey = sk
 			cfg.LocalIP = localIP
 			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+			cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
+			cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
+			if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			proxyConfs[cfg.ProxyName] = cfg
+			pxyCfgs = append(pxyCfgs, 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 := &v1.STCPVisitorConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.STCPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.SecretKey = sk
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
+			if err := validation.ValidateVisitorConfigurer(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			visitorConfs[cfg.ProxyName] = cfg
+			visitorCfgs = append(visitorCfgs, cfg)
 		default:
 			fmt.Println("invalid role")
 			os.Exit(1)
 		}
 
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
+		err = startService(clientCfg, pxyCfgs, visitorCfgs, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 9 - 7
cmd/frpc/sub/stop.go

@@ -25,6 +25,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
@@ -35,7 +36,7 @@ var stopCmd = &cobra.Command{
 	Use:   "stop",
 	Short: "Stop the running frpc",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		cfg, _, _, err := config.ParseClientConfig(cfgFile)
+		cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
@@ -51,19 +52,20 @@ var stopCmd = &cobra.Command{
 	},
 }
 
-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")
+func stopClient(clientCfg *v1.ClientCommonConfig) error {
+	if clientCfg.WebServer.Port == 0 {
+		return fmt.Errorf("the port of web server 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)
+		clientCfg.WebServer.Addr+":"+
+		fmt.Sprintf("%d", clientCfg.WebServer.Port)+"/api/stop", nil)
 	if err != nil {
 		return err
 	}
 
-	authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
-		clientCfg.AdminPwd))
+	authStr := "Basic " + base64.StdEncoding.EncodeToString(
+		[]byte(clientCfg.WebServer.User+":"+clientCfg.WebServer.Password))
 
 	req.Header.Add("Authorization", authStr)
 	resp, err := http.DefaultClient.Do(req)

+ 25 - 27
cmd/frpc/sub/sudp.go

@@ -20,7 +20,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -38,7 +40,7 @@ func init() {
 	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")
+	sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(sudpCmd)
 }
@@ -53,8 +55,8 @@ var sudpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
+		pxyCfgs := make([]v1.ProxyConfigurer, 0)
+		visitorCfgs := make([]v1.VisitorConfigurer, 0)
 
 		var prefix string
 		if user != "" {
@@ -63,50 +65,46 @@ var sudpCmd = &cobra.Command{
 
 		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 := &v1.SUDPProxyConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.SUDPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.Secretkey = sk
 			cfg.LocalIP = localIP
 			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+			cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
+			cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
+			if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			proxyConfs[cfg.ProxyName] = cfg
+			pxyCfgs = append(pxyCfgs, 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 := &v1.SUDPVisitorConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.SUDPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.SecretKey = sk
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
+			if err := validation.ValidateVisitorConfigurer(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			visitorConfs[cfg.ProxyName] = cfg
+			visitorCfgs = append(visitorCfgs, cfg)
 		default:
 			fmt.Println("invalid role")
 			os.Exit(1)
 		}
 
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
+		err = startService(clientCfg, pxyCfgs, visitorCfgs, "")
 		if err != nil {
 			os.Exit(1)
 		}

+ 13 - 16
cmd/frpc/sub/tcp.go

@@ -20,7 +20,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -34,7 +36,7 @@ func init() {
 	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")
+	tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(tcpCmd)
 }
@@ -49,35 +51,30 @@ var tcpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		cfg := &config.TCPProxyConf{}
+		cfg := &v1.TCPProxyConfig{}
 		var prefix string
 		if user != "" {
 			prefix = user + "."
 		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.TCPProxy
+		cfg.Name = prefix + proxyName
+		cfg.Type = consts.TCPProxy
 		cfg.LocalIP = localIP
 		cfg.LocalPort = localPort
 		cfg.RemotePort = remotePort
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+		cfg.Transport.UseEncryption = useEncryption
+		cfg.Transport.UseCompression = useCompression
+		cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
+		cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.ValidateForClient()
-		if err != nil {
+		if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
+		err = startService(clientCfg, []v1.ProxyConfigurer{cfg}, nil, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 13 - 15
cmd/frpc/sub/tcpmux.go

@@ -21,7 +21,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -37,7 +39,7 @@ func init() {
 	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")
+	tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(tcpMuxCmd)
 }
@@ -52,37 +54,33 @@ var tcpMuxCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		cfg := &config.TCPMuxProxyConf{}
+		cfg := &v1.TCPMuxProxyConfig{}
 		var prefix string
 		if user != "" {
 			prefix = user + "."
 		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.TCPMuxProxy
+		cfg.Name = prefix + proxyName
+		cfg.Type = 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)
+		cfg.Transport.UseEncryption = useEncryption
+		cfg.Transport.UseCompression = useCompression
+		cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
+		cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.ValidateForClient()
-		if err != nil {
+		if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
+		err = startService(clientCfg, []v1.ProxyConfigurer{cfg}, nil, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 13 - 15
cmd/frpc/sub/udp.go

@@ -20,7 +20,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -34,7 +36,7 @@ func init() {
 	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")
+	udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(udpCmd)
 }
@@ -49,35 +51,31 @@ var udpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		cfg := &config.UDPProxyConf{}
+		cfg := &v1.UDPProxyConfig{}
 		var prefix string
 		if user != "" {
 			prefix = user + "."
 		}
-		cfg.ProxyName = prefix + proxyName
-		cfg.ProxyType = consts.UDPProxy
+		cfg.Name = prefix + proxyName
+		cfg.Type = consts.UDPProxy
 		cfg.LocalIP = localIP
 		cfg.LocalPort = localPort
 		cfg.RemotePort = remotePort
-		cfg.UseEncryption = useEncryption
-		cfg.UseCompression = useCompression
-		cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+		cfg.Transport.UseEncryption = useEncryption
+		cfg.Transport.UseCompression = useCompression
+		cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		cfg.BandwidthLimitMode = bandwidthLimitMode
+		cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
 
-		err = cfg.ValidateForClient()
-		if err != nil {
+		if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		proxyConfs := map[string]config.ProxyConf{
-			cfg.ProxyName: cfg,
-		}
-		err = startService(clientCfg, proxyConfs, nil, "")
+		err = startService(clientCfg, []v1.ProxyConfigurer{cfg}, nil, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 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)

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

@@ -20,7 +20,9 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"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/config/v1/validation"
 	"github.com/fatedier/frp/pkg/consts"
 )
 
@@ -38,7 +40,7 @@ func init() {
 	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")
+	xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode")
 
 	rootCmd.AddCommand(xtcpCmd)
 }
@@ -53,8 +55,8 @@ var xtcpCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		proxyConfs := make(map[string]config.ProxyConf)
-		visitorConfs := make(map[string]config.VisitorConf)
+		pxyCfgs := make([]v1.ProxyConfigurer, 0)
+		visitorCfgs := make([]v1.VisitorConfigurer, 0)
 
 		var prefix string
 		if user != "" {
@@ -63,50 +65,48 @@ var xtcpCmd = &cobra.Command{
 
 		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 := &v1.XTCPProxyConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.XTCPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.Secretkey = sk
 			cfg.LocalIP = localIP
 			cfg.LocalPort = localPort
-			cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit)
+			cfg.Transport.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidthLimit)
 			if err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			cfg.BandwidthLimitMode = bandwidthLimitMode
-			err = cfg.ValidateForClient()
-			if err != nil {
+			cfg.Transport.BandwidthLimitMode = bandwidthLimitMode
+
+			if err := validation.ValidateProxyConfigurerForClient(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			proxyConfs[cfg.ProxyName] = cfg
+			pxyCfgs = append(pxyCfgs, 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 := &v1.XTCPVisitorConfig{}
+			cfg.Name = prefix + proxyName
+			cfg.Type = consts.XTCPProxy
+			cfg.Transport.UseEncryption = useEncryption
+			cfg.Transport.UseCompression = useCompression
+			cfg.SecretKey = sk
 			cfg.ServerName = serverName
 			cfg.BindAddr = bindAddr
 			cfg.BindPort = bindPort
-			err = cfg.Validate()
-			if err != nil {
+
+			if err := validation.ValidateVisitorConfigurer(cfg); err != nil {
 				fmt.Println(err)
 				os.Exit(1)
 			}
-			visitorConfs[cfg.ProxyName] = cfg
+			visitorCfgs = append(visitorCfgs, cfg)
 		default:
 			fmt.Println("invalid role")
 			os.Exit(1)
 		}
 
-		err = startService(clientCfg, proxyConfs, visitorConfs, "")
+		err = startService(clientCfg, pxyCfgs, visitorCfgs, "")
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 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()
 }

+ 51 - 62
cmd/frps/root.go

@@ -21,19 +21,15 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/fatedier/frp/pkg/auth"
 	"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/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
@@ -104,24 +100,35 @@ 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
 			}
-			cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
+			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")
+			}
 		} else {
-			cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil)
+			if svrCfg, err = parseServerConfigFromCmd(); err != nil {
+				return err
+			}
+		}
+
+		warning, err := validation.ValidateServerConfig(svrCfg)
+		if warning != nil {
+			fmt.Printf("WARNING: %v\n", warning)
 		}
 		if err != nil {
 			return err
 		}
 
-		err = runServer(cfg)
-		if err != nil {
+		if err := runServer(svrCfg); err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
@@ -135,26 +142,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()
+func parseServerConfigFromCmd() (*v1.ServerConfig, error) {
+	cfg := &v1.ServerConfig{}
 
 	cfg.BindAddr = bindAddr
 	cfg.BindPort = bindPort
@@ -163,42 +152,42 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
 	cfg.VhostHTTPPort = vhostHTTPPort
 	cfg.VhostHTTPSPort = vhostHTTPSPort
 	cfg.VhostHTTPTimeout = vhostHTTPTimeout
-	cfg.DashboardAddr = dashboardAddr
-	cfg.DashboardPort = dashboardPort
-	cfg.DashboardUser = dashboardUser
-	cfg.DashboardPwd = dashboardPwd
+	cfg.WebServer.Addr = dashboardAddr
+	cfg.WebServer.Port = dashboardPort
+	cfg.WebServer.User = dashboardUser
+	cfg.WebServer.Password = dashboardPwd
 	cfg.EnablePrometheus = enablePrometheus
-	cfg.DashboardTLSCertFile = dashboardTLSCertFile
-	cfg.DashboardTLSKeyFile = dashboardTLSKeyFile
-	cfg.DashboardTLSMode = dashboardTLSMode
-	cfg.LogFile = logFile
-	cfg.LogLevel = logLevel
-	cfg.LogMaxDays = logMaxDays
+	if dashboardTLSMode {
+		cfg.WebServer.TLS = &v1.TLSConfig{
+			CertFile: dashboardTLSCertFile,
+			KeyFile:  dashboardTLSKeyFile,
+		}
+	}
+	cfg.Log.To = logFile
+	cfg.Log.Level = logLevel
+	cfg.Log.MaxDays = logMaxDays
+	cfg.Log.DisablePrintColor = disableLogColor
 	cfg.SubDomainHost = subDomainHost
-	cfg.TLSOnly = tlsOnly
+	cfg.TLS.Force = tlsOnly
+	cfg.MaxPortsPerClient = maxPortsPerClient
 
 	// 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
-		}
+	cfg.Auth.Token = token
 
-		for _, port := range ports {
-			cfg.AllowPorts[int(port)] = struct{}{}
+	if len(allowPorts) > 0 {
+		portsRanges, err := types.NewPortsRangeSliceFromString(allowPorts)
+		if err != nil {
+			return cfg, fmt.Errorf("allow_ports format error: %v", err)
 		}
+		cfg.AllowPorts = portsRanges
 	}
-	cfg.MaxPortsPerClient = maxPortsPerClient
-	cfg.DisableLogColor = disableLogColor
-	return
+
+	cfg.Complete()
+	return cfg, nil
 }
 
-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
 	},

+ 4 - 5
go.mod

@@ -3,12 +3,12 @@ module github.com/fatedier/frp
 go 1.20
 
 require (
+	github.com/BurntSushi/toml v0.3.1
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc/v3 v3.6.0
 	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
@@ -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.

+ 7 - 12
go.sum

@@ -1,6 +1,7 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
 github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -32,19 +33,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 +85,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=
@@ -147,7 +137,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 +264,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 +277,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=

+ 10 - 61
pkg/auth/auth.go

@@ -17,76 +17,26 @@ package auth
 import (
 	"fmt"
 
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/consts"
 	"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 {
+func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) {
+	switch cfg.Method {
 	case consts.TokenAuthMethod:
-		authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
+		authProvider = NewTokenAuth(cfg.AdditionalAuthScopes, cfg.Token)
 	case consts.OidcAuthMethod:
-		authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig)
+		authProvider = NewOidcAuthSetter(cfg.AdditionalAuthScopes, cfg.OIDC)
 	default:
-		panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod))
+		panic(fmt.Sprintf("wrong method: '%s'", cfg.Method))
 	}
-
 	return authProvider
 }
 
@@ -96,13 +46,12 @@ type Verifier interface {
 	VerifyNewWorkConn(*msg.NewWorkConn) error
 }
 
-func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) {
-	switch cfg.AuthenticationMethod {
+func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
+	switch cfg.Method {
 	case consts.TokenAuthMethod:
-		authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig)
+		authVerifier = NewTokenAuth(cfg.AdditionalAuthScopes, cfg.Token)
 	case consts.OidcAuthMethod:
-		authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig)
+		authVerifier = NewOidcAuthVerifier(cfg.AdditionalAuthScopes, 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


+ 77 - 84
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".
@@ -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 = conf.ClientConfig.AuthenticationMethod
+	out.Auth.Token = conf.ClientConfig.Token
+	if conf.ClientConfig.AuthenticateHeartBeats {
+		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeHeartBeats)
+	}
+	if conf.ClientConfig.AuthenticateNewWorkConns {
+		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, 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 = conf.ServerConfig.AuthenticationMethod
+	out.Auth.Token = conf.ServerConfig.Token
+	if conf.ServerConfig.AuthenticateHeartBeats {
+		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, v1.AuthScopeHeartBeats)
+	}
+	if conf.ServerConfig.AuthenticateNewWorkConns {
+		out.Auth.AdditionalAuthScopes = append(out.Auth.AdditionalAuthScopes, 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.MaxPortsPerClient = conf.MaxPortsPerClient
+
+	out.TLS.Force = conf.TLSOnly
+	out.TLS.CertFile = conf.TLSCertFile
+	out.TLS.KeyFile = conf.TLSKeyFile
+	out.TLS.TrustedCaFile = conf.TLSTrustedCaFile
+
+	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

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

@@ -0,0 +1,375 @@
+// 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"
+	"github.com/fatedier/frp/pkg/consts"
+)
+
+// 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{}),
+	}
+)
+
+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 string) 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 string) 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 := 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
+	}
+	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
+}

+ 18 - 58
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,25 +12,29 @@
 // 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".
@@ -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"

+ 42 - 119
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,13 +12,12 @@
 // 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"
 
 	"github.com/fatedier/frp/pkg/consts"
@@ -38,8 +37,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 string) VisitorConf {
+	v, ok := visitorConfTypeMap[visitorType]
+	if !ok {
+		return nil
+	}
+	return reflect.New(v).Interface().(VisitorConf)
 }
 
 type BaseVisitorConf struct {
@@ -59,96 +66,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 +95,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 +110,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 +125,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 +158,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 := 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
 }

+ 283 - 0
pkg/config/load.go

@@ -0,0 +1,283 @@
+// 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"
+
+	"github.com/BurntSushi/toml"
+	"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/consts"
+	"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, consts.TCPProxy)
+
+	configurer := v1.NewProxyConfigurerByType(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
+}

+ 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)
-	}
-}

+ 61 - 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,62 @@ 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 {
+	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 string `json:"method,omitempty"`
+	// Specify whether to include auth info in additional scope.
+	// Current supported scopes are: "HeartBeats", "NewWorkConns".
+	AdditionalAuthScopes []AuthScope `json:"additionalAuthScopes,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.Equal("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))
+}

+ 110 - 0
pkg/config/v1/common.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 v1
+
+import (
+	"github.com/fatedier/frp/pkg/util/util"
+)
+
+type AuthScope string
+
+const (
+	AuthScopeHeartBeats   AuthScope = "HeartBeats"
+	AuthScopeNewWorkConns AuthScope = "NewWorkConns"
+)
+
+// QUIC protocol options
+type QUICOptions struct {
+	KeepalivePeriod    int `json:"quicKeepalivePeriod,omitempty" validate:"gte=0"`
+	MaxIdleTimeout     int `json:"quicMaxIdleTimeout,omitempty" validate:"gte=0"`
+	MaxIncomingStreams int `json:"quicMaxIncomingStreams,omitempty" validate:"gte=0"`
+}
+
+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"`
+}

+ 117 - 0
pkg/config/v1/plugin.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 (
+	"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)
+	}
+	if err := json.Unmarshal(b, v); err != nil {
+		return err
+	}
+	c.ClientPluginOptions = v
+	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"`
+}

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

@@ -0,0 +1,420 @@
+// 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/consts"
+	"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(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)
+}
+
+var proxyConfigTypeMap = map[string]reflect.Type{
+	consts.TCPProxy:    reflect.TypeOf(TCPProxyConfig{}),
+	consts.UDPProxy:    reflect.TypeOf(UDPProxyConfig{}),
+	consts.HTTPProxy:   reflect.TypeOf(HTTPProxyConfig{}),
+	consts.HTTPSProxy:  reflect.TypeOf(HTTPSProxyConfig{}),
+	consts.TCPMuxProxy: reflect.TypeOf(TCPMuxProxyConfig{}),
+	consts.STCPProxy:   reflect.TypeOf(STCPProxyConfig{}),
+	consts.XTCPProxy:   reflect.TypeOf(XTCPProxyConfig{}),
+	consts.SUDPProxy:   reflect.TypeOf(SUDPProxyConfig{}),
+}
+
+func NewProxyConfigurerByType(proxyType string) 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
+}
+
+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" validate:"gte=0,lte=65535"`
+	// 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" validate:"gte=0,lte=65535"`
+	// QUICBindPort specifies the QUIC port that the server listens on.
+	// Set this value to 0 will disable this feature.
+	QUICBindPort int `json:"quicBindPort,omitempty" validate:"gte=0,lte=65535"`
+	// 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" validate:"gte=0,lte=65535"`
+	// 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" validate:"gte=0,lte=65535"`
+	// 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" validate:"gte=0,lte=65535"`
+	// 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"`
+
+	TLS TLSServerConfig `json:"tls,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.TLS.TrustedCaFile != "" {
+		c.TLS.Force = true
+	}
+
+	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               string               `json:"method,omitempty"`
+	AdditionalAuthScopes []AuthScope          `json:"additionalAuthScopes,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"`
+}
+
+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()
+}
+
+type TLSServerConfig struct {
+	// Force specifies whether to only accept TLS-encrypted connections.
+	Force bool `json:"force,omitempty"`
+
+	TLSConfig
+}

+ 32 - 0
pkg/config/v1/server_test.go

@@ -0,0 +1,32 @@
+// 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 TestServerConfigComplete(t *testing.T) {
+	require := require.New(t)
+	c := &ServerConfig{}
+	c.Complete()
+
+	require.Equal("token", c.Auth.Method)
+	require.Equal(true, lo.FromPtr(c.Transport.TCPMux))
+	require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient))
+}

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

@@ -0,0 +1,90 @@
+// 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 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([]string{"tcp", "kcp", "quic", "websocket", "wss"}, c.Transport.Protocol) {
+		errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, only support tcp, kcp, quic, websocket, wss"))
+	}
+
+	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 err, warnings
+		}
+	}
+
+	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
+}

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

@@ -0,0 +1,41 @@
+// 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"
+
+	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 nil
+}
+
+// ValidatePort checks that the network port is in range
+func ValidatePort(port int) error {
+	if 0 <= port && port <= 65535 {
+		return nil
+	}
+	return fmt.Errorf("port number %d must be in the range 0..65535", port)
+}

+ 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
+}

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

@@ -0,0 +1,234 @@
+// 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"
+	"github.com/fatedier/frp/pkg/consts"
+)
+
+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); 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{consts.HTTPConnectTCPMultiplexer}, 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 == consts.HTTPConnectTCPMultiplexer &&
+		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
+}

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

@@ -0,0 +1,37 @@
+// 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 (
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
+	var (
+		warnings Warning
+		errs     error
+	)
+	if err := validateWebServerConfig(&c.WebServer); err != nil {
+		errs = AppendError(errs, err)
+	}
+
+	errs = AppendError(errs, ValidatePort(c.BindPort))
+	errs = AppendError(errs, ValidatePort(c.KCPBindPort))
+	errs = AppendError(errs, ValidatePort(c.QUICBindPort))
+	errs = AppendError(errs, ValidatePort(c.VhostHTTPPort))
+	errs = AppendError(errs, ValidatePort(c.VhostHTTPSPort))
+	errs = AppendError(errs, ValidatePort(c.TCPMuxHTTPConnectPort))
+	return warnings, errs
+}

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

@@ -0,0 +1,28 @@
+// 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"
+)
+
+type Warning error
+
+func AppendError(err error, errs ...error) error {
+	if len(errs) == 0 {
+		return err
+	}
+	return errors.Join(append([]error{err}, errs...)...)
+}

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

@@ -0,0 +1,59 @@
+// 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 should not be empty")
+	}
+
+	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
+}

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

@@ -0,0 +1,155 @@
+// 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/consts"
+	"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:"sk,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
+}
+
+var visitorConfigTypeMap = map[string]reflect.Type{
+	consts.STCPProxy: reflect.TypeOf(STCPVisitorConfig{}),
+	consts.XTCPProxy: reflect.TypeOf(XTCPVisitorConfig{}),
+	consts.SUDPProxy: 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(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 string) 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)
-	}
-}

+ 12 - 33
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)
 			}
 		},
@@ -103,7 +82,7 @@ func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _
 }
 
 func (p *HTTP2HTTPSPlugin) Name() string {
-	return PluginHTTP2HTTPS
+	return v1.PluginHTTP2HTTPS
 }
 
 func (p *HTTP2HTTPSPlugin) Close() error {

+ 14 - 17
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,7 +62,7 @@ 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) {
@@ -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
 	}

+ 14 - 41
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
 	}
@@ -128,7 +101,7 @@ func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _
 }
 
 func (p *HTTPS2HTTPPlugin) Name() string {
-	return PluginHTTPS2HTTP
+	return v1.PluginHTTPS2HTTP
 }
 
 func (p *HTTPS2HTTPPlugin) Close() error {

+ 14 - 40
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
 	}
@@ -133,7 +107,7 @@ func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _
 }
 
 func (p *HTTPS2HTTPSPlugin) Name() string {
-	return PluginHTTPS2HTTPS
+	return v1.PluginHTTPS2HTTPS
 }
 
 func (p *HTTPS2HTTPSPlugin) Close() error {

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

@@ -21,21 +21,23 @@ import (
 	"sync"
 
 	"github.com/fatedier/golib/errors"
+
+	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) {
 	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)
 	}

+ 7 - 9
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)
@@ -57,7 +55,7 @@ func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []b
 }
 
 func (sp *Socks5Plugin) Name() string {
-	return PluginSocks5
+	return v1.PluginSocks5
 }
 
 func (sp *Socks5Plugin) Close() error {

+ 11 - 21
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,
 	}
@@ -82,7 +72,7 @@ func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _
 }
 
 func (sp *StaticFilePlugin) Name() string {
-	return PluginStaticFile
+	return v1.PluginStaticFile
 }
 
 func (sp *StaticFilePlugin) Close() error {

+ 7 - 12
pkg/plugin/client/unix_domain_socket.go

@@ -15,31 +15,26 @@
 package plugin
 
 import (
-	"fmt"
 	"io"
 	"net"
 
 	libio "github.com/fatedier/golib/io"
-)
 
-const PluginUnixDomainSocket = "unix_domain_socket"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
 
 func init() {
-	Register(PluginUnixDomainSocket, NewUnixDomainSocketPlugin)
+	Register(v1.PluginUnixDomainSocket, NewUnixDomainSocketPlugin)
 }
 
 type UnixDomainSocketPlugin struct {
 	UnixAddr *net.UnixAddr
 }
 
-func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) {
-	unixPath, ok := params["plugin_unix_path"]
-	if !ok {
-		err = fmt.Errorf("plugin_unix_path not found")
-		return
-	}
+func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err error) {
+	opts := options.(*v1.UnixDomainSocketPluginOptions)
 
-	unixAddr, errRet := net.ResolveUnixAddr("unix", unixPath)
+	unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath)
 	if errRet != nil {
 		err = errRet
 		return
@@ -66,7 +61,7 @@ func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, e
 }
 
 func (uds *UnixDomainSocketPlugin) Name() string {
-	return PluginUnixDomainSocket
+	return v1.PluginUnixDomainSocket
 }
 
 func (uds *UnixDomainSocketPlugin) Close() error {

+ 4 - 10
pkg/plugin/server/http.go

@@ -25,24 +25,18 @@ import (
 	"net/url"
 	"reflect"
 	"strings"
-)
 
-type HTTPPluginOptions struct {
-	Name      string   `ini:"name"`
-	Addr      string   `ini:"addr"`
-	Path      string   `ini:"path"`
-	Ops       []string `ini:"ops"`
-	TLSVerify bool     `ini:"tls_verify"`
-}
+	v1 "github.com/fatedier/frp/pkg/config/v1"
+)
 
 type httpPlugin struct {
-	options HTTPPluginOptions
+	options v1.HTTPPluginOptions
 
 	url    string
 	client *http.Client
 }
 
-func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin {
+func NewHTTPPluginOptions(options v1.HTTPPluginOptions) Plugin {
 	url := fmt.Sprintf("%s%s", options.Addr, options.Path)
 
 	var client *http.Client

+ 4 - 5
pkg/util/log/log.go

@@ -29,15 +29,14 @@ func init() {
 	Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)
 }
 
-func InitLog(logWay string, logFile string, logLevel string, maxdays int64, disableLogColor bool) {
-	SetLogFile(logWay, logFile, maxdays, disableLogColor)
+func InitLog(logFile string, logLevel string, maxdays int64, disableLogColor bool) {
+	SetLogFile(logFile, maxdays, disableLogColor)
 	SetLogLevel(logLevel)
 }
 
 // SetLogFile to configure log params
-// logWay: file or console
-func SetLogFile(logWay string, logFile string, maxdays int64, disableLogColor bool) {
-	if logWay == "console" {
+func SetLogFile(logFile string, maxdays int64, disableLogColor bool) {
+	if logFile == "console" {
 		params := ""
 		if disableLogColor {
 			params = `{"color": false}`

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

@@ -1,4 +1,4 @@
-// Copyright 2020 guylewin, guy@lewin.co.il
+// 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.

+ 23 - 0
pkg/util/util/types.go

@@ -0,0 +1,23 @@
+// 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 util
+
+func EmptyOr[T comparable](v T, fallback T) T {
+	var zero T
+	if zero == v {
+		return fallback
+	}
+	return v
+}

+ 17 - 14
server/control.go

@@ -26,9 +26,11 @@ import (
 	"github.com/fatedier/golib/control/shutdown"
 	"github.com/fatedier/golib/crypto"
 	"github.com/fatedier/golib/errors"
+	"github.com/samber/lo"
 
 	"github.com/fatedier/frp/pkg/auth"
 	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	pkgerr "github.com/fatedier/frp/pkg/errors"
 	"github.com/fatedier/frp/pkg/msg"
 	plugin "github.com/fatedier/frp/pkg/plugin/server"
@@ -151,7 +153,7 @@ type Control struct {
 	mu sync.RWMutex
 
 	// Server configuration information
-	serverCfg config.ServerCommonConf
+	serverCfg *v1.ServerConfig
 
 	xl  *xlog.Logger
 	ctx context.Context
@@ -165,11 +167,11 @@ func NewControl(
 	authVerifier auth.Verifier,
 	ctlConn net.Conn,
 	loginMsg *msg.Login,
-	serverCfg config.ServerCommonConf,
+	serverCfg *v1.ServerConfig,
 ) *Control {
 	poolCount := loginMsg.PoolCount
-	if poolCount > int(serverCfg.MaxPoolCount) {
-		poolCount = int(serverCfg.MaxPoolCount)
+	if poolCount > int(serverCfg.Transport.MaxPoolCount) {
+		poolCount = int(serverCfg.Transport.MaxPoolCount)
 	}
 	ctl := &Control{
 		rc:              rc,
@@ -320,7 +322,7 @@ func (ctl *Control) writer() {
 	defer ctl.allShutdown.Start()
 	defer ctl.writerShutdown.Done()
 
-	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Token))
+	encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
 	if err != nil {
 		xl.Error("crypto new writer error: %v", err)
 		ctl.allShutdown.Start()
@@ -352,7 +354,7 @@ func (ctl *Control) reader() {
 	defer ctl.allShutdown.Start()
 	defer ctl.readerShutdown.Done()
 
-	encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Token))
+	encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
 	for {
 		m, err := msg.ReadMsg(encReader)
 		if err != nil {
@@ -400,7 +402,7 @@ func (ctl *Control) stoper() {
 	for _, pxy := range ctl.proxies {
 		pxy.Close()
 		ctl.pxyManager.Del(pxy.GetName())
-		metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
+		metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
 
 		notifyContent := &plugin.CloseProxyContent{
 			User: plugin.UserInfo{
@@ -450,7 +452,7 @@ func (ctl *Control) manager() {
 	var heartbeatCh <-chan time.Time
 	// Don't need application heartbeat if TCPMux is enabled,
 	// yamux will do same thing.
-	if !ctl.serverCfg.TCPMux && ctl.serverCfg.HeartbeatTimeout > 0 {
+	if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
 		heartbeat := time.NewTicker(time.Second)
 		defer heartbeat.Stop()
 		heartbeatCh = heartbeat.C
@@ -459,7 +461,7 @@ func (ctl *Control) manager() {
 	for {
 		select {
 		case <-heartbeatCh:
-			if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
+			if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
 				xl.Warn("heartbeat timeout")
 				return
 			}
@@ -491,7 +493,8 @@ func (ctl *Control) manager() {
 				}
 				if err != nil {
 					xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
-					resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient)
+					resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName),
+						err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
 				} else {
 					resp.RemoteAddr = remoteAddr
 					xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
@@ -524,7 +527,7 @@ func (ctl *Control) manager() {
 				if err != nil {
 					xl.Warn("received invalid ping: %v", err)
 					ctl.sendCh <- &msg.Pong{
-						Error: util.GenerateResponseErrorString("invalid ping", err, ctl.serverCfg.DetailedErrorsToClient),
+						Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
 					}
 					return
 				}
@@ -549,9 +552,9 @@ func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) {
 }
 
 func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
-	var pxyConf config.ProxyConf
+	var pxyConf v1.ProxyConfigurer
 	// Load configures from NewProxy message and validate.
-	pxyConf, err = config.NewProxyConfFromMsg(pxyMsg, ctl.serverCfg)
+	pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.serverCfg)
 	if err != nil {
 		return
 	}
@@ -632,7 +635,7 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
 	delete(ctl.proxies, closeMsg.ProxyName)
 	ctl.mu.Unlock()
 
-	metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
+	metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
 
 	notifyContent := &plugin.CloseProxyContent{
 		User: plugin.UserInfo{

+ 4 - 4
server/dashboard.go

@@ -39,7 +39,7 @@ func (svr *Service) RunDashboardServer(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)
@@ -49,7 +49,7 @@ func (svr *Service) RunDashboardServer(address string) (err error) {
 
 	subRouter := router.NewRoute().Subrouter()
 
-	user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd
+	user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
 	subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
 
 	// metrics
@@ -82,8 +82,8 @@ func (svr *Service) RunDashboardServer(address string) (err error) {
 		return err
 	}
 
-	if svr.cfg.DashboardTLSMode {
-		cert, err := tls.LoadX509KeyPair(svr.cfg.DashboardTLSCertFile, svr.cfg.DashboardTLSKeyFile)
+	if svr.cfg.WebServer.TLS != nil {
+		cert, err := tls.LoadX509KeyPair(svr.cfg.WebServer.TLS.CertFile, svr.cfg.WebServer.TLS.KeyFile)
 		if err != nil {
 			return err
 		}

+ 12 - 11
server/dashboard_api.go

@@ -20,7 +20,8 @@ import (
 
 	"github.com/gorilla/mux"
 
-	"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/consts"
 	"github.com/fatedier/frp/pkg/metrics/mem"
 	"github.com/fatedier/frp/pkg/util/log"
@@ -81,11 +82,11 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
 		KCPBindPort:           svr.cfg.KCPBindPort,
 		QUICBindPort:          svr.cfg.QUICBindPort,
 		SubdomainHost:         svr.cfg.SubDomainHost,
-		MaxPoolCount:          svr.cfg.MaxPoolCount,
+		MaxPoolCount:          svr.cfg.Transport.MaxPoolCount,
 		MaxPortsPerClient:     svr.cfg.MaxPortsPerClient,
-		HeartBeatTimeout:      svr.cfg.HeartbeatTimeout,
-		AllowPortsStr:         svr.cfg.AllowPortsStr,
-		TLSOnly:               svr.cfg.TLSOnly,
+		HeartBeatTimeout:      svr.cfg.Transport.HeartbeatTimeout,
+		AllowPortsStr:         types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
+		TLSOnly:               svr.cfg.TLS.Force,
 
 		TotalTrafficIn:  serverStats.TotalTrafficIn,
 		TotalTrafficOut: serverStats.TotalTrafficOut,
@@ -99,7 +100,7 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
 }
 
 type BaseOutConf struct {
-	config.BaseProxyConf
+	v1.ProxyBaseConfig
 }
 
 type TCPOutConf struct {
@@ -109,7 +110,7 @@ type TCPOutConf struct {
 
 type TCPMuxOutConf struct {
 	BaseOutConf
-	config.DomainConf
+	v1.DomainConfig
 	Multiplexer string `json:"multiplexer"`
 }
 
@@ -120,14 +121,14 @@ type UDPOutConf struct {
 
 type HTTPOutConf struct {
 	BaseOutConf
-	config.DomainConf
+	v1.DomainConfig
 	Locations         []string `json:"locations"`
 	HostHeaderRewrite string   `json:"host_header_rewrite"`
 }
 
 type HTTPSOutConf struct {
 	BaseOutConf
-	config.DomainConf
+	v1.DomainConfig
 }
 
 type STCPOutConf struct {
@@ -204,7 +205,7 @@ func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxySt
 	for _, ps := range proxyStats {
 		proxyInfo := &ProxyStatsInfo{}
 		if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok {
-			content, err := json.Marshal(pxy.GetConf())
+			content, err := json.Marshal(pxy.GetConfigurer())
 			if err != nil {
 				log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err)
 				continue
@@ -278,7 +279,7 @@ func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName strin
 		msg = "no proxy info found"
 	} else {
 		if pxy, ok := svr.pxyManager.GetByName(proxyName); ok {
-			content, err := json.Marshal(pxy.GetConf())
+			content, err := json.Marshal(pxy.GetConfigurer())
 			if err != nil {
 				log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err)
 				code = 400

+ 11 - 3
server/ports/ports.go

@@ -6,6 +6,8 @@ import (
 	"strconv"
 	"sync"
 	"time"
+
+	"github.com/fatedier/frp/pkg/config/types"
 )
 
 const (
@@ -39,7 +41,7 @@ type Manager struct {
 	mu       sync.Mutex
 }
 
-func NewManager(netType string, bindAddr string, allowPorts map[int]struct{}) *Manager {
+func NewManager(netType string, bindAddr string, allowPorts []types.PortsRange) *Manager {
 	pm := &Manager{
 		reservedPorts: make(map[string]*PortCtx),
 		usedPorts:     make(map[int]*PortCtx),
@@ -48,8 +50,14 @@ func NewManager(netType string, bindAddr string, allowPorts map[int]struct{}) *M
 		netType:       netType,
 	}
 	if len(allowPorts) > 0 {
-		for port := range allowPorts {
-			pm.freePorts[port] = struct{}{}
+		for _, pair := range allowPorts {
+			if pair.Single > 0 {
+				pm.freePorts[pair.Single] = struct{}{}
+			} else {
+				for i := pair.Start; i <= pair.End; i++ {
+					pm.freePorts[i] = struct{}{}
+				}
+			}
 		}
 	} else {
 		for i := MinPort; i <= MaxPort; i++ {

+ 20 - 24
server/proxy/http.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/util/limit"
 	utilnet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/util"
@@ -31,18 +31,18 @@ import (
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.HTTPProxyConf{}), NewHTTPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.HTTPProxyConfig{}), NewHTTPProxy)
 }
 
 type HTTPProxy struct {
 	*BaseProxy
-	cfg *config.HTTPProxyConf
+	cfg *v1.HTTPProxyConfig
 
 	closeFuncs []func()
 }
 
-func NewHTTPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.HTTPProxyConf)
+func NewHTTPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -57,9 +57,9 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 	routeConfig := vhost.RouteConfig{
 		RewriteHost:     pxy.cfg.HostHeaderRewrite,
 		RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
-		Headers:         pxy.cfg.Headers,
+		Headers:         pxy.cfg.RequestHeaders.Set,
 		Username:        pxy.cfg.HTTPUser,
-		Password:        pxy.cfg.HTTPPwd,
+		Password:        pxy.cfg.HTTPPassword,
 		CreateConnFn:    pxy.GetRealConn,
 	}
 
@@ -87,14 +87,14 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 			tmpRouteConfig := routeConfig
 
 			// handle group
-			if pxy.cfg.Group != "" {
-				err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig)
+			if pxy.cfg.LoadBalancer.Group != "" {
+				err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig)
 				if err != nil {
 					return
 				}
 
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
+					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
 				})
 			} else {
 				// no group
@@ -108,7 +108,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 			}
 			addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort))
 			xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
-				routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
+				routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser)
 		}
 	}
 
@@ -120,14 +120,14 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 			tmpRouteConfig := routeConfig
 
 			// handle group
-			if pxy.cfg.Group != "" {
-				err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig)
+			if pxy.cfg.LoadBalancer.Group != "" {
+				err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig)
 				if err != nil {
 					return
 				}
 
 				pxy.closeFuncs = append(pxy.closeFuncs, func() {
-					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
+					pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
 				})
 			} else {
 				err = pxy.rc.HTTPReverseProxy.Register(routeConfig)
@@ -141,17 +141,13 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 			addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort))
 
 			xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
-				routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
+				routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser)
 		}
 	}
 	remoteAddr = strings.Join(addrs, ",")
 	return
 }
 
-func (pxy *HTTPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) {
 	xl := pxy.xl
 	rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr)
@@ -167,14 +163,14 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
 	}
 
 	var rwc io.ReadWriteCloser = tmpConn
-	if pxy.cfg.UseEncryption {
-		rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
+	if pxy.cfg.Transport.UseEncryption {
+		rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if pxy.cfg.UseCompression {
+	if pxy.cfg.Transport.UseCompression {
 		rwc = libio.WithCompression(rwc)
 	}
 
@@ -186,13 +182,13 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
 
 	workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn)
 	workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
-	metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType)
+	metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
 	return
 }
 
 func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) {
 	name := pxy.GetName()
-	proxyType := pxy.GetConf().GetBaseConfig().ProxyType
+	proxyType := pxy.GetConfigurer().GetBaseConfig().Type
 	metrics.Server.CloseConnection(name, proxyType)
 	metrics.Server.AddTrafficIn(name, proxyType, totalWrite)
 	metrics.Server.AddTrafficOut(name, proxyType, totalRead)

+ 5 - 9
server/proxy/https.go

@@ -18,22 +18,22 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.HTTPSProxyConf{}), NewHTTPSProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.HTTPSProxyConfig{}), NewHTTPSProxy)
 }
 
 type HTTPSProxy struct {
 	*BaseProxy
-	cfg *config.HTTPSProxyConf
+	cfg *v1.HTTPSProxyConfig
 }
 
-func NewHTTPSProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.HTTPSProxyConf)
+func NewHTTPSProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPSProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -86,10 +86,6 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
 	return
 }
 
-func (pxy *HTTPSProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *HTTPSProxy) Close() {
 	pxy.BaseProxy.Close()
 }

+ 27 - 21
server/proxy/proxy.go

@@ -27,7 +27,8 @@ import (
 	libio "github.com/fatedier/golib/io"
 	"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/server"
 	"github.com/fatedier/frp/pkg/util/limit"
@@ -37,9 +38,9 @@ import (
 	"github.com/fatedier/frp/server/metrics"
 )
 
-var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{}
+var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy) Proxy{}
 
-func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) {
+func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy) Proxy) {
 	proxyFactoryRegistry[proxyConfType] = factory
 }
 
@@ -49,7 +50,7 @@ type Proxy interface {
 	Context() context.Context
 	Run() (remoteAddr string, err error)
 	GetName() string
-	GetConf() config.ProxyConf
+	GetConfigurer() v1.ProxyConfigurer
 	GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error)
 	GetUsedPortsNum() int
 	GetResourceController() *controller.ResourceController
@@ -66,11 +67,11 @@ type BaseProxy struct {
 	usedPortsNum  int
 	poolCount     int
 	getWorkConnFn GetWorkConnFn
-	serverCfg     config.ServerCommonConf
+	serverCfg     *v1.ServerConfig
 	limiter       *rate.Limiter
 	userInfo      plugin.UserInfo
 	loginMsg      *msg.Login
-	pxyConf       config.ProxyConf
+	configurer    v1.ProxyConfigurer
 
 	mu  sync.RWMutex
 	xl  *xlog.Logger
@@ -105,6 +106,10 @@ func (pxy *BaseProxy) GetLimiter() *rate.Limiter {
 	return pxy.limiter
 }
 
+func (pxy *BaseProxy) GetConfigurer() v1.ProxyConfigurer {
+	return pxy.configurer
+}
+
 func (pxy *BaseProxy) Close() {
 	xl := xlog.FromContextSafe(pxy.ctx)
 	xl.Info("proxy closing")
@@ -209,13 +214,13 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
 	defer userConn.Close()
 
 	serverCfg := pxy.serverCfg
-	cfg := pxy.pxyConf.GetBaseConfig()
+	cfg := pxy.configurer.GetBaseConfig()
 	// server plugin hook
 	rc := pxy.GetResourceController()
 	content := &plugin.NewUserConnContent{
 		User:       pxy.GetUserInfo(),
 		ProxyName:  pxy.GetName(),
-		ProxyType:  cfg.ProxyType,
+		ProxyType:  cfg.Type,
 		RemoteAddr: userConn.RemoteAddr().String(),
 	}
 	_, err := rc.PluginManager.NewUserConn(content)
@@ -232,15 +237,16 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
 	defer workConn.Close()
 
 	var local io.ReadWriteCloser = workConn
-	xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t", cfg.UseEncryption, cfg.UseCompression)
-	if cfg.UseEncryption {
-		local, err = libio.WithEncryption(local, []byte(serverCfg.Token))
+	xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t",
+		cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
+	if cfg.Transport.UseEncryption {
+		local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token))
 		if err != nil {
 			xl.Error("create encryption stream error: %v", err)
 			return
 		}
 	}
-	if cfg.UseCompression {
+	if cfg.Transport.UseCompression {
 		var recycleFn func()
 		local, recycleFn = libio.WithCompressionFromPool(local)
 		defer recycleFn()
@@ -256,7 +262,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
 		workConn.RemoteAddr().String(), userConn.LocalAddr().String(), userConn.RemoteAddr().String())
 
 	name := pxy.GetName()
-	proxyType := cfg.ProxyType
+	proxyType := cfg.Type
 	metrics.Server.OpenConnection(name, proxyType)
 	inCount, outCount, _ := libio.Join(local, userConn)
 	metrics.Server.CloseConnection(name, proxyType)
@@ -266,18 +272,18 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
 }
 
 func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.ResourceController, poolCount int,
-	getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf, loginMsg *msg.Login,
+	getWorkConnFn GetWorkConnFn, configurer v1.ProxyConfigurer, serverCfg *v1.ServerConfig, loginMsg *msg.Login,
 ) (pxy Proxy, err error) {
-	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseConfig().ProxyName)
+	xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(configurer.GetBaseConfig().Name)
 
 	var limiter *rate.Limiter
-	limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes()
-	if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeServer {
+	limitBytes := configurer.GetBaseConfig().Transport.BandwidthLimit.Bytes()
+	if limitBytes > 0 && configurer.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeServer {
 		limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes))
 	}
 
 	basePxy := BaseProxy{
-		name:          pxyConf.GetBaseConfig().ProxyName,
+		name:          configurer.GetBaseConfig().Name,
 		rc:            rc,
 		listeners:     make([]net.Listener, 0),
 		poolCount:     poolCount,
@@ -288,14 +294,14 @@ func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.Reso
 		ctx:           xlog.NewContext(ctx, xl),
 		userInfo:      userInfo,
 		loginMsg:      loginMsg,
-		pxyConf:       pxyConf,
+		configurer:    configurer,
 	}
 
-	factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)]
+	factory := proxyFactoryRegistry[reflect.TypeOf(configurer)]
 	if factory == nil {
 		return pxy, fmt.Errorf("proxy type not support")
 	}
-	pxy = factory(&basePxy, pxyConf)
+	pxy = factory(&basePxy)
 	if pxy == nil {
 		return nil, fmt.Errorf("proxy not created")
 	}

+ 6 - 10
server/proxy/stcp.go

@@ -17,20 +17,20 @@ package proxy
 import (
 	"reflect"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.STCPProxyConf{}), NewSTCPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.STCPProxyConfig{}), NewSTCPProxy)
 }
 
 type STCPProxy struct {
 	*BaseProxy
-	cfg *config.STCPProxyConf
+	cfg *v1.STCPProxyConfig
 }
 
-func NewSTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.STCPProxyConf)
+func NewSTCPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.STCPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -47,7 +47,7 @@ func (pxy *STCPProxy) Run() (remoteAddr string, err error) {
 	if len(allowUsers) == 0 {
 		allowUsers = []string{pxy.GetUserInfo().User}
 	}
-	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers)
+	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers)
 	if errRet != nil {
 		err = errRet
 		return
@@ -59,10 +59,6 @@ func (pxy *STCPProxy) Run() (remoteAddr string, err error) {
 	return
 }
 
-func (pxy *STCPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *STCPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.VisitorManager.CloseListener(pxy.GetName())

+ 6 - 10
server/proxy/sudp.go

@@ -17,20 +17,20 @@ package proxy
 import (
 	"reflect"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
 }
 
 type SUDPProxy struct {
 	*BaseProxy
-	cfg *config.SUDPProxyConf
+	cfg *v1.SUDPProxyConfig
 }
 
-func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.SUDPProxyConf)
+func NewSUDPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.SUDPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -47,7 +47,7 @@ func (pxy *SUDPProxy) Run() (remoteAddr string, err error) {
 	if len(allowUsers) == 0 {
 		allowUsers = []string{pxy.GetUserInfo().User}
 	}
-	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers)
+	listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers)
 	if errRet != nil {
 		err = errRet
 		return
@@ -59,10 +59,6 @@ func (pxy *SUDPProxy) Run() (remoteAddr string, err error) {
 	return
 }
 
-func (pxy *SUDPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *SUDPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.VisitorManager.CloseListener(pxy.GetName())

+ 10 - 13
server/proxy/tcp.go

@@ -20,22 +20,22 @@ import (
 	"reflect"
 	"strconv"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.TCPProxyConf{}), NewTCPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.TCPProxyConfig{}), NewTCPProxy)
 }
 
 type TCPProxy struct {
 	*BaseProxy
-	cfg *config.TCPProxyConf
+	cfg *v1.TCPProxyConfig
 
 	realBindPort int
 }
 
-func NewTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.TCPProxyConf)
+func NewTCPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -48,8 +48,9 @@ func NewTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
 
 func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
-	if pxy.cfg.Group != "" {
-		l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort)
+	if pxy.cfg.LoadBalancer.Group != "" {
+		l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey,
+			pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort)
 		if errRet != nil {
 			err = errRet
 			return
@@ -61,7 +62,7 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 		}()
 		pxy.realBindPort = realBindPort
 		pxy.listeners = append(pxy.listeners, l)
-		xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.Group)
+		xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.LoadBalancer.Group)
 	} else {
 		pxy.realBindPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
 		if err != nil {
@@ -87,13 +88,9 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
 	return
 }
 
-func (pxy *TCPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *TCPProxy) Close() {
 	pxy.BaseProxy.Close()
-	if pxy.cfg.Group == "" {
+	if pxy.cfg.LoadBalancer.Group == "" {
 		pxy.rc.TCPPortManager.Release(pxy.realBindPort)
 	}
 }

+ 11 - 14
server/proxy/tcpmux.go

@@ -20,23 +20,23 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/consts"
 	"github.com/fatedier/frp/pkg/util/util"
 	"github.com/fatedier/frp/pkg/util/vhost"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.TCPMuxProxyConf{}), NewTCPMuxProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.TCPMuxProxyConfig{}), NewTCPMuxProxy)
 }
 
 type TCPMuxProxy struct {
 	*BaseProxy
-	cfg *config.TCPMuxProxyConf
+	cfg *v1.TCPMuxProxyConfig
 }
 
-func NewTCPMuxProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.TCPMuxProxyConf)
+func NewTCPMuxProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPMuxProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -57,8 +57,9 @@ func (pxy *TCPMuxProxy) httpConnectListen(
 		Username:        httpUser,
 		Password:        httpPwd,
 	}
-	if pxy.cfg.Group != "" {
-		l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, *routeConfig)
+	if pxy.cfg.LoadBalancer.Group != "" {
+		l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer,
+			pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, *routeConfig)
 	} else {
 		l, err = pxy.rc.TCPMuxHTTPConnectMuxer.Listen(pxy.ctx, routeConfig)
 	}
@@ -66,7 +67,7 @@ func (pxy *TCPMuxProxy) httpConnectListen(
 		return nil, err
 	}
 	pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]",
-		domain, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
+		domain, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser)
 	pxy.listeners = append(pxy.listeners, l)
 	return append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.TCPMuxHTTPConnectPort)), nil
 }
@@ -78,7 +79,7 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
 			continue
 		}
 
-		addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPwd, addrs)
+		addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs)
 		if err != nil {
 			return "", err
 		}
@@ -86,7 +87,7 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
 
 	if pxy.cfg.SubDomain != "" {
 		addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost,
-			pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPwd, addrs)
+			pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs)
 		if err != nil {
 			return "", err
 		}
@@ -111,10 +112,6 @@ func (pxy *TCPMuxProxy) Run() (remoteAddr string, err error) {
 	return remoteAddr, err
 }
 
-func (pxy *TCPMuxProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *TCPMuxProxy) Close() {
 	pxy.BaseProxy.Close()
 }

+ 10 - 14
server/proxy/udp.go

@@ -26,7 +26,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"
@@ -35,12 +35,12 @@ 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
 
 	realBindPort int
 
@@ -63,8 +63,8 @@ type UDPProxy struct {
 	isClosed bool
 }
 
-func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.UDPProxyConf)
+func NewUDPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.UDPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -140,7 +140,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 					pxy.readCh <- m
 					metrics.Server.AddTrafficOut(
 						pxy.GetName(),
-						pxy.GetConf().GetBaseConfig().ProxyType,
+						pxy.GetConfigurer().GetBaseConfig().Type,
 						int64(len(m.Content)),
 					)
 				}); errRet != nil {
@@ -170,7 +170,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 				xl.Trace("send message to udp workConn: %s", udpMsg.Content)
 				metrics.Server.AddTrafficIn(
 					pxy.GetName(),
-					pxy.GetConf().GetBaseConfig().ProxyType,
+					pxy.GetConfigurer().GetBaseConfig().Type,
 					int64(len(udpMsg.Content)),
 				)
 				continue
@@ -204,15 +204,15 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 			}
 
 			var rwc io.ReadWriteCloser = workConn
-			if pxy.cfg.UseEncryption {
-				rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token))
+			if pxy.cfg.Transport.UseEncryption {
+				rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
 				if err != nil {
 					xl.Error("create encryption stream error: %v", err)
 					workConn.Close()
 					continue
 				}
 			}
-			if pxy.cfg.UseCompression {
+			if pxy.cfg.Transport.UseCompression {
 				rwc = libio.WithCompression(rwc)
 			}
 
@@ -245,10 +245,6 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 	return remoteAddr, nil
 }
 
-func (pxy *UDPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *UDPProxy) Close() {
 	pxy.mu.Lock()
 	defer pxy.mu.Unlock()

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů