From bf81f70064932aaf5ce0266e609e3a14dfce2d72 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 5 Sep 2023 20:49:53 +0800 Subject: [PATCH] support yaml/json/toml configuration format, make ini deprecated --- .golangci.yml | 22 +- Release.md | 2 - client/admin.go | 4 +- client/admin_api.go | 71 +- client/control.go | 28 +- client/health/health.go | 35 +- client/proxy/general_tcp.go | 16 +- client/proxy/proxy.go | 59 +- client/proxy/proxy_manager.go | 18 +- client/proxy/proxy_wrapper.go | 31 +- client/proxy/sudp.go | 16 +- client/proxy/udp.go | 16 +- client/proxy/xtcp.go | 24 +- client/service.go | 90 ++- client/visitor/stcp.go | 16 +- client/visitor/sudp.go | 16 +- client/visitor/visitor.go | 16 +- client/visitor/visitor_manager.go | 28 +- client/visitor/xtcp.go | 26 +- cmd/frpc/sub/http.go | 30 +- cmd/frpc/sub/https.go | 28 +- cmd/frpc/sub/nathole.go | 7 +- cmd/frpc/sub/reload.go | 16 +- cmd/frpc/sub/root.go | 84 ++- cmd/frpc/sub/status.go | 15 +- cmd/frpc/sub/stcp.go | 52 +- cmd/frpc/sub/stop.go | 16 +- cmd/frpc/sub/sudp.go | 52 +- cmd/frpc/sub/tcp.go | 29 +- cmd/frpc/sub/tcpmux.go | 28 +- cmd/frpc/sub/udp.go | 28 +- cmd/frpc/sub/verify.go | 16 +- cmd/frpc/sub/xtcp.go | 54 +- cmd/frps/main.go | 6 - cmd/frps/root.go | 113 ++- cmd/frps/verify.go | 11 +- go.mod | 12 +- go.sum | 21 +- pkg/auth/auth.go | 71 +- pkg/auth/legacy/legacy.go | 145 ++++ pkg/auth/oidc.go | 117 +-- pkg/auth/token.go | 37 +- pkg/config/client_test.go | 680 ----------------- pkg/config/{ => legacy}/README.md | 0 pkg/config/{ => legacy}/client.go | 161 ++--- pkg/config/legacy/conversion.go | 350 +++++++++ pkg/config/{ => legacy}/parse.go | 3 +- pkg/config/legacy/proxy.go | 375 ++++++++++ pkg/config/{ => legacy}/server.go | 76 +- pkg/config/{ => legacy}/utils.go | 2 +- pkg/config/{ => legacy}/value.go | 2 +- pkg/config/{ => legacy}/visitor.go | 161 ++--- pkg/config/load.go | 282 ++++++++ pkg/config/proxy.go | 921 ------------------------ pkg/config/proxy_test.go | 478 ------------ pkg/config/server_test.go | 217 ------ pkg/config/{ => types}/types.go | 62 +- pkg/config/{ => types}/types_test.go | 46 +- pkg/config/v1/api.go | 19 + pkg/config/v1/client.go | 199 +++++ pkg/config/v1/client_test.go | 31 + pkg/config/v1/common.go | 111 +++ pkg/config/v1/plugin.go | 117 +++ pkg/config/v1/proxy.go | 420 +++++++++++ pkg/config/v1/proxy_test.go | 49 ++ pkg/config/v1/server.go | 191 +++++ pkg/config/v1/server_test.go | 31 + pkg/config/v1/validation/client.go | 90 +++ pkg/config/v1/validation/common.go | 41 ++ pkg/config/v1/validation/plugin.go | 72 ++ pkg/config/v1/validation/proxy.go | 234 ++++++ pkg/config/v1/validation/server.go | 37 + pkg/config/v1/validation/validation.go | 28 + pkg/config/v1/validation/visitor.go | 59 ++ pkg/config/v1/visitor.go | 155 ++++ pkg/config/visitor_test.go | 112 --- pkg/plugin/client/http2https.go | 45 +- pkg/plugin/client/http_proxy.go | 31 +- pkg/plugin/client/https2http.go | 55 +- pkg/plugin/client/https2https.go | 54 +- pkg/plugin/client/plugin.go | 8 +- pkg/plugin/client/socks5.go | 16 +- pkg/plugin/client/static_file.go | 32 +- pkg/plugin/client/unix_domain_socket.go | 19 +- pkg/plugin/server/http.go | 14 +- pkg/util/log/log.go | 9 +- pkg/util/util/http.go | 2 +- pkg/util/util/types.go | 23 + server/control.go | 31 +- server/dashboard.go | 8 +- server/dashboard_api.go | 23 +- server/ports/ports.go | 14 +- server/proxy/http.go | 44 +- server/proxy/https.go | 14 +- server/proxy/proxy.go | 48 +- server/proxy/stcp.go | 16 +- server/proxy/sudp.go | 16 +- server/proxy/tcp.go | 23 +- server/proxy/tcpmux.go | 25 +- server/proxy/udp.go | 24 +- server/proxy/xtcp.go | 16 +- server/service.go | 57 +- test/e2e/e2e_test.go | 2 +- 103 files changed, 4176 insertions(+), 3827 deletions(-) create mode 100644 pkg/auth/legacy/legacy.go delete mode 100644 pkg/config/client_test.go rename pkg/config/{ => legacy}/README.md (100%) rename pkg/config/{ => legacy}/client.go (97%) create mode 100644 pkg/config/legacy/conversion.go rename pkg/config/{ => legacy}/parse.go (98%) create mode 100644 pkg/config/legacy/proxy.go rename pkg/config/{ => legacy}/server.go (87%) rename pkg/config/{ => legacy}/utils.go (98%) rename pkg/config/{ => legacy}/value.go (99%) rename pkg/config/{ => legacy}/visitor.go (74%) create mode 100644 pkg/config/load.go delete mode 100644 pkg/config/proxy.go delete mode 100644 pkg/config/proxy_test.go delete mode 100644 pkg/config/server_test.go rename pkg/config/{ => types}/types.go (58%) rename pkg/config/{ => types}/types_test.go (54%) create mode 100644 pkg/config/v1/api.go create mode 100644 pkg/config/v1/client.go create mode 100644 pkg/config/v1/client_test.go create mode 100644 pkg/config/v1/common.go create mode 100644 pkg/config/v1/plugin.go create mode 100644 pkg/config/v1/proxy.go create mode 100644 pkg/config/v1/proxy_test.go create mode 100644 pkg/config/v1/server.go create mode 100644 pkg/config/v1/server_test.go create mode 100644 pkg/config/v1/validation/client.go create mode 100644 pkg/config/v1/validation/common.go create mode 100644 pkg/config/v1/validation/plugin.go create mode 100644 pkg/config/v1/validation/proxy.go create mode 100644 pkg/config/v1/validation/server.go create mode 100644 pkg/config/v1/validation/validation.go create mode 100644 pkg/config/v1/validation/visitor.go create mode 100644 pkg/config/v1/visitor.go delete mode 100644 pkg/config/visitor_test.go create mode 100644 pkg/util/util/types.go 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 @@ ### 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..c113d5aacae 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,17 @@ 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 github.com/hashicorp/yamux v0.1.1 + github.com/jinzhu/copier v0.4.0 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 github.com/pion/stun v0.6.1 @@ -30,6 +31,7 @@ require ( gopkg.in/ini.v1 v1.67.0 k8s.io/apimachinery v0.27.4 k8s.io/client-go v0.27.4 + k8s.io/utils v0.0.0-20230209194617-a36077c30491 ) require ( @@ -37,11 +39,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 +51,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 +74,10 @@ 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..6b49ab36ded 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= @@ -86,6 +78,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo= @@ -93,8 +87,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 +139,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 +266,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 +279,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/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/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..ea91652a61a --- /dev/null +++ b/pkg/config/load.go @@ -0,0 +1,282 @@ +// 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 { + 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/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.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/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/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/client_test.go b/pkg/config/v1/client_test.go new file mode 100644 index 00000000000..e6b0e4168e2 --- /dev/null +++ b/pkg/config/v1/client_test.go @@ -0,0 +1,31 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package 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() + + // TODO: validate more default values + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) +} diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go new file mode 100644 index 00000000000..c9f85e2cee5 --- /dev/null +++ b/pkg/config/v1/common.go @@ -0,0 +1,111 @@ +// 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.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/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.go b/pkg/config/v1/server.go new file mode 100644 index 00000000000..3e774c19a69 --- /dev/null +++ b/pkg/config/v1/server.go @@ -0,0 +1,191 @@ +// 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() { + if c.ProxyBindAddr == "" { + c.ProxyBindAddr = c.BindAddr + } + if c.TLS.TrustedCaFile != "" { + c.TLS.Force = true + } + + 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.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/server_test.go b/pkg/config/v1/server_test.go new file mode 100644 index 00000000000..6fe52eec0c6 --- /dev/null +++ b/pkg/config/v1/server_test.go @@ -0,0 +1,31 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package 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() + + // TODO: validate more default values + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) +} 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/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/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 { 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()) }