Repository: fatedier/frp (language: go)

Issue:
Configuration: use YAML/JSON instead of INI
In the early stage of development, I choose INI as configure format because it looks simple and easy to write. 

However, with the increase of project complexity, INI can't describe complex configures. It doesn't support hierarchical configuration. We use same prefix to represent arrays or maps. It's not an elegant way and difficult to unmarshal to a struct. 

We want to use YAML and JSON to replace INI in a smooth way:

* YAML and JSON will be supported in a future v0.x relase and can fully replace INI.
* INI is still supported in v0.x versions but will be removed in v1 versions.
* Complicated new featrues won't be supported by INI in future release. Users who want to use those features should change their configuration format to YAML/JSON.


Discussion / hints:
maybe TOML?
JSON is hard to write by a human since its commas and quotes. And YAML needs lots of spaces for indenting, which causes hard to read.

TOML will be more flexible:
 - You can indent your config if you have a complicated setup
 - You can just write a flat text file for a simple server config
TOML maybe not suitable for multiple levels configurations. It's redundant.

We will support JSON and YAML first, it's widely used and easy to auto generate. And then, other format can also be supported if it can simple map file content to a golang struct.
Is this planned in v1.0?
@cloudwindy For compatibility reasons, this issue has been temporarily shelved. In the next major version, it will be directly supported, not v1, but v2.

Fix patch:
diff --git a/.golangci.yml b/.golangci.yml
index 0fe5d726a5d..18cbaf0be8a 100644
--- a/.golangci.yml
+++ b/.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
diff --git a/Release.md b/Release.md
index e9d6a662b74..cff4dbc12b7 100644
--- a/Release.md
+++ b/Release.md
@@ -1,3 +1,1 @@
 ### Features
-
-* Support Go 1.21.
diff --git a/client/admin.go b/client/admin.go
index 29e86a6e121..da8bab1bd5c 100644
--- a/client/admin.go
+++ b/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
diff --git a/client/admin_api.go b/client/admin_api.go
index b1d1885cbd3..a348e8dd332 100644
--- a/client/admin_api.go
+++ b/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)
diff --git a/client/control.go b/client/control.go
index e344046c439..63c6c331fc9 100644
--- a/client/control.go
+++ b/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
diff --git a/client/health/health.go b/client/health/health.go
index 0ee3d04ac9c..63eec721811 100644
--- a/client/health/health.go
+++ b/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,
diff --git a/client/proxy/general_tcp.go b/client/proxy/general_tcp.go
index cea9ef5b32c..c3923085503 100644
--- a/client/proxy/general_tcp.go
+++ b/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,
 	}
diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index 62b3cbba51f..4bdd4285fe5 100644
--- a/client/proxy/proxy.go
+++ b/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
 	}
 
diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go
index 9e551ced700..db66cb26397 100644
--- a/client/proxy/proxy_manager.go
+++ b/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
diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go
index 913094272bf..346c6d076a8 100644
--- a/client/proxy/proxy_wrapper.go
+++ b/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")
 	}
 
diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go
index 347b86a6227..e67a33974f0 100644
--- a/client/proxy/sudp.go
+++ b/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)
diff --git a/client/proxy/udp.go b/client/proxy/udp.go
index c0edc31256b..0a5cefcc291 100644
--- a/client/proxy/udp.go
+++ b/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)
diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go
index ccf390791aa..8271099bb50 100644
--- a/client/proxy/xtcp.go
+++ b/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))
 	}
 }
diff --git a/client/service.go b/client/service.go
index 954045a366f..5ddc23c3f29 100644
--- a/client/service.go
+++ b/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),
 	)
diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go
index 56d55055bf7..58433879492 100644
--- a/client/visitor/stcp.go
+++ b/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()
diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go
index 5b2d5177937..159f46ee074 100644
--- a/client/visitor/sudp.go
+++ b/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
diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go
index 7020df63997..dcd1f7b3047 100644
--- a/client/visitor/visitor.go
+++ b/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
diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go
index 344101aeb64..4b235cdb113 100644
--- a/client/visitor/visitor_manager.go
+++ b/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)
diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go
index dc5b807f768..c180621c29e 100644
--- a/client/visitor/xtcp.go
+++ b/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)
diff --git a/cmd/frpc/sub/http.go b/cmd/frpc/sub/http.go
index 1d152585360..55c45b70bed 100644
--- a/cmd/frpc/sub/http.go
+++ b/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)
diff --git a/cmd/frpc/sub/https.go b/cmd/frpc/sub/https.go
index 2eb6ed6b40a..d6bf4a6e3a4 100644
--- a/cmd/frpc/sub/https.go
+++ b/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)
diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go
index 459f8d63339..92977cd2221 100644
--- a/cmd/frpc/sub/nathole.go
+++ b/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")
 	}
diff --git a/cmd/frpc/sub/reload.go b/cmd/frpc/sub/reload.go
index 258317e7d6a..a01a34f37de 100644
--- a/cmd/frpc/sub/reload.go
+++ b/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)
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index 783514b99d0..01c73c01762 100644
--- a/cmd/frpc/sub/root.go
+++ b/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
 }
diff --git a/cmd/frpc/sub/status.go b/cmd/frpc/sub/status.go
index db7471e380a..1db4a956b18 100644
--- a/cmd/frpc/sub/status.go
+++ b/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)
diff --git a/cmd/frpc/sub/stcp.go b/cmd/frpc/sub/stcp.go
index 24aa955a24c..47ad3628663 100644
--- a/cmd/frpc/sub/stcp.go
+++ b/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)
diff --git a/cmd/frpc/sub/stop.go b/cmd/frpc/sub/stop.go
index 6c8f5f0aea3..c0ec5da5e91 100644
--- a/cmd/frpc/sub/stop.go
+++ b/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)
diff --git a/cmd/frpc/sub/sudp.go b/cmd/frpc/sub/sudp.go
index 553e42528dc..28f17ee6019 100644
--- a/cmd/frpc/sub/sudp.go
+++ b/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)
 		}
diff --git a/cmd/frpc/sub/tcp.go b/cmd/frpc/sub/tcp.go
index 2da9ad61be1..1a65a2a9da3 100644
--- a/cmd/frpc/sub/tcp.go
+++ b/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)
diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go
index 4b993f9cec1..a217591b316 100644
--- a/cmd/frpc/sub/tcpmux.go
+++ b/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)
diff --git a/cmd/frpc/sub/udp.go b/cmd/frpc/sub/udp.go
index 9a4803dc94d..e617070a802 100644
--- a/cmd/frpc/sub/udp.go
+++ b/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)
diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go
index c86b1e82b9c..a84f54f2a8f 100644
--- a/cmd/frpc/sub/verify.go
+++ b/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)
diff --git a/cmd/frpc/sub/xtcp.go b/cmd/frpc/sub/xtcp.go
index 60483afa709..a4bdd8031b8 100644
--- a/cmd/frpc/sub/xtcp.go
+++ b/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)
diff --git a/cmd/frps/main.go b/cmd/frps/main.go
index 6ae5378be48..34676a2ba6b 100644
--- a/cmd/frps/main.go
+++ b/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()
 }
diff --git a/cmd/frps/root.go b/cmd/frps/root.go
index e1859376a9a..af94aa47d9a 100644
--- a/cmd/frps/root.go
+++ b/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)
diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go
index 42f2ca74b08..4f0cefb18fb 100644
--- a/cmd/frps/verify.go
+++ b/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
 	},
diff --git a/go.mod b/go.mod
index e9042ecf802..49e74d5739f 100644
--- a/go.mod
+++ b/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.
diff --git a/go.sum b/go.sum
index d1b794c9f1f..f7ed6f4be41 100644
--- a/go.sum
+++ b/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=
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go
index 894da2471c7..2a79149c32f 100644
--- a/pkg/auth/auth.go
+++ b/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
 }
diff --git a/pkg/auth/legacy/legacy.go b/pkg/auth/legacy/legacy.go
new file mode 100644
index 00000000000..c16d38f2c91
--- /dev/null
+++ b/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: "",
+	}
+}
diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go
index c8ef923d3fc..f428a04dacf 100644
--- a/pkg/auth/oidc.go
+++ b/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
 	}
 
diff --git a/pkg/auth/token.go b/pkg/auth/token.go
index 9b2e3f7ce02..0b34b05e417 100644
--- a/pkg/auth/token.go
+++ b/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
 	}
 
diff --git a/pkg/config/README.md b/pkg/config/legacy/README.md
similarity index 100%
rename from pkg/config/README.md
rename to pkg/config/legacy/README.md
diff --git a/pkg/config/client.go b/pkg/config/legacy/client.go
similarity index 97%
rename from pkg/config/client.go
rename to pkg/config/legacy/client.go
index 029470dc1de..92d3a956aef 100644
--- a/pkg/config/client.go
+++ b/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
+}
diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go
new file mode 100644
index 00000000000..8d9074bfd22
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/parse.go b/pkg/config/legacy/parse.go
similarity index 98%
rename from pkg/config/parse.go
rename to pkg/config/legacy/parse.go
index 9f4cdb6b74b..637783bc73b 100644
--- a/pkg/config/parse.go
+++ b/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
diff --git a/pkg/config/legacy/proxy.go b/pkg/config/legacy/proxy.go
new file mode 100644
index 00000000000..a6ce55aba4d
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/server.go b/pkg/config/legacy/server.go
similarity index 87%
rename from pkg/config/server.go
rename to pkg/config/legacy/server.go
index 7d3310f647c..a10db34b7e0 100644
--- a/pkg/config/server.go
+++ b/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
diff --git a/pkg/config/utils.go b/pkg/config/legacy/utils.go
similarity index 98%
rename from pkg/config/utils.go
rename to pkg/config/legacy/utils.go
index aef674d4308..9366f8c76c1 100644
--- a/pkg/config/utils.go
+++ b/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"
diff --git a/pkg/config/value.go b/pkg/config/legacy/value.go
similarity index 99%
rename from pkg/config/value.go
rename to pkg/config/legacy/value.go
index 2f228823376..ecf805c9912 100644
--- a/pkg/config/value.go
+++ b/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"
diff --git a/pkg/config/visitor.go b/pkg/config/legacy/visitor.go
similarity index 74%
rename from pkg/config/visitor.go
rename to pkg/config/legacy/visitor.go
index 31a8a02bd4f..5ee36a1ac1c 100644
--- a/pkg/config/visitor.go
+++ b/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
 }
diff --git a/pkg/config/load.go b/pkg/config/load.go
new file mode 100644
index 00000000000..2f454eb289e
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go
deleted file mode 100644
index 1169fd7527e..00000000000
--- a/pkg/config/proxy.go
+++ /dev/null
@@ -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()
-}
diff --git a/pkg/config/types.go b/pkg/config/types/types.go
similarity index 58%
rename from pkg/config/types.go
rename to pkg/config/types/types.go
index 7aefee12600..b6fb7a85471 100644
--- a/pkg/config/types.go
+++ b/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
+}
diff --git a/pkg/config/v1/api.go b/pkg/config/v1/api.go
new file mode 100644
index 00000000000..0991fc93513
--- /dev/null
+++ b/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"`
+}
diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go
new file mode 100644
index 00000000000..878ae2b7075
--- /dev/null
+++ b/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"`
+}
diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go
new file mode 100644
index 00000000000..dcb7b5ec9dc
--- /dev/null
+++ b/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"`
+}
diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go
new file mode 100644
index 00000000000..e593c6ff58d
--- /dev/null
+++ b/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"`
+}
diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go
new file mode 100644
index 00000000000..60107ed6498
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go
new file mode 100644
index 00000000000..65c5dc46c9c
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go
new file mode 100644
index 00000000000..537f386df83
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/validation/common.go b/pkg/config/v1/validation/common.go
new file mode 100644
index 00000000000..f2d5251ebb8
--- /dev/null
+++ b/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)
+}
diff --git a/pkg/config/v1/validation/plugin.go b/pkg/config/v1/validation/plugin.go
new file mode 100644
index 00000000000..9c88d142cb2
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/validation/proxy.go b/pkg/config/v1/validation/proxy.go
new file mode 100644
index 00000000000..541a4376ee0
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go
new file mode 100644
index 00000000000..9f0fbfad0d6
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/validation/validation.go b/pkg/config/v1/validation/validation.go
new file mode 100644
index 00000000000..b081e909a25
--- /dev/null
+++ b/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...)...)
+}
diff --git a/pkg/config/v1/validation/visitor.go b/pkg/config/v1/validation/visitor.go
new file mode 100644
index 00000000000..c478b490e0f
--- /dev/null
+++ b/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
+}
diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go
new file mode 100644
index 00000000000..523b863b416
--- /dev/null
+++ b/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
+	}
+}
diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go
index beacab00570..712a24be223 100644
--- a/pkg/plugin/client/http2https.go
+++ b/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 {
diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go
index f9e5dae81d1..6af7c103d5e 100644
--- a/pkg/plugin/client/http_proxy.go
+++ b/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
 	}
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index 71cc025c646..0adfabf0f1e 100644
--- a/pkg/plugin/client/https2http.go
+++ b/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 {
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index d925ec24ad2..374755e6c6d 100644
--- a/pkg/plugin/client/https2https.go
+++ b/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 {
diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go
index 6850919a3c1..98e1bb73a09 100644
--- a/pkg/plugin/client/plugin.go
+++ b/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)
 	}
diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go
index 805f956b907..f71d579b795 100644
--- a/pkg/plugin/client/socks5.go
+++ b/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 {
diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go
index 45f731bbf6e..9140ffd6057 100644
--- a/pkg/plugin/client/static_file.go
+++ b/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 {
diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go
index 03371356033..266a529e3d1 100644
--- a/pkg/plugin/client/unix_domain_socket.go
+++ b/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 {
diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go
index 0f561204ce4..7108b7fb185 100644
--- a/pkg/plugin/server/http.go
+++ b/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
diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go
index 01a454d595d..a1391b3260a 100644
--- a/pkg/util/log/log.go
+++ b/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}`
diff --git a/pkg/util/util/http.go b/pkg/util/util/http.go
index d89af08b76a..7d6200a25ec 100644
--- a/pkg/util/util/http.go
+++ b/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.
diff --git a/pkg/util/util/types.go b/pkg/util/util/types.go
new file mode 100644
index 00000000000..784b8f193ad
--- /dev/null
+++ b/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
+}
diff --git a/server/control.go b/server/control.go
index abf17ab3b6b..5f6eb9fd47d 100644
--- a/server/control.go
+++ b/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{
diff --git a/server/dashboard.go b/server/dashboard.go
index a5c46ba3133..1f290cf9a5a 100644
--- a/server/dashboard.go
+++ b/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
 		}
diff --git a/server/dashboard_api.go b/server/dashboard_api.go
index 198a111aa61..e54ebe7732d 100644
--- a/server/dashboard_api.go
+++ b/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
diff --git a/server/ports/ports.go b/server/ports/ports.go
index f852f843596..5a73fbc5b3e 100644
--- a/server/ports/ports.go
+++ b/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++ {
diff --git a/server/proxy/http.go b/server/proxy/http.go
index 432dbc101b3..cafaf8f3d9a 100644
--- a/server/proxy/http.go
+++ b/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)
diff --git a/server/proxy/https.go b/server/proxy/https.go
index 74f0e7ca26e..dc9b77ebe00 100644
--- a/server/proxy/https.go
+++ b/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()
 }
diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go
index d1e6625523a..0cd7a6e5371 100644
--- a/server/proxy/proxy.go
+++ b/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")
 	}
diff --git a/server/proxy/stcp.go b/server/proxy/stcp.go
index 7c1511ab288..4f08a7c12c3 100644
--- a/server/proxy/stcp.go
+++ b/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())
diff --git a/server/proxy/sudp.go b/server/proxy/sudp.go
index 492b09542bd..a6d7c798f1b 100644
--- a/server/proxy/sudp.go
+++ b/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())
diff --git a/server/proxy/tcp.go b/server/proxy/tcp.go
index 5bb5c1abfaf..4196ad4ae3a 100644
--- a/server/proxy/tcp.go
+++ b/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)
 	}
 }
diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go
index 6d25a84ecb2..15824223f26 100644
--- a/server/proxy/tcpmux.go
+++ b/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()
 }
diff --git a/server/proxy/udp.go b/server/proxy/udp.go
index 20108bf3933..772c3f0d1b9 100644
--- a/server/proxy/udp.go
+++ b/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()
diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go
index acf127d7f26..fe60c630c57 100644
--- a/server/proxy/xtcp.go
+++ b/server/proxy/xtcp.go
@@ -20,23 +20,23 @@ import (
 
 	"github.com/fatedier/golib/errors"
 
-	"github.com/fatedier/frp/pkg/config"
+	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/msg"
 )
 
 func init() {
-	RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy)
+	RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
 }
 
 type XTCPProxy struct {
 	*BaseProxy
-	cfg *config.XTCPProxyConf
+	cfg *v1.XTCPProxyConfig
 
 	closeCh chan struct{}
 }
 
-func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy {
-	unwrapped, ok := cfg.(*config.XTCPProxyConf)
+func NewXTCPProxy(baseProxy *BaseProxy) Proxy {
+	unwrapped, ok := baseProxy.GetConfigurer().(*v1.XTCPProxyConfig)
 	if !ok {
 		return nil
 	}
@@ -58,7 +58,7 @@ func (pxy *XTCPProxy) Run() (remoteAddr string, err error) {
 	if len(allowUsers) == 0 {
 		allowUsers = []string{pxy.GetUserInfo().User}
 	}
-	sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Sk, allowUsers)
+	sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Secretkey, allowUsers)
 	if err != nil {
 		return "", err
 	}
@@ -86,10 +86,6 @@ func (pxy *XTCPProxy) Run() (remoteAddr string, err error) {
 	return
 }
 
-func (pxy *XTCPProxy) GetConf() config.ProxyConf {
-	return pxy.cfg
-}
-
 func (pxy *XTCPProxy) Close() {
 	pxy.BaseProxy.Close()
 	pxy.rc.NatHoleController.CloseClient(pxy.GetName())
diff --git a/server/service.go b/server/service.go
index 002468814d1..c81ea4d2a5b 100644
--- a/server/service.go
+++ b/server/service.go
@@ -22,17 +22,17 @@ import (
 	"io"
 	"net"
 	"net/http"
-	"sort"
 	"strconv"
 	"time"
 
 	"github.com/fatedier/golib/net/mux"
 	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"
 	modelmetrics "github.com/fatedier/frp/pkg/metrics"
 	"github.com/fatedier/frp/pkg/msg"
 	"github.com/fatedier/frp/pkg/nathole"
@@ -98,7 +98,7 @@ type Service struct {
 
 	tlsConfig *tls.Config
 
-	cfg config.ServerCommonConf
+	cfg *v1.ServerConfig
 
 	// service context
 	ctx context.Context
@@ -106,11 +106,11 @@ type Service struct {
 	cancel context.CancelFunc
 }
 
-func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
+func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
 	tlsConfig, err := transport.NewServerTLSConfig(
-		cfg.TLSCertFile,
-		cfg.TLSKeyFile,
-		cfg.TLSTrustedCaFile)
+		cfg.TLS.CertFile,
+		cfg.TLS.KeyFile,
+		cfg.TLS.TrustedCaFile)
 	if err != nil {
 		return
 	}
@@ -125,7 +125,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 			UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
 		},
 		httpVhostRouter: vhost.NewRouters(),
-		authVerifier:    auth.NewAuthVerifier(cfg.ServerConfig),
+		authVerifier:    auth.NewAuthVerifier(cfg.Auth),
 		tlsConfig:       tlsConfig,
 		cfg:             cfg,
 		ctx:             context.Background(),
@@ -150,15 +150,9 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 	}
 
 	// Init all plugins
-	pluginNames := make([]string, 0, len(cfg.HTTPPlugins))
-	for n := range cfg.HTTPPlugins {
-		pluginNames = append(pluginNames, n)
-	}
-	sort.Strings(pluginNames)
-
-	for _, name := range pluginNames {
-		svr.pluginManager.Register(plugin.NewHTTPPluginOptions(cfg.HTTPPlugins[name]))
-		log.Info("plugin [%s] has been registered", name)
+	for _, p := range cfg.HTTPPlugins {
+		svr.pluginManager.Register(plugin.NewHTTPPluginOptions(p))
+		log.Info("plugin [%s] has been registered", p.Name)
 	}
 	svr.rc.PluginManager = svr.pluginManager
 
@@ -196,7 +190,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 	}
 
 	svr.muxer = mux.NewMux(ln)
-	svr.muxer.SetKeepAlive(time.Duration(cfg.TCPKeepAlive) * time.Second)
+	svr.muxer.SetKeepAlive(time.Duration(cfg.Transport.TCPKeepAlive) * time.Second)
 	go func() {
 		_ = svr.muxer.Serve()
 	}()
@@ -221,9 +215,9 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 		quicTLSCfg := tlsConfig.Clone()
 		quicTLSCfg.NextProtos = []string{"frp"}
 		svr.quicListener, err = quic.ListenAddr(address, quicTLSCfg, &quic.Config{
-			MaxIdleTimeout:     time.Duration(cfg.QUICMaxIdleTimeout) * time.Second,
-			MaxIncomingStreams: int64(cfg.QUICMaxIncomingStreams),
-			KeepAlivePeriod:    time.Duration(cfg.QUICKeepalivePeriod) * time.Second,
+			MaxIdleTimeout:     time.Duration(cfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
+			MaxIncomingStreams: int64(cfg.Transport.QUIC.MaxIncomingStreams),
+			KeepAlivePeriod:    time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
 		})
 		if err != nil {
 			err = fmt.Errorf("listen on quic udp address %s error: %v", address, err)
@@ -305,11 +299,11 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 
 	var statsEnable bool
 	// Create dashboard web server.
-	if cfg.DashboardPort > 0 {
+	if cfg.WebServer.Port > 0 {
 		// Init dashboard assets
-		assets.Load(cfg.AssetsDir)
+		assets.Load(cfg.WebServer.AssetsDir)
 
-		address := net.JoinHostPort(cfg.DashboardAddr, strconv.Itoa(cfg.DashboardPort))
+		address := net.JoinHostPort(cfg.WebServer.Addr, strconv.Itoa(cfg.WebServer.Port))
 		err = svr.RunDashboardServer(address)
 		if err != nil {
 			err = fmt.Errorf("create dashboard web server error, %v", err)
@@ -416,7 +410,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
 			xl.Warn("register control error: %v", err)
 			_ = msg.WriteMsg(conn, &msg.LoginResp{
 				Version: version.Full(),
-				Error:   util.GenerateResponseErrorString("register control error", err, svr.cfg.DetailedErrorsToClient),
+				Error:   util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
 			})
 			conn.Close()
 		}
@@ -429,7 +423,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
 			xl.Warn("register visitor conn error: %v", err)
 			_ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{
 				ProxyName: m.ProxyName,
-				Error:     util.GenerateResponseErrorString("register visitor conn error", err, svr.cfg.DetailedErrorsToClient),
+				Error:     util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
 			})
 			conn.Close()
 		} else {
@@ -461,7 +455,7 @@ func (svr *Service) HandleListener(l net.Listener) {
 		log.Trace("start check TLS connection...")
 		originConn := c
 		var isTLS, custom bool
-		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLSOnly, connReadTimeout)
+		c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLS.Force, connReadTimeout)
 		if err != nil {
 			log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
 			originConn.Close()
@@ -471,9 +465,9 @@ func (svr *Service) HandleListener(l net.Listener) {
 
 		// Start a new goroutine to handle connection.
 		go func(ctx context.Context, frpConn net.Conn) {
-			if svr.cfg.TCPMux {
+			if lo.FromPtr(svr.cfg.Transport.TCPMux) {
 				fmuxCfg := fmux.DefaultConfig()
-				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
+				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
 				fmuxCfg.LogOutput = io.Discard
 				fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 				session, err := fmux.Server(frpConn, fmuxCfg)
@@ -594,7 +588,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
 	if err != nil {
 		xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID)
 		_ = msg.WriteMsg(workConn, &msg.StartWorkConn{
-			Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, ctl.serverCfg.DetailedErrorsToClient),
+			Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
 		})
 		return fmt.Errorf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
 	}
@@ -603,7 +597,8 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
 
 func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error {
 	visitorUser := ""
-	// TODO: Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID.
+	// TODO(deprecation): Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID.
+	// If runID is required, it is not compatible with versions prior to v0.50.0.
 	if newMsg.RunID != "" {
 		ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
 		if !exist {


Test patch:
diff --git a/pkg/config/client_test.go b/pkg/config/client_test.go
deleted file mode 100644
index 187324a11a6..00000000000
--- a/pkg/config/client_test.go
+++ /dev/null
@@ -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)
-}
diff --git a/pkg/config/proxy_test.go b/pkg/config/proxy_test.go
deleted file mode 100644
index 99d60811cf5..00000000000
--- a/pkg/config/proxy_test.go
+++ /dev/null
@@ -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)
-	}
-}
diff --git a/pkg/config/server_test.go b/pkg/config/server_test.go
deleted file mode 100644
index 7218397b7ac..00000000000
--- a/pkg/config/server_test.go
+++ /dev/null
@@ -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)
-	}
-}
diff --git a/pkg/config/types_test.go b/pkg/config/types/types_test.go
similarity index 54%
rename from pkg/config/types_test.go
rename to pkg/config/types/types_test.go
index ab03dfd2a34..8843de5af81 100644
--- a/pkg/config/types_test.go
+++ b/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)
 }
diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go
new file mode 100644
index 00000000000..01acbf0b395
--- /dev/null
+++ b/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))
+}
diff --git a/pkg/config/v1/proxy_test.go b/pkg/config/v1/proxy_test.go
new file mode 100644
index 00000000000..64753250818
--- /dev/null
+++ b/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)
+}
diff --git a/pkg/config/v1/server_test.go b/pkg/config/v1/server_test.go
new file mode 100644
index 00000000000..8ce5b2c19f2
--- /dev/null
+++ b/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))
+}
diff --git a/pkg/config/visitor_test.go b/pkg/config/visitor_test.go
deleted file mode 100644
index 46cbef5ab50..00000000000
--- a/pkg/config/visitor_test.go
+++ /dev/null
@@ -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)
-	}
-}
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index f809288e1ca..af2ca261112 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
 		os.Exit(1)
 	}
 
-	log.InitLog("console", "", framework.TestContext.LogLevel, 0, true)
+	log.InitLog("console", framework.TestContext.LogLevel, 0, true)
 	os.Exit(m.Run())
 }