diff --git a/.golangci.yml b/.golangci.yml index 3ba2c60fe63..f7c0e8bdc45 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,6 +39,7 @@ linters: - G404 - G501 - G115 + - G204 severity: low confidence: low govet: diff --git a/README.md b/README.md index 00a3498cb63..a99b2277094 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,6 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

-

- - Recall.ai - API for meeting recordings
-
- If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. -
-

-

- - -
- Warp, built for collaborating with AI Agents -
- Available for macOS, Linux and Windows -
-

@@ -36,13 +20,7 @@ frp is an open source project with its ongoing development made possible entirel The complete IDE crafted for professional Go developers

-

- - -
- Secure and Elastic Infrastructure for Running Your AI-Generated Code -
-

+

@@ -52,6 +30,34 @@ frp is an open source project with its ongoing development made possible entirel An open source, self-hosted alternative to public clouds, built for data ownership and privacy

+ +
+ +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +
+

+ + +
+ Requestly - Free & Open-Source alternative to Postman +
+ All-in-one platform to Test, Mock and Intercept APIs. +
+

+

+ + +
+ Warp, built for collaborating with AI Agents +
+ Available for macOS, Linux and Windows +
+

## What is frp? diff --git a/README_zh.md b/README_zh.md index ea63d7261cb..727299bc554 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,22 +15,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

Gold Sponsors

-

- - Recall.ai - API for meeting recordings
-
- If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. -
-

-

- - -
- Warp, built for collaborating with AI Agents -
- Available for macOS, Linux and Windows -
-

@@ -38,13 +22,7 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者 The complete IDE crafted for professional Go developers

-

- - -
- Secure and Elastic Infrastructure for Running Your AI-Generated Code -
-

+

@@ -54,6 +32,33 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者 An open source, self-hosted alternative to public clouds, built for data ownership and privacy

+
+ +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +
+

+ + +
+ Requestly - Free & Open-Source alternative to Postman +
+ All-in-one platform to Test, Mock and Intercept APIs. +
+

+

+ + +
+ Warp, built for collaborating with AI Agents +
+ Available for macOS, Linux and Windows +
+

## 为什么使用 frp ? @@ -126,9 +131,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 - -### 知识星球 - -如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: - -![zsxq](/doc/pic/zsxq.jpg) diff --git a/Release.md b/Release.md index 2ea047fa5de..c1861c58839 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,13 @@ ## Features -* Add NAT traversal configuration options for XTCP proxies and visitors. Support disabling assisted addresses to avoid using slow VPN connections during NAT hole punching. -* Enhanced OIDC client configuration with support for custom TLS certificate verification and proxy settings. Added `trustedCaFile`, `insecureSkipVerify`, and `proxyURL` options for OIDC token endpoint connections. -* Added detailed Prometheus metrics with `proxy_counts_detailed` metric that includes both proxy type and proxy name labels, enabling monitoring of individual proxy connections instead of just aggregate counts. +* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities. +* Individual frpc proxies and visitors now accept an `enabled` flag (defaults to true), letting you disable specific entries without relying on the global `start` list—disabled blocks are skipped when client configs load. +* OIDC authentication now supports a `tokenSource` field to dynamically obtain tokens from external sources. You can use `type = "file"` to read a token from a file, or `type = "exec"` to run an external command (e.g., a cloud CLI or secrets manager) and capture its stdout as the token. The `exec` type requires the `--allow-unsafe=TokenSourceExec` CLI flag for security reasons. + +## Improvements + +* **VirtualNet**: Implemented intelligent reconnection with exponential backoff. When connection errors occur repeatedly, the reconnect interval increases from 60s to 300s (max), reducing unnecessary reconnection attempts. Normal disconnections still reconnect quickly at 10s intervals. + +## Fixes + +* Fix deadlock issue when TCP connection is closed. Previously, sending messages could block forever if the connection handler had already stopped. diff --git a/client/admin_api.go b/client/admin_api.go index f161d588b4d..b726dc33b43 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -92,7 +92,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) { log.Warnf("reload frpc proxy config error: %s", res.Msg) return } - if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil { + if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil { res.Code = 400 res.Msg = err.Error() log.Warnf("reload frpc proxy config error: %s", res.Msg) diff --git a/client/control.go b/client/control.go index 4bd6a2f737a..0f48c36dd52 100644 --- a/client/control.go +++ b/client/control.go @@ -43,8 +43,8 @@ type SessionContext struct { Conn net.Conn // Indicates whether the connection is encrypted. ConnEncrypted bool - // Sets authentication based on selected method - AuthSetter auth.Setter + // Auth runtime used for login, heartbeats, and encryption. + Auth *auth.ClientAuth // Connector is used to create new connections, which could be real TCP connections or virtual streams. Connector Connector // Virtual net controller @@ -91,7 +91,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro ctl.lastPong.Store(time.Now()) if sessionCtx.ConnEncrypted { - cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token)) + cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey()) if err != nil { return nil, err } @@ -100,9 +100,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) } ctl.registerMsgHandlers() - ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) + ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) - ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController) + ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController) ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController) return ctl, nil @@ -133,7 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) { m := &msg.NewWorkConn{ RunID: ctl.sessionCtx.RunID, } - if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil { + if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil { xl.Warnf("error during NewWorkConn authentication: %v", err) workConn.Close() return @@ -243,7 +243,7 @@ func (ctl *Control) heartbeatWorker() { sendHeartBeat := func() (bool, error) { xl.Debugf("send heartbeat to server") pingMsg := &msg.Ping{} - if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil { + if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil { xl.Warnf("error during ping authentication: %v, skip sending ping message", err) return false, err } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 876ca579d38..8faff38d09b 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -57,6 +57,7 @@ func NewProxy( ctx context.Context, pxyConf v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, ) (pxy Proxy) { @@ -69,6 +70,7 @@ func NewProxy( baseProxy := BaseProxy{ baseCfg: pxyConf.GetBaseConfig(), clientCfg: clientCfg, + encryptionKey: encryptionKey, limiter: limiter, msgTransporter: msgTransporter, vnetController: vnetController, @@ -86,6 +88,7 @@ func NewProxy( type BaseProxy struct { baseCfg *v1.ProxyBaseConfig clientCfg *v1.ClientCommonConfig + encryptionKey []byte msgTransporter transport.MessageTransporter vnetController *vnet.Controller limiter *rate.Limiter @@ -129,7 +132,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { return } } - pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token)) + pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey) } // Common handler for tcp work connections. diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index ea5cc553112..4615e9a2631 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -40,7 +40,8 @@ type Manager struct { closed bool mu sync.RWMutex - clientCfg *v1.ClientCommonConfig + encryptionKey []byte + clientCfg *v1.ClientCommonConfig ctx context.Context } @@ -48,6 +49,7 @@ type Manager struct { func NewManager( ctx context.Context, clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, ) *Manager { @@ -56,6 +58,7 @@ func NewManager( msgTransporter: msgTransporter, vnetController: vnetController, closed: false, + encryptionKey: encryptionKey, clientCfg: clientCfg, ctx: ctx, } @@ -163,7 +166,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { for _, cfg := range proxyCfgs { name := cfg.GetBaseConfig().Name if _, ok := pm.proxies[name]; !ok { - pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController) + pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController) if pm.inWorkConnCallback != nil { pxy.SetInWorkConnCallback(pm.inWorkConnCallback) } diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index f3f17e2b10c..4698320a8a0 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -92,6 +92,7 @@ func NewWrapper( ctx context.Context, cfg v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, eventHandler event.Handler, msgTransporter transport.MessageTransporter, vnetController *vnet.Controller, @@ -122,7 +123,7 @@ func NewWrapper( xl.Tracef("enable health check monitor") } - pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController) + pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController) return pw } diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index 13741d0ddc7..3a7af19c93f 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { }) } if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { conn.Close() xl.Errorf("create encryption stream error: %v", err) diff --git a/client/proxy/udp.go b/client/proxy/udp.go index b70ffe4a0c8..1fca99044e5 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { }) } if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { conn.Close() xl.Errorf("create encryption stream error: %v", err) diff --git a/client/service.go b/client/service.go index f906e4d0a82..b282163e220 100644 --- a/client/service.go +++ b/client/service.go @@ -31,6 +31,7 @@ import ( "github.com/fatedier/frp/pkg/auth" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/policy/security" httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" netpkg "github.com/fatedier/frp/pkg/util/net" @@ -64,6 +65,8 @@ type ServiceOptions struct { ProxyCfgs []v1.ProxyConfigurer VisitorCfgs []v1.VisitorConfigurer + UnsafeFeatures *security.UnsafeFeatures + // ConfigFilePath is the path to the configuration file used to initialize. // If it is empty, it means that the configuration file is not used for initialization. // It may be initialized using command line parameters or called directly. @@ -108,8 +111,8 @@ type Service struct { // Uniq id got from frps, it will be attached to loginMsg. runID string - // Sets authentication based on selected method - authSetter auth.Setter + // Auth runtime and encryption materials + auth *auth.ClientAuth // web server for admin UI and apis webServer *httppkg.Server @@ -122,6 +125,8 @@ type Service struct { visitorCfgs []v1.VisitorConfigurer clientSpec *msg.ClientSpec + unsafeFeatures *security.UnsafeFeatures + // The configuration file used to initialize this client, or an empty // string if no configuration file was used. configFilePath string @@ -150,17 +155,18 @@ func NewService(options ServiceOptions) (*Service, error) { webServer = ws } - authSetter, err := auth.NewAuthSetter(options.Common.Auth) + authRuntime, err := auth.BuildClientAuth(&options.Common.Auth) if err != nil { return nil, err } s := &Service{ ctx: context.Background(), - authSetter: authSetter, + auth: authRuntime, webServer: webServer, common: options.Common, configFilePath: options.ConfigFilePath, + unsafeFeatures: options.UnsafeFeatures, proxyCfgs: options.ProxyCfgs, visitorCfgs: options.VisitorCfgs, clientSpec: options.ClientSpec, @@ -290,7 +296,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) { } // Add auth - if err = svr.authSetter.SetLogin(loginMsg); err != nil { + if err = svr.auth.Setter.SetLogin(loginMsg); err != nil { return } @@ -344,7 +350,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE RunID: svr.runID, Conn: conn, ConnEncrypted: connEncrypted, - AuthSetter: svr.authSetter, + Auth: svr.auth, Connector: connector, VnetController: svr.vnetController, } diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index 124202eb8d5..31f6f1743e5 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -15,6 +15,7 @@ package visitor import ( + "fmt" "io" "net" "strconv" @@ -81,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() { func (sv *STCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) - defer userConn.Close() + var tunnelErr error + defer func() { + // If there was an error and connection supports CloseWithError, use it + if tunnelErr != nil { + if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok { + _ = eConn.CloseWithError(tunnelErr) + return + } + } + userConn.Close() + }() xl.Debugf("get a new stcp user connection") visitorConn, err := sv.helper.ConnectServer() if err != nil { + tunnelErr = err return } defer visitorConn.Close() @@ -102,6 +114,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { err = msg.WriteMsg(visitorConn, newVisitorConnMsg) if err != nil { xl.Warnf("send newVisitorConnMsg to server error: %v", err) + tunnelErr = err return } @@ -110,12 +123,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) if err != nil { xl.Warnf("get newVisitorConnRespMsg error: %v", err) + tunnelErr = err return } _ = visitorConn.SetReadDeadline(time.Time{}) if newVisitorConnRespMsg.Error != "" { xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) + tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error) return } @@ -125,6 +140,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) if err != nil { xl.Errorf("create encryption stream error: %v", err) + tunnelErr = err return } } diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index fb2b3e118a1..87e4f29ff89 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -71,7 +71,7 @@ func NewVisitor( Name: cfg.GetBaseConfig().Name, Ctx: ctx, VnetController: helper.VNetController(), - HandleConn: func(conn net.Conn) { + SendConnToVisitor: func(conn net.Conn) { _ = baseVisitor.AcceptConn(conn) }, }, diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 353577db659..cdfeb1ab919 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -162,8 +162,16 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) isConnTransferred := false + var tunnelErr error defer func() { if !isConnTransferred { + // If there was an error and connection supports CloseWithError, use it + if tunnelErr != nil { + if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok { + _ = eConn.CloseWithError(tunnelErr) + return + } + } userConn.Close() } }() @@ -181,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { tunnelConn, err := sv.openTunnel(ctx) if err != nil { xl.Errorf("open tunnel error: %v", err) + tunnelErr = err + // no fallback, just return if sv.cfg.FallbackTo == "" { return @@ -200,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) if err != nil { xl.Errorf("create encryption stream error: %v", err) + tunnelErr = err return } } diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 67bd774f7a3..ef7fe67fddb 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -24,6 +24,7 @@ import ( "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/policy/security" ) var proxyTypes = []v1.ProxyType{ @@ -77,7 +78,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm fmt.Println(err) os.Exit(1) } - if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) } @@ -88,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "") + err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "") if err != nil { fmt.Println(err) os.Exit(1) @@ -106,7 +110,9 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) } @@ -117,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "") + err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "") if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index ee89c48947a..1c2d8d5eaae 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -21,6 +21,7 @@ import ( "os" "os/signal" "path/filepath" + "strings" "sync" "syscall" "time" @@ -31,7 +32,8 @@ import ( "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/featuregate" + "github.com/fatedier/frp/pkg/policy/featuregate" + "github.com/fatedier/frp/pkg/policy/security" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) @@ -41,6 +43,7 @@ var ( cfgDir string showVersion bool strictConfigMode bool + allowUnsafe []string ) func init() { @@ -48,6 +51,9 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors") + + rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", "))) } var rootCmd = &cobra.Command{ @@ -59,15 +65,17 @@ var rootCmd = &cobra.Command{ return nil } + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + // If cfgDir is not empty, run multiple frpc service for each config file in cfgDir. // Note that it's only designed for testing. It's not guaranteed to be stable. if cfgDir != "" { - _ = runMultipleClients(cfgDir) + _ = runMultipleClients(cfgDir, unsafeFeatures) return nil } // Do not show command usage here. - err := runClient(cfgFile) + err := runClient(cfgFile, unsafeFeatures) if err != nil { fmt.Println(err) os.Exit(1) @@ -76,7 +84,7 @@ var rootCmd = &cobra.Command{ }, } -func runMultipleClients(cfgDir string) error { +func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error { var wg sync.WaitGroup err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { @@ -86,7 +94,7 @@ func runMultipleClients(cfgDir string) error { time.Sleep(time.Millisecond) go func() { defer wg.Done() - err := runClient(path) + err := runClient(path, unsafeFeatures) if err != nil { fmt.Printf("frpc service error for config file [%s]\n", path) } @@ -111,7 +119,7 @@ func handleTermSignal(svr *client.Service) { svr.GracefulClose(500 * time.Millisecond) } -func runClient(cfgFilePath string) error { +func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error { cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) if err != nil { return err @@ -127,20 +135,22 @@ func runClient(cfgFilePath string) error { } } - warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs) + warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } if err != nil { return err } - return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath) + + return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath) } func startService( cfg *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures *security.UnsafeFeatures, cfgFile string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) @@ -153,6 +163,7 @@ func startService( Common: cfg, ProxyCfgs: proxyCfgs, VisitorCfgs: visitorCfgs, + UnsafeFeatures: unsafeFeatures, ConfigFilePath: cfgFile, }) if err != nil { diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index 4b971f531c7..830f7bf1118 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -22,6 +22,7 @@ import ( "github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/policy/security" ) func init() { @@ -42,7 +43,8 @@ var verifyCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/cmd/frps/root.go b/cmd/frps/root.go index c1bfc880624..e6ab008a343 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -18,12 +18,14 @@ import ( "context" "fmt" "os" + "strings" "github.com/spf13/cobra" "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/policy/security" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/server" @@ -33,6 +35,7 @@ var ( cfgFile string showVersion bool strictConfigMode bool + allowUnsafe []string serverCfg v1.ServerConfig ) @@ -41,6 +44,8 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps") rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors") + rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", "))) config.RegisterServerConfigFlags(rootCmd, &serverCfg) } @@ -77,7 +82,9 @@ var rootCmd = &cobra.Command{ svrCfg = &serverCfg } - warning, err := validation.ValidateServerConfig(svrCfg) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + warning, err := validator.ValidateServerConfig(svrCfg) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go index 33ad3f63229..7ddef1abcf6 100644 --- a/cmd/frps/verify.go +++ b/cmd/frps/verify.go @@ -22,6 +22,7 @@ import ( "github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/policy/security" ) func init() { @@ -42,7 +43,9 @@ var verifyCmd = &cobra.Command{ os.Exit(1) } - warning, err := validation.ValidateServerConfig(svrCfg) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + warning, err := validator.ValidateServerConfig(svrCfg) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 6b86907e459..ad7953beb56 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -143,6 +143,11 @@ transport.tls.enable = true # Default is empty, means all proxies. # start = ["ssh", "dns"] +# Alternative to 'start': You can control each proxy individually using the 'enabled' field. +# Set 'enabled = false' in a proxy configuration to disable it. +# If 'enabled' is not set or set to true, the proxy is enabled by default. +# The 'enabled' field provides more granular control and is recommended over 'start'. + # Specify udp packet size, unit is byte. If not set, the default value is 1500. # This parameter should be same between client and server. # It affects the udp and sudp proxy. @@ -169,6 +174,8 @@ metadatas.var2 = "123" # If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' name = "ssh" type = "tcp" +# Enable or disable this proxy. true or omit this field to enable, false to disable. +# enabled = true localIP = "127.0.0.1" localPort = 22 # Limit bandwidth for this proxy, unit is KB and MB @@ -253,6 +260,8 @@ healthCheck.httpHeaders=[ [[proxies]] name = "web02" type = "https" +# Disable this proxy by setting enabled to false +# enabled = false localIP = "127.0.0.1" localPort = 8000 subdomain = "web02" diff --git a/doc/pic/sponsor_daytona.png b/doc/pic/sponsor_daytona.png index a5271cc9502..e36b6bba4af 100644 Binary files a/doc/pic/sponsor_daytona.png and b/doc/pic/sponsor_daytona.png differ diff --git a/doc/pic/sponsor_lokal.png b/doc/pic/sponsor_lokal.png deleted file mode 100644 index 82386356fca..00000000000 Binary files a/doc/pic/sponsor_lokal.png and /dev/null differ diff --git a/doc/pic/sponsor_workos.png b/doc/pic/sponsor_workos.png deleted file mode 100644 index 5bc5e627d33..00000000000 Binary files a/doc/pic/sponsor_workos.png and /dev/null differ diff --git a/doc/pic/zsxq.jpg b/doc/pic/zsxq.jpg deleted file mode 100644 index 0bb1f1d516d..00000000000 Binary files a/doc/pic/zsxq.jpg and /dev/null differ diff --git a/go.mod b/go.mod index af633af4135..e23facdeab5 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pion/stun/v2 v2.0.0 github.com/pires/go-proxyproto v0.7.0 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.53.0 + github.com/quic-go/quic-go v0.55.0 github.com/rodaine/table v1.2.0 github.com/samber/lo v1.47.0 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 @@ -26,10 +26,10 @@ require ( github.com/tidwall/gjson v1.17.1 github.com/vishvananda/netlink v1.3.0 github.com/xtaci/kcp-go/v5 v5.6.13 - golang.org/x/crypto v0.37.0 - golang.org/x/net v0.39.0 + golang.org/x/crypto v0.41.0 + golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.13.0 + golang.org/x/sync v0.16.0 golang.org/x/time v0.5.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 gopkg.in/ini.v1 v1.67.0 @@ -67,11 +67,10 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index a411ff30581..f73117a3b9b 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI= -github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= @@ -156,24 +156,24 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -187,8 +187,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= @@ -197,8 +197,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -213,24 +213,24 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -241,8 +241,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index b954fc80eaa..366b62ef31e 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -15,6 +15,7 @@ package auth import ( + "context" "fmt" v1 "github.com/fatedier/frp/pkg/config/v1" @@ -27,14 +28,51 @@ type Setter interface { SetNewWorkConn(*msg.NewWorkConn) error } +type ClientAuth struct { + Setter Setter + key []byte +} + +func (a *ClientAuth) EncryptionKey() []byte { + return a.key +} + +// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime. +// Caller must run validation before calling this function. +func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) { + if cfg == nil { + return nil, fmt.Errorf("auth config is nil") + } + resolved := *cfg + if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil { + token, err := resolved.TokenSource.Resolve(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err) + } + resolved.Token = token + } + setter, err := NewAuthSetter(resolved) + if err != nil { + return nil, err + } + return &ClientAuth{ + Setter: setter, + key: []byte(resolved.Token), + }, nil +} + func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) { switch cfg.Method { case v1.AuthMethodToken: authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: - authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) - if err != nil { - return nil, err + if cfg.OIDC.TokenSource != nil { + authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource) + } else { + authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + if err != nil { + return nil, err + } } default: return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method) @@ -48,6 +86,35 @@ type Verifier interface { VerifyNewWorkConn(*msg.NewWorkConn) error } +type ServerAuth struct { + Verifier Verifier + key []byte +} + +func (a *ServerAuth) EncryptionKey() []byte { + return a.key +} + +// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime. +// Caller must run validation before calling this function. +func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) { + if cfg == nil { + return nil, fmt.Errorf("auth config is nil") + } + resolved := *cfg + if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil { + token, err := resolved.TokenSource.Resolve(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err) + } + resolved.Token = token + } + return &ServerAuth{ + Verifier: NewAuthVerifier(resolved), + key: []byte(resolved.Token), + }, nil +} + func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { switch cfg.Method { case v1.AuthMethodToken: diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index c5f636402c8..d9377f324c0 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -152,6 +152,51 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e return err } +type OidcTokenSourceAuthProvider struct { + additionalAuthScopes []v1.AuthScope + + valueSource *v1.ValueSource +} + +func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider { + return &OidcTokenSourceAuthProvider{ + additionalAuthScopes: additionalAuthScopes, + valueSource: valueSource, + } +} + +func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) { + ctx := context.Background() + accessToken, err = auth.valueSource.Resolve(ctx) + if err != nil { + return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err) + } + return +} + +func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) { + loginMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + pingMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + type TokenVerifier interface { Verify(context.Context, string) (*oidc.IDToken, error) } diff --git a/pkg/config/load.go b/pkg/config/load.go index 3852af9a886..6e8c251de02 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -281,6 +281,17 @@ func LoadClientConfig(path string, strict bool) ( }) } + // Filter by enabled field in each proxy + // nil or true means enabled, false means disabled + proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { + enabled := c.GetBaseConfig().Enabled + return enabled == nil || *enabled + }) + visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { + enabled := c.GetBaseConfig().Enabled + return enabled == nil || *enabled + }) + if cliCfg != nil { if err := cliCfg.Complete(); err != nil { return nil, nil, nil, isLegacyFormat, err diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index c6cf97a69be..2c5ccc6f1f9 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -15,8 +15,6 @@ package v1 import ( - "context" - "fmt" "os" "github.com/samber/lo" @@ -198,17 +196,6 @@ type AuthClientConfig struct { func (c *AuthClientConfig) Complete() error { c.Method = util.EmptyOr(c.Method, "token") - - // Resolve tokenSource during configuration loading - if c.Method == AuthMethodToken && c.TokenSource != nil { - token, err := c.TokenSource.Resolve(context.Background()) - if err != nil { - return fmt.Errorf("failed to resolve auth.tokenSource: %w", err) - } - // Move the resolved token to the Token field and clear TokenSource - c.Token = token - c.TokenSource = nil - } return nil } @@ -239,6 +226,10 @@ type AuthOIDCClientConfig struct { // Supports http, https, socks5, and socks5h proxy protocols. // If empty, no proxy is used for OIDC connections. ProxyURL string `json:"proxyURL,omitempty"` + + // TokenSource specifies a custom dynamic source for the authorization token. + // This is mutually exclusive with every other field of this structure. + TokenSource *ValueSource `json:"tokenSource,omitempty"` } type VirtualNetConfig struct { diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go index 120c4fd42a3..5473a5f6a76 100644 --- a/pkg/config/v1/client_test.go +++ b/pkg/config/v1/client_test.go @@ -15,8 +15,6 @@ package v1 import ( - "os" - "path/filepath" "testing" "github.com/samber/lo" @@ -38,68 +36,9 @@ func TestClientConfigComplete(t *testing.T) { } func TestAuthClientConfig_Complete(t *testing.T) { - // Create a temporary file for testing - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test_token") - testContent := "client-token-value" - err := os.WriteFile(testFile, []byte(testContent), 0o600) - require.NoError(t, err) - - tests := []struct { - name string - config AuthClientConfig - expectToken string - expectPanic bool - }{ - { - name: "tokenSource resolved to token", - config: AuthClientConfig{ - Method: AuthMethodToken, - TokenSource: &ValueSource{ - Type: "file", - File: &FileSource{ - Path: testFile, - }, - }, - }, - expectToken: testContent, - expectPanic: false, - }, - { - name: "direct token unchanged", - config: AuthClientConfig{ - Method: AuthMethodToken, - Token: "direct-token", - }, - expectToken: "direct-token", - expectPanic: false, - }, - { - name: "invalid tokenSource should panic", - config: AuthClientConfig{ - Method: AuthMethodToken, - TokenSource: &ValueSource{ - Type: "file", - File: &FileSource{ - Path: "/non/existent/file", - }, - }, - }, - expectPanic: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.expectPanic { - err := tt.config.Complete() - require.Error(t, err) - } else { - err := tt.config.Complete() - require.NoError(t, err) - require.Equal(t, tt.expectToken, tt.config.Token) - require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution") - } - }) - } + require := require.New(t) + cfg := &AuthClientConfig{} + err := cfg.Complete() + require.NoError(err) + require.EqualValues("token", cfg.Method) } diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 37701b6d785..1bbe5ac3c9c 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -108,8 +108,11 @@ type DomainConfig struct { } type ProxyBaseConfig struct { - Name string `json:"name"` - Type string `json:"type"` + Name string `json:"name"` + Type string `json:"type"` + // Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled. + // This allows individual control over each proxy, complementing the global "start" field. + Enabled *bool `json:"enabled,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` Transport ProxyTransport `json:"transport,omitempty"` // metadata info for each proxy diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 54aac080bd0..a92aac97aab 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -15,9 +15,6 @@ package v1 import ( - "context" - "fmt" - "github.com/samber/lo" "github.com/fatedier/frp/pkg/config/types" @@ -138,17 +135,6 @@ type AuthServerConfig struct { func (c *AuthServerConfig) Complete() error { c.Method = util.EmptyOr(c.Method, "token") - - // Resolve tokenSource during configuration loading - if c.Method == AuthMethodToken && c.TokenSource != nil { - token, err := c.TokenSource.Resolve(context.Background()) - if err != nil { - return fmt.Errorf("failed to resolve auth.tokenSource: %w", err) - } - // Move the resolved token to the Token field and clear TokenSource - c.Token = token - c.TokenSource = nil - } return nil } diff --git a/pkg/config/v1/server_test.go b/pkg/config/v1/server_test.go index 21d18fb7653..cd9381c509d 100644 --- a/pkg/config/v1/server_test.go +++ b/pkg/config/v1/server_test.go @@ -15,8 +15,6 @@ package v1 import ( - "os" - "path/filepath" "testing" "github.com/samber/lo" @@ -35,68 +33,9 @@ func TestServerConfigComplete(t *testing.T) { } func TestAuthServerConfig_Complete(t *testing.T) { - // Create a temporary file for testing - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test_token") - testContent := "file-token-value" - err := os.WriteFile(testFile, []byte(testContent), 0o600) - require.NoError(t, err) - - tests := []struct { - name string - config AuthServerConfig - expectToken string - expectPanic bool - }{ - { - name: "tokenSource resolved to token", - config: AuthServerConfig{ - Method: AuthMethodToken, - TokenSource: &ValueSource{ - Type: "file", - File: &FileSource{ - Path: testFile, - }, - }, - }, - expectToken: testContent, - expectPanic: false, - }, - { - name: "direct token unchanged", - config: AuthServerConfig{ - Method: AuthMethodToken, - Token: "direct-token", - }, - expectToken: "direct-token", - expectPanic: false, - }, - { - name: "invalid tokenSource should panic", - config: AuthServerConfig{ - Method: AuthMethodToken, - TokenSource: &ValueSource{ - Type: "file", - File: &FileSource{ - Path: "/non/existent/file", - }, - }, - }, - expectPanic: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.expectPanic { - err := tt.config.Complete() - require.Error(t, err) - } else { - err := tt.config.Complete() - require.NoError(t, err) - require.Equal(t, tt.expectToken, tt.config.Token) - require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution") - } - }) - } + require := require.New(t) + cfg := &AuthServerConfig{} + err := cfg.Complete() + require.NoError(err) + require.EqualValues("token", cfg.Method) } diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 0c8575c990c..eb4a0253dbf 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -23,55 +23,109 @@ import ( "github.com/samber/lo" v1 "github.com/fatedier/frp/pkg/config/v1" - "github.com/fatedier/frp/pkg/featuregate" + "github.com/fatedier/frp/pkg/policy/featuregate" + "github.com/fatedier/frp/pkg/policy/security" ) -func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { +func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { var ( warnings Warning errs error ) - // validate feature gates + + validators := []func() (Warning, error){ + func() (Warning, error) { return validateFeatureGates(c) }, + func() (Warning, error) { return v.validateAuthConfig(&c.Auth) }, + func() (Warning, error) { return nil, validateLogConfig(&c.Log) }, + func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) }, + func() (Warning, error) { return validateTransportConfig(&c.Transport) }, + func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) }, + } + + for _, validator := range validators { + w, err := validator() + warnings = AppendError(warnings, w) + errs = AppendError(errs, err) + } + return warnings, errs +} + +func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) { if c.VirtualNet.Address != "" { if !featuregate.Enabled(featuregate.VirtualNet) { - return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag") + return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag") } } + return nil, nil +} - if !slices.Contains(SupportedAuthMethods, c.Auth.Method) { +func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) { + var errs error + if !slices.Contains(SupportedAuthMethods, c.Method) { errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) } - if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { + if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) { errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) } // Validate token/tokenSource mutual exclusivity - if c.Auth.Token != "" && c.Auth.TokenSource != nil { + if c.Token != "" && c.TokenSource != nil { errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource")) } // Validate tokenSource if specified - if c.Auth.TokenSource != nil { - if err := c.Auth.TokenSource.Validate(); err != nil { + if c.TokenSource != nil { + if c.TokenSource.Type == "exec" { + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) + } + } + if err := c.TokenSource.Validate(); err != nil { errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) } } - if err := validateLogConfig(&c.Log); err != nil { + if err := v.validateOIDCConfig(&c.OIDC); err != nil { errs = AppendError(errs, err) } + return nil, errs +} - if err := validateWebServerConfig(&c.WebServer); err != nil { - errs = AppendError(errs, err) +func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error { + if c.TokenSource == nil { + return nil } + var errs error + // Validate oidc.tokenSource mutual exclusivity with other fields of oidc + if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" || + c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 || + c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" { + errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) + } + if c.TokenSource.Type == "exec" { + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) + } + } + if err := c.TokenSource.Validate(); err != nil { + errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err)) + } + return errs +} - if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 { - if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval { +func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + + if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 { + if c.HeartbeatTimeout < c.HeartbeatInterval { errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval")) } } - if !lo.FromPtr(c.Transport.TLS.Enable) { + if !lo.FromPtr(c.TLS.Enable) { checkTLSConfig := func(name string, value string) Warning { if value != "" { return fmt.Errorf("%s is invalid when transport.tls.enable is false", name) @@ -79,16 +133,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { 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)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile)) } - if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) { + if !slices.Contains(SupportedTransportProtocols, c.Protocol) { errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols)) } + return warnings, errs +} - for _, f := range c.IncludeConfigFiles { +func validateIncludeFiles(files []string) (Warning, error) { + var errs error + for _, f := range files { absDir, err := filepath.Abs(filepath.Dir(f)) if err != nil { errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err)) @@ -98,13 +156,19 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f)) } } - return warnings, errs + return nil, errs } -func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { +func ValidateAllClientConfig( + c *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures *security.UnsafeFeatures, +) (Warning, error) { + validator := NewConfigValidator(unsafeFeatures) var warnings Warning if c != nil { - warning, err := ValidateClientCommonConfig(c) + warning, err := validator.ValidateClientCommonConfig(c) warnings = AppendError(warnings, warning) if err != nil { return warnings, err diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go index 5694227299f..338ecc821b2 100644 --- a/pkg/config/v1/validation/server.go +++ b/pkg/config/v1/validation/server.go @@ -21,9 +21,10 @@ import ( "github.com/samber/lo" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/policy/security" ) -func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { +func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { var ( warnings Warning errs error @@ -42,6 +43,11 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { // Validate tokenSource if specified if c.Auth.TokenSource != nil { + if c.Auth.TokenSource.Type == "exec" { + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) + } + } if err := c.Auth.TokenSource.Validate(); err != nil { errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) } diff --git a/pkg/config/v1/validation/validator.go b/pkg/config/v1/validation/validator.go new file mode 100644 index 00000000000..1cfe3b21299 --- /dev/null +++ b/pkg/config/v1/validation/validator.go @@ -0,0 +1,28 @@ +package validation + +import ( + "fmt" + + "github.com/fatedier/frp/pkg/policy/security" +) + +// ConfigValidator holds the context dependencies for configuration validation. +type ConfigValidator struct { + unsafeFeatures *security.UnsafeFeatures +} + +// NewConfigValidator creates a new ConfigValidator instance. +func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator { + return &ConfigValidator{ + unsafeFeatures: unsafeFeatures, + } +} + +// ValidateUnsafeFeature checks if a specific unsafe feature is enabled. +func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error { + if !v.unsafeFeatures.IsEnabled(feature) { + return fmt.Errorf("unsafe feature %q is not enabled. "+ + "To enable it, ensure it is allowed in the configuration or command line flags", feature) + } + return nil +} diff --git a/pkg/config/v1/value_source.go b/pkg/config/v1/value_source.go index 624a2658965..88dbaff3641 100644 --- a/pkg/config/v1/value_source.go +++ b/pkg/config/v1/value_source.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "os" + "os/exec" "strings" ) @@ -27,6 +28,7 @@ import ( type ValueSource struct { Type string `json:"type"` File *FileSource `json:"file,omitempty"` + Exec *ExecSource `json:"exec,omitempty"` } // FileSource specifies how to load a value from a file. @@ -34,6 +36,18 @@ type FileSource struct { Path string `json:"path"` } +// ExecSource specifies how to get a value from another program launched as subprocess. +type ExecSource struct { + Command string `json:"command"` + Args []string `json:"args,omitempty"` + Env []ExecEnvVar `json:"env,omitempty"` +} + +type ExecEnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + // Validate validates the ValueSource configuration. func (v *ValueSource) Validate() error { if v == nil { @@ -46,8 +60,13 @@ func (v *ValueSource) Validate() error { return errors.New("file configuration is required when type is 'file'") } return v.File.Validate() + case "exec": + if v.Exec == nil { + return errors.New("exec configuration is required when type is 'exec'") + } + return v.Exec.Validate() default: - return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type) + return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type) } } @@ -60,6 +79,8 @@ func (v *ValueSource) Resolve(ctx context.Context) (string, error) { switch v.Type { case "file": return v.File.Resolve(ctx) + case "exec": + return v.Exec.Resolve(ctx) default: return "", fmt.Errorf("unsupported value source type: %s", v.Type) } @@ -91,3 +112,47 @@ func (f *FileSource) Resolve(_ context.Context) (string, error) { // Trim whitespace, which is important for file-based tokens return strings.TrimSpace(string(content)), nil } + +// Validate validates the ExecSource configuration. +func (e *ExecSource) Validate() error { + if e == nil { + return errors.New("execSource cannot be nil") + } + + if e.Command == "" { + return errors.New("exec command cannot be empty") + } + + for _, env := range e.Env { + if env.Name == "" { + return errors.New("exec env name cannot be empty") + } + if strings.Contains(env.Name, "=") { + return errors.New("exec env name cannot contain '='") + } + } + return nil +} + +// Resolve reads and returns the content captured from stdout of launched subprocess. +func (e *ExecSource) Resolve(ctx context.Context) (string, error) { + if err := e.Validate(); err != nil { + return "", err + } + + cmd := exec.CommandContext(ctx, e.Command, e.Args...) + if len(e.Env) != 0 { + cmd.Env = os.Environ() + for _, env := range e.Env { + cmd.Env = append(cmd.Env, env.Name+"="+env.Value) + } + } + + content, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err) + } + + // Trim whitespace, which is important for exec-based tokens + return strings.TrimSpace(string(content)), nil +} diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 7629875a6f2..5017f57d8d2 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -32,8 +32,11 @@ type VisitorTransport struct { } type VisitorBaseConfig struct { - Name string `json:"name"` - Type string `json:"type"` + Name string `json:"name"` + Type string `json:"type"` + // Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled. + // This allows individual control over each visitor, complementing the global "start" field. + Enabled *bool `json:"enabled,omitempty"` Transport VisitorTransport `json:"transport,omitempty"` SecretKey string `json:"secretKey,omitempty"` // if the server user is not set, it defaults to the current user diff --git a/pkg/msg/handler.go b/pkg/msg/handler.go index cb1eb15a307..243e599ab76 100644 --- a/pkg/msg/handler.go +++ b/pkg/msg/handler.go @@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error { } } -func (d *Dispatcher) SendChannel() chan Message { - return d.sendCh -} - func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) { d.msgHandlers[reflect.TypeOf(msg)] = handler } diff --git a/pkg/plugin/visitor/plugin.go b/pkg/plugin/visitor/plugin.go index 94adce093b0..27eecc82fd1 100644 --- a/pkg/plugin/visitor/plugin.go +++ b/pkg/plugin/visitor/plugin.go @@ -23,11 +23,20 @@ import ( "github.com/fatedier/frp/pkg/vnet" ) +// PluginContext provides the necessary context and callbacks for visitor plugins. type PluginContext struct { - Name string - Ctx context.Context + // Name is the unique identifier for this visitor, used for logging and routing. + Name string + + // Ctx manages the plugin's lifecycle and carries the logger for structured logging. + Ctx context.Context + + // VnetController manages TUN device routing. May be nil if virtual networking is disabled. VnetController *vnet.Controller - HandleConn func(net.Conn) + + // SendConnToVisitor sends a connection to the visitor's internal processing queue. + // Does not return error; failures are handled by closing the connection. + SendConnToVisitor func(net.Conn) } // Creators is used for create plugins to handle connections. diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go index f660c0c89e5..8193ce03cbd 100644 --- a/pkg/plugin/visitor/virtual_net.go +++ b/pkg/plugin/visitor/virtual_net.go @@ -42,6 +42,8 @@ type VirtualNetPlugin struct { controllerConn net.Conn closeSignal chan struct{} + consecutiveErrors int // Tracks consecutive connection errors for exponential backoff + ctx context.Context cancel context.CancelFunc } @@ -98,7 +100,6 @@ func (p *VirtualNetPlugin) Start() { func (p *VirtualNetPlugin) run() { xl := xlog.FromContextSafe(p.ctx) - reconnectDelay := 10 * time.Second for { currentCloseSignal := make(chan struct{}) @@ -121,7 +122,10 @@ func (p *VirtualNetPlugin) run() { p.controllerConn = controllerConn p.mu.Unlock() - pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() { + // Wrap with CloseNotifyConn which supports both close notification and error recording + var closeErr error + pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) { + closeErr = err close(currentCloseSignal) // Signal the run loop on close. }) @@ -129,9 +133,9 @@ func (p *VirtualNetPlugin) run() { p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn) xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name) - // Pass the CloseNotifyConn to HandleConn. - // HandleConn is responsible for calling Close() on pluginNotifyConn. - p.pluginCtx.HandleConn(pluginNotifyConn) + // Pass the CloseNotifyConn to the visitor for handling. + // The visitor can call CloseWithError to record the failure reason. + p.pluginCtx.SendConnToVisitor(pluginNotifyConn) // Wait for context cancellation or connection close. select { @@ -140,8 +144,32 @@ func (p *VirtualNetPlugin) run() { p.cleanupControllerConn(xl) return case <-currentCloseSignal: - xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name) - // HandleConn closed the plugin side. Close the controller side. + // Determine reconnect delay based on error with exponential backoff + var reconnectDelay time.Duration + if closeErr != nil { + p.consecutiveErrors++ + xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v", + p.pluginCtx.Name, p.consecutiveErrors, closeErr) + + // Exponential backoff: 60s, 120s, 240s, 300s (capped) + baseDelay := 60 * time.Second + reconnectDelay = baseDelay * time.Duration(1< 300*time.Second { + reconnectDelay = 300 * time.Second + } + } else { + // Reset consecutive errors on successful connection + if p.consecutiveErrors > 0 { + xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)", + p.pluginCtx.Name, p.consecutiveErrors) + p.consecutiveErrors = 0 + } else { + xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name) + } + reconnectDelay = 10 * time.Second + } + + // The visitor closed the plugin side. Close the controller side. p.cleanupControllerConn(xl) xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name) @@ -184,7 +212,7 @@ func (p *VirtualNetPlugin) Close() error { } // Explicitly close the controller side of the pipe. - // This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end. + // This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end. p.cleanupControllerConn(xl) xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) diff --git a/pkg/featuregate/feature_gate.go b/pkg/policy/featuregate/feature_gate.go similarity index 100% rename from pkg/featuregate/feature_gate.go rename to pkg/policy/featuregate/feature_gate.go diff --git a/pkg/policy/security/unsafe.go b/pkg/policy/security/unsafe.go new file mode 100644 index 00000000000..b3f84a85278 --- /dev/null +++ b/pkg/policy/security/unsafe.go @@ -0,0 +1,34 @@ +package security + +const ( + TokenSourceExec = "TokenSourceExec" +) + +var ( + ClientUnsafeFeatures = []string{ + TokenSourceExec, + } + + ServerUnsafeFeatures = []string{ + TokenSourceExec, + } +) + +type UnsafeFeatures struct { + features map[string]bool +} + +func NewUnsafeFeatures(allowed []string) *UnsafeFeatures { + features := make(map[string]bool) + for _, f := range allowed { + features[f] = true + } + return &UnsafeFeatures{features: features} +} + +func (u *UnsafeFeatures) IsEnabled(feature string) bool { + if u == nil { + return false + } + return u.features[feature] +} diff --git a/pkg/transport/message.go b/pkg/transport/message.go index dd43fbdc0ed..40165f5dc4a 100644 --- a/pkg/transport/message.go +++ b/pkg/transport/message.go @@ -35,15 +35,19 @@ type MessageTransporter interface { DispatchWithType(m msg.Message, msgType, laneKey string) bool } -func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter { +type MessageSender interface { + Send(msg.Message) error +} + +func NewMessageTransporter(sender MessageSender) MessageTransporter { return &transporterImpl{ - sendCh: sendCh, + sender: sender, registry: make(map[string]map[string]chan msg.Message), } } type transporterImpl struct { - sendCh chan msg.Message + sender MessageSender // First key is message type and second key is lane key. // Dispatch will dispatch message to related channel by its message type @@ -53,9 +57,7 @@ type transporterImpl struct { } func (impl *transporterImpl) Send(m msg.Message) error { - return errors.PanicToError(func() { - impl.sendCh <- m - }) + return impl.sender.Send(m) } func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) { diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index 20468f1b8c5..914a7bb58e1 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -135,11 +135,11 @@ type CloseNotifyConn struct { // 1 means closed closeFlag int32 - closeFn func() + closeFn func(error) } -// closeFn will be only called once -func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn { +// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called) +func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn { return &CloseNotifyConn{ Conn: c, closeFn: closeFn, @@ -149,14 +149,27 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn { func (cc *CloseNotifyConn) Close() (err error) { pflag := atomic.SwapInt32(&cc.closeFlag, 1) if pflag == 0 { - err = cc.Close() + err = cc.Conn.Close() if cc.closeFn != nil { - cc.closeFn() + cc.closeFn(nil) } } return } +// CloseWithError closes the connection and passes the error to the close callback. +func (cc *CloseNotifyConn) CloseWithError(err error) error { + pflag := atomic.SwapInt32(&cc.closeFlag, 1) + if pflag == 0 { + closeErr := cc.Conn.Close() + if cc.closeFn != nil { + cc.closeFn(err) + } + return closeErr + } + return nil +} + type StatsConn struct { net.Conn diff --git a/pkg/util/net/websocket.go b/pkg/util/net/websocket.go index 263b3a1d98d..3ca8b332299 100644 --- a/pkg/util/net/websocket.go +++ b/pkg/util/net/websocket.go @@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) { muxer := http.NewServeMux() muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) { notifyCh := make(chan struct{}) - conn := WrapCloseNotifyConn(c, func() { + conn := WrapCloseNotifyConn(c, func(_ error) { close(notifyCh) }) wl.acceptCh <- conn diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 0f4ec433898..65f30f06c9a 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.65.0" +var version = "0.66.0" func Full() string { return version diff --git a/server/control.go b/server/control.go index b70d8d1268a..af9c9de36a6 100644 --- a/server/control.go +++ b/server/control.go @@ -106,6 +106,8 @@ type Control struct { // verifies authentication based on selected method authVerifier auth.Verifier + // key used for connection encryption + encryptionKey []byte // other components can use this to communicate with client msgTransporter transport.MessageTransporter @@ -157,6 +159,7 @@ func NewControl( pxyManager *proxy.Manager, pluginManager *plugin.Manager, authVerifier auth.Verifier, + encryptionKey []byte, ctlConn net.Conn, ctlConnEncrypted bool, loginMsg *msg.Login, @@ -171,6 +174,7 @@ func NewControl( pxyManager: pxyManager, pluginManager: pluginManager, authVerifier: authVerifier, + encryptionKey: encryptionKey, conn: ctlConn, loginMsg: loginMsg, workConnCh: make(chan net.Conn, poolCount+10), @@ -186,7 +190,7 @@ func NewControl( ctl.lastPing.Store(time.Now()) if ctlConnEncrypted { - cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) + cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey) if err != nil { return nil, err } @@ -195,7 +199,7 @@ func NewControl( ctl.msgDispatcher = msg.NewDispatcher(ctl.conn) } ctl.registerMsgHandlers() - ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) + ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) return ctl, nil } @@ -478,6 +482,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err GetWorkConnFn: ctl.GetWorkConn, Configurer: pxyConf, ServerCfg: ctl.serverCfg, + EncryptionKey: ctl.encryptionKey, }) if err != nil { return remoteAddr, err diff --git a/server/controller/resource.go b/server/controller/resource.go index 9d14b18dfbe..717c961676b 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -35,6 +35,9 @@ type ResourceController struct { // HTTP Group Controller HTTPGroupCtl *group.HTTPGroupController + // HTTPS Group Controller + HTTPSGroupCtl *group.HTTPSGroupController + // TCP Mux Group Controller TCPMuxGroupCtl *group.TCPMuxGroupCtl diff --git a/server/group/https.go b/server/group/https.go new file mode 100644 index 00000000000..4089b0cbd01 --- /dev/null +++ b/server/group/https.go @@ -0,0 +1,197 @@ +// Copyright 2025 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 group + +import ( + "context" + "net" + "sync" + + gerr "github.com/fatedier/golib/errors" + + "github.com/fatedier/frp/pkg/util/vhost" +) + +type HTTPSGroupController struct { + groups map[string]*HTTPSGroup + + httpsMuxer *vhost.HTTPSMuxer + + mu sync.Mutex +} + +func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController { + return &HTTPSGroupController{ + groups: make(map[string]*HTTPSGroup), + httpsMuxer: httpsMuxer, + } +} + +func (ctl *HTTPSGroupController) Listen( + ctx context.Context, + group, groupKey string, + routeConfig vhost.RouteConfig, +) (l net.Listener, err error) { + indexKey := group + ctl.mu.Lock() + g, ok := ctl.groups[indexKey] + if !ok { + g = NewHTTPSGroup(ctl) + ctl.groups[indexKey] = g + } + ctl.mu.Unlock() + + return g.Listen(ctx, group, groupKey, routeConfig) +} + +func (ctl *HTTPSGroupController) RemoveGroup(group string) { + ctl.mu.Lock() + defer ctl.mu.Unlock() + delete(ctl.groups, group) +} + +type HTTPSGroup struct { + group string + groupKey string + domain string + + acceptCh chan net.Conn + httpsLn *vhost.Listener + lns []*HTTPSGroupListener + ctl *HTTPSGroupController + mu sync.Mutex +} + +func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup { + return &HTTPSGroup{ + lns: make([]*HTTPSGroupListener, 0), + ctl: ctl, + acceptCh: make(chan net.Conn), + } +} + +func (g *HTTPSGroup) Listen( + ctx context.Context, + group, groupKey string, + routeConfig vhost.RouteConfig, +) (ln *HTTPSGroupListener, err error) { + g.mu.Lock() + defer g.mu.Unlock() + if len(g.lns) == 0 { + // the first listener, listen on the real address + httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig) + if errRet != nil { + return nil, errRet + } + ln = newHTTPSGroupListener(group, g, httpsLn.Addr()) + + g.group = group + g.groupKey = groupKey + g.domain = routeConfig.Domain + g.httpsLn = httpsLn + g.lns = append(g.lns, ln) + go g.worker() + } else { + // route config in the same group must be equal + if g.group != group || g.domain != routeConfig.Domain { + return nil, ErrGroupParamsInvalid + } + if g.groupKey != groupKey { + return nil, ErrGroupAuthFailed + } + ln = newHTTPSGroupListener(group, g, g.lns[0].Addr()) + g.lns = append(g.lns, ln) + } + return +} + +func (g *HTTPSGroup) worker() { + for { + c, err := g.httpsLn.Accept() + if err != nil { + return + } + err = gerr.PanicToError(func() { + g.acceptCh <- c + }) + if err != nil { + return + } + } +} + +func (g *HTTPSGroup) Accept() <-chan net.Conn { + return g.acceptCh +} + +func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) { + g.mu.Lock() + defer g.mu.Unlock() + for i, tmpLn := range g.lns { + if tmpLn == ln { + g.lns = append(g.lns[:i], g.lns[i+1:]...) + break + } + } + if len(g.lns) == 0 { + close(g.acceptCh) + if g.httpsLn != nil { + g.httpsLn.Close() + } + g.ctl.RemoveGroup(g.group) + } +} + +type HTTPSGroupListener struct { + groupName string + group *HTTPSGroup + + addr net.Addr + closeCh chan struct{} +} + +func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener { + return &HTTPSGroupListener{ + groupName: name, + group: group, + addr: addr, + closeCh: make(chan struct{}), + } +} + +func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) { + var ok bool + select { + case <-ln.closeCh: + return nil, ErrListenerClosed + case c, ok = <-ln.group.Accept(): + if !ok { + return nil, ErrListenerClosed + } + return c, nil + } +} + +func (ln *HTTPSGroupListener) Addr() net.Addr { + return ln.addr +} + +func (ln *HTTPSGroupListener) Close() (err error) { + close(ln.closeCh) + + // remove self from HTTPSGroup + ln.group.CloseListener(ln) + return +} diff --git a/server/proxy/http.go b/server/proxy/http.go index 9a02dcddce3..2c4f1fd4c8d 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -165,7 +165,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err var rwc io.ReadWriteCloser = tmpConn if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { xl.Errorf("create encryption stream error: %v", err) return diff --git a/server/proxy/https.go b/server/proxy/https.go index 4575ac13d08..f137ea7ad6d 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -15,6 +15,7 @@ package proxy import ( + "net" "reflect" "strings" @@ -58,27 +59,24 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { continue } - routeConfig.Domain = domain - l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig) - if errRet != nil { - err = errRet - return + l, err := pxy.listenForDomain(routeConfig, domain) + if err != nil { + return "", err } - xl.Infof("https proxy listen for host [%s]", routeConfig.Domain) pxy.listeners = append(pxy.listeners, l) - addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort)) + addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort)) + xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group) } if pxy.cfg.SubDomain != "" { - routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost - l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig) - if errRet != nil { - err = errRet - return + domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost + l, err := pxy.listenForDomain(routeConfig, domain) + if err != nil { + return "", err } - xl.Infof("https proxy listen for host [%s]", routeConfig.Domain) pxy.listeners = append(pxy.listeners, l) - addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort)) + addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort)) + xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group) } pxy.startCommonTCPListenersHandler() @@ -89,3 +87,18 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { func (pxy *HTTPSProxy) Close() { pxy.BaseProxy.Close() } + +func (pxy *HTTPSProxy) listenForDomain(routeConfig *vhost.RouteConfig, domain string) (net.Listener, error) { + tmpRouteConfig := *routeConfig + tmpRouteConfig.Domain = domain + + if pxy.cfg.LoadBalancer.Group != "" { + return pxy.rc.HTTPSGroupCtl.Listen( + pxy.ctx, + pxy.cfg.LoadBalancer.Group, + pxy.cfg.LoadBalancer.GroupKey, + tmpRouteConfig, + ) + } + return pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &tmpRouteConfig) +} diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index c7c18c32223..564eca282c4 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -68,6 +68,7 @@ type BaseProxy struct { poolCount int getWorkConnFn GetWorkConnFn serverCfg *v1.ServerConfig + encryptionKey []byte limiter *rate.Limiter userInfo plugin.UserInfo loginMsg *msg.Login @@ -213,7 +214,6 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { xl := xlog.FromContextSafe(pxy.Context()) defer userConn.Close() - serverCfg := pxy.serverCfg cfg := pxy.configurer.GetBaseConfig() // server plugin hook rc := pxy.GetResourceController() @@ -240,7 +240,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { xl.Tracef("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)) + local, err = libio.WithEncryption(local, pxy.encryptionKey) if err != nil { xl.Errorf("create encryption stream error: %v", err) return @@ -279,6 +279,7 @@ type Options struct { GetWorkConnFn GetWorkConnFn Configurer v1.ProxyConfigurer ServerCfg *v1.ServerConfig + EncryptionKey []byte } func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { @@ -298,6 +299,7 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { poolCount: options.PoolCount, getWorkConnFn: options.GetWorkConnFn, serverCfg: options.ServerCfg, + encryptionKey: options.EncryptionKey, limiter: limiter, xl: xl, ctx: xlog.NewContext(ctx, xl), diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 53a07d52676..3751dc9bfad 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -205,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { var rwc io.ReadWriteCloser = workConn if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { xl.Errorf("create encryption stream error: %v", err) workConn.Close() diff --git a/server/service.go b/server/service.go index 7ca80dc89ba..de3af837610 100644 --- a/server/service.go +++ b/server/service.go @@ -113,8 +113,8 @@ type Service struct { sshTunnelGateway *ssh.Gateway - // Verifies authentication based on selected method - authVerifier auth.Verifier + // Auth runtime and encryption materials + auth *auth.ServerAuth tlsConfig *tls.Config @@ -149,6 +149,11 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { } } + authRuntime, err := auth.BuildServerAuth(&cfg.Auth) + if err != nil { + return nil, err + } + svr := &Service{ ctlManager: NewControlManager(), pxyManager: proxy.NewManager(), @@ -160,7 +165,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { }, sshTunnelListener: netpkg.NewInternalListener(), httpVhostRouter: vhost.NewRouters(), - authVerifier: auth.NewAuthVerifier(cfg.Auth), + auth: authRuntime, webServer: webServer, tlsConfig: tlsConfig, cfg: cfg, @@ -322,6 +327,9 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { if err != nil { return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err) } + + // Init HTTPS group controller after HTTPSMuxer is created + svr.rc.HTTPSGroupCtl = group.NewHTTPSGroupController(svr.rc.VhostHTTPSMuxer) } // frp tls listener @@ -583,7 +591,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch) // Check auth. - authVerifier := svr.authVerifier + authVerifier := svr.auth.Verifier if internal && loginMsg.ClientSpec.AlwaysAuthPass { authVerifier = auth.AlwaysPassVerifier } @@ -592,7 +600,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter } // TODO(fatedier): use SessionContext - ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg) + ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg) if err != nil { xl.Warnf("create new controller error: %v", err) // don't return detailed errors to client diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 10b3611bf6e..0b837e39fa9 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -75,8 +75,8 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { if err != nil { return p, p.StdOutput(), err } - // sleep for a while to get std output - time.Sleep(2 * time.Second) + // Give frps extra time to finish binding ports before proceeding. + time.Sleep(4 * time.Second) return p, p.StdOutput(), nil } diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go index fe0c957ba8b..f6bb1856e38 100644 --- a/test/e2e/v1/features/group.go +++ b/test/e2e/v1/features/group.go @@ -1,6 +1,7 @@ package features import ( + "crypto/tls" "fmt" "strconv" "sync" @@ -8,6 +9,7 @@ import ( "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/httpserver" @@ -112,6 +114,80 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) }) + + ginkgo.It("HTTPS", func() { + vhostHTTPSPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + clientConf := consts.DefaultClientConfig + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + + fooPort := f.AllocPort() + fooServer := httpserver.New( + httpserver.WithBindPort(fooPort), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("foo"))), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := httpserver.New( + httpserver.WithBindPort(barPort), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("bar"))), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "https" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "https" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, barPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + fooCount := 0 + barCount := 0 + for i := 0; i < 10; i++ { + framework.NewRequestExpect(f). + Explain("times " + strconv.Itoa(i)). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) }) ginkgo.Describe("Health Check", func() {