From 69cc422edf695a7f1f524e427fa175aee79ac3e3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Jul 2024 14:28:17 +0800 Subject: [PATCH 01/83] client plugin: added plugin tls2raw (#4341) --- .golangci.yml | 1 + Release.md | 7 +-- client/proxy/proxy.go | 2 +- conf/frpc_full_example.toml | 10 +++ pkg/config/v1/plugin.go | 11 ++++ pkg/config/v1/validation/plugin.go | 9 +++ pkg/plugin/client/http2http.go | 5 +- pkg/plugin/client/http2https.go | 3 +- pkg/plugin/client/http_proxy.go | 3 +- pkg/plugin/client/https2http.go | 24 +------ pkg/plugin/client/https2https.go | 24 +------ pkg/plugin/client/plugin.go | 3 +- pkg/plugin/client/socks5.go | 3 +- pkg/plugin/client/static_file.go | 3 +- pkg/plugin/client/tls2raw.go | 83 +++++++++++++++++++++++++ pkg/plugin/client/unix_domain_socket.go | 6 +- test/e2e/v1/plugin/client.go | 46 ++++++++++++++ 17 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 pkg/plugin/client/tls2raw.go diff --git a/.golangci.yml b/.golangci.yml index 2b2ccb08030..fb625c9b98c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -88,6 +88,7 @@ linters-settings: excludes: - G401 - G402 + - G404 - G501 issues: diff --git a/Release.md b/Release.md index 6e142deff52..1f6118fd556 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,3 @@ ### Features -* Added a new plugin "http2http" which allows forwarding HTTP requests to another HTTP server, supporting options like local address binding, host header rewrite, and custom request headers. -* Added `enableHTTP2` option to control whether to enable HTTP/2 in plugin https2http and https2https, default is true. - -### Changes - -* Plugin https2http & https2https: return 421 `Misdirected Request` if host not match sni. +* Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service. diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index cc02f809a9c..5cb5ccf2ab4 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -192,7 +192,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor if pxy.proxyPlugin != nil { // if plugin is set, let plugin handle connection first xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name()) - pxy.proxyPlugin.Handle(remote, workConn, &extraInfo) + pxy.proxyPlugin.Handle(pxy.ctx, remote, workConn, &extraInfo) xl.Debugf("handle by plugin finished") return } diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 51b89c2a536..3bf868663b3 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -325,6 +325,16 @@ localAddr = "127.0.0.1:80" hostHeaderRewrite = "127.0.0.1" requestHeaders.set.x-from-where = "frp" +[[proxies]] +name = "plugin_tls2raw" +type = "https" +remotePort = 6008 +[proxies.plugin] +type = "tls2raw" +localAddr = "127.0.0.1:80" +crtPath = "./server.crt" +keyPath = "./server.key" + [[proxies]] name = "secret_tcp" # If the type is secret tcp, remotePort is useless diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go index 7ae4a4d4233..cdf3cf263e9 100644 --- a/pkg/config/v1/plugin.go +++ b/pkg/config/v1/plugin.go @@ -83,6 +83,7 @@ const ( PluginSocks5 = "socks5" PluginStaticFile = "static_file" PluginUnixDomainSocket = "unix_domain_socket" + PluginTLS2Raw = "tls2raw" ) var clientPluginOptionsTypeMap = map[string]reflect.Type{ @@ -94,6 +95,7 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{ PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), + PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), } type HTTP2HTTPSPluginOptions struct { @@ -174,3 +176,12 @@ type UnixDomainSocketPluginOptions struct { } func (o *UnixDomainSocketPluginOptions) Complete() {} + +type TLS2RawPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +func (o *TLS2RawPluginOptions) Complete() {} diff --git a/pkg/config/v1/validation/plugin.go b/pkg/config/v1/validation/plugin.go index 9c88d142cb2..30a66d83728 100644 --- a/pkg/config/v1/validation/plugin.go +++ b/pkg/config/v1/validation/plugin.go @@ -32,6 +32,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error { return validateStaticFilePluginOptions(v) case *v1.UnixDomainSocketPluginOptions: return validateUnixDomainSocketPluginOptions(v) + case *v1.TLS2RawPluginOptions: + return validateTLS2RawPluginOptions(v) } return nil } @@ -70,3 +72,10 @@ func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions) } return nil } + +func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go index 689b90b6d4a..d7c4c7e492a 100644 --- a/pkg/plugin/client/http2http.go +++ b/pkg/plugin/client/http2http.go @@ -12,9 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( + "context" "io" stdlog "log" "net" @@ -77,7 +80,7 @@ func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTP2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (p *HTTP2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index b9a799bb861..66f9098912c 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -17,6 +17,7 @@ package plugin import ( + "context" "crypto/tls" "io" stdlog "log" @@ -88,7 +89,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go index ed8d97ec521..b7491bd1082 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -18,6 +18,7 @@ package plugin import ( "bufio" + "context" "encoding/base64" "io" "net" @@ -68,7 +69,7 @@ func (hp *HTTPProxy) Name() string { return v1.PluginHTTPProxy } -func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (hp *HTTPProxy) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) sc, rd := libnet.NewSharedConn(wrapConn) diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index 3bb12c2252b..9632a6fbc80 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -17,6 +17,7 @@ package plugin import ( + "context" "crypto/tls" "fmt" "io" @@ -85,16 +86,7 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { rp.ServeHTTP(w, r) }) - var ( - tlsConfig *tls.Config - err error - ) - if opts.CrtPath != "" || opts.KeyPath != "" { - tlsConfig, err = p.genTLSConfig() - } else { - tlsConfig, err = transport.NewServerTLSConfig("", "", "") - tlsConfig.InsecureSkipVerify = true - } + tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "") if err != nil { return nil, fmt.Errorf("gen TLS config error: %v", err) } @@ -114,17 +106,7 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath) - if err != nil { - return nil, err - } - - config := &tls.Config{Certificates: []tls.Certificate{cert}} - return config, nil -} - -func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { +func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) if extra.SrcAddr != nil { wrapConn.SetRemoteAddr(extra.SrcAddr) diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index c315c8e3244..8121e0943de 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -17,6 +17,7 @@ package plugin import ( + "context" "crypto/tls" "fmt" "io" @@ -91,16 +92,7 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { rp.ServeHTTP(w, r) }) - var ( - tlsConfig *tls.Config - err error - ) - if opts.CrtPath != "" || opts.KeyPath != "" { - tlsConfig, err = p.genTLSConfig() - } else { - tlsConfig, err = transport.NewServerTLSConfig("", "", "") - tlsConfig.InsecureSkipVerify = true - } + tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "") if err != nil { return nil, fmt.Errorf("gen TLS config error: %v", err) } @@ -120,17 +112,7 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath) - if err != nil { - return nil, err - } - - config := &tls.Config{Certificates: []tls.Certificate{cert}} - return config, nil -} - -func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { +func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) if extra.SrcAddr != nil { wrapConn.SetRemoteAddr(extra.SrcAddr) diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go index 0e5548e9f4e..3dce8592629 100644 --- a/pkg/plugin/client/plugin.go +++ b/pkg/plugin/client/plugin.go @@ -15,6 +15,7 @@ package plugin import ( + "context" "fmt" "io" "net" @@ -57,7 +58,7 @@ type ExtraInfo struct { type Plugin interface { Name() string - Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) + Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) Close() error } diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go index a230bf55bdb..7478ebe1414 100644 --- a/pkg/plugin/client/socks5.go +++ b/pkg/plugin/client/socks5.go @@ -17,6 +17,7 @@ package plugin import ( + "context" "io" "log" "net" @@ -50,7 +51,7 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { return } -func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (sp *Socks5Plugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { defer conn.Close() wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = sp.Server.ServeConn(wrapConn) diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go index 493710205c4..02cb9930728 100644 --- a/pkg/plugin/client/static_file.go +++ b/pkg/plugin/client/static_file.go @@ -17,6 +17,7 @@ package plugin import ( + "context" "io" "net" "net/http" @@ -69,7 +70,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { return sp, nil } -func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (sp *StaticFilePlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = sp.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/tls2raw.go b/pkg/plugin/client/tls2raw.go new file mode 100644 index 00000000000..adcc77417d8 --- /dev/null +++ b/pkg/plugin/client/tls2raw.go @@ -0,0 +1,83 @@ +// Copyright 2024 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. + +//go:build !frps + +package plugin + +import ( + "context" + "crypto/tls" + "io" + "net" + + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func init() { + Register(v1.PluginTLS2Raw, NewTLS2RawPlugin) +} + +type TLS2RawPlugin struct { + opts *v1.TLS2RawPluginOptions + + tlsConfig *tls.Config +} + +func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.TLS2RawPluginOptions) + + p := &TLS2RawPlugin{ + opts: opts, + } + + tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "") + if err != nil { + return nil, err + } + p.tlsConfig = tlsConfig + return p, nil +} + +func (p *TLS2RawPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { + xl := xlog.FromContextSafe(ctx) + + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) + tlsConn := tls.Server(wrapConn, p.tlsConfig) + + if err := tlsConn.Handshake(); err != nil { + xl.Warnf("tls handshake error: %v", err) + return + } + rawConn, err := net.Dial("tcp", p.opts.LocalAddr) + if err != nil { + xl.Warnf("dial to local addr error: %v", err) + return + } + + libio.Join(tlsConn, rawConn) +} + +func (p *TLS2RawPlugin) Name() string { + return v1.PluginTLS2Raw +} + +func (p *TLS2RawPlugin) Close() error { + return nil +} diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index df68ffb469d..b6aa6075040 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -17,12 +17,14 @@ package plugin import ( + "context" "io" "net" libio "github.com/fatedier/golib/io" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/xlog" ) func init() { @@ -48,9 +50,11 @@ func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err er return } -func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) { +func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) { + xl := xlog.FromContextSafe(ctx) localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) if err != nil { + xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) return } if extra.ProxyProtocolHeader != nil { diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go index 3499e882d72..73e2d863442 100644 --- a/test/e2e/v1/plugin/client.go +++ b/test/e2e/v1/plugin/client.go @@ -402,4 +402,50 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { Ensure() }) }) + + ginkgo.It("tls2raw", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("tls2raw_server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("tls2raw_server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "tls2raw-test" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "tls2raw" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) }) From e8045194cd133b0981ad7ab781095e9ba76795cf Mon Sep 17 00:00:00 2001 From: Yurun Date: Tue, 30 Jul 2024 11:19:26 +0800 Subject: [PATCH 02/83] Fix loginFailExit = false bug (#4354) * Fixed the issue that when loginFailExit = false, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup * Update Release.md --- Release.md | 2 ++ client/service.go | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Release.md b/Release.md index 1f6118fd556..c1649b92e2f 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,5 @@ ### Features * Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service. + +* Fixed the issue that when `loginFailExit = false`, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup. diff --git a/client/service.go b/client/service.go index 0cbd8757e84..b06706a6a48 100644 --- a/client/service.go +++ b/client/service.go @@ -169,6 +169,15 @@ func (svr *Service) Run(ctx context.Context) error { netpkg.SetDefaultDNSAddress(svr.common.DNSServer) } + if svr.webServer != nil { + go func() { + log.Infof("admin server listen on %s", svr.webServer.Address()) + if err := svr.webServer.Run(); err != nil { + log.Warnf("admin server exit with error: %v", err) + } + }() + } + // first login to frps svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit)) if svr.ctl == nil { @@ -179,14 +188,6 @@ func (svr *Service) Run(ctx context.Context) error { go svr.keepControllerWorking() - if svr.webServer != nil { - go func() { - log.Infof("admin server listen on %s", svr.webServer.Address()) - if err := svr.webServer.Run(); err != nil { - log.Warnf("admin server exit with error: %v", err) - } - }() - } <-svr.ctx.Done() svr.stop() return nil From ae73ec2fedc13cc2c44039dabba719a318221be5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 30 Jul 2024 18:12:22 +0800 Subject: [PATCH 03/83] added a 30s timeout for frpc subcommands to avoid long delays (#4359) --- README.md | 5 ---- README_zh.md | 5 ---- Release.md | 3 +++ cmd/frpc/sub/admin.go | 46 +++++++++++++++++++-------------- pkg/sdk/client/client.go | 25 +++++++++--------- pkg/util/version/version.go | 2 +- test/e2e/legacy/basic/client.go | 9 ++++--- test/e2e/legacy/basic/server.go | 5 ++-- test/e2e/v1/basic/client.go | 9 ++++--- test/e2e/v1/basic/config.go | 3 ++- test/e2e/v1/basic/server.go | 5 ++-- 11 files changed, 62 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 915e1e8e07b..97f148364ce 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,6 @@

Gold Sponsors

-

- - - -

diff --git a/README_zh.md b/README_zh.md index 95ede3ee6bb..a07e840cc6c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -11,11 +11,6 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP

Gold Sponsors

-

- - - -

diff --git a/Release.md b/Release.md index c1649b92e2f..d0414efec04 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,8 @@ ### Features * Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service. +* Added a default timeout of 30 seconds for the frpc subcommands to prevent commands from being stuck for a long time due to network issues. + +### Fixes * Fixed the issue that when `loginFailExit = false`, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup. diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go index 5d478d44a58..abe8063585c 100644 --- a/cmd/frpc/sub/admin.go +++ b/cmd/frpc/sub/admin.go @@ -15,9 +15,11 @@ package sub import ( + "context" "fmt" "os" "strings" + "time" "github.com/rodaine/table" "github.com/spf13/cobra" @@ -27,24 +29,24 @@ import ( clientsdk "github.com/fatedier/frp/pkg/sdk/client" ) -func init() { - rootCmd.AddCommand(NewAdminCommand( - "reload", - "Hot-Reload frpc configuration", - ReloadHandler, - )) +var adminAPITimeout = 30 * time.Second - rootCmd.AddCommand(NewAdminCommand( - "status", - "Overview of all proxies status", - StatusHandler, - )) +func init() { + commands := []struct { + name string + description string + handler func(*v1.ClientCommonConfig) error + }{ + {"reload", "Hot-Reload frpc configuration", ReloadHandler}, + {"status", "Overview of all proxies status", StatusHandler}, + {"stop", "Stop the running frpc", StopHandler}, + } - rootCmd.AddCommand(NewAdminCommand( - "stop", - "Stop the running frpc", - StopHandler, - )) + for _, cmdConfig := range commands { + cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler) + cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls") + rootCmd.AddCommand(cmd) + } } func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command { @@ -73,7 +75,9 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) - if err := client.Reload(strictConfigMode); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + if err := client.Reload(ctx, strictConfigMode); err != nil { return err } fmt.Println("reload success") @@ -83,7 +87,9 @@ func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { func StatusHandler(clientCfg *v1.ClientCommonConfig) error { client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) - res, err := client.GetAllProxyStatus() + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + res, err := client.GetAllProxyStatus(ctx) if err != nil { return err } @@ -109,7 +115,9 @@ func StatusHandler(clientCfg *v1.ClientCommonConfig) error { func StopHandler(clientCfg *v1.ClientCommonConfig) error { client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) - if err := client.Stop(); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + if err := client.Stop(ctx); err != nil { return err } fmt.Println("stop success") diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go index 57bf77469f0..13713e2711e 100644 --- a/pkg/sdk/client/client.go +++ b/pkg/sdk/client/client.go @@ -1,6 +1,7 @@ package client import ( + "context" "encoding/json" "fmt" "io" @@ -31,8 +32,8 @@ func (c *Client) SetAuth(user, pwd string) { c.authPwd = pwd } -func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil) +func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) if err != nil { return nil, err } @@ -54,8 +55,8 @@ func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) { return nil, fmt.Errorf("no proxy status found") } -func (c *Client) GetAllProxyStatus() (client.StatusResp, error) { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil) +func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) if err != nil { return nil, err } @@ -70,7 +71,7 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) { return allStatus, nil } -func (c *Client) Reload(strictMode bool) error { +func (c *Client) Reload(ctx context.Context, strictMode bool) error { v := url.Values{} if strictMode { v.Set("strictConfig", "true") @@ -79,7 +80,7 @@ func (c *Client) Reload(strictMode bool) error { if len(v) > 0 { queryStr = "?" + v.Encode() } - req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload"+queryStr, nil) + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/reload"+queryStr, nil) if err != nil { return err } @@ -87,8 +88,8 @@ func (c *Client) Reload(strictMode bool) error { return err } -func (c *Client) Stop() error { - req, err := http.NewRequest("POST", "http://"+c.address+"/api/stop", nil) +func (c *Client) Stop(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, "POST", "http://"+c.address+"/api/stop", nil) if err != nil { return err } @@ -96,16 +97,16 @@ func (c *Client) Stop() error { return err } -func (c *Client) GetConfig() (string, error) { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/config", nil) +func (c *Client) GetConfig(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/config", nil) if err != nil { return "", err } return c.do(req) } -func (c *Client) UpdateConfig(content string) error { - req, err := http.NewRequest("PUT", "http://"+c.address+"/api/config", strings.NewReader(content)) +func (c *Client) UpdateConfig(ctx context.Context, content string) error { + req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+c.address+"/api/config", strings.NewReader(content)) if err != nil { return err } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 561a52e702b..a60d71d2fa6 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.59.0" +var version = "0.60.0" func Full() string { return version diff --git a/test/e2e/legacy/basic/client.go b/test/e2e/legacy/basic/client.go index 98f675b823a..daed8b22f0b 100644 --- a/test/e2e/legacy/basic/client.go +++ b/test/e2e/legacy/basic/client.go @@ -1,6 +1,7 @@ package basic import ( + "context" "fmt" "strconv" "strings" @@ -54,7 +55,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.NewRequestExpect(f).Port(p3Port).Ensure() client := f.APIClientForFrpc(adminPort) - conf, err := client.GetConfig() + conf, err := client.GetConfig(context.Background()) framework.ExpectNoError(err) newP2Port := f.AllocPort() @@ -65,10 +66,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { newClientConf = newClientConf[:p3Index] } - err = client.UpdateConfig(newClientConf) + err = client.UpdateConfig(context.Background(), newClientConf) framework.ExpectNoError(err) - err = client.Reload(true) + err = client.Reload(context.Background(), true) framework.ExpectNoError(err) time.Sleep(time.Second) @@ -120,7 +121,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.NewRequestExpect(f).Port(testPort).Ensure() client := f.APIClientForFrpc(adminPort) - err := client.Stop() + err := client.Stop(context.Background()) framework.ExpectNoError(err) time.Sleep(3 * time.Second) diff --git a/test/e2e/legacy/basic/server.go b/test/e2e/legacy/basic/server.go index 62bfd62ae71..4399439d0fd 100644 --- a/test/e2e/legacy/basic/server.go +++ b/test/e2e/legacy/basic/server.go @@ -1,6 +1,7 @@ package basic import ( + "context" "fmt" "net" "strconv" @@ -101,7 +102,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { client := f.APIClientForFrpc(adminPort) // tcp random port - status, err := client.GetProxyStatus("tcp") + status, err := client.GetProxyStatus(context.Background(), "tcp") framework.ExpectNoError(err) _, portStr, err := net.SplitHostPort(status.RemoteAddr) @@ -112,7 +113,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { framework.NewRequestExpect(f).Port(port).Ensure() // udp random port - status, err = client.GetProxyStatus("udp") + status, err = client.GetProxyStatus(context.Background(), "udp") framework.ExpectNoError(err) _, portStr, err = net.SplitHostPort(status.RemoteAddr) diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go index ef5e262f709..fd269d758d9 100644 --- a/test/e2e/v1/basic/client.go +++ b/test/e2e/v1/basic/client.go @@ -1,6 +1,7 @@ package basic import ( + "context" "fmt" "strconv" "strings" @@ -57,7 +58,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.NewRequestExpect(f).Port(p3Port).Ensure() client := f.APIClientForFrpc(adminPort) - conf, err := client.GetConfig() + conf, err := client.GetConfig(context.Background()) framework.ExpectNoError(err) newP2Port := f.AllocPort() @@ -68,10 +69,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { newClientConf = newClientConf[:p3Index] } - err = client.UpdateConfig(newClientConf) + err = client.UpdateConfig(context.Background(), newClientConf) framework.ExpectNoError(err) - err = client.Reload(true) + err = client.Reload(context.Background(), true) framework.ExpectNoError(err) time.Sleep(time.Second) @@ -124,7 +125,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { framework.NewRequestExpect(f).Port(testPort).Ensure() client := f.APIClientForFrpc(adminPort) - err := client.Stop() + err := client.Stop(context.Background()) framework.ExpectNoError(err) time.Sleep(3 * time.Second) diff --git a/test/e2e/v1/basic/config.go b/test/e2e/v1/basic/config.go index 7dc3cedb3d9..314e7fc48c2 100644 --- a/test/e2e/v1/basic/config.go +++ b/test/e2e/v1/basic/config.go @@ -1,6 +1,7 @@ package basic import ( + "context" "fmt" "github.com/onsi/ginkgo/v2" @@ -72,7 +73,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() { client := f.APIClientForFrpc(adminPort) checkProxyFn := func(name string, localPort, remotePort int) { - status, err := client.GetProxyStatus(name) + status, err := client.GetProxyStatus(context.Background(), name) framework.ExpectNoError(err) framework.ExpectContainSubstring(status.LocalAddr, fmt.Sprintf(":%d", localPort)) diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go index fe8af4d1108..a3fe5992638 100644 --- a/test/e2e/v1/basic/server.go +++ b/test/e2e/v1/basic/server.go @@ -1,6 +1,7 @@ package basic import ( + "context" "fmt" "net" "strconv" @@ -112,7 +113,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { client := f.APIClientForFrpc(adminPort) // tcp random port - status, err := client.GetProxyStatus("tcp") + status, err := client.GetProxyStatus(context.Background(), "tcp") framework.ExpectNoError(err) _, portStr, err := net.SplitHostPort(status.RemoteAddr) @@ -123,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { framework.NewRequestExpect(f).Port(port).Ensure() // udp random port - status, err = client.GetProxyStatus("udp") + status, err = client.GetProxyStatus(context.Background(), "udp") framework.ExpectNoError(err) _, portStr, err = net.SplitHostPort(status.RemoteAddr) From f1fb2d721a901cd1e91368f0a5f8b9618bb530c3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 2 Aug 2024 17:04:59 +0800 Subject: [PATCH 04/83] update .github/FUNDING.yml (#4365) --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2d8fe2e060d..e87e0d49f2b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms github: [fatedier] -custom: ["https://afdian.net/a/fatedier"] +custom: ["https://afdian.com/a/fatedier"] From d47e138bc9d181b1ec1550787f3ebcb145220d9b Mon Sep 17 00:00:00 2001 From: Wang Xiang Date: Tue, 6 Aug 2024 17:33:14 +0800 Subject: [PATCH 05/83] bump templexxx/cpu version and add support for linux/loong64 (#4367) * support linux/loong64 * bump cpu version --- Makefile.cross-compiles | 2 +- go.mod | 2 +- go.sum | 3 ++- package.sh | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index 564e99f2f09..b75792a798c 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin export GO111MODULE=on LDFLAGS := -s -w -os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64 +os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64 all: build diff --git a/go.mod b/go.mod index 72bbc0acd0f..1cac66c3ac8 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/templexxx/cpu v0.1.0 // indirect + github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/xorsimd v0.4.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect diff --git a/go.sum b/go.sum index efcb18ee7da..61448c436be 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,9 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40= github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= +github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= diff --git a/package.sh b/package.sh index df36108ed3d..4699f59977d 100755 --- a/package.sh +++ b/package.sh @@ -18,7 +18,7 @@ rm -rf ./release/packages mkdir -p ./release/packages os_all='linux windows darwin freebsd android' -arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64' +arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64' extra_all='_ hf' cd ./release From 2dcdb24cc4a0792a5e75527d43ccb8fd2e40da92 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 7 Aug 2024 11:18:17 +0800 Subject: [PATCH 06/83] replace github.com/templexxx/xorsimd to the new version (#4373) --- go.mod | 4 +++- go.sum | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1cac66c3ac8..f9f301b691d 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,6 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/xorsimd v0.4.2 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -82,3 +81,6 @@ require ( // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d + +// TODO(faiteder): https://github.com/fatedier/frp/pull/4367, until github.com/xtaci/kcp-go/v5 imports the new version. +replace github.com/templexxx/xorsimd => github.com/templexxx/xorsimd v0.4.3 diff --git a/go.sum b/go.sum index 61448c436be..37d0ad5fe7e 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ 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= github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= @@ -136,11 +136,10 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= -github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= -github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= +github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= +github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= From 03c8d7bf962bbc536a021fc7710da2307701b1b5 Mon Sep 17 00:00:00 2001 From: Wang Xiang Date: Fri, 16 Aug 2024 22:24:27 +0800 Subject: [PATCH 07/83] bump kcp-go to add linux/loong64 support (#4384) --- go.mod | 7 ++----- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index f9f301b691d..cb1768bb888 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 - github.com/xtaci/kcp-go/v5 v5.6.8 + github.com/xtaci/kcp-go/v5 v5.6.13 golang.org/x/crypto v0.22.0 golang.org/x/net v0.24.0 golang.org/x/oauth2 v0.16.0 @@ -60,7 +60,7 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/templexxx/cpu v0.1.1 // indirect - github.com/templexxx/xorsimd v0.4.2 // indirect + github.com/templexxx/xorsimd v0.4.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect @@ -81,6 +81,3 @@ require ( // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d - -// TODO(faiteder): https://github.com/fatedier/frp/pull/4367, until github.com/xtaci/kcp-go/v5 imports the new version. -replace github.com/templexxx/xorsimd => github.com/templexxx/xorsimd v0.4.3 diff --git a/go.sum b/go.sum index 37d0ad5fe7e..fee1bacd57d 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/xtaci/kcp-go/v5 v5.6.8 h1:jlI/0jAyjoOjT/SaGB58s4bQMJiNS41A2RKzR6TMWeI= -github.com/xtaci/kcp-go/v5 v5.6.8/go.mod h1:oE9j2NVqAkuKO5o8ByKGch3vgVX3BNf8zqP8JiGq0bM= +github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= +github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= From edd7cf8967a95d9009dfa0bd82233f35fd6c0500 Mon Sep 17 00:00:00 2001 From: crystalstall Date: Fri, 6 Sep 2024 11:39:22 +0800 Subject: [PATCH 08/83] chore: fix function name (#4416) Signed-off-by: crystalstall --- client/control.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/control.go b/client/control.go index eeea1285ec7..3e20c312822 100644 --- a/client/control.go +++ b/client/control.go @@ -230,7 +230,7 @@ func (ctl *Control) registerMsgHandlers() { ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong) } -// headerWorker sends heartbeat to server and check heartbeat timeout. +// heartbeatWorker sends heartbeat to server and check heartbeat timeout. func (ctl *Control) heartbeatWorker() { xl := ctl.xl From fe4ca1b54e9045ea91ffa4645ac8f39bb2e770ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8B=BE=E5=85=89=EF=BC=8C?= Date: Fri, 6 Sep 2024 11:41:11 +0800 Subject: [PATCH 09/83] Update README_zh.md (#4421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复爱发电链接无法访问问题 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index a07e840cc6c..63a49325df5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -95,7 +95,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。 -国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。 +国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 From 2855ac71e3fc3fb2859f4c75f97f97e99f131f1b Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 9 Oct 2024 14:04:30 +0800 Subject: [PATCH 10/83] frpc visitor: add --server-user option to specify server proxy username (#4477) --- Release.md | 7 +------ pkg/config/flags.go | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Release.md b/Release.md index d0414efec04..fed36e2408a 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,3 @@ ### Features -* Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service. -* Added a default timeout of 30 seconds for the frpc subcommands to prevent commands from being stuck for a long time due to network issues. - -### Fixes - -* Fixed the issue that when `loginFailExit = false`, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup. +* The frpc visitor command-line parameter adds the `--server-user` option to specify the username of the server-side proxy to connect to. diff --git a/pkg/config/flags.go b/pkg/config/flags.go index 98f617be481..ce2582dd84c 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -140,6 +140,7 @@ func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key") cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name") + cmd.Flags().StringVarP(&c.ServerUser, "server-user", "", "", "server user") cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr") cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") } From 2466e65f43f61fa87fce0069cc06defe94f1e856 Mon Sep 17 00:00:00 2001 From: RobKenis Date: Sat, 12 Oct 2024 12:52:47 +0200 Subject: [PATCH 11/83] support multiple subjects in oidc ping (#4475) Resolves: #4466 --- pkg/auth/auth.go | 3 +- pkg/auth/oidc.go | 27 ++++++++++++------ pkg/auth/oidc_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 pkg/auth/oidc_test.go diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 9d8db7b686b..ae706986708 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -50,7 +50,8 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { case v1.AuthMethodToken: authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: - authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) + tokenVerifier := NewTokenVerifier(cfg.OIDC) + authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier) } return authVerifier } diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index d87420ff764..40ce060faa9 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -87,14 +87,18 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e return err } +type TokenVerifier interface { + Verify(context.Context, string) (*oidc.IDToken, error) +} + type OidcAuthConsumer struct { additionalAuthScopes []v1.AuthScope - verifier *oidc.IDTokenVerifier - subjectFromLogin string + verifier TokenVerifier + subjectsFromLogin []string } -func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer { +func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) if err != nil { panic(err) @@ -105,9 +109,14 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCSer SkipExpiryCheck: cfg.SkipExpiryCheck, SkipIssuerCheck: cfg.SkipIssuerCheck, } + return provider.Verifier(&verifierConf) +} + +func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer { return &OidcAuthConsumer{ additionalAuthScopes: additionalAuthScopes, - verifier: provider.Verifier(&verifierConf), + verifier: verifier, + subjectsFromLogin: []string{}, } } @@ -116,7 +125,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { if err != nil { return fmt.Errorf("invalid OIDC token in login: %v", err) } - auth.subjectFromLogin = token.Subject + if !slices.Contains(auth.subjectsFromLogin, token.Subject) { + auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject) + } return nil } @@ -125,11 +136,11 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err if err != nil { return fmt.Errorf("invalid OIDC token in ping: %v", err) } - if token.Subject != auth.subjectFromLogin { + if !slices.Contains(auth.subjectsFromLogin, token.Subject) { return fmt.Errorf("received different OIDC subject in login and ping. "+ - "original subject: %s, "+ + "original subjects: %s, "+ "new subject: %s", - auth.subjectFromLogin, token.Subject) + auth.subjectsFromLogin, token.Subject) } return nil } diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go new file mode 100644 index 00000000000..58054186e11 --- /dev/null +++ b/pkg/auth/oidc_test.go @@ -0,0 +1,64 @@ +package auth_test + +import ( + "context" + "testing" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/auth" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type mockTokenVerifier struct{} + +func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.IDToken, error) { + return &oidc.IDToken{ + Subject: subject, + }, nil +} + +func TestPingWithEmptySubjectFromLoginFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-without-login", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} + +func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "ping-after-login", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-after-login", + Timestamp: time.Now().UnixMilli(), + }) + r.NoError(err) +} + +func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "login-with-first-subject", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-with-different-subject", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} From b14192a8d3bb5b5a844977ea82de9a7d87dbdf06 Mon Sep 17 00:00:00 2001 From: 0x7fff <4812302+blizard863@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:55:56 +0800 Subject: [PATCH 12/83] feat: bump (#4490) Co-authored-by: Coder123 --- Release.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Release.md b/Release.md index fed36e2408a..3661ea4de86 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,5 @@ ### Features * The frpc visitor command-line parameter adds the `--server-user` option to specify the username of the server-side proxy to connect to. + +* Support multiple frpc instances with different subjects when using oidc authentication. \ No newline at end of file From 3a08c2aeb0fd5639e40c3f91d6c5ac98150ec194 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 17 Oct 2024 16:27:41 +0800 Subject: [PATCH 13/83] conf: fix example for tls2raw (#4494) --- conf/frpc_full_example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 3bf868663b3..eb44739239d 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -327,7 +327,7 @@ requestHeaders.set.x-from-where = "frp" [[proxies]] name = "plugin_tls2raw" -type = "https" +type = "tcp" remotePort = 6008 [proxies.plugin] type = "tls2raw" From f7a06cbe61b0eb703a65b9ebabed036ed45fe8ca Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 17 Oct 2024 17:22:41 +0800 Subject: [PATCH 14/83] use go1.23 (#4495) --- .github/workflows/golangci-lint.yml | 4 ++-- .github/workflows/goreleaser.yml | 2 +- .golangci.yml | 5 +++-- Release.md | 3 +-- client/proxy/proxy_wrapper.go | 2 +- dockerfiles/Dockerfile-for-frpc | 2 +- dockerfiles/Dockerfile-for-frps | 2 +- pkg/config/types/types.go | 8 ++++---- pkg/nathole/utils.go | 10 +++++----- pkg/util/util/util.go | 18 +++++++++--------- pkg/util/version/version.go | 2 +- server/proxy/proxy.go | 12 ++++++------ 12 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index b3e2cb43c39..8058592853a 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,13 +17,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.57 + version: v1.61 # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 7c01f3764a0..d55913fda6c 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - name: Make All run: | diff --git a/.golangci.yml b/.golangci.yml index fb625c9b98c..f7456d65043 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ service: - golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.61.x # use the fixed version to not introduce new linters unexpectedly run: concurrency: 4 @@ -14,7 +14,7 @@ linters: enable: - unused - errcheck - - exportloopref + - copyloopvar - gocritic - gofumpt - goimports @@ -90,6 +90,7 @@ linters-settings: - G402 - G404 - G501 + - G115 # integer overflow conversion issues: # List of regexps of issue texts to exclude, empty list by default. diff --git a/Release.md b/Release.md index 3661ea4de86..ee388be1b1c 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,4 @@ ### Features * The frpc visitor command-line parameter adds the `--server-user` option to specify the username of the server-side proxy to connect to. - -* Support multiple frpc instances with different subjects when using oidc authentication. \ No newline at end of file +* Support multiple frpc instances with different subjects when using oidc authentication. diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 487e3702b9e..95048f29907 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -137,7 +137,7 @@ func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error { pw.Phase = ProxyPhaseStartErr pw.Err = respErr pw.lastStartErr = time.Now() - return fmt.Errorf(pw.Err) + return fmt.Errorf("%s", pw.Err) } if err := pw.pxy.Run(); err != nil { diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index 99b3120778a..326c75b9ce7 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -1,4 +1,4 @@ -FROM golang:1.22 AS building +FROM golang:1.23 AS building COPY . /building WORKDIR /building diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 3d2c387a431..0772686c333 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -1,4 +1,4 @@ -FROM golang:1.22 AS building +FROM golang:1.23 AS building COPY . /building WORKDIR /building diff --git a/pkg/config/types/types.go b/pkg/config/types/types.go index a6cd2e71b0e..8fa3105a5f3 100644 --- a/pkg/config/types/types.go +++ b/pkg/config/types/types.go @@ -159,18 +159,18 @@ func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) { out = append(out, PortsRange{Single: int(singleNum)}) case 2: // range numbers - min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + minNum, 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) + maxNum, 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 { + if maxNum < minNum { return nil, fmt.Errorf("range number is invalid") } - out = append(out, PortsRange{Start: int(min), End: int(max)}) + out = append(out, PortsRange{Start: int(minNum), End: int(maxNum)}) default: return nil, fmt.Errorf("range number is invalid") } diff --git a/pkg/nathole/utils.go b/pkg/nathole/utils.go index 3896a2153f0..5f32142ec1b 100644 --- a/pkg/nathole/utils.go +++ b/pkg/nathole/utils.go @@ -78,9 +78,9 @@ func ListAllLocalIPs() ([]net.IP, error) { return ips, nil } -func ListLocalIPsForNatHole(max int) ([]string, error) { - if max <= 0 { - return nil, fmt.Errorf("max must be greater than 0") +func ListLocalIPsForNatHole(maxItems int) ([]string, error) { + if maxItems <= 0 { + return nil, fmt.Errorf("maxItems must be greater than 0") } ips, err := ListAllLocalIPs() @@ -88,9 +88,9 @@ func ListLocalIPsForNatHole(max int) ([]string, error) { return nil, err } - filtered := make([]string, 0, max) + filtered := make([]string, 0, maxItems) for _, ip := range ips { - if len(filtered) >= max { + if len(filtered) >= maxItems { break } diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 7758054d948..774af2cfaa2 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -85,21 +85,21 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { numbers = append(numbers, singleNum) case 2: // range numbers - min, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + minValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - max, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + maxValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - if max < min { + if maxValue < minValue { err = fmt.Errorf("range number is invalid") return } - for i := min; i <= max; i++ { + for i := minValue; i <= maxValue; i++ { numbers = append(numbers, i) } default: @@ -118,13 +118,13 @@ func GenerateResponseErrorString(summary string, err error, detailed bool) strin } func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Duration { - min := int64(minRatio * 1000.0) - max := int64(maxRatio * 1000.0) + minValue := int64(minRatio * 1000.0) + maxValue := int64(maxRatio * 1000.0) var n int64 - if max <= min { - n = min + if maxValue <= minValue { + n = minValue } else { - n = mathrand.Int64N(max-min) + min + n = mathrand.Int64N(maxValue-minValue) + minValue } d := duration * time.Duration(n) / time.Duration(1000) time.Sleep(d) diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index a60d71d2fa6..91b2ed98a2c 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.60.0" +var version = "0.61.0" func Full() string { return version diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index d5ab0f13ae5..c7c18c32223 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -137,17 +137,17 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, dstAddr string srcPortStr string dstPortStr string - srcPort int - dstPort int + srcPort uint64 + dstPort uint64 ) if src != nil { srcAddr, srcPortStr, _ = net.SplitHostPort(src.String()) - srcPort, _ = strconv.Atoi(srcPortStr) + srcPort, _ = strconv.ParseUint(srcPortStr, 10, 16) } if dst != nil { dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String()) - dstPort, _ = strconv.Atoi(dstPortStr) + dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16) } err := msg.WriteMsg(workConn, &msg.StartWorkConn{ ProxyName: pxy.GetName(), @@ -190,8 +190,8 @@ func (pxy *BaseProxy) startCommonTCPListenersHandler() { } else { tempDelay *= 2 } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max + if maxTime := 1 * time.Second; tempDelay > maxTime { + tempDelay = maxTime } xl.Infof("met temporary error: %s, sleep for %s ...", err, tempDelay) time.Sleep(tempDelay) From 62352c7ba5c4f783062f904949f2b5d7f1e3f7fc Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 18 Oct 2024 12:03:17 +0800 Subject: [PATCH 15/83] dockerfiles: add tzdata (#4499) --- dockerfiles/Dockerfile-for-frpc | 2 ++ dockerfiles/Dockerfile-for-frps | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index 326c75b9ce7..d8d4437ad22 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -7,6 +7,8 @@ RUN make frpc FROM alpine:3 +RUN apk add --no-cache tzdata + COPY --from=building /building/bin/frpc /usr/bin/frpc ENTRYPOINT ["/usr/bin/frpc"] diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 0772686c333..00fb51a89d8 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -7,6 +7,8 @@ RUN make frps FROM alpine:3 +RUN apk add --no-cache tzdata + COPY --from=building /building/bin/frps /usr/bin/frps ENTRYPOINT ["/usr/bin/frps"] From 9d5638cae6dfecb10bff7c6d3e52625fe264fee1 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 18 Oct 2024 12:31:55 +0800 Subject: [PATCH 16/83] update Release.md (#4500) --- Release.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Release.md b/Release.md index ee388be1b1c..aea4b71bdf3 100644 --- a/Release.md +++ b/Release.md @@ -1,4 +1,3 @@ ### Features -* The frpc visitor command-line parameter adds the `--server-user` option to specify the username of the server-side proxy to connect to. -* Support multiple frpc instances with different subjects when using oidc authentication. +* `tzdata` is installed by default in the container image, and the time zone can be set using the `TZ` environment variable. From 6ba849fc750749d796c020987dbe467cf3a361b0 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 22 Oct 2024 10:48:24 +0800 Subject: [PATCH 17/83] readme: update sponsor (#4504) --- README.md | 9 +++++++++ README_zh.md | 9 +++++++++ doc/pic/sponsor_jetbrains.jpg | Bin 0 -> 35889 bytes 3 files changed, 18 insertions(+) create mode 100644 doc/pic/sponsor_jetbrains.jpg diff --git a/README.md b/README.md index 97f148364ce..0d88a6b1511 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,17 @@ [README](README.md) | [中文文档](README_zh.md) +## Sponsors + +frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors. If you'd like to join them, please consider [sponsoring frp's development](https://github.com/sponsors/fatedier). +

Gold Sponsors

+

+ + + +

diff --git a/README_zh.md b/README_zh.md index 63a49325df5..6190cec54b6 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,8 +9,17 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 +## Sponsors + +frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。 +

Gold Sponsors

+

+ + + +

diff --git a/doc/pic/sponsor_jetbrains.jpg b/doc/pic/sponsor_jetbrains.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be2113cbc49aff52efd96fcf1fb558b9256c8482 GIT binary patch literal 35889 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 literal 0 HcmV?d00001 From 4383756fd4e15d7f03d1aa6b13cdf93dbafe40d9 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 30 Oct 2024 15:12:31 +0800 Subject: [PATCH 18/83] frps: add support for quic-bind-port parameter in frps (#4519) --- Release.md | 1 + pkg/config/flags.go | 1 + 2 files changed, 2 insertions(+) diff --git a/Release.md b/Release.md index aea4b71bdf3..34f72a29178 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,4 @@ ### Features * `tzdata` is installed by default in the container image, and the time zone can be set using the `TZ` environment variable. +* The `quic-bind-port` command line parameter is supported in frps, which specifies the port for accepting frpc connections using the QUIC protocol. \ No newline at end of file diff --git a/pkg/config/flags.go b/pkg/config/flags.go index ce2582dd84c..f9d9e3e4495 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -227,6 +227,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address") cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port") cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") + cmd.PersistentFlags().IntVarP(&c.QUICBindPort, "quic_bind_port", "", 0, "quic bind udp port") cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") From dff56cb0ca4b03d4ee9e3cb76ab863df3a4a83a2 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 7 Nov 2024 17:14:40 +0800 Subject: [PATCH 19/83] update .golangci.yml (#4527) --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index f7456d65043..5651ef5cf78 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,7 @@ service: run: concurrency: 4 # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 20m + timeout: 20m build-tags: - integ - integfuzz From 8593eff752117aaa3775db498eb0e553a251c4f6 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 20 Nov 2024 15:14:51 +0800 Subject: [PATCH 20/83] update sponsor info (#4545) --- README.md | 4 ++-- README_zh.md | 4 ++-- doc/pic/sponsor_olares.jpeg | Bin 0 -> 46760 bytes doc/pic/sponsor_terminusos.jpeg | Bin 31450 -> 0 bytes 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 doc/pic/sponsor_olares.jpeg delete mode 100644 doc/pic/sponsor_terminusos.jpeg diff --git a/README.md b/README.md index 0d88a6b1511..16fd3c31ab9 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ frp is an open source project with its ongoing development made possible entirel

- - + +

diff --git a/README_zh.md b/README_zh.md index 6190cec54b6..ebd3109ba0d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -31,8 +31,8 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

- - + +

diff --git a/doc/pic/sponsor_olares.jpeg b/doc/pic/sponsor_olares.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..375c2a7ddb86f2a31bd1de168c601fe1bb5afdb2 GIT binary patch literal 46760 zcmb5VXIK+o^e#Fnq#z{}6{H0S1n>ujqBMa7A_RIFB6R;pP!Vp+vRiq~x^!e_#I^0X_sM5&9AW zk^;bdAP67mUke}#01yBI`k(RszZVG10fR%Jh{LZ}Pyh%50Yf1$2n5c-4TB&-hrdFg zk{rq~eu9O7qYqq4g_iPEQ2*6Y%a77$eElj1ejRh7yq-ReQB^xlw4$fxH4Xlj#i|=b zrU}WoSlie--?+&rm_2M;@c-`f|LOaGO8Ne;k^G<1aexc*zx(9_^nfM6xxK_&T+T-s za>EGMGGSeeoV6Fa+cSJLA?r_oJXu@1k*S;#NS$)l+W-l$cgG3$cE(i0D?$XsK509 z=k=7TG5{fs5D%4~J3Ay_+dCnp#e};=I=-tU`^(2=E+%ZV=~sYToq6VG#w0SBy!upt z52+91Lc31m%)dpMghnfa%`cvVwvkU}$+KzknQ|#&bKG>kIS?4#9-COAK_-j`5+Xt1 zs2OseLlatj_8l4>NMOMSC8bkoIiC>T+mf<8m!ZAh^w+v32${j>q8@R4ax(e4akMlU zAP$i!2RDZCWgtzB<(Ib4z!;TNR=;& ze|fjK(vX+ZMNl1sB$e55?^{WnEgW(=qRdX9JtW4ZrbR`{@dfA+;bcf157647j>*8! zAn`uS4~Y~@tlUYQBN`3;@ts!3@i~;~fNqyYY~VctV~P2~_zi-}i6(nI1e0?=kyy|# zrOiZ4GNq&hV#Q;FN1>2}ZG3!7v|8gG96&C~X=9Sm$jLGs&2+Xn6&5H=1|>(* zWWZvx@_Gf^aPsA>7y^!|aj8+=!eCy;60AZ;X$D5J8*jE|`_we*wEJ>Dcw&N(3D) zm&03>b1?44Nh5vz&tWJm=|CpBdlJ1+y4GF%|dlQVKUud2qRfjGz> zg+Jg(9!EM7b+5L?ci-fQYL~pQ%m-J6D?iqfyuTMKDX-*~9ia+NiOO^@?ZxPrf6?AH zfq5ehpjBor1>e{9``b{XTpH^HyemSO#2;|mA(yqsge`alG!at?AZ40p0|lASO3rDM zS8Hrs%*4#7(61d>@qL%HBH{E+)A(7*hVVLKFw-aPAx1dh3_+VahlPILIDT^WA?W?FIipDU1t2fWUyCUQ!=^ZPO3NAAT0U%TuQDC zcM*q?jX?Jl##BJ2d2Jdu18@Gx06NArwhv5#rw2r$$hD$O7Z1Z}(yi6`3YoFn=UN?3 z1E!`95-Ah-1pg7|coGJs8m2e0`u@qDnbYO+M& zUTP*gO_bf`w@A<9P30l;v}35=1zL7Es<)2^mzfRTMZAP(0eKv?Iz?mL3`?EYNEp)S zMu!hdaFupw3gLak&fvFD=}o4;z*bm@3rnfRi=MH7RO@=+>JpaRf8xMv1_cpq?%_ih zaMez&lz?SG{Jx3x$w?aWrlu$jIWV#_j3sH@F~%Vrl|=a=f>~HZ0loUt^K{1#Cg&3o zd~AB`_Qy56u&fGWV=*g510TyV_}J3bS-#^dN)DI2uvn5Bz!@BiZsP1NTsKmim(+WN z0=sq!B*$berC=+;=wSKWP)T*NqcDJ2q$j0x+Oy6W8jnFdqF`oZbS|Yy^!%k@J8UJ- z1ll|hOPS)ezIlcqdq(bronZVbOC&ql+R1XE64PV^IJ+W`E|F5T&}ZpRhU|E< zjOJ)cB}_#N6Np}lk8MJ)4)s9Th0Jy10BysLn=X;jmZW4%LXG{MCV|t(NY9ifwWNu7ljDmV$${sWz(=6 zpzLCbgLW@pH)+f`v$69CA#>C6NU>8%nh077COBi{Afu@bOD>!o)EN*jQE-T}sawa) zvQ&=x3E{rg!Ax(c)|6Q~1zW@Um*4e=WIqjV@H$)`Y;5{~Dh`P_sv9tLtAr5dn(k&M zjhSUe@!xdLvs`82xA^Jvkr{m~DSM-)6i|ysC24dE-`5x->jZN&ct4-k6|HY{;a$J# zZx??fV*GNKSbPz7+nd%BA4O5&hqit?pA{S-gl}tBcr3{zSDbm@WjU^P z)x@WwlOG||OH>5kJZ;9c&RJe4A#HV@PGSkS8}9N zng6ZYMf-4Q_WdI922!$>&MWEnA^?(=2pr%5J6bG^ zne`z?TbNqYQZgOc+g9kTaC5=6Z;x;Ko^$RG=5+8%f4K2+$?^oLh<-PCzijzR_? zFcWp7ymYZ`yl?Lua6V19R-UZ)qWx4^dLRNCrz0tpp^Xj=8@$bw!Ds)j1L9XQ8qcp| zhtZdWB~DvNS=0DKACivy$*~XJ++txHe7rC8aJq8-7GTdC$c+ZSGGV|F2U5I0>G!5-i44goYXM$w4(FvVWM?I5i+-W&+rk_-#%>N?_nx3UASBh1_ zLiE%5LDtqn$}C74zauW9jDRB`v;TGigg%#6VjhhBw~~r|*@^Skx8OG#(BP;32`NyG zii0*iWkG~*YptY(6tg2cU$ab~eC@oM|0H?f?X@Sgp<1%T4QU1ljTzHK@Uz>9A_7-el zq077`(9(KkDO4`U>rUFG9=-&;FaX^WG9)w+*W#q~VSHaGZ(lz1rrXhH0vH+@jf*H% z-O>#_c_n~E-jUTV&^@JgG&R7O`C}<=$s(hP1M)~f7R42Z5Yj+VK>!a;3>TSL0s)Bo zf(_jAYpq7s)?J*iAw>|B8ep7Vuds=KHwZnRGG@_e!S~e|KMRdgyDO`6l+u|jLq(t6 zg*GzO@CF2YH-^Xs?cK|hfxebrOsI^RE*{atz#jv`(#W)^n2+GtsK-FEcA4Jsyp*@P zx8!C}$*QSR76{>Fez<_L2-E+elJ4|vczxVb31OR2CGITkP_jG*f*Bjmgfc}Vwewt+ zqYxPZ3j#`rzs21}P%T&^8iw)8l;Rgb$o(lm(N-8Yt>7K!A73+ihldwibA6{(BBF$n zb7ox-W-e=Z#!D{A95#1{?k!-=C+zKkngvKG!_4F{1vIuIQ{M{Ql#u1@*kb z+fLUB%m0~T*2@?}L%4?%4^0YV)Iz^6) zwu|ObTF6-(4-}|`g63s#%JY^vahv!^Cks^8EJz?bSssquI1i9;)TsrpU$hJWyO{oo zjasgBRgF4w_N#G15`~CC>RIzR7ILfHfKC;m0UADjDfNzqP{;X_!(AkPh*pJm6tMo< zju4s(oyZgxzt3%WpPq#xB|9TTl5jR_RKc{Ih3_@mJ7{N|`6C=B7t_hq(vNAZLCnB* zV9z*d2_Bc_inGdb>24)~6dSdZFtFevb&{*tQL!i)1hUBojUiy?5MZDaX4;|od;Xcj zBd$mW=b1$-mTZ!yd>TX!C8oQOz(}GPPnM;LbixSZmRTNgt=5{~U5hE=DFeY&gSg(K zuN_t~Z$r3{cxIY5_OC!+=% z&Ps))dFexOP8K^)t?w|Z13SQx8~~)@##R0nG>ku}755C4LcMu5*-VZHx@qgMR5%ux z7eo)lw_uDe;n$X85^ZU!^K^TX2Mt>(HVC)D$1-w%C8^LNKk#LcoQ{g#0PhtL*ck@N z=bu12tf!4thO)|H@uK>n^ZZ}=Rplgp5!*4-QVz~#h_eNP$X8uD&Io~5noAiFMj(kd zh#?5=F2XUEvVOQA3VNaf&1QB=y_(FeSLLuAFvJ)dOF(x?5BUsJCWQ0QgOXGodbKRTUMyjT+6lW1MpyffGcY`PdkCQuVAL7)h zR3#5Den;y09BF1kT1r~f1nL{{NM>I66y7Yep45rB#pZ8hqpWM5BK>$4sM0Pk=~sxM zEwc8k?~-&^F)PKKC+KrMD{&$u!rZNS45->1fVdpVe$9Ude;72FxHyFkiiMj4Rphsz zra+_yl~r>g62lDpx8`K;N@FeWiO8b1O?Bc^rxe=u3iXxgYZlo!>sU5=M?tAhj-mspHUMjyFA9ZFW_)yODLMfs`p$(>&6P%6}9k_w%` zf^g>PBU`3c65ViF&S4Bt{3bzGOSH)s8KqMjORCmE>1ML2_zVp*r$=CLfQBi-UVCxn zM&dA~Cv@x|z&G&weEFSQG~0wLH?K^EB+hyh);ViUBR5xXop5&#ZJpV9b9*zTr+4+` z$ya-~hdfql#kMy~dwQ~MPaK6I=DpqTJ(ciAh5yxA*1P9_foFW%e0cdkr5w%M6T@X7 z_tsx}loj6Xqpv3xcRhP(A8Ljb%f@N4^$Jxlc~x=8=Xy^}Cm7tXD7&=q;^}N#ZL??oqPY{t)uNL+_-10 zN9~TQo=kcchF z0UVp6F7Hzmeb(ay4sLTV_i^)+0TtVO!OLk;OKc`oqo?>a;*Do%|$iVKY%mjRYAnHA1@~Ebgg(OJ!emRu4V3$>xySAl=yl( zq%(p^dPIpAaw9iFJjxC8uNyLXokv|Xi_wvvFV@sU%gx3fy0SvG2fpqod17%AvhRB& z9ID?pL3!)r1;0OPiCf`^^L0tc802w8zQt2b%}Ko6BCYLN3`C2w!XccK!l1ijqo8Y> z#10mcE(wln%6@5NV^noY>uF!25iz{qT=ps&wy|!5h9bE^Iv<;mSbpkC+!V5rlohMz z0!(%^@l2Fvwa(T(p9XdFs4~Pap5g6*ipuGbSp5AI_WxeX(|3E87o)&X+Pk-f$7j5j-y8W0R+NEK-DG87xYq zI&9iNsuzSfK zqJg_AFnd;^Jl_W!KFugpR+-pBOLA54TLtxhyRwR5F%qeGgDyjG>B1zLG04czlE;6@m_%bBV83`EG8RBZs9^ZSD_OZxQq?i;$LI43XGSd`W$GG)TeDMo%>JU zLKdI?DRPy8m$2#9I4$0DRyb<*V6rnRmt>+mPs_dMb>B{uxud{pkWp;Bvq;O>fo|`c z2Qna-9dtM@{Srb-d9q*u2KxlC(pvN{G~3+K>6b$p5ucy|^3FDyVMt_ZIg$jVd#t+) zCN*2c`b9uJxoR2xAl14&s3{vR#HMXq@u@h12A#{y*dfew1@DASp)E$gNB1XzcNlVLtLn7-!lW>LaCRgB)ywcY8WB69~sPTo@j6}O#bb1Y|&F0>$ z!asmMm3{N2?z+cqYFHOZ!uOa zuYO){2_?P`Gn-zpDg6g{*Zc$cuZ{e9ylOXl@7F_RC)7%MHDg3)=gEJc-rSE| zc=N60;%;WaYV*JwwR8H)y+70%*JE-k9*=4K7=zS4uq}Va-e#1)IGI^)SSy)G3ByxK zIeEO46MA5~TlVTNuDFl435?;)9YYn%qfIK5{7Cl<9PSKWbn?&T^x03SW_m7fnvgM8 zWdx3U&rDk=AMN1}f+Q|Zbw27D?txdcHHO0B$JP1J?ZWMs4>-574d}JLMY#sX;Uhg) zddqf`DmpvRpjhn9q&lAH<;2eqbh1xaFT7$bKO6j&@wxHlG7#|bw6aU1 z+RXt)pOrTW+ZmtN%}!;`$1Uvo9|+Byyz3qUdzSap_WHeWU8Ca@^(bRSMOg>>H_cp| zE{#6w<~6Q&4<7y5vf;9*ci6uA{jUGl>&AqPEytyuNsEZnI)+c46il2YNgv>*ci`!M zYa3fz-)u8y&)bDuaKC(~c*wz>2wFXRIJ+a1Exp#h&(FnO3Cn_2^L}1d_#V{}z^7yn06E`f9jHvYDG%yWJLYfdxEY@6V(Pa= z2FZYV64KhOc3Q$l%qTF^Pk^JQ7=Qa@Cr2Gf@-?D+PBlAO%1ADiYOGR16TeS>w9J6% zN*S-QbNB^>n^bWq6cSBgLG;wN5%`;yeu?M4+5xi;Ljt9*SlG-}fg>(4xQ`gl_|nM6FhuDl_;q8--X<>Ivl$ zofa<1eE4$@>Hy-D4})nEZoP>|W)e4btmxh*spvjyE@GL;DZQYx5?vw{L;)2^?(A41 zSHJ}jE+rEq?ZYl{5hiGg@RUx;^arEGTB42)V@xfC`^v6LAJ*RKtX(WHiw%1S<+q3U zMLSGOizNrrX@3X*e!~Ft!~F>)wrhCE_bDwFqo`DA6^j9oSQGMY3#_BsmwuBI)_#&H zACa9l3gzN`j9eqAK)1~#zZy%-(6XTSCOM3lCOB%JTHl3a5+B3k23!)FY+zC%e*e0z z@ETc|V8w}FiSH0gi(`Se6Gshbknag;$wsNr0Y$KuBcMTkT6^n48_FCCsgtfVjW=6ql#LA}jgk`qg3RHy5T5}I+Eq6F5zMZ z9uiL%tc?}3As0+1Bm>TJm>&$X^kWC`lL81rbnj&`1|mYaAf085kITlqclH4n_PXxH(4ce@$_Sdgk*VR4W-CV5C+*Huc?A7TYe15}w=V5}S8Vi|F#3SZ& zDK+AP(C)(?jr3TrKl%0*&#t|Re`d$Tx$fI`t4=XZ5w-?`?Ds_8Qpi=&+(8M~X z={yP2PO*0$yT{>kv_gF1>eq#uGuHnFvuq1*-*`GW*#9~0H+h2{`NsV*EVCuV@A+vv z+er&-hVebzwwo}=t&RN{@7jtPt)G_uIy>Aqjkd5Wb$DUSQiIkWX{Ov~GC0)a(NjNn z)251hcT67|;c$S?l0R1UFDfuXV!B)+Mp4YY;PRRAC*AMp=W`4PXH!4a?}=R1A1o}K zd#`=}=)IE*!~CMh^ddJ;W;;h!^_cv+`{Cron-OKvHn*LYzn>k9lv_Hv{miDnicRUL zn>P%3aKI`bJhSx)&6)X3XQ%Vy*RJROG3~7o48^}HzW&XBGrQO0zH@a`V3V+$MV%sadIKAl z-~3xciqWZXB5xR)XRqMfIT9a$01F7QKXAKVdD%~?t9w*vZ&8znNvlsXqz&hEt^Wgr z_pCk#`}kCS)!M;P`q;>s5e zpWY{}A;INdXOEN_?1Wy_vhP!7eQ>||)!eZ*|GwG*|KLQ)ZKqdxyLqvKmqdeOF31J{-BASOcp5lvfWB8DcoH>(PQ>c2 zwDX9ejKw

f6{s3rz%1Wr^TZ$=od_&#w#(5f-Q?G9~RY zI3oqSIS?{2lSDe$0L-Q7&TOh5GD|4xNsbdOElDkgC8O(z&_o9u136*0l$^2%I|Zz?oNB`eh+n!+j8@fTFw+WS!=K0jgoT3^ zEpiBRuNGPBxk!}|7l^YQh4^OpD7CgDG|zk16&Wu{LbTDQf23U?qot)U6(SuRA@RG+ zl?)DJ0+E`Zwhn?F#Y;=VojIUY$^Bvz-X&R0`e-wJ)*Kk!ox*6Rsxm~UrF<%pZZaRX zr28nP5U6;x{fr^Mh~gmp&}u}GbakOY=r9=$Up?_}|Xg6fxPOzMfo)6l2Z(9X7?!YH80H^XNOHqIGa8gF{M z>4WMV9@l!`sV>9M{g%?>mkRxu^iFBDMZ72IdTfPQaIn*2pqa{UmBO*(y9sgYafF?T zdlc;#(qB9&^@f^#FMf$mVN)71+uAvHJK_aL&t?GHSn@ub?;8$Hu+Oi%!fW%^37)7qn^c)jawhKp4^TztZ}C=|{|; zbQ`bYlJT=&Zh__>>VLH6ew!aLh~6pB`fE;Di>!mTz7PvP5-F|yCLGeuSiL``%>Gy!sk2ZSZN*!mza% zS5?66o~{nXQQ&VeyjdIgT05%$fzsLC@Rv7A0#CkpmUG2n3+@{ZvSaBv9L*-4$TGh0 z#(CN^(>wQ?&#uxZclSHaZYJY{59Ipu8apYYlI0MG<4-dh5qg@xv6Z^g{fgfLiB`kq zrE|U2IRS}t_wBNl3SJI8yE|}i&wZ+-1=Nsm{yc9q&cBo^Az4KEFrTa8PMmj z+x5(?E-}Gk{ZC-^8+52P&mL`)khb;XZ(B{Po`l&RuRSPm)$4BeeW8Cq?$AN=7Z71w zhjzDRv*M@RLE_awnYZ>2MuqffpY!h?OJdIrzS)oHimAH4Ta>ppu4V{ym1!*XYcnML zM|$2rgio?A7VAHNeki;Mdz0Ut{pexIp5xTMZs`r}XAcGsxjt@U$|eP}W`FTP?HAkG zBkNbTN{c)|KUD^;5!j(SJ0a7!QjjJxc4ecY5* z{EPj0>1Tq;z4-BSk5)j-7=DZvVxsJCZx}`yvbI*e|q+K;8ge@0%Mc~( zf)wGJ)}v>R1y|};E#4Kn6Y4)aYbI`q^(9||`-tUo?rFXnIYq9{PT(`mz%5q%Ct5Dv z;DBT|==fj-7PD+h>-MZFdIO4$Yj4PI-TDVWHwj~#yk4Tp?w2YDqc@lCt_zcu?+s-{ zZe3SA<+Xcc5p?nV@TDf-`8ACsMs#zDdAZ-^D$fPoawEs*MT0*i@WY?d=v%DA&wa@+ANKnx_NG4}EoVKOCzV%j zKTebXEOibB-o1BFGBMoL6lybTGAaMmD!;fP&Ah?K6T3Q%Gg5u;3v8}k&%QlUdhOXy z+mgkH;^(urYg1=JQV&&qUg7n>)~_Gi^~$$~{MJxz_;bJhy4O2Ro9UV}ClWw=AD;*9 z>+nU+4ZnZ{{a|@ zk_~bB0gXp-r3JSIUy%40op~MB-P$iZHCH7!NV<(D%%|NoJZPHU+E%;T-5IYb@%3z7 z#ZcCfPw}%nLir_+UZpF_KI}QO^d<2tk+8!Ybqb9N`_0nqMt>w|Cv;i>J13!hl6Xx; zJV+tZd0i0d$)+mywK$@}h7ec;`Y_auTGH3`be zm?FD*E3idasL3PoA@!P=7;abs`F0>b1+lfz3KNu@CphvK&kC9-dNLnrz|xkl8)lYe@mib zv5T2hILVSgM~N1JQe)fc&LdVN8as;+btJv}M~~Fl-?qh)6=$TM2mQQ*5T>Bhw>vHO zbkHt_OAQo9P;z~TP}+Sqr@byw<_wY35U7LNK>}r7X6o5oKZ~&&#L=(F^zwV#_!?ad zx2`srREDlR4`yC)Yf0P0|H+sC9!r9BAA&$qcLAZ*Dwm1Zo`z>Q@SesUp$aWMkK2(_ z7E6{cVoO-Z)6gd2l9DP=GzfhIOBD(`)KzFt?V$BtwxZT7GB)ujych~cAP7;dAAQ6) zDrYb_wW7v;MF|K+sX6dsAW@=GgI5D)?Q%G)D zVr=FbovOu{W#OsYm099WwBM+E9>mpGc!0oHX_~6T8WIBTAi2W&x!VQkDSxO7oNuiU zE!7iMX_uq*_lPmtSa7I}lnh|1W5I9rEl>4OKpek{oHDYG&DgY=r-2O-oe0r-*%N^T zF>N&M9yq|@?b}8PUjNXn(z|BXT#(^u96D?;H24XU8cf{4-y73Q^#O;uijE%|Vcr;$DKf-?IM2`+PVZl7A6xgs?MtsT82>tW z_Tyk(7U5u-tR<^^It!M05WH?u!sBg&IMJXm!PJMPkCSmt$gfFbw3>| z5H{XW^LkbF+yHgi_V+*FkH|kD^HxZO=K}?4Zjex&{vvH1C zB=O!?+j4b*(S4(6f zl4m?azQt4>d4J`HVvTEU-?!jqyLDWHcBt-t&i8iY{s>=#0 zQ@m)$wu7#?9rWL?_3AG<)&A=ER$aK9H@2j+exP$)_^QBY1E0mfpR%G{KXa@6(wA25 zakQDLy|kZ7;sxk^=+qH%Vb4mt^-aNK1X!H~>RB;ftojGsrpS+M`9I@~oOTx<5 zO{$~b-mPB2{aYTt)mw_*NO8+X-x0b!ea7Y^w}Y5W5o1wPNEEc9W5?=xmM zPiDTTU*f$GTJgryJl0uWg;o3(o3XEHw@G-p=cl*F?WK3{^nXAS$00}xnsWE~2mD<4 z2RQzuGM9B5{hZ4aKgdW7bU~JDVsBsfQB>rA@(=jB>jw&g&y@W_>d7|lK3{!#>QBy_ z{TqkXp7_iejm-9Vj>w){HvI9D!ut||qud&)tuQ?#JW6fzBZXSylzec2*cZyQnbY{W zhviy-iQJpBRHeKxj|Pk9VwH6L4t?1kwkF?iS%U28fN+i7iX3Tr61#h6?Rp`wPAFK+ zX%8|mf1}18t!sS+fAoek@lmcmN$*@!?d#}@AS3(IqHCz%q8sjN2De`>O;5ZMR_(e?aq9>D|+MpWjEgUN78d{?=GIJiK=Y+_>DWHG4(eqVRAP^-2LfzC}Miix@~4tQUASZVKif6WA2bI z^E`7V`^->LV{4Vcl>va~qegZo4}AGB1^sbE?W;(2$*fz^6;&2irUjq#T}^%QqE=OP zX=t$F=lGz9i)Wr_#v^5+xsNAxD$))Xt|Vt?8tq%zG9owONVdIBla`J4dF>|#m#Tuc zMVEg(Un$5O4-h-%G(T(fzPi*i@Ji=!}8K@5D)*PSocqUY^n_swtRM zOVclW`dtS(pC%zS(si7DfSy{kcWc!0uh4e8HdtL~IsQ5Z@)jETJ>+lFxsRv63V+t{ zVoxkqpZ09WeM$N6?uI?iJm9+PwE>MQNk=X%s9&_+n0Rs}LS*cc@2elrnyY(vB2RjM z_E3vZpPnc`o&M;H;7{Bi|C0A_!mtMyV|93Q0u~!iDiqrm9>34XnLq0>pCJi%{0IE{ z2mAxJ_5K0uyZ?Z5b>47L18w2D#s0HD^H<*{O0);rN59s~98%HtsI8_g8{Drt5f3pB z->f&ibv4bL@B8d3Z%UHP*FSm>o@=xjdG2SZeQoZ#7Mb+kc>i6S^Or@Gt8tSv=)aIC z5@rsZ8(;{&(4EGzcGe_wF_X=dp?DEt(~u@mCNUfZbq>O-NS7FqCZR@Q=*#o+y^BUI z`gnd{bNd?_otXk_pTMK$Xdx-V#IC8$wIdXb_v6^E!=Byqn$>4!kwIxD+z;RD&|Vx zv9;nPXqZk)?ZF1}2MfzQ9~7lMrTU7DP@&ghOwGk6d>N2Bu&TUohtJx@$JE96W9^SGGLdjapeC;B9RI4GsrU((I!*%H^-%4{Jb;+_MM5QLS7tQ;kxrb3 zu%y3hq0_S*^ex%#hfM!#;X_zLFaEj+c7DQe@SwFUN10Iy_vvtT2U`3M0Zn#6I$z2* zF=K%NQBi$m^NNaN3UtNVD2;0H(;_FzT!cY#l*2P6rapX$CE}X*eZ#GJyxfp`ENO%s*>w)t(rAeR zGwO&fyUfy-8&U(^-!N3lkIh3xZwESd3|>r0)YoCBAdzu2-4dZabh3)6;2w)+{%bc% z+{?BG6G-?uT0D3>;i9!DEdljR669FxGfM@Bvir3wV@ve9JMy9#Y(cf!k8tFVC&TD7IiFM}6FUfz32CY72vIY8 z=^=TrlZgtF0nO+f(Re-fjrX_f=^oZ`yKA5)&w|{uCEgEwJG)c<-1rVLOXekBx`fbN zu^+iO+~FVd`o@*y?#a``u@dOH9?HJ%hvL$Sh0j#o_n&MBi?jI2ys7*Cw#m=VZFEq# z9_?S83+G>+#I{y{IZpY$@FnGP{@*VK`74pHZXGn&`u05>fB5NhzPZ=nDre@7`qzKJ zyPEJ*_(6(^{o`xdo;RhC#bmgI)!Utl4+%HzEsC!_h+G)H6)LJl#~k&EPv}SEQKli1 z1>aG4_+n;G2iU;`x)9HCX1<0#O-H4~j>G3Z^L1R5qgxymP+AsW;tS*<9 z{PI74qE{&**Z$S(e}Lh1_qPVz28e<;_D)E)7P@vkVEE2|KgaFWbQ40ninemfr4YNP zm}x>5zlGzW5{#hSdn-ZC*WV7lAjqZ_Ws_(Y_olr!C4$9{VfY6T7`|lvebu(VNcYyq zm$?0I-9FR!B;>pEuF^k1ZNFxP_lKe#X;@81zc8lw_MZFw&{wlQlVY*!2(2VosN1p6 z!%G_34{xvW*j^hH?M+&r_%iI;RmQdsFZkto<=RRAJ#*Id-=4t!xa%3H$oD>{s(bib zSsNR&dKPt3A{O2M6*NzF3$WomKHL{=-hbXy7*Bj^|K;tAh0tCdKnxIyUQ;N(uzCB? z#c8ev$Gpii)}i7pi*`pYo1`6C)cIKMPD~egd1R9p_|-pcD)46LD}3hq-!~m%x3zuN zP~TFA-ly-Kib!1yIe2e!%Fwes=^udo`{Qo*C}ne^wAy9f+%NZ#-EsH(|A5Qkq?d4vV}<=8`qS}a)Q zH2Lo4wFb3{A@NRf=;=Yde5-x;e(!7A2jM^OF1*pJvb7uIJ)CpZH}>bl)jJpWe}InF z-nkp^lm7v~u@?>!?rfS8c1c(u>9i&;#}WSHo9DRmvIeW>U2*e=8t2Hmvpqo+UBWyOV#aD|~1@!fraT zd$h3#xB9BuSM|@_0?%!@5BPYR+(BJ2(nm#P4QaU3{lm3lo;p8uJ6*maB;Hm%k|{yv zdnI;!Oi$y&E0p^)OiNT`<17yn{e6FZv4yf z#m~Lf{_BeJ?+hA_n5DGbJ&y^CC5)^r?kOC0?CxhkD87>R;99d+)0OGkv6HtCIxCcJ zjZfIIZQN~xZk#zYQt-B=5yHBjU2`>X^c&bVd`KhTGVii6^?>t9>$TZ`K=%e1zp6SU zS^FFQMh6o0=J8`iR?O2GyVJ?HEQ>A?Zb?77#sxTgJve%C;x1xRZK%9xG0FMY{D)|% z!Lx0#uRcs@=+3YQXWUPiKAX1s^2xb9BIn%{56{S`f_48=*KHn0 z%gG@7p;wRiPU_EAxSX&}e|mLs_w>kZ*@2k+<)7NwFK&J1nQ9AuSilw^{F8rAQP|Zj zWc>4Ri%P;L+^>(LVvTDV*TT*%3&aKGeEAi)UG7#pOOaahk@@!(>%o_Pn`#e&M(Dp zX%UeoI4dmDH4i=@Pd7In7fih=$zceyP~qp-zHu0E^I{AKEj?DFTr0tWIs+)n?o|C? z2JqEF_2r(l4n4SoN`qjgVs$L%x3}0O#+qQk%oGrZ(IfuE)T~q|FsAn-1SCCnD#Tnf zk%Uho7r(NC);g(_tOo}(r8yI?FlGUnQ`s@*D3B~JMfy-vyMB1|D#wk$cBvOZdlej~ zKwSEC(NK67W%mbAy4{D*oX2=3sX-dK$=pB6%;x9Xkp?eG64ro9K-1F?mZD?HsNmQ` z)rUjNov~B$Y({;DJbI&@aN!Sr%?O{I41?+$`U*Z!Id}9e_e#PGvQ!w6F zqUW(1B4z2V&5P8aL-Nbg{gv3u-1qU&JlVqpAQ6tjcsx$moE+#xtSZzopNq=h+7)b! z%9GNKmBTkF^9#mQsre?< zRyZdP)_5py(17G^@To-hM6IKY@Xt4;%i`vI9@<6mzO=M^yO>MdWr`@z7sTm2B=K0P z91POkbXI=2k6t<9AU<37;R!Zl&2TS@(`STe93Rb=!cv~R#nb4R8+P>tGn$X3<%Kx_ z(5NVOCx9e`%Tz&$1vI6{CD~l%N0XZ%j#Wj|-6&bSA!(fh3>INW_LoWNrwe(Rt2l(@ zLyi2gQlWul;ly?!R7f#0L`pim9}ec17QO&?5lU1XKIwK=vg~^Q**7KQI>VECC-dRo zeH*mBdASE>R+RFKzkcz%^{DQR6YslU>cF;YL)pG^$)$l2wTUN3Qq;U|)uqiwTmQv!{#pr)K40ziS% z7n!2<{#F-8xMew+XNu+KAr2u;PDEFJI;}&@T-}sk=oS%)?Ky>WkoNjEADLna7(3F&47q$Nj4BOsll zL%O89Ly#^7Mhem;-JMdSq#K|8p6hx3-59i07{;}>5?YzO%FJ!Zkx{6Wg_nMBb`!TNf!yrXBcog_q)$QsL^ zZ=aN$VvsSP0^Q)5a}rv#SQ1d+nLf8wLd>{M8vG8bq2%@O4durS z(|aP z={E!#s!~d#+(9(uPmii;vJV0J?8HX*%qpU2KFk83klYvt>4cT~C~x>lM|s+$<`ieX zs^iSk%^c-$d}AB7PWy-R!_&rt{y%_HiT^daeUtJzFymalk&-)D`bxjky9Qg|=D!&_ zhxfntQ$4vjJBTvkMMK+Ey<)guPkpeD@Sdd+I9lW>kDw*4$$c*YGID47}%DS4ap^#taG$TD-^K)I+}7yjCd~{Ra1m8*uptc#%FM z9@a1ijS;y&)ys>RM5T;{DpUq@PcVNHeC_^fi)=Mlevk@wd)waQ`J;n@T$Oya_19VT zV5bPj8j+J*Sc>`Kwt^%+LU3d?Dub5=8wb(f+v znVP{mnMv&dLVj~2a`VQ|r%RtHRS6_{H_dw8V^u$hmc%oYAdIN4I12V(P`OpixtBRj zaTMzhzAL#CZQU^&bPfB?|FsHOg#n-=&n@CKkxPf(S`?Kq*4VH7&d%d+bqgU(+OK^kE^rvZ z%Nw#m7WkYYkXD6Ve%~o$XJl4zj?VW zM%x1>Q8<_F6U`SG0%qW5th};zH?B zW}qQV@}P7H8+i~;9~u+}=j#(DnqHf#VF+l9g@GM{T3EgMyDZ?HL!z!Q#ttBFV*09v zM=#jBJert~T!moFO^*2!ywMW4#+vs9noN+i05HYIK_?nCSx9-42Zr90@G?L?LGSY| zFptRqN%U$##9WyGvl(81Z*prsRdS!~!a4LWO#L^Z`-_Z&^H9jk`s2X1iGnLK3gTO*hE&@XtsNRTpv98 zTIe+k87MY~j2QjTQ_qv(Dx37}3vS#N6ZxqLb!s3U2b-6xc*j|bX%!HSEQdi2b{I+6 zy-YkA4g_z^q;tj~BxbId8Kc?S6ezIcB zfV_ZxS4hru*;;0HPsTB~v``P_Jw7Zo2jP1qXNy13c9|0wWN^AB6FFfm$h}R6F@sp` zlFnfWK$mmi3ey6BY7Ga&MU<6={v9vb#HDij+i|c8V-=@32Jl%G6Sx@4#6%|r@&!X_ zz&WQ=<%N{=)-X=xL*8V-@|Jn?((ng~zhqh}-0@^yKVD-FmqCKxhRSpf$=RU=2qubN z*>=U;F0VC9(pJ_AFn974)cR622_cChIYVV&7!()>M?cIHZ<%n85w*`wT$C-i6>uKD z{udP30Rt7scQOJ+ah06#{3fwi2D-4!?UUV-BWwe5n-xN*l8MyQVQXeMpu+%8Iw@dw zZiEwLK%NB6$^b-Jj_W(Y ziYn0=aePE3@-o|)0E{4s0m3gInMKPu7$Au4C`$`P(%dVXV$&+Jox%BnwLrx^fpn!Lo*_LzC+dD13?&Mi9BjFxZ;`s znS7E%yC4z5c&hAdCRf=`MsDf+jQxdY3^HNk>QkLHSaC3(he^dip$j3(Z49Q%fxd071KMOIr z%mhYED!K{oh(L4?48$~p?HMA&k3q?r-3g@{VuH9qndk&jLF3#J^HPB1So$?^g)^i4 z?3_(6`W+y8o4;L3E(_cX<}Uj&gq^4>bQrLp6iqHJ zmX!VUrQM2T2!0i>InQd@nY;Jq2+u$skRJgd%R7bs#rO>4=rY41k@gThF7TMU0NVnaBc+J<)f#$nK2PJAVWiV$j?k!O<0W`UL+&I|fAcbX&AQy!*xd`AO?_TK< zkhwE5nK-9)Y=K4WWjZ<*42tVg<22!wH1A$SJdm$^gDeB^fMa%OqJ+~iZ$by2`}zlj zd7k`PkO)WZB>K_O*0aAK;M95c~>l&2i+ zw(trhA+wG=;z@URkAwFa99}|iJB3xVRWrHqRC4z))(MhAy2Ed9MN1}xBVdrg*R< zOROa=67tq$%Gr-e{agjo=$u*z)p5~i5HEdsAWMOaQoB5f5NjGvQY~>S z05`GXV;};Y+&NZ`whZ7VFtMiL#+Hf(MUsaQkp57Vb6r$5Dk2TR5yHq|WXL3e0nh|X z2Md^rhI!+-ED;1Y4@z+2+|H@ZPzLhOfRb-R0)l_2pxd>|_Y>bkI!Q7$%tnAruF4@f zkoC?BE&9X!TV*qCRqbVc#~JvDXkko3l39z6PwD-;_80-2z9DW&X`#divHC5gm* zEjFmAyu%2b01;(fL&fHmy}(ej5wNfffSk5RIU{)#QH$#gNFooRUsY^Om|zjm4+?lT zQLFnxT+04N<~RsJitgqodetqk1jz~}tB`=mlagPw?^Eri$F@ze41&%M)z%f>EC;^< zF}VAVD=-W;3agsFoe?DVI8JimoxqG{jt&1V^-Son!n7qGfz4f-pW1?;VRELMiYM@p z$p|C~{3NJU?UUaiFRtYn6l4gwV;3EJCn`ZSUTTkn*&=7>5XeG`6G#Qcz}FyU*2KUX zM0fmaGlb$=fPLiAU}-{t0*nxh8K2z&9R??E@J^7eTCuPcc*^vtfzYdQiMgvV>Zy~JLJg`;slIi z1Q;qRt5$$L*CF8m)|`++ryvDTu+aNz+V3Q0=^sr6637BL86bglL#4w-AY5p3=gGzU zQ7jT5h6T1MDi8s1O7Lwgh0Buh$lvD!=_ZNn04#fDW!SPz=45ABpmI*qUkk*br1=ma z#2p6`@BAI=6tnfGtfLQf%N&RhqL1Ve;QHwbIwTct~w#2Mm)9>MZnVRSD2y7&( zv?bN-__~>^ivss)&)^DrfVTvFRr{~tzCQhH`jc3~s0AzmKc(j%PA%zBgow(DRYU$_ zTB`-*I)jkSYFkOh`eNSP)T6fG0`Wsy=|6zc+F|~^5Z*t)mm03>a1x%Q`YdJY&*F_C z!X?~ZGL_hP&SAewPjjk5SMzO_B#NM@Pg$UVInsERd_#T5cem)4)^z#4nA-2f=Y-zV zDERX-pxBrbLXW9h9<~;8{(O{{f?{^1zu~{fvN8kXr71+1^<3EZoIqRE)^mK1E010s zT4^(*#fNZ1J@j6Lm2{JDn0$vx;w#p=6Ao)5Bf>#&tK!P0M0z}X2X59YlA`b|LA@zfaA)EehRLDH{~UiwAYO|J;)cPi(F@G z?-s>W)Co(c>H_w6B-`vi5Xbxt$lj0?GMHP$ra_4~lpZlDRoo!n=g(FGI3bY$cM@5T zC!ya}G>ceSCPx~@C9dnU=j~aJwAb}sjb=C^>#HR0dU+Vm^o7+QW(v#Id>rgI*)lB( zDpVKIVUEzM(>!|A3x1uwyBBhk_9D6Ds423$xb(0qz!pvZ{!s#Xi`Ve8en-2TduTm% z)yUC$@g)AvLGPcV;Ys;N?d{H=x=;U2asF4W8tkW-VY?=OcFwnz(-!e)inYnholTcU zw~@;|)wCR?!*6|a+)$xXr6c^aPmjodG`i|!k*G#TcEfwdzCt9W*P|hZg@0V{ zlL7-+&2FSmlyTRj`MOT2O7U#*A3$q{pFMN-;vXQ%r0T<{6&NA3`#r8qS6nXYCMwFU zro)Q3!ckVfc!={4B2$t#X9`8)wvY&Vg}{$9GH@PyHeHDD@X@QnZo!V!=_}s)#hTM{ zb!JG^ZB$bI*Uw&KDKka2dwK`BP@-PN!*VPxMR2u8V~HPb_0zP+wYprh(96rm*(jk% zDr*%LAeM0Ak~0A4TK0rxzbn+9Mz{Bi=IbqfloHxJfk9NadY<%Ihr3R zmN5|(`+=VysusloD(WT36B7AN4GU>jQrdysW# zN6fEZ=fX`dQ#Qe1+@9DSY2#0mGmSYZ&}ZR$XM1xVzR#=s(_&>L0C>4KS2cR9Qj(lu zeXu=)trLm)MwC;$!~5xnPoMV7Yww~JZ?03}vgyiQzFst=n7iTkdD5*-M`hD}qISBv zYX37;?4Kg75pvF9jNtO5gauv@iO%bmmaS??t;J6E?;-8y^O$061z9L)&{q5|8SJ(2a7kTd#3wOQgM)5JAIhQXk#$61bKVx#S4(fJ}9 zJbs98FxDfdU6_Lp4k#j%QvUY`qmlKup{pt%xtSnI=NOifUP$Df^Dvns%^XpQ-Uv+3Sl!gLSkY3lK|2t_LFsSjq48^ zVTNnV)W{Lb`k(cVAL zFE4E;oBQc7Ond&0xT*N?Koo+yy2zC;QVedUU;p1@djRw@HgxXC(kawCyW5yXF?jS; zZD~5ArXZbnUf{!TIA^LOrb3Pp*3Qg9T>Ew1rLR-0htI?a`cq-_6**2szbcKLLvxU` zWgbRRp(-|R?7}15{iH!^?nH?^CCTShybHF=eqIQ(&TF?L_UXpTDb>^>mX}RRi$rpX zo%ifBT$du=8Nb?V+%_@|a`~l>(MU)hH2Cu?ivCLN3fr%1%jzDl^#o?<`yGi0Aq?d^ zVix^+MLTsm?+NnQe(VZ7RI+kc^))bHNz}9JlDw&Cy+HJ{7Pwc&wtS+9}2|=GsuC%&*<(}Zy#J^tVg>F|`qdz_eP^Ht&NnB-E zRBKM#8m?%Sx|XM8a?IN*NjHlKIM=0xV^}Z0H>#?d%`vLuYw@Uhkfw1SntE0=xiHpB zUn;<5O{-98<#7eCUz&oYSW}2$@lV3epV|N9^>Fs@_V!yDQ6+1$;Xtea6mB{FkNltD zQ@=_*uG0Fgo%{55!fhS%Cz=XPE#)qLb5)g`Aw_717p=s|3&7ag)E?>3{YsMyR@G)^7aF(c`yIJi!v! zvldkN@$>WO3=-)QonmUC>yQIWTIQdL9j4FP5tu@I?V|)+_IhD$s_$lPBsP#Xo{Te+ z8>|u^SxU1lunu~E%6l3s75Uw$_9EtX(9+a$77CnvhdyKHmT<@hrjK`jS+4&pYHT8 zDq42c+*E8*d_T)I=l`_m*)72CUY-2qXq&odd>Dt>&q>d+>D6rd?P!R_;lL*zihte^ z?pS7M`|`pytu3vZr3=C4LVBDrrmT3tF^x1LTc5Zo4_JG5Wo;nc>&sgcBdJ3nSdSTOgY1}L zEIH?2%Kju7+d0$xwZj8?6Cth!pc1W;TvVU7uNTpa&GB)zJMd1>=`7XfaEJ7pF=zz4 zfMvalS|%Se4jQQ&Vw`2Kc8Yp?k&dy2QQ5j1uN2OS^mgkdSf=+*VLzrEkL?#66{-+c zaE=|XvG;$Wu$ew7Y{7zQq@Q;QmQzg0rqO-dyF&u&^0Q;hp5~tH4#^G zw)$T!&(rsI?E8a89Yz2lbjf2Vs2Gd)MJOMjDC94dj9}KrB#{9aVH2j1_Qf> zxc}v&bGomPhnxrc;@7aE3!9gT!n7adltDB}%87X&^up^z4@Q*2P_u0Zbsxu|J-3dzEQ*o%~$x-H4wmsHL(0-Jd>Hb~h-*{5cTY>=jH? zIi2ZRnbWxQ!KZY5X5pLdn4flgNyuUf2>$Ih!oyjJH@;S4hF|}Y_e$sR9(C2u5;L|w z&9M_JpMEI`r+d~VuLTywk6X0Kw(ZH}$2SD8h~(7C*SN4_wO0(XY(Z!-K*V|P&h~sQ zYn-kcmh3flHj!A+7d{J9o3&Etn88 z>W*-%SQZRF&?-`o!73{;?n)3sy~g&5*tKhCJ-Z|LP97H5D&`XGvgr$XO)I*6(JJPRbZke%D`}v99 zUFL{;bH0v;u$}0pb9Tq8mo=u+ah{pMNOs$UF`T5K4a7RK$AKD%c>_Mo5~MoQ)2WZ~ z&>TOkw6ShvUYW5G`~Rhi(T0QAjO(Lmiw+6gPY=C1@6Ip`sk{DfXvgkfDlK6@uz!N>6N&j)08|=89VdI`vP=0` zOH96@!g-LMAaDdKKnUlR+$l69a2ZVVmgw%=)-gFrsab)l#C`~oh!obno01q>91R^j zsTBeD|HraF0TT5(lQv5ha#xN{zy11SXyQ*GnT0^caHlA<$TvL#`X%rujRZ0>9xb#2 zy8TP7vK<*^kZ580?r=U%fa%*wIh8$b8HP+l-tW-A8dihH;Y3Edv2X$N{As_Ev0EAq z|NZ@^3aZ2KC&g+18pkVb%SHa8m22Ggn5s7YFU55@B?_U(0Y1-SuRfHl?%&zpb5$O5 z7~G9;yeJb6iP&ev`umAUz(6pBny!DAu}f~#;B&8sFc=-a?7PvCn&r4z9o=kC@llMj zZE7hzTAdBVh9uyKaM4AsA89s*NY=khISf%LL+_~7){Z!KqpfsWHr~sIwz15X-$Bs{ zq%`3^*$xir1&5R}>${Zm6W9B!Pc2c>?`=3un7!p;#rlmJ{*=NETm;jEOAmq)+_&O? z8x>nT>X*I{7ipl9_NT3FGvCt&H!XNM*MLxPWSVW}3&ELFdbai)uGpOSw*dz%La27U zWu1P(E6ypbybsl-t8(y?Pp4`zvF`NwAgXHM`lDm`C8|+px~w+pXhk`7nrM)N33I|# za!f)=b)BAehVP3O7r)P!Vx)Um?aGW&sK5!!Tb849{lMq&wmzD0G2EJ4wa3+XM>#Pu{RCgYaR&YH<~uMQJu4m-~goMInYqB7qpm0z$) z=g6kWQD{0HuIBe_mG`cm8dKY3+P~*4PCnn_?e3Q4%#M`(BYNgBuHNm|DP=*<@7yyIQo>AAX}( zCpp2Mde6>}O+l|3{&#HmBK04DQb}$0W+}ohEJd$x{l_@N#$t0=$wlnM1-{!lJ#j^M zs#|qUus@v(fWtZuH~8Ci&Lak);G#q3qu>{76d%mYVV(^eNUGi>d80a_Q}4j|>4&O( z$-AxiCH%}7I{K&ZVc1>O)FNl3rtU_&tqoC?o6V_4mUSb#2}Fb;7X6SWx#2)I<0zEc z+Y*+6&Rl18qa@m}kw2@({!5qz+~%(t7fZgo`r!O&Hg)zTmz(KaMwj7T!kNKVaG5iq zKI$Lf&yluf6WZF4%=ssAf#D~yuHk6gO9QnssS2+Q^`dG=Cz%%mTtb$SLhSkv?g@%j zy)TnCX1Z&D* zvL9}*7)|Kufi6;rR-?3k%`|OAJrjLvY7|AiF8}r2bf&7*F8PD0V~cjA$2TXpk(7~M z{nnvm8{yrIvFj)k{<PbUfl3VlJ}!c+&p^#Aj#vPAv7< zOtocMEmp<7z?;jo*hX>zw{nr#Z|kBJ@Jnq3p1Zx5*z8&-S8TS^ zx_c{Vm|U3Dzw;;Q7grB)=7;S0qP|Ffa<6aCGU=oP9JdqCFMsIBL@F-& z;ic>gx!ly3c}ABm6t*92>qRHg*$2D4s<`ENdD12Yi?ELf(01dWioaCxk zwP;C6&7W@n{ z;m)I2PcU1f-}O~WsxvjlJ|!#q{L=v23UvfftQ$j$5RT>F!e{uvfc%xR^VG=Gn$sV8 zqxUN!4(ILi!;(wiXEgCJ2Us(-(c<@`Pm5*GwV4KnW4k%!bl#0E_4RcxVl^tXXg%6S z*^77ZAQeY!4L(dOBSaaDO1ULQ{qwxh z>9J_^*x%LAdT&yASK-)*p%k0)_ep#^617h^R5>WWAo0Zoskn2QKN2lY>! z{fF{H?-wG;^@JiR+QM-bRm{z?BrpOp9E0kx$cn~Ac88TBqbTR{%v{z+e-^GNu_;p0 z4A}e*mX6R>yYY=U8%sA=g?KRyL$9k@YWi~Ye_SvBUP?g5jX&Aoxuu_w1&?-aQJsO~ zgQ*UGbva^9{@qHSE!&hkQN5NLJmB+Gzy7&6IQ47}9M>xz_|P0P+;@;}eJRu*Y^&AS zSX<3H&9igETqKw>cNXPB?^rd>+ltw@-9m5i>w8gIeL8Qs-jkyb8If=6^ittgde9Sju`IV{Ho|PaMTN}vZY?7 z6ZEOFCs#>QJrTZUfNo_`y?s@4*T`Ac*-&c zKV$irV%>P>Ca!uqtMjtxxM*-o10h_P`4OE^m=uC=pSI`RG1D zwAHY;LQg2T@QkNOgRy8K7Olxk^mCHnsEtG_wzCs9y}m#%`ooyZQd?AjXPm{hji!RW^{@B)FM%4+DlaBRlKsKDKKW` zXG3K6Nk5X&E$I$C?pO5>AZYJfL-QD3<7mD%qPVsn_w<`$t^ba2jKt|5Ao!E;@f-My z^ubt95a|K-VZ8%U>t+?c#@7--l~#)!{o>3+x58*lV4oQ!gE@mG`jqNcN9F13+M<3V zh)*!swF7|JnODZHc~w+|(u*xJx#y5T?N=78Z^=gG0&~B=-0I?E*LNz;k=jh!*Rmj? z9g5*VDCcy-2AfKOM?}Zi4@cqm>_yIvl(7^I1w+sE^^P}{sL#A~u{CM#ILIUG#f^T^ zEiX?@*4U`K{Wf5$UD^I-#slI=TQtT;QMcM72_>Sc8DK>ztb&{z%rMLvW3l{xT@?Mq z&6j0^h33?&na>nwnkGy`785(j_w*^=v_96dsQfac+;6ipzGJ+h^31B4^A|>tEgNq? zx|hG@USBM-PE~2FjQr=Qz)Tand;Futn9w%W*U<`f=Xa#7@yoglu`Zb~{_rdFmBv^?A$ny+Q62X@l2)BIZE^4@LPcK=lB)0=2+FEUCm-Pl~7E7qMAtdnIK_eWxl zl%4+rG$9+s#$Joxd#2wr&7X&FAhic_y>)Me?eF@D)ux3sibLgYnejL*$oyIDUXBgp z8YB+=j=Is;^sKFGD>R6e-k9L0QuD)kBfw=x#1K8X#FVaCDu4F+*Ru9Thwgd~?;X+8 zQku}F1np);i+g5jq15r!)o!*#4%@pYe45{A<80V)D?^`-(t4KnY&K)bM01fwjP~2k zX%t_5~9{e(}+z9-xx{#g9@@7$Jd@iSn}P){^}I}iok>3LH)B&ZsX@a`n7gzfHzkH@K8J#n9wsoUERb8f&rU}{{MmDUl{ z`_E~;NKUKYRNiCHUv9157)ZuFTGZb&Pl^$~+SMwZ7;{A*sboKGc}Vg~Ua5U-t4@0o zeUIh!j4ivZSO#rsZFT~D6JmP*w*cQj*<;uBar(3bKbB>ZMlmV#cdLu>yV3d4BMCVM zX!A~%U!N~{v4PKy{fkA<8N897n%F(z@o&IY0c-4Ae@5EAP)Rb;-5aSf=lgQeg`LRF zF-d;1{gVw5Z6@jU6-4A^U72lig?c_J@N&)Ir9{bX6J-9Tk7F7N(7y%MC$RBwHUs2(>1m)Gx&$q#)$Mcp*kRO;IE z?~3Wix)I1#FOY9n^{Wjd2!*QyXO$z@@v*gMBXN}}>$i+Nz-$EDXk*PItbQ0B zKHv8@d9$f2#$JKDIMjHBcR5}r5gS;dS>Df6$YIStj25?0LrBNVHu1FB|L&B@#t6bo z??tN2C?QR$lmNFk!g00Ar|;+P@c#iCdLyrbA8w|9eH**9*C(zcWYGM+sr03>#cc)2 z`)Mb9wqOUtYyPAB8us3gpNOxv6uxHuMoWrqH459=NY+8jiT1Y3BF}eqG&Q*^0UFm z8tS~p|6q9p`t-&|W|Ez5kwY^#yP>M&*686U&|O+`F;p1sa1yQ06G9^ssP#C)pESjI z>huKZC(NJE3hu&jo@RY&Vv}y3Ovkq1#cYU=5OUtyox9J4eLq>>-}$k#r%qYFuJmj6 z(No#}X7VU+?1A^!y{_Wm;xfl2MGu~()9+O&Z2X~PfVMU8?YNmagCz3fd2c0AuDZ>r zs)EO9%G8ZTeLueSXX4HqMf54P`F||*e`8c$*B3ck3~*I_;}hyApSrdDd33HPn^)yr zL%5K`nG)JzhWqD*y}{YoUQWba2<4N|A4fVHtU3P|-(1k;E4$_H2)or)`>M9pJRpxY z{p2C@wn;J&sr2|Mx29U7Og4$la~b(Vqi)AV5W~gNE%~ad*cc^Q`wP~u(RRs@}>l% z&w!uie*LKuy}S0eFc(jovh@_MwuN#vc{!haH>RkM5|`E)cP&}mzveHeKunjYL}l+N zc?WRUwvSdGS9+@C(EPS(xP0f4t62G;tIA(B>-O0PSWjxe7|VK(d#=aWUqj*$F@e8f zX&Z7Z8yRo!C-Qz)`qMjQHDz`s9VzMi*h?@7U@m%COaAANv=VWs>dWS`I}(VA=_tnK zu-CT~61`lbyc!|sj;--%d(wqR%zlg_ML zF>3LMt-cg5mqV3S`k>_#lM{>Aeq*ZtVs4Iq>4Npq4|FN|U+LL~Y*MZs*b#`mayWIH zB-a?Fd9P%b9$M)mQQ!Py1CNjMsC;SW)aboP<$q3N9~*+h(xIK=8qtG7xWP;#tgFsA zY~uU!f5?)LRTt2X`FTYwQ2jIgQd6sm(8-q@)i)1QsDuPlv778Pw zHGq_xp%Z*}UxPjAv|vy`a!lZmPdXi0oVQs4mIB)9s~ke$5QKr@p-}71ECuogV}4Xc zu;Igao8dSJbK2+houD(p#udT0+1~<_WzwaigG9lYKp~i!ib=7=n=^RuSUkcWb4G^= zfq7hd92)`oQTX;+V^%O3$0+3Ki!3!RiiEb%N{-!vn-^D<@-7)*GIm`?=x99SO&k4! z8*&Zf0E9dUbUt11ZuAX>F{?>ufkfWU@JwJ}p(5gA#uRemX@jjoNpL0JdB2JC?CZ$~ z10oy~XIokC} z761!XVZsfd*$cvic0Ouhg)pOSVHh9DTxEOVs)L-L+Vu!JhcM};0{lpT$>3B3qBR|u zEoVNA$*oDrqY2F?37VicCHbg60}RQhB*_4quY>+75+VAj{a-GkEY&8t9Bn_4F|3b! zF(-w{bI?Sq8lUv)Y&~)OtaFPI|Bt6`X~0Ia(OZiROnmO1Ob1Tp-$VuDY{Ix5W4nK z0Du&KtHz1`eXGJLGk1F@kK)lzrXhH=HW4>wuSK$O zxGgjda3h9jv53t{vq@u7v69W0F4-199;eu5Ske#qVDKRZmP~9fO-#FW5!Nd;5|$BA zzs%=S7P3GyCkrAhFk`+wsjw`wNWN)-NAl+g(g|K6XudseG@4BnhH(o=0{#s7ktVyQ zL%rPfD3JR2zYpkyJ}l}qag?Lq%nW}{tiLaBYpchD;aASq&X!%ug>H`8p{i7WYAf(n z9=|L`j7nIsaFdg%y<%aL4;1A>$CxPJy#9f~la-18QDQHG=~6`ooJ6y0J{1Rvvx3qR z5U`K|PbQce2LWLL0o(X5dKx{hXeHg8i`B}E{sLIhjQ(&UFTIrF5L(FYthXpfahhB8 z{(9DJ9!hY^uFh9B>98sIx|8_9?TfMRU`%!feX zCh1HLgXiX8WxKYXY*(CZ%z}@=S0p50tiiKr&|Ozfwt2p=Ec%8crf1q4(qk>hhTTp9 zV3PpL`eNu%rUS8+({hBie1y0USiRvj2@X`~e};4!T+04(-yjQYh*vQQiGU(-A2(-Q z?G=y`SGZNGDw@8G0)Qp>hDe>4#;as#^@FOFLjOwt2nM0m*r`09RHaPdn`rO+G(M~W z<$4(@IP&A4w!&7aG!fT?*Q`+eU8IU#8a+!J{Ms z^)GQr6iuVSkCRr{y0}Sr9hf9Rv=bb$k29(06$^b0L#%26VrnNy8aVg}N4MiAdGOjA z4qB*=>sz@4{@7hfl%*0aTq+NKlQAUndizgc%m!=lPWFKOAB;e{&U3gZ zv53y(G`@$L}5tE%pC3TjDT@^ z+L~{`w-rp^=9a~|0+V(E0}ywwd=rG_|M-n#A~IxGG=VfB_?scZ*(2N8UQ9LRh-9pB z3=NS>6<$^{3_yWX5O9Ge1Sllq1`7_zevS{O1ykAWicg^@@el&vH|cj})@NEP7%P~} zWIQ;^CSV(aG}?9c0Pg1**mR1(=HU&H*Ks;!7Y=VzYy@V#GTL%IL*f}x6Xex8Ak zfP4nKK@nRNq;if5nD{mEHYkwqJaRAi)OA6O;T;4WHWn*Q8qvrYrdlev1e5M%6In<1 zuuR>L%P&VnYLO*lV%J(h2ADo-uHn-4u#ox2lkP+-Grdi2u?s4I(7WfEhRA#~83_67zQEf+!W=;!lGDt{mTu+X z;(=$hQ%5)cENNh)$tY&eg7CM@6`YzHY)2w*S5Fpi*VH=1XiyY6e+?r9N#Jy%l4vRC z6gsr2nPo-31_MEX!yBRw7$sYDQ-s&eaf5HJ3tO<51-l?k@kE1UG!lkW3`E?4m`#yG zX1}yJB0b8RlR|Jui0nwg7xFmtZCaKEV3*xDj7_cz$G$0;7y@{D<5)}EJa3QtM=;Mc z4hj1d?Rm)KToW1-aRVe?MrW5KNksB(*)i+McSfE;17p7@tq{K1_BbIfP$h~^xPbZK z0wr?GsTppwB_hIMyvf)v=_7qQTjL7wsN&PtMxZ-+5%Onn>BwcvK~P0qY z9K12Czo6D(WQX1J*+sF*wI?5eeH<6b<*9(p%H5h!f;mK@Ej8y^quX9vW^Y|A`gp06 zu|rMk1rR5`>6#x9@1L+rz6t95Y3k+g13%L+{VJW3J)h2Ud6&O?XI%S0~VVPETvP!7+mz>Fb?L&_g@se^J`ORM2y&afuQrz5A9#7 zDmhMWPT&;qC9UN7ek?I1g>9=u5%f1NRB;`i%o|!GRK7dlngzybU5uLAXkXR49!prT z3*hlpET)OakSvU;S@^F1Q`@L$CDlxwSLhoky7 zn;0dsr#4rpWuk-kKWiC(02x-c85c(TOQ8G4)sPnmvoLgai zyE|F-R&Tb@O!glj&RT!DF*gB8guw02PsbiI+l%pDOhVGJ$P~{`)>UYYl@6IDi-?%* zqJ3J6s7$|u2X_6T7dFI0{Xsg&-Why7EM)H|b(4PHWq|HdHN{j#CUJwAU4@z_f=$1k zFOO-()NN>IE1Jg9XsGQHUyt#q@f(DKVOUN*;Q~h>>%Hvg14EagMx}m}StrwsA8j!> z`&c{mgTX-qMW%bOo5=??fnR3^l?YtG0KG<|CT|N4rzC-SO=d$)@xt_T2d+|$vYFUY zAbnff0=EBpy}k{@55g63Td6Yd#T42t&&7}5>YsA1QNf;OOX^iVv~$QV>A-lMA-9}2 zIuF9r9HAd#jY%9AY#Tk?Q?m)R?)YuOl5z&{S2#^sR_S#m-Og%5E9#xCgtU)F`L_C> zhYP#<8Y~?Ln`~Ce9w<#1A}lq#%jqYE_0WExo2Ej;GP4qmB3n0=oE1wxQKpO-@bALn zrOHnwHxq|fSzHV1DH(9SFF|yG^n0#~XpXAUoRYb})|u5!#r=};h?i&Ccx!d>iyan>D%J84G(($5RQ%iY^%MYL}ULkbhV;Xtps6& zbXnWhqoo8;0`49ez4z+-GWw{)(j24YG+tz>I`#YfTKzo0n*+ z^1f11o8Hs4u4(1BmJ56TYL`f-1s6bHhp&k&;Nx{u(jPB3`tttg`9lo_8pF^3)alb_@q>|2$61C|9P9=r1cd?1f@o#@QmfuzL+Y5|- zFGToBrKqmBL5as|yPUS(EI2t$zZw6=Nh4Je1MM8%;{=n>nS5{IEHSVzewZtiNklI|4Bc;8pJ z64Mba0m>)52_}Ej>`+j=TxmkkjbaQRsMb3qIp5rk7CGn>BM5|U ziQ$ATJIQyOQEoJ58XupjCoTA*UD$@!4GlH*rK~MQzpb0jg2BO}cN{+I0xvv27yjqz z-X>ya!qHlx%)NDvaL}2mhWi;kA4kER5jolkkV~;5EHnL05k13|qBg+^=L)#McSPO$ zV)3n|&lOyXn=!q_JsMRiMP#@ysMqx|eILHedbZ3iqeP@f@ZQZhWG=@3M|R4D-6TiR z+%Wr++O3LS$@a@bO_9U+stL|DlukhsPId2R{Jy2>?;GQ#>9X&(HW;cUZYM^^tBXY< zZWZWxub34%3TV2byVf%*k;)m(TVHT$TdC>>PgX8MNnj{h#jDMi@M(tb`WosTc;(;4 z?vJ>CcIf2=Qf(I96!~pxucFE(z*?T3VWX39ty$M0ej1hPuMYiU7{F2i(HBvh?>Q(P8|Y^e}#z2E%DE+SZ`*Il07*nta+ZnYMVz0(f8~vXLH5o zB8%oK)-|R%(6}Z)5}35TVp3VL2w%q|nz3z5##h2My^a+7$P+a_8{lRV?bxewDNWYAlPHE4tifKf*)m6KcdXlMr@z zaul1JA^B;6#DnWf%lvMwxrnhiw^8eqzH#XRY#26?IqeDSJ8o><^MmN%; zfYOp8rGt@54Mx}K5>X^4Af=MhASodYQ(6&u-}`)j$NeW<$8}w=c%J9^I66X_6vR#p99|+-=b+tCC7XlUc$NmGvzN^r!fi6IqFBtn(-IqPiLge%HWXb#+ zm3w{je=+jzGa09+tE-J60b8GWHnEIFW{)ea??X8Csx5HOCAF!vt0zx#lY0+*iX1*S zo?OvI42C?^%sPwPFMO+EZM8ei8uBzc@WG&2>hkaZx4N@+5;!Y#gs)M}zx1l{Xufu{ z&U4<~)2-}&FYf??M#QWC{Kg;V#OacX?PUep)!3Oqq5gdGklo+jx(HHV`c$0GIR~CBU5ubey_a6K1 z0W#$%@QJUnJICw?LEe9JGKT8(XB;!l7`TSn`gx_^+DNvY-7$Mw7GyxEw95%txU|Fy zu8p{Cx`@m%lx%u_AA7a7QY$|G*KEqe-o)hv!}_q3IK!iOVSTSznnkamC&8K)O?8Iv z88@6Fu0TrN?`3Y3Y`gKw;Aa_OkP-6+V30Jr;QTstQN%=x3&#Wlu$*OYN^J{sO z7*EZaN%6x%<`?#6logQfGJZ8t9AEo<@J#2@J_bGV{OxNGW!pD*804<_n`f;bX#LA5 z@ZPlvl`mOtX_&6u@fPqI-!$6hm3#Xksv#%Gs{Y#X-16w{rwz~2zUTh-#3^d09@N^T zH?msRSrD=mC|-Rb5AsC`vdg4)@#7!|v0iQ3C+Ics^JIVqsZ+)03H1(>QhrG)xfh_KRG;P1`vl#!esjb2B zeU10>ixgMaXM5Ve7sIZlh8KNDQwXog7tmYHA!X68awOkun@E0B{ohVE=`ChAepS@M z#m0Mf&j)$YJ@w4=^LHzkjjBqM3V)kg%1)QV#Q~rJs607!Al>CFJ4ipxb$>Wl#$;u- z^~LG$$Sh62SD^ZSlKjc`w1@q`>K6!lFYTs8&Al4K^bqfG-rriWRMxw*9v4h^BHBkT z4*mlW@2h1Sk4?1{l-O`t|3vQs`{c#vKIeMbnbd`==WG12HwImP`sGSWpW0}ynYXOw z)t%jEi4}#@opA15yAyS6%h^unU_U?6TI;vT+EEi67t2KTbndyEPkKw^^vQ>iCqfNN z)3Ry8z<_YLCo9M2|Lwvp9YwkomjAa4-?s{NmsQTiSykQSMmC5UgKkkA`Oo?pqD^rJNR8>J9`S=&&$)9(6>H&SGI_0XHbZdOZY40eh-vy>}NtnvUz^FX$>bXokHVIKe>Q(SSuF24EEo^YECafa%sRd;7TR-%#0qjV0+qrQ`@ z^42YBj3E!xWXAX)GGT%1yb`NAa{{)$2{RqfiXz_{B_d6+eUv|`{O>RJV(%I`Y)f;lmIWu{90|fDVLGT zjXgV~Gr%?DG&VgyRrrGw369l|a4YwwC#4gbewK`a{GT|*x1-o)DcO|i$n%LZti~+_oD3HfStpCR9DbjC*=l;B$xdLie~bx+|h0A+h)3i)qKb zt+}u70=)xC)32MFgA_}=89RO|*|MAK((!yOUR+w7+fnxF5nZwwdX}sBBL2KCsR>tm z-(h=ZW+vljc;nOd$yo^x8>kvtMnCY-7R{zjK=ypZv;#PfvW2l_T*#0IyuVpnNwSeI}h*QO{Jv;LGxo zH+J_emOaI{QhphqlksM6MRy_x%CZ{$=sm05Uo-V|Ajh+pK%uz)kDvV zbT&4?nt^bfop<7EH`c(#V#}t$#-739E7@mUq1wM^!$TzFZ3Y^LPfmOt&iDQUh@L{} zxnqBCUGoUX$0t?Fk)GZ*b6PcSYHK8wZXyPn?|v@P{(W-aA&sbDICkCbMErMh>xP`e zSiBF8S16~qpbDLz;@Cic-E5*##i+f?%jVN-pNjt5pY=C6DYx?Px~uf;NsZ&=8@o2F z%=&h*5VO3`*9LU*@e{}BSMSEJ)62BKsd)DH%fivKs61Jo#JzV-A=(J5v48pM=QFANlkl}?s>$2|(0FQ&W0Dw4YsAKQd<3zW#U zL0gg+nr!~W>GIB9`KLDf;kxP56`@**;0sHw`kFx{&^fv=1nk3Fe5 zoDH7anoC@&)i#~3F&6&DOA`%ShqM+R)PHlT)(`^h;W$ri3^O%Mp10R7xTSB)H>yNV z7`EI_=ddHsg0&aDzfgx|*tj$%ei{y;^9i5$volz3`1G8>HhleVhj-<_`xbSEwG+Ah z1`V+v3_9+wRXJR3P<=RJ)!IHruALivFXuJ`dH$QyNY!N0qUSa07?4PF-s8|ZDfuSb z=la;XB!bJHoEC?bLXEf_&be|-X9orcJNbL8-mBhxW})A-AvPANr*rFJ)o~!|z1;W0?Kvssv5W-+8T35Me=9rB{OD%Q?92 zN{E*}LSM@mdS73LuJfPQ!TKxqtGPTpX-71kgoggT5;OnZ_Cd1GnVkyn$H~{7Y|)JV zIkQkHmWFn3O8%tCw|dc}LU(atxWr?aQO$p=qb6LqUGhfDl->EGXd9|D%0g8bRI2HC zV(m*}P}ZgQ*_!J}3--0WTkE+Snt4Qu?v(lx1n+2Py<6wx>s$GK=IYNkK4-U9JVF_X z=QsM9LB-0*Y0)j^rOZ(Ozx=1OP0cqq`QPjK!}j>XId~%O8?;Z_``c&IwI%dRK0O*v zdO!AfRZF=Ptiqk*A)h1pu|&D-O=h6`Uxk*|!Z=7y-ucZ6^r&P&?%;Vk+19%GZdsk% z?W1fFw(q5{|CstVZOLs|9~i`ix3{mCO|dE-T9P&>=D8QYEKJ_tmgvrO;C4K(^{8ge zSj+2V+pixvue)C3&f4^~xwTZ>G{9eW&vAfJRr>5|z`Smz=ChyWGk2I$MO==vQu!d% z?fCDniR$te^?+Tt8L9~+rtBO-jHEl_)$BhX0M7CW?BIlzE5?mQZ;2hlu|9DWBHHcc zKjQH6Qorn-Y|prVaOsUm6akLu)?_fFNH$#=QF&maR4IVA#vI&09x$6GLVC}BtKL9u zk3WRS;79>pssIp?bYJ1jR0^V=h+723exQD57J;?ED>Y5}dv#Sx907iM)TF|0EZDit zYq2XGt$6cj3pbeC!&g!jy*KOOpGtKQimrrit^eefM?F>5g(^lB@* z%H#LPlaM1m*oYMTA>2%cR#@gKN?{n>cZhlH9gjOjVG@o9G8qF3QVPxUf3;F;^#NE$ z7ci(eGo^o5y&aR{4$=IYf1>6y0Fg+#b!A7E6*WFw7RAVHI9Z1#q_MhMiYPLmWP&Xh9BM1^)0j=_}Nk;)G&A&-jM`*>p6s2s`XW!Z2-;Q?A zvQwArf3?#kHj8;uqUw+_Dk1i$f5vbXL4Vc0MP`^jo*&MR7eWAe9!W()q-1d8aE27= zbQDM?ii-{z8ZAjBB)t%&!bb&TPPa!SKPwaj&VsYCk#xe7Jc02ptS#OgnCn2|0Z4qS z;mwK1j(+udlIzWwJ|46uD~;C<;}PRlqvVyb*OCop{((;nSAK@(+}4Wq1CD~4&5qEG z<8@k%K!B>expK+$pteS)Q5Bc9tXb=%?)Y{(uvQ#@C&uZnmTdMqzIlN$ z_52y@pQe%B`{%^LNm7}=Tfn^SjzOS&>znFv?iFPPwO5ev7op|9LcQJG^*^*Q0_KhpO%^YKZqItZkN#NP1uedD0D>Q?VM*Fsxq@9Cj!V0&$QnSkM3 zwDOJY_43X!wFnx{mvT>LIqV=j0xALOcm!FFJD5E@E61A^gc2&14$3FVhUuUMK*kz<0vK|!_~rT-b1IZLKAc zRUyAsI17BDnm2F$TCZTPEBcEc5E))Xvu$`e-Z-VFdr$VePuI|nmyb}%$PVi`7=JHP< zmIx3vO8@jeBW&lMO?l$)R|An{_nC$_)!#59E@vb zzYn=Wnk%Hf7ruDUwCL-B$-W?*jxLz^jk|W+pyd}sk=Lre_DhAmwv%D&9JPGQW#!CQ zy#3PTB!7OVAC@(K0W({2Rc;rmK0E5!5^Xj5vi~>jo$;Ht`>Rr6vyxb4na{M+z7K9* z0zt~*o;HSc8W!)%EYjUL%5x1Y%>oV)@~WhNSIwu7NfwE;Inf`)LDc2MuD#Hku{zD~ z8dpLrmOk6cmzCJt)XE>W{QZzBW2oDiJZm(zYckOivaylvRJ>Cwb?bZ9CR8B>9SKG5 zsq>spxP>loO#euAq>f&mORnj;7EP9vpqM+}`hxa445j>c%_n4gjO!EE`@Br)C@e=x z9`6*L(>xt8Sba%`dJJ%$;TA*N@f{oEUS3V~}1e@Bo+@ z$Q!sDRB8PU>-|Q|*rRFl8T0m{dmdlutb-M$8G6*qJ)zZ~gGW#nzyKN9YY(*5%k>tf zK$zSNC--?Va*3{+vP9U~^NJ0eYjbN;TiKfJy$YLxg&`k4$CjWtY6JE&<;HzsMLXr( zD=YN?wxh-S^wVP$n$+Zk+f>u*t%KSEB@L%a@A=Cb?yiu#i5uAtt2?uqj0X#!U*fW4 zQQx?eg3yiitLalkHC{en%>0ccEaA(yFZGtk#VU-AGnPnvK$nvUO4Og zSFTYTXYM$gQ+yA}0E;rsVZ&9?_tjeUpZiT)) ze6s*I>`wZp%Q$XrIuW~TMQh~d)wBK5`2_+OS{L->iD|y_RMoqa|MwAA1g>jaPQ3BR z4Pl)Wx8TJ$0!98#w||n&4X)Z^aZekxCVMP*ArEzrRu^Rk4V&seylbv_Vx6h)7$|?Z z3LARz7Tt8OrNjd>MG*naKmQEHf_C0=Y4lQMqzu;bAEqt-j+yY!9{3GrhCiKswoAya z@=Yo%UnuZ+WFq&9VMOih(&X&^O^$7O%ZzJh@&vK@mT3>JIh0%-9Fw9P_m{w#_XFD| z?l4QyJ+6AY$&}cBwc5mLgr46Fto&|m^6@lsKmnAKxq{2<_W7L!Y00`H$SFY)(Ze?7 z$H^2CozlSyPEHM^sM!|(znU%K-^thh@7nv`4EZ@Za&DE|vGo&kJ6dlq`NLS;4>hNa zf+80U`f>t-F*aI>GvotqTyAS;r)`O(JK%Zl>G(b-f_3!s%FK z3;uZ5!FJx#>H30M`A$W81R2mqQKo3U8#y_l7xs(dLi4oAfI`$;p%RO=;^*SZE_~Zx zU!430AQkU4y_DO{G@I~lv>l_b95d>5QN!nfIpx}iuaNj_8{Gq|(zu3vd>j-)-V{Fk zHCQtsYqy_shP8axBgd@~C-+EZY?^Q(ZT4&8c7<>~qJRsUq`v)-7Wev&R; z7ude=Nx|7P<>}vWs;*i0lN-+@PnX`edVA*WIx6=|sinTyBhK%W_bGx#)718~V*n*z z?Njj00oy6{?&3~G;!Vw_CW+WXfAdRx|K-orzBp8bp;F}Cb}d@LV{W){+#VkAvuO(g zE^F$8on7h_-Xs}_$OIYr8B<)w&kWj$ z1gN=CajH}K5-u@-kU9O!oP_>Da{jLyAuV8#u+AChZmJKy~yh`2VCH$-ox87L>cUYpw6t4IvCPM>zy@V z(DTxH0Q3w)WOr53Rp{!5jIIQ?Fg$eJd}gWt#Ym8pG#P%VPS~s#Yj_PK@;BD%DO!f1_epOuu#* zshz|5iw>Eh!B-Ros8p#F!we>444uG8?Q@Y9Q!W8Q!zSsN>K2r9ejpyGQ7# z!D7{Mi^}XEF-|Zrj~w|_>Jc*yj45fGys{OkmwY3J@Wm{zf>I#R`g05n=c0k?-y!_% z+yt5=7rPK~qR6}izMt!!UHS=e-4+lPJ>`^$SVzZona4#K42WDH1YmSDZyg8VNJY{K z$P*yd=Aqcvka84=)&|M;)!4*Gl>zP(Qu^?Lh+{(#s+tngo&)P)2BsPond1Uf_;f#S zj28t9mnpY>s#U$B&wv-`_X-6slIu+-<@bpC^?dGi{BjlB=uLDGuIh~dPU zL__YjAc*PMUEMoz9`TYJw+q>y9#D9$k45TX++{SZR%5)?uCja9=4Q2e_+j5cJ#2Eh z?%#lsIfI5ovVBuQQwgdcR8s6XKJ+G@X zU-@!xrP}5$F6%pN`1TYG(A(dsB>4K5Yc`+!O1Un0Brp-|$rm;kohV^uSo_#ULp%=O zqJlfTzRPxkhK$j?^D6TdkkHCDJ)HT4?%+)r4RbK;DgSn8I1#gBwxT;HHx=@*Ryz|i zA=K&2)WV%HuvjGOeNa#-7vvVE?O(29*iz?W_{!}XEup8i0DxsgsjRu2nT+v}uW}fK zU~iAF-+Y9+g^W)Y%4W#^42O`Y=;#aZRpw*Yx0Ulf{Pnt7D%BEF0Y8yOch@uwR`1OI z2dFRfeQY?}aB-78;&YLw=2NzvCd&kEXMc^>2Q(_N$LDieue1hMh5~h6dUKrdnaq<6 z+a5U@fi?2)8Vxv(3&c&TA>HeEL7gM9w|(f>3)^AZHKtw}l#7+&q~fzABBQDmWeQ(E z_l7XIPg|a?DTnK!uNZ!>=ahMD&B|MiGSoI_Q@s_t=+V=7yql9)PX^xZS-p!rpFNL81W7rJ1+a`75#nFw%0q}F7vo7 z4*j_NgNRK_;z1s(PW= z2XDxH;W-8w6LT*(45bw@yiN z8$WZ#gMpKDE5Q!d9)v*cCAXk}YO~5w`u4G)>qbwzza}`BjUs~3(c`kA0~}?a>nf*8 z|9*&x@iPHSCw>V(y)qFKkekS9Q03*f^CWbaM3n94hk`1(Gt7TLRj&F=;b0Y8XYw`$ z6gJfH;7yEdj}E@$_mS0LqD)6}r$R;$L-{6dShin4lDpH^DQWlf;Q$3`ucsH2X<8Kv zaQrDMmwSbW|IsTi^=|U~ucFMEvqK>Ji~ps{ z%{S3BHuI*BEg^Q~YxqqlfQu%JIE6i}6Ll zHUpVr)P!w+jpQH%qfV!-c@+1>=z~C2o{iK00GO8Y^9M~gC98%of}tOr`c94FDG%)M zIgx|0{EJzOm%2Lw4A4e}z8+Qlx?_hz7(WOc=4kx)si~-9#*1_QNyG@x?iZ)C^Yp|i zS?*Bl=JF@rUYw@6T`uBbUHHi~@Yk^;TW8ITa=OG6Nmqd@#*?mgcD~>LjyL0c4JXxd z<1$i}nZ8A;FCc?P?j9z}%$!#2%Y1n*((`LGmuU+}JIrZ0d2-#O*1Pdzv^&{7z^f}( zrL_>%(d>4QVYS@7Yw{D#{O`MzFGtPf%sP{WF_M-}fywP2$A=s9-aB5h*IyTsK-JUK7n_B`xala>8jpT7IgwseId?LZpNSXuWugEEsKlSav!DLHY~(v(ndNa0ji z0tF#?-Q+_}nc8^Sm4TR&TpzA0+#iiB-VNbt9@ht-BRjW9Li;g|!7qY*4L`10ft%2I zhWvp2g2BzQ;$3Cckm0fG|C9L!FF3I;($q{yvr+h+f=@I88WWPTJ95=ai^{7TD+?#w ze{6rKRn&Jnxb3yAeV!k!8b)7aD#&(q`lG+k@SbLDI^-*#{v=-1m+0966PT#OavTyX zw)TDMn^^`gMG!i7WFqBA9O=U;73FqKb8jnQp`_-fFa6<@f95)Qg7XBjdUY${3Q?dI zD2#!VrQ2Q+$7Y*tkk5s_|s*gGUNxcR?i=&{!35H*9v_h%;gW zz+i^O3kx&sE0M&bW>QILNm8>D$KvEV8erZ161sODt{7B|4 z5t6_q_nu^FCJJtU!;=M8O13d&mhP+wDo zTp#P?a%v-@e{?A6z9(U!PoIjlurOvZ138g&sqk$q8l{j)&{||^UBIsWh;b=a!yu20 zG+7Ksjax5T#iNJLW+@o&=ULDu#-v5@dTmMjlIpqhLKi9XDhT zu{!$%v#**WY3fp13`H7g&gsN=bE*CSBpmB|niN!aiULGlq`I&pyD9$&PKN5SqOKpE zumTD+;irHGz>YMu+w_OZ$#K_M@|+24m0N}SXDG)kBOn-qNl%lb|~It1-rH zDDTg8e$#9rwK%>Z@W_EXBz zSGi9XTWTIs<6*WUIR&){2uo!@HZ(1WW1Hvqi)hvSJTJtqw1W-kOUfl0nPtyPAuBUk zGrE|v6IfVVx;44SejSTPP=j&8W@(3{8*0d8b1jdgsH4tF)%?%Z)(oy+bD~e!G?AIe1!V%A6DncpO#`D~13kraz;i`m#``(9c_7??^Qa9NpKY zZ{*xW4HzRQ6vMl(_);4KvUeh#*tZC2QHw3z4&jktsXdl2ohgE;51Aii^pmk55-cJq zKJ^iRf`HlQspQ(QffFCy87Q`+11Jg}e(&$x?0&eUC z^WV=gT%|+*M%QletJ5%a0XzTQ%c_KN$H>%vaE`i)NC5|TD`8bmFnGZ}loqY$ptffG z?hsa!E(!=s2peU1q|QY#%djNhsNh!^um{5<%nHl|eX9s%`ez1_9L_ra@;%&{e+*EnOee|GG zWTmE{{CBbCKvaSYzay_;uewPbO0$6ls&cHkmq15+s9Y?3JAsJa$@^Q$dLtXPmvW-d zr3*TG0fwcj@SE_{0{(zM|1Ek4#s7fwO^%ow0zfnUAm9zzAC%gffId7mu(Mnre1&p0 z6$K56yz5652T`OIZi|3;%n2%o3%&72d>#gYNtvjf^@9Y&j?JXGUFbU_jR9Z#;JR1d z!yZs`rz#;!bt#qh-yp?|`cJh67^wUr-{NJS_oMR1K)F96Fl+`*tabnuekGU-TTJ3u_zY*fHSM$hPmQI2z=Am}+e zIU7cKAO0v5Kmi3`;}j8SF*dqUD9b`4li?3tV65gMTc(j9c>@ZfNyKstPaHL93D^k_ zo9a_Fk%`o3UyDL#bDnBE=U>j|r!htpTJUuTrJF5M<)w>&wT%lt=>%|}BaagVh+?cL zLOfU7cw{P?A0ccf6QO^{#b8!vD8L*_X^{~6rWn=Lg?{SDor?c~6;SCm`i(BA^yMC1 z71+Rnm}F`mr1SG@il-sse#_JtQ#2vK#PExpXtMG@Q9mmM&>=Ocaa~ zW1ds1B-x)MTeP-7ZTt|sV)PDHs}C-t%l=DK zgp4B-vd~b*SD>8?!_9})Hu9o)0Y2cN5m-nfzgv0%Nx7s?aZ?%ia4l6GQDKGc>(g%c z;>QFee4pK3(k;+$<|MM$*Ntgj%T}T`M#x0c%YO)73Nl}#0q7?eKZc4_b{jsCRtaZb zpgdtH@rtG=q+bNf=18sCm)&W+CLt`%j_T1CyOQ4}ZP#=wUSYngjV zmJvW1n!iNnIMlTUoWwvz%wa#Tg<+OdsX~4VteZx|pNbCvo8POxo%)A(w^qb45`X}R z)YJV)_`}ya@*65Bdrb!v@JCArh=h7G2J8O_Iz-7OxOLJJ4ZDpgPJze>RDl(uoWY0g zs?+L`DVI|I55EX3!wOS=O;INk2n?JJ zf;M1B@ItVSk+Z@QcM7mJKlS2Z;7qrGTxH zaCRaNghnv~j*Xb=O_(=oi0G@37ESL$Nhmh^+lpcym>J|^EFZ;^bXmy*=+xsZH|1nU z0hrj0;D}vOsOwOmAp((2j`ZUOL&*v1&wOx`NKb6Cpk`F>fOY`wW8ZooWq}`gfhep_ zXJ&{nzYw=B=(Se=nwUO97Y@qgH(@3zp>8cwI;@yx`Iu>_fO*{syvC7#(5Z1hNyrFh zl0RsSQecfR4oVh874!)vKTBhRoyYENX z43)7!8wDU5x^7AN317SfZvvc1V83iM8Did#qW@5;$!@O35=Vf&%P6%{b1znND~yUl zSSeQn=)?e2n8nYA7DiK45{Nv8zPwUy{!pSy-xeXkDd3a`4N{8$0>~^M7u^DM?ul{N zN63Sdsxq)xP(4nNh+$I!Q6Ygyf%H-sf_Xsz;uo>XfS_k51SF0_*t<{% z-}oWMaf-JNHHmgbiuii;(y<5|6yEa^%#9V@F&laa7fr}Gub>z46b>?DZq{T$?UevK zgYJpbv4Z%hQG1bN4PaT9TLc7$3}PM74+``p=ZR4w^{X&c0;bzBbUv(zUv(qeT7-L4 z(pa+_=~r$fD!9kI7eT|&=dVfMto8M~m}(c1=LeXP;1#DcobmVVtx53{&Hi{a6825~ zismv*KaNV7C72kgx`^ewFRIG&F_+Jw2g(BdCPFXZ%tZ%AAER11^8|kZvS2vQNc?jK zS|Cvir5}S!iMbFM1BZ?qQ~J380_iPh>QFG-4p!uKHw+m4`T28ZB@4<< z&p<_E-imuhMOQ6xmTo8$LDNj&F;22Pg~FK=24M6at#1`ju7%O^&!3oY=p!lT`;T(Q z%EV2mumW155Dd9|il`Iql)-N&zsiz64Dn~k>3)=(Miepq3 zAS%c4LaV*aC#8xh*?O%pZ&5oNmt`#IsG;n8TF=X++0=#U zkk54u55~ZkRm%ldsB^)mLb6FdSax>(;8N!Yvb0jn(0T88mMcD?;!JNb58jOm(*Vhl zm}t}k&I|kkF~grp5^i)?+lM$scf$>wnZ>j%{zDzM-Wk=OmNkf`5^!Ts-|Vv-uNES zq*g^l2tU`!e@h4yWs0XZPA-lBM~5OEFa!i54oYd%T25j7(`C%9!ndeu4Bj(Q>P9k7 zNy$VYeBeK$2v5<-EOI1Qa7@T`uzr=#8HW2R9f6J^9`27|hEyxN02FBCchce&qrR>x zSNeQjrah#X>cFkaN~&=6E6Bvji9XP8VHwm;jF(y^hc(3#dALT- zdwfR_aEunEqGh0hIS;m*W&NpgJj>sC4V1zP{)AD5M8FEiZsea6!G%J%JCzxPOnREP NB7VZ6c>kOI{{Rt*u!#Tw literal 0 HcmV?d00001 diff --git a/doc/pic/sponsor_terminusos.jpeg b/doc/pic/sponsor_terminusos.jpeg deleted file mode 100644 index 798302abd076133b13773c60771d64bff8f2ac75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31450 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 From 03581139489a19d7215dacdaf65173b6ce6c0910 Mon Sep 17 00:00:00 2001 From: Guiwoo Park Date: Mon, 2 Dec 2024 12:56:52 +0900 Subject: [PATCH 21/83] samber lo version up (#4569) --- go.mod | 16 ++++++++-------- go.sum | 36 ++++++++++++++++++------------------ pkg/nathole/controller.go | 4 ++-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index cb1768bb888..30584583ad2 100644 --- a/go.mod +++ b/go.mod @@ -18,16 +18,16 @@ require ( github.com/prometheus/client_golang v1.19.0 github.com/quic-go/quic-go v0.42.0 github.com/rodaine/table v1.2.0 - github.com/samber/lo v1.39.0 + github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 github.com/xtaci/kcp-go/v5 v5.6.13 - golang.org/x/crypto v0.22.0 - golang.org/x/net v0.24.0 + golang.org/x/crypto v0.23.0 + golang.org/x/net v0.25.0 golang.org/x/oauth2 v0.16.0 - golang.org/x/sync v0.6.0 + golang.org/x/sync v0.7.0 golang.org/x/time v0.5.0 gopkg.in/ini.v1 v1.67.0 k8s.io/apimachinery v0.28.8 @@ -66,10 +66,10 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index fee1bacd57d..0ae44a2b5dc 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLS github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -161,8 +161,8 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh 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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -171,8 +171,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -186,8 +186,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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= @@ -196,8 +196,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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -211,16 +211,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 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= @@ -228,8 +228,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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= @@ -240,8 +240,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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/pkg/nathole/controller.go b/pkg/nathole/controller.go index 2eca5929455..c08c81c98bd 100644 --- a/pkg/nathole/controller.go +++ b/pkg/nathole/controller.go @@ -371,8 +371,8 @@ func getRangePorts(addrs []string, difference, maxNumber int) []msg.PortsRange { return nil } - addr, err := lo.Last(addrs) - if err != nil { + addr, isLast := lo.Last(addrs) + if !isLast { return nil } var ports []msg.PortsRange From c73096f2bf6e8b0341bc0cc2c45d8d54f5946ff3 Mon Sep 17 00:00:00 2001 From: Roy Reznik <4046118+AdallomRoy@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:07:38 +0000 Subject: [PATCH 22/83] Upgrade packages to resolve CVE-2024-53259 (#4577) --- go.mod | 36 +++++++++++++-------------- go.sum | 78 +++++++++++++++++++++++++++------------------------------- 2 files changed, 54 insertions(+), 60 deletions(-) diff --git a/go.mod b/go.mod index 30584583ad2..957fc7f493e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.22 +go 1.22.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 @@ -10,13 +10,13 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/yamux v0.1.1 - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.32.0 + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.34.2 github.com/pelletier/go-toml/v2 v2.2.0 github.com/pion/stun/v2 v2.0.0 github.com/pires/go-proxyproto v0.7.0 - github.com/prometheus/client_golang v1.19.0 - github.com/quic-go/quic-go v0.42.0 + github.com/prometheus/client_golang v1.19.1 + github.com/quic-go/quic-go v0.48.2 github.com/rodaine/table v1.2.0 github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.0 @@ -24,10 +24,10 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 github.com/xtaci/kcp-go/v5 v5.6.13 - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.30.0 + golang.org/x/net v0.32.0 golang.org/x/oauth2 v0.16.0 - golang.org/x/sync v0.7.0 + golang.org/x/sync v0.10.0 golang.org/x/time v0.5.0 gopkg.in/ini.v1 v1.67.0 k8s.io/apimachinery v0.28.8 @@ -40,12 +40,12 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect @@ -64,14 +64,14 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect - go.uber.org/mock v0.4.0 // indirect - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.28.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect diff --git a/go.sum b/go.sum index 0ae44a2b5dc..4b771361ee0 100644 --- a/go.sum +++ b/go.sum @@ -9,9 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= @@ -30,10 +27,10 @@ github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvF github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -58,15 +55,14 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8= +github.com/google/pprof v0.0.0-20241206021119-61a79c692802/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -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/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= @@ -79,10 +75,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= -github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -101,8 +97,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= @@ -110,8 +106,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.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= -github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 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= @@ -129,7 +125,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -153,26 +148,26 @@ github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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= @@ -186,8 +181,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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= @@ -196,12 +191,11 @@ 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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -211,16 +205,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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= @@ -228,8 +222,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= @@ -240,8 +234,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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 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= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -262,8 +256,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From bb912d6c37e0e1156667eea30cb74767e9f06a48 Mon Sep 17 00:00:00 2001 From: Sword Date: Fri, 13 Dec 2024 14:37:07 +0800 Subject: [PATCH 23/83] enable h2c for vhost server (#4582) --- pkg/util/vhost/http.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 30f9631e31e..bc458a5c48a 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -29,6 +29,8 @@ import ( libio "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" @@ -41,7 +43,7 @@ type HTTPReverseProxyOptions struct { } type HTTPReverseProxy struct { - proxy *httputil.ReverseProxy + proxy http.Handler vhostRouter *Routers responseHeaderTimeout time.Duration @@ -138,7 +140,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * _, _ = rw.Write(getNotFoundPageContent()) }, } - rp.proxy = proxy + rp.proxy = h2c.NewHandler(proxy, &http2.Server{}) return rp } From f47d8ab97fd48b8ced82eb143bc52126db51a19e Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 16 Dec 2024 19:33:58 +0800 Subject: [PATCH 24/83] update Release.md (#4589) --- Release.md | 3 ++- pkg/util/version/version.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Release.md b/Release.md index 34f72a29178..d75b07cb7c2 100644 --- a/Release.md +++ b/Release.md @@ -1,4 +1,5 @@ ### Features * `tzdata` is installed by default in the container image, and the time zone can be set using the `TZ` environment variable. -* The `quic-bind-port` command line parameter is supported in frps, which specifies the port for accepting frpc connections using the QUIC protocol. \ No newline at end of file +* The `quic-bind-port` command line parameter is supported in frps, which specifies the port for accepting frpc connections using the QUIC protocol. +* The vhost HTTP proxy of frps supports the h2c protocol. diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 91b2ed98a2c..9b75f23645b 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.61.0" +var version = "0.61.1" func Full() string { return version From 01fed8d1a97dce7d19d520877949f6e19054ee54 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 19 Dec 2024 18:13:25 +0800 Subject: [PATCH 25/83] Update stale workflow (#4600) --- .github/workflows/stale.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f5cc538f5e1..784053e8f96 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -21,14 +21,14 @@ jobs: steps: - uses: actions/stale@v9 with: - stale-issue-message: 'Issues go stale after 21d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.' - stale-pr-message: "PRs go stale after 21d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close." + stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.' + stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close." stale-issue-label: 'lifecycle/stale' exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' stale-pr-label: 'lifecycle/stale' exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' - days-before-stale: 21 - days-before-close: 7 + days-before-stale: 14 + days-before-close: 3 debug-only: ${{ github.event.inputs.debug-only }} exempt-all-pr-milestones: true exempt-all-pr-assignees: true From 092e5d3f94c76f744daed396195e6ca2f322ee96 Mon Sep 17 00:00:00 2001 From: Gabriel Marin Date: Thu, 2 Jan 2025 05:24:08 +0200 Subject: [PATCH 26/83] client, pkg, server, test: replaced 'interface{}' with 'any' (#4611) --- client/event/event.go | 2 +- client/proxy/proxy_manager.go | 2 +- pkg/config/legacy/client.go | 4 ++-- pkg/config/legacy/server.go | 2 +- pkg/config/load.go | 2 +- pkg/msg/ctl.go | 2 +- pkg/msg/msg.go | 2 +- pkg/plugin/server/http.go | 2 +- pkg/plugin/server/manager.go | 10 ++++----- pkg/plugin/server/plugin.go | 2 +- pkg/plugin/server/types.go | 14 ++++++------- pkg/util/log/log.go | 12 +++++------ pkg/util/vhost/router.go | 4 ++-- pkg/util/xlog/xlog.go | 10 ++++----- server/dashboard_api.go | 34 +++++++++++++++---------------- test/e2e/framework/expect.go | 32 ++++++++++++++--------------- test/e2e/framework/log.go | 6 +++--- test/e2e/framework/mockservers.go | 6 +++--- test/e2e/framework/request.go | 6 +++--- 19 files changed, 77 insertions(+), 77 deletions(-) diff --git a/client/event/event.go b/client/event/event.go index bf7090d9a30..acdc96ba2b9 100644 --- a/client/event/event.go +++ b/client/event/event.go @@ -8,7 +8,7 @@ import ( var ErrPayloadType = errors.New("error payload type") -type Handler func(payload interface{}) error +type Handler func(payload any) error type StartProxyPayload struct { NewProxyMsg *msg.NewProxy diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 95778ce0213..d42aedc00eb 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -96,7 +96,7 @@ func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWo } } -func (pm *Manager) HandleEvent(payload interface{}) error { +func (pm *Manager) HandleEvent(payload any) error { var m msg.Message switch e := payload.(type) { case *event.StartProxyPayload: diff --git a/pkg/config/legacy/client.go b/pkg/config/legacy/client.go index 7c16c73dbbe..0d677d9cc56 100644 --- a/pkg/config/legacy/client.go +++ b/pkg/config/legacy/client.go @@ -170,7 +170,7 @@ type ClientCommonConf struct { } // Supported sources including: string(file path), []byte, Reader interface. -func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { +func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: false, InsensitiveSections: false, @@ -203,7 +203,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { // otherwise just start proxies in startProxy map func LoadAllProxyConfsFromIni( prefix string, - source interface{}, + source any, start []string, ) (map[string]ProxyConf, map[string]VisitorConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ diff --git a/pkg/config/legacy/server.go b/pkg/config/legacy/server.go index c58f76ad8b5..1cfa1bdca7f 100644 --- a/pkg/config/legacy/server.go +++ b/pkg/config/legacy/server.go @@ -217,7 +217,7 @@ func GetDefaultServerConf() ServerCommonConf { } } -func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { +func UnmarshalServerConfFromIni(source any) (ServerCommonConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: false, InsensitiveSections: false, diff --git a/pkg/config/load.go b/pkg/config/load.go index f9a705eb213..fa814fd0b0a 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -118,7 +118,7 @@ func LoadConfigure(b []byte, c any, strict bool) error { defer v1.DisallowUnknownFieldsMu.Unlock() v1.DisallowUnknownFields = strict - var tomlObj interface{} + var tomlObj any // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML). if err := toml.Unmarshal(b, &tomlObj); err == nil { b, err = json.Marshal(&tomlObj) diff --git a/pkg/msg/ctl.go b/pkg/msg/ctl.go index bf0c71a779b..57681b12fd1 100644 --- a/pkg/msg/ctl.go +++ b/pkg/msg/ctl.go @@ -39,6 +39,6 @@ func ReadMsgInto(c io.Reader, msg Message) (err error) { return msgCtl.ReadMsgInto(c, msg) } -func WriteMsg(c io.Writer, msg interface{}) (err error) { +func WriteMsg(c io.Writer, msg any) (err error) { return msgCtl.WriteMsg(c, msg) } diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index a6344d08fe3..d466f231404 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -40,7 +40,7 @@ const ( TypeNatHoleReport = '6' ) -var msgTypeMap = map[byte]interface{}{ +var msgTypeMap = map[byte]any{ TypeLogin: Login{}, TypeLoginResp: LoginResp{}, TypeNewProxy: NewProxy{}, diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 7108b7fb185..6046c38a848 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -72,7 +72,7 @@ func (p *httpPlugin) IsSupport(op string) bool { return false } -func (p *httpPlugin) Handle(ctx context.Context, op string, content interface{}) (*Response, interface{}, error) { +func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) { r := &Request{ Version: APIVersion, Op: op, diff --git a/pkg/plugin/server/manager.go b/pkg/plugin/server/manager.go index ed96444ac22..8f23b529fd8 100644 --- a/pkg/plugin/server/manager.go +++ b/pkg/plugin/server/manager.go @@ -75,7 +75,7 @@ func (m *Manager) Login(content *LoginContent) (*LoginContent, error) { Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) reqid, _ := util.RandID() @@ -109,7 +109,7 @@ func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) { Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) reqid, _ := util.RandID() @@ -168,7 +168,7 @@ func (m *Manager) Ping(content *PingContent) (*PingContent, error) { Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) reqid, _ := util.RandID() @@ -202,7 +202,7 @@ func (m *Manager) NewWorkConn(content *NewWorkConnContent) (*NewWorkConnContent, Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) reqid, _ := util.RandID() @@ -236,7 +236,7 @@ func (m *Manager) NewUserConn(content *NewUserConnContent) (*NewUserConnContent, Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) reqid, _ := util.RandID() diff --git a/pkg/plugin/server/plugin.go b/pkg/plugin/server/plugin.go index 0d34de5467d..9456ee9b039 100644 --- a/pkg/plugin/server/plugin.go +++ b/pkg/plugin/server/plugin.go @@ -32,5 +32,5 @@ const ( type Plugin interface { Name() string IsSupport(op string) bool - Handle(ctx context.Context, op string, content interface{}) (res *Response, retContent interface{}, err error) + Handle(ctx context.Context, op string, content any) (res *Response, retContent any, err error) } diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go index d7d98cb6535..c9fc4a40225 100644 --- a/pkg/plugin/server/types.go +++ b/pkg/plugin/server/types.go @@ -19,16 +19,16 @@ import ( ) type Request struct { - Version string `json:"version"` - Op string `json:"op"` - Content interface{} `json:"content"` + Version string `json:"version"` + Op string `json:"op"` + Content any `json:"content"` } type Response struct { - Reject bool `json:"reject"` - RejectReason string `json:"reject_reason"` - Unchange bool `json:"unchange"` - Content interface{} `json:"content"` + Reject bool `json:"reject"` + RejectReason string `json:"reject_reason"` + Unchange bool `json:"unchange"` + Content any `json:"content"` } type LoginContent struct { diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go index e2125365808..327d4ef654d 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -67,27 +67,27 @@ func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bo Logger = Logger.WithOptions(options...) } -func Errorf(format string, v ...interface{}) { +func Errorf(format string, v ...any) { Logger.Errorf(format, v...) } -func Warnf(format string, v ...interface{}) { +func Warnf(format string, v ...any) { Logger.Warnf(format, v...) } -func Infof(format string, v ...interface{}) { +func Infof(format string, v ...any) { Logger.Infof(format, v...) } -func Debugf(format string, v ...interface{}) { +func Debugf(format string, v ...any) { Logger.Debugf(format, v...) } -func Tracef(format string, v ...interface{}) { +func Tracef(format string, v ...any) { Logger.Tracef(format, v...) } -func Logf(level log.Level, offset int, format string, v ...interface{}) { +func Logf(level log.Level, offset int, format string, v ...any) { Logger.Logf(level, offset, format, v...) } diff --git a/pkg/util/vhost/router.go b/pkg/util/vhost/router.go index 4df79aa7b44..315ff9e7392 100644 --- a/pkg/util/vhost/router.go +++ b/pkg/util/vhost/router.go @@ -24,7 +24,7 @@ type Router struct { httpUser string // store any object here - payload interface{} + payload any } func NewRouters() *Routers { @@ -33,7 +33,7 @@ func NewRouters() *Routers { } } -func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error { +func (r *Routers) Add(domain, location, httpUser string, payload any) error { domain = strings.ToLower(domain) r.mutex.Lock() diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index 184c0c1f7a5..a1a58d42a27 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -94,22 +94,22 @@ func (l *Logger) Spawn() *Logger { return nl } -func (l *Logger) Errorf(format string, v ...interface{}) { +func (l *Logger) Errorf(format string, v ...any) { log.Logger.Errorf(l.prefixString+format, v...) } -func (l *Logger) Warnf(format string, v ...interface{}) { +func (l *Logger) Warnf(format string, v ...any) { log.Logger.Warnf(l.prefixString+format, v...) } -func (l *Logger) Infof(format string, v ...interface{}) { +func (l *Logger) Infof(format string, v ...any) { log.Logger.Infof(l.prefixString+format, v...) } -func (l *Logger) Debugf(format string, v ...interface{}) { +func (l *Logger) Debugf(format string, v ...any) { log.Logger.Debugf(l.prefixString+format, v...) } -func (l *Logger) Tracef(format string, v ...interface{}) { +func (l *Logger) Tracef(format string, v ...any) { log.Logger.Tracef(l.prefixString+format, v...) } diff --git a/server/dashboard_api.go b/server/dashboard_api.go index f34da4ef513..a29433a6235 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -196,15 +196,15 @@ func getConfByType(proxyType string) any { // Get proxy info. type ProxyStatsInfo struct { - Name string `json:"name"` - Conf interface{} `json:"conf"` - ClientVersion string `json:"clientVersion,omitempty"` - TodayTrafficIn int64 `json:"todayTrafficIn"` - TodayTrafficOut int64 `json:"todayTrafficOut"` - CurConns int64 `json:"curConns"` - LastStartTime string `json:"lastStartTime"` - LastCloseTime string `json:"lastCloseTime"` - Status string `json:"status"` + Name string `json:"name"` + Conf any `json:"conf"` + ClientVersion string `json:"clientVersion,omitempty"` + TodayTrafficIn int64 `json:"todayTrafficIn"` + TodayTrafficOut int64 `json:"todayTrafficOut"` + CurConns int64 `json:"curConns"` + LastStartTime string `json:"lastStartTime"` + LastCloseTime string `json:"lastCloseTime"` + Status string `json:"status"` } type GetProxyInfoResp struct { @@ -272,14 +272,14 @@ func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxySt // Get proxy info by name. type GetProxyStatsResp struct { - Name string `json:"name"` - Conf interface{} `json:"conf"` - TodayTrafficIn int64 `json:"todayTrafficIn"` - TodayTrafficOut int64 `json:"todayTrafficOut"` - CurConns int64 `json:"curConns"` - LastStartTime string `json:"lastStartTime"` - LastCloseTime string `json:"lastCloseTime"` - Status string `json:"status"` + Name string `json:"name"` + Conf any `json:"conf"` + TodayTrafficIn int64 `json:"todayTrafficIn"` + TodayTrafficOut int64 `json:"todayTrafficOut"` + CurConns int64 `json:"curConns"` + LastStartTime string `json:"lastStartTime"` + LastCloseTime string `json:"lastCloseTime"` + Status string `json:"status"` } // /api/proxy/:type/:name diff --git a/test/e2e/framework/expect.go b/test/e2e/framework/expect.go index 3c357bb02cd..8e9044895ee 100644 --- a/test/e2e/framework/expect.go +++ b/test/e2e/framework/expect.go @@ -5,75 +5,75 @@ import ( ) // ExpectEqual expects the specified two are the same, otherwise an exception raises -func ExpectEqual(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqual(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.Equal(extra), explain...) } // ExpectEqualValues expects the specified two are the same, it not strict about type -func ExpectEqualValues(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqualValues(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.BeEquivalentTo(extra), explain...) } -func ExpectEqualValuesWithOffset(offset int, actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqualValuesWithOffset(offset int, actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1+offset, actual).To(gomega.BeEquivalentTo(extra), explain...) } // ExpectNotEqual expects the specified two are not the same, otherwise an exception raises -func ExpectNotEqual(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectNotEqual(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).NotTo(gomega.Equal(extra), explain...) } // ExpectError expects an error happens, otherwise an exception raises -func ExpectError(err error, explain ...interface{}) { +func ExpectError(err error, explain ...any) { gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred(), explain...) } -func ExpectErrorWithOffset(offset int, err error, explain ...interface{}) { +func ExpectErrorWithOffset(offset int, err error, explain ...any) { gomega.ExpectWithOffset(1+offset, err).To(gomega.HaveOccurred(), explain...) } // ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error. -func ExpectNoError(err error, explain ...interface{}) { +func ExpectNoError(err error, explain ...any) { ExpectNoErrorWithOffset(1, err, explain...) } // ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller // (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f"). -func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) { +func ExpectNoErrorWithOffset(offset int, err error, explain ...any) { gomega.ExpectWithOffset(1+offset, err).NotTo(gomega.HaveOccurred(), explain...) } -func ExpectContainSubstring(actual, substr string, explain ...interface{}) { +func ExpectContainSubstring(actual, substr string, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.ContainSubstring(substr), explain...) } // ExpectConsistOf expects actual contains precisely the extra elements. The ordering of the elements does not matter. -func ExpectConsistOf(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectConsistOf(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.ConsistOf(extra), explain...) } -func ExpectContainElements(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectContainElements(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.ContainElements(extra), explain...) } -func ExpectNotContainElements(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectNotContainElements(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).NotTo(gomega.ContainElements(extra), explain...) } // ExpectHaveKey expects the actual map has the key in the keyset -func ExpectHaveKey(actual interface{}, key interface{}, explain ...interface{}) { +func ExpectHaveKey(actual any, key any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.HaveKey(key), explain...) } // ExpectEmpty expects actual is empty -func ExpectEmpty(actual interface{}, explain ...interface{}) { +func ExpectEmpty(actual any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.BeEmpty(), explain...) } -func ExpectTrue(actual interface{}, explain ...interface{}) { +func ExpectTrue(actual any, explain ...any) { gomega.ExpectWithOffset(1, actual).Should(gomega.BeTrue(), explain...) } -func ExpectTrueWithOffset(offset int, actual interface{}, explain ...interface{}) { +func ExpectTrueWithOffset(offset int, actual any, explain ...any) { gomega.ExpectWithOffset(1+offset, actual).Should(gomega.BeTrue(), explain...) } diff --git a/test/e2e/framework/log.go b/test/e2e/framework/log.go index 3cd990632ba..a466f68bf1f 100644 --- a/test/e2e/framework/log.go +++ b/test/e2e/framework/log.go @@ -11,18 +11,18 @@ func nowStamp() string { return time.Now().Format(time.StampMilli) } -func log(level string, format string, args ...interface{}) { +func log(level string, format string, args ...any) { fmt.Fprintf(ginkgo.GinkgoWriter, nowStamp()+": "+level+": "+format+"\n", args...) } // Logf logs the info. -func Logf(format string, args ...interface{}) { +func Logf(format string, args ...any) { log("INFO", format, args...) } // Failf logs the fail info, including a stack trace starts with its direct caller // (for example, for call chain f -> g -> Failf("foo", ...) error would be logged for "g"). -func Failf(format string, args ...interface{}) { +func Failf(format string, args ...any) { msg := fmt.Sprintf(format, args...) skip := 1 ginkgo.Fail(msg, skip) diff --git a/test/e2e/framework/mockservers.go b/test/e2e/framework/mockservers.go index 2aea36da446..6b6c2868d31 100644 --- a/test/e2e/framework/mockservers.go +++ b/test/e2e/framework/mockservers.go @@ -67,8 +67,8 @@ func (m *MockServers) Close() { os.Remove(m.udsEchoServer.BindAddr()) } -func (m *MockServers) GetTemplateParams() map[string]interface{} { - ret := make(map[string]interface{}) +func (m *MockServers) GetTemplateParams() map[string]any { + ret := make(map[string]any) ret[TCPEchoServerPort] = m.tcpEchoServer.BindPort() ret[UDPEchoServerPort] = m.udpEchoServer.BindPort() ret[UDSEchoServerAddr] = m.udsEchoServer.BindAddr() @@ -76,7 +76,7 @@ func (m *MockServers) GetTemplateParams() map[string]interface{} { return ret } -func (m *MockServers) GetParam(key string) interface{} { +func (m *MockServers) GetParam(key string) any { params := m.GetTemplateParams() if v, ok := params[key]; ok { return v diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go index 1fc67fe86ab..f56fc973a9f 100644 --- a/test/e2e/framework/request.go +++ b/test/e2e/framework/request.go @@ -42,7 +42,7 @@ type RequestExpect struct { f *Framework expectResp []byte expectError bool - explain []interface{} + explain []any } func NewRequestExpect(f *Framework) *RequestExpect { @@ -51,7 +51,7 @@ func NewRequestExpect(f *Framework) *RequestExpect { f: f, expectResp: []byte(consts.TestString), expectError: false, - explain: make([]interface{}, 0), + explain: make([]any, 0), } } @@ -94,7 +94,7 @@ func (e *RequestExpect) ExpectError(expectErr bool) *RequestExpect { return e } -func (e *RequestExpect) Explain(explain ...interface{}) *RequestExpect { +func (e *RequestExpect) Explain(explain ...any) *RequestExpect { e.explain = explain return e } From 6542dcd4ed6bf660a142db7634bcc296bd00f8c5 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Thu, 2 Jan 2025 04:33:56 +0100 Subject: [PATCH 27/83] Fix typos (#4615) --- README.md | 4 ++-- web/frpc/src/components/ClientConfigure.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 16fd3c31ab9..ab9ef37b271 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ frp also offers a P2P connect mode. * [Client Plugins](#client-plugins) * [Server Manage Plugins](#server-manage-plugins) * [SSH Tunnel Gateway](#ssh-tunnel-gateway) -* [Releated Projects](#releated-projects) +* [Related Projects](#related-projects) * [Contributing](#contributing) * [Donation](#donation) * [GitHub Sponsors](#github-sponsors) @@ -1260,7 +1260,7 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information. -## Releated Projects +## Related Projects * [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios. * [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources. diff --git a/web/frpc/src/components/ClientConfigure.vue b/web/frpc/src/components/ClientConfigure.vue index d276437db9a..d22d092640c 100644 --- a/web/frpc/src/components/ClientConfigure.vue +++ b/web/frpc/src/components/ClientConfigure.vue @@ -8,7 +8,7 @@ type="textarea" autosize v-model="textarea" - placeholder="frpc configrue file, can not be empty..." + placeholder="frpc configure file, can not be empty..." > From 27db6217ecda9236f5bc25c65824f1e723810751 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 6 Jan 2025 14:22:57 +0800 Subject: [PATCH 28/83] frpc: support metadatas and annotations in frpc proxy commands (#4623) --- Release.md | 4 +--- pkg/config/flags.go | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Release.md b/Release.md index d75b07cb7c2..046e4d48039 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,3 @@ ### Features -* `tzdata` is installed by default in the container image, and the time zone can be set using the `TZ` environment variable. -* The `quic-bind-port` command line parameter is supported in frps, which specifies the port for accepting frpc connections using the QUIC protocol. -* The vhost HTTP proxy of frps supports the h2c protocol. +* Support metadatas and annotations in frpc proxy commands. diff --git a/pkg/config/flags.go b/pkg/config/flags.go index f9d9e3e4495..6027b622411 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -106,6 +106,8 @@ func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opt } cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name") + cmd.Flags().StringToStringVarP(&c.Metadatas, "metadatas", "", nil, "metadata key-value pairs (e.g., key1=value1,key2=value2)") + cmd.Flags().StringToStringVarP(&c.Annotations, "annotations", "", nil, "annotation key-value pairs (e.g., key1=value1,key2=value2)") if !options.sshMode { cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip") From 450b8393bc5a06fd1182690b967e8deb0c20b606 Mon Sep 17 00:00:00 2001 From: "Jeb.Wang" Date: Thu, 16 Jan 2025 10:50:57 +0800 Subject: [PATCH 29/83] Fix goroutine leaks * Fix goroutine leaks --- server/proxy/xtcp.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index f69d0790d65..1ccf331c49d 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -17,8 +17,7 @@ package proxy import ( "fmt" "reflect" - - "github.com/fatedier/golib/errors" + "sync" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" @@ -32,7 +31,8 @@ type XTCPProxy struct { *BaseProxy cfg *v1.XTCPProxyConfig - closeCh chan struct{} + closeCh chan struct{} + closeOnce sync.Once } func NewXTCPProxy(baseProxy *BaseProxy) Proxy { @@ -43,6 +43,7 @@ func NewXTCPProxy(baseProxy *BaseProxy) Proxy { return &XTCPProxy{ BaseProxy: baseProxy, cfg: unwrapped, + closeCh: make(chan struct{}), } } @@ -87,9 +88,9 @@ func (pxy *XTCPProxy) Run() (remoteAddr string, err error) { } func (pxy *XTCPProxy) Close() { - pxy.BaseProxy.Close() - pxy.rc.NatHoleController.CloseClient(pxy.GetName()) - _ = errors.PanicToError(func() { + pxy.closeOnce.Do(func() { + pxy.BaseProxy.Close() + pxy.rc.NatHoleController.CloseClient(pxy.GetName()) close(pxy.closeCh) }) } From b8d3ace1139f5bcfee816d9844bba4780d052ba8 Mon Sep 17 00:00:00 2001 From: hansmi Date: Fri, 7 Feb 2025 04:33:11 +0100 Subject: [PATCH 30/83] Use text/template instead of html/template for config pre-processing (#4656) --- pkg/config/load.go | 2 +- pkg/config/load_test.go | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/config/load.go b/pkg/config/load.go index fa814fd0b0a..fa394dda115 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -18,10 +18,10 @@ import ( "bytes" "encoding/json" "fmt" - "html/template" "os" "path/filepath" "strings" + "text/template" toml "github.com/pelletier/go-toml/v2" "github.com/samber/lo" diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index b3f77800449..980a332ad1b 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -112,6 +112,29 @@ func TestLoadServerConfigStrictMode(t *testing.T) { } } +func TestRenderWithTemplate(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + {"toml", tomlServerContent, tomlServerContent}, + {"yaml", yamlServerContent, yamlServerContent}, + {"json", jsonServerContent, jsonServerContent}, + {"template numeric", `key = {{ 123 }}`, "key = 123"}, + {"template string", `key = {{ "xyz" }}`, "key = xyz"}, + {"template quote", `key = {{ printf "%q" "with space" }}`, `key = "with space"`}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + got, err := RenderWithTemplate([]byte(test.content), nil) + require.NoError(err) + require.EqualValues(test.want, string(got)) + }) + } +} + func TestCustomStructStrictMode(t *testing.T) { require := require.New(t) From 8b86e1473ca8d180e03b57c2ca50daa6f2880a82 Mon Sep 17 00:00:00 2001 From: ubergeek77 Date: Tue, 11 Feb 2025 21:28:30 -0600 Subject: [PATCH 31/83] Fix ports not being released on Service.Close() (#4666) --- server/service.go | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/server/service.go b/server/service.go index 27c4110d92a..86d82a2ad33 100644 --- a/server/service.go +++ b/server/service.go @@ -77,7 +77,7 @@ type Service struct { muxer *mux.Mux // Accept connections from client - listener net.Listener + muxListener net.Listener // Accept connections using kcp kcpListener net.Listener @@ -125,6 +125,11 @@ type Service struct { ctx context.Context // call cancel to stop service cancel context.CancelFunc + + // Track listeners so they can be closed manually + vhostHTTPSListener net.Listener + tcpmuxHTTPConnectListener net.Listener + tcpListener net.Listener } func NewService(cfg *v1.ServerConfig) (*Service, error) { @@ -180,6 +185,8 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { return nil, fmt.Errorf("create server listener error, %v", err) } + // Save listener so it can be closed in svr.Close() + svr.tcpmuxHTTPConnectListener = l svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout) if err != nil { return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) @@ -226,14 +233,16 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { return nil, fmt.Errorf("create server listener error, %v", err) } + // Save listener so it can be closed in svr.Close() + svr.tcpListener = ln + svr.muxer = mux.NewMux(ln) svr.muxer.SetKeepAlive(time.Duration(cfg.Transport.TCPKeepAlive) * time.Second) go func() { _ = svr.muxer.Serve() }() ln = svr.muxer.DefaultListener() - - svr.listener = ln + svr.muxListener = ln log.Infof("frps tcp listen on %s", address) // Listen for accepting connections from client using kcp protocol. @@ -318,7 +327,8 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { } log.Infof("https service listen on %s", address) } - + // Save listener so it can be closed in svr.Close() + svr.vhostHTTPSListener = l svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) if err != nil { return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err) @@ -374,11 +384,11 @@ func (svr *Service) Run(ctx context.Context) { go svr.sshTunnelGateway.Run() } - svr.HandleListener(svr.listener, false) + svr.HandleListener(svr.muxListener, false) <-svr.ctx.Done() // service context may not be canceled by svr.Close(), we should call it here to release resources - if svr.listener != nil { + if svr.muxListener != nil { svr.Close() } } @@ -400,9 +410,25 @@ func (svr *Service) Close() error { svr.tlsListener.Close() svr.tlsConfig = nil } - if svr.listener != nil { - svr.listener.Close() - svr.listener = nil + if svr.muxListener != nil { + svr.muxListener.Close() + svr.muxListener = nil + } + if svr.vhostHTTPSListener != nil { + svr.vhostHTTPSListener.Close() + svr.vhostHTTPSListener = nil + } + if svr.tcpmuxHTTPConnectListener != nil { + svr.tcpmuxHTTPConnectListener.Close() + svr.tcpmuxHTTPConnectListener = nil + } + if svr.webServer != nil { + svr.webServer.Close() + svr.webServer = nil + } + if svr.tcpListener != nil { + svr.tcpListener.Close() + svr.tcpListener = nil } svr.ctlManager.Close() if svr.cancel != nil { From e0dd947e6af0bc1116e1120ee422f5a8ab18869d Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 12 Feb 2025 12:22:57 +0800 Subject: [PATCH 32/83] frps: release resources in service.Close() (#4667) --- go.mod | 2 +- go.sum | 4 +-- pkg/ssh/gateway.go | 4 +++ pkg/util/vhost/vhost.go | 4 +++ server/controller/resource.go | 10 ++++++++ server/service.go | 48 ++++++++++------------------------- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index 957fc7f493e..5809def87a1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/coreos/go-oidc/v3 v3.10.0 - github.com/fatedier/golib v0.5.0 + github.com/fatedier/golib v0.5.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 diff --git a/go.sum b/go.sum index 4b771361ee0..e1053561e04 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs= -github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= +github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= +github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= 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/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= diff --git a/pkg/ssh/gateway.go b/pkg/ssh/gateway.go index 5716f04038d..4d90f338385 100644 --- a/pkg/ssh/gateway.go +++ b/pkg/ssh/gateway.go @@ -112,6 +112,10 @@ func (g *Gateway) Run() { } } +func (g *Gateway) Close() error { + return g.ln.Close() +} + func (g *Gateway) handleConn(conn net.Conn) { defer conn.Close() diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index e62a1caf354..75b472b6ee4 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -100,6 +100,10 @@ func (v *Muxer) SetRewriteHostFunc(f hostRewriteFunc) *Muxer { return v } +func (v *Muxer) Close() error { + return v.listener.Close() +} + type ChooseEndpointFunc func() (string, error) type CreateConnFunc func(remoteAddr string) (net.Conn, error) diff --git a/server/controller/resource.go b/server/controller/resource.go index 47236c9e0d0..9d14b18dfbe 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -59,3 +59,13 @@ type ResourceController struct { // All server manager plugin PluginManager *plugin.Manager } + +func (rc *ResourceController) Close() error { + if rc.VhostHTTPSMuxer != nil { + rc.VhostHTTPSMuxer.Close() + } + if rc.TCPMuxHTTPConnectMuxer != nil { + rc.TCPMuxHTTPConnectMuxer.Close() + } + return nil +} diff --git a/server/service.go b/server/service.go index 86d82a2ad33..d1dd68a1c90 100644 --- a/server/service.go +++ b/server/service.go @@ -77,7 +77,7 @@ type Service struct { muxer *mux.Mux // Accept connections from client - muxListener net.Listener + listener net.Listener // Accept connections using kcp kcpListener net.Listener @@ -125,11 +125,6 @@ type Service struct { ctx context.Context // call cancel to stop service cancel context.CancelFunc - - // Track listeners so they can be closed manually - vhostHTTPSListener net.Listener - tcpmuxHTTPConnectListener net.Listener - tcpListener net.Listener } func NewService(cfg *v1.ServerConfig) (*Service, error) { @@ -185,8 +180,6 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { return nil, fmt.Errorf("create server listener error, %v", err) } - // Save listener so it can be closed in svr.Close() - svr.tcpmuxHTTPConnectListener = l svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout) if err != nil { return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) @@ -233,16 +226,14 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { return nil, fmt.Errorf("create server listener error, %v", err) } - // Save listener so it can be closed in svr.Close() - svr.tcpListener = ln - svr.muxer = mux.NewMux(ln) svr.muxer.SetKeepAlive(time.Duration(cfg.Transport.TCPKeepAlive) * time.Second) go func() { _ = svr.muxer.Serve() }() ln = svr.muxer.DefaultListener() - svr.muxListener = ln + + svr.listener = ln log.Infof("frps tcp listen on %s", address) // Listen for accepting connections from client using kcp protocol. @@ -327,8 +318,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { } log.Infof("https service listen on %s", address) } - // Save listener so it can be closed in svr.Close() - svr.vhostHTTPSListener = l + svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) if err != nil { return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err) @@ -384,11 +374,11 @@ func (svr *Service) Run(ctx context.Context) { go svr.sshTunnelGateway.Run() } - svr.HandleListener(svr.muxListener, false) + svr.HandleListener(svr.listener, false) <-svr.ctx.Done() // service context may not be canceled by svr.Close(), we should call it here to release resources - if svr.muxListener != nil { + if svr.listener != nil { svr.Close() } } @@ -396,40 +386,30 @@ func (svr *Service) Run(ctx context.Context) { func (svr *Service) Close() error { if svr.kcpListener != nil { svr.kcpListener.Close() - svr.kcpListener = nil } if svr.quicListener != nil { svr.quicListener.Close() - svr.quicListener = nil } if svr.websocketListener != nil { svr.websocketListener.Close() - svr.websocketListener = nil } if svr.tlsListener != nil { svr.tlsListener.Close() - svr.tlsConfig = nil - } - if svr.muxListener != nil { - svr.muxListener.Close() - svr.muxListener = nil } - if svr.vhostHTTPSListener != nil { - svr.vhostHTTPSListener.Close() - svr.vhostHTTPSListener = nil + if svr.sshTunnelListener != nil { + svr.sshTunnelListener.Close() } - if svr.tcpmuxHTTPConnectListener != nil { - svr.tcpmuxHTTPConnectListener.Close() - svr.tcpmuxHTTPConnectListener = nil + if svr.listener != nil { + svr.listener.Close() } if svr.webServer != nil { svr.webServer.Close() - svr.webServer = nil } - if svr.tcpListener != nil { - svr.tcpListener.Close() - svr.tcpListener = nil + if svr.sshTunnelGateway != nil { + svr.sshTunnelGateway.Close() } + svr.rc.Close() + svr.muxer.Close() svr.ctlManager.Close() if svr.cancel != nil { svr.cancel() From 1e8db667434f8f43cd60c16e9a6d694e9bd6e420 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 12 Feb 2025 12:30:37 +0800 Subject: [PATCH 33/83] update Release.md (#4668) --- Release.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Release.md b/Release.md index 046e4d48039..11b788a62bb 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,7 @@ ### Features * Support metadatas and annotations in frpc proxy commands. + +### Fixes + +* Properly release resources in service.Close() to prevent resource leaks when used as a library. From 9757a351c66c89c15812a20e2fbd4f152bdefe9f Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 7 Mar 2025 16:56:08 +0800 Subject: [PATCH 34/83] fix golangci lint config (#4698) --- .circleci/config.yml | 2 +- .golangci.yml | 3 ++- go.mod | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3dff8a52ca6..ede1f5fef64 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: go-version-latest: docker: - - image: cimg/go:1.22-node + - image: cimg/go:1.23-node resource_class: large steps: - checkout diff --git a/.golangci.yml b/.golangci.yml index 5651ef5cf78..a45e4ba3a2d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,7 +48,8 @@ linters-settings: check-blank: false govet: # report about shadowed variables - check-shadowing: false + disable: + - shadow maligned: # print struct with more effective memory layout or not, false by default suggest-new: true diff --git a/go.mod b/go.mod index 5809def87a1..ecf202114f6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.22.0 +go 1.23.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 From 773169e0c44070facd652c4b8b3fe6e6ff4d78f3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 7 Mar 2025 17:22:51 +0800 Subject: [PATCH 35/83] update version (#4699) --- pkg/util/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 9b75f23645b..f56e1ec072c 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.61.1" +var version = "0.61.2" func Full() string { return version From a78814a2e9fef204d0fb06fa0b0b7ae0d8b39589 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 16 Apr 2025 16:05:54 +0800 Subject: [PATCH 36/83] virtual-net: initial (#4751) --- README.md | 45 ++- README_zh.md | 5 - Release.md | 9 +- client/control.go | 8 +- client/proxy/proxy.go | 29 +- client/proxy/proxy_manager.go | 6 +- client/proxy/proxy_wrapper.go | 7 +- client/service.go | 32 +- client/visitor/stcp.go | 4 + client/visitor/visitor.go | 34 +- client/visitor/visitor_manager.go | 14 +- client/visitor/xtcp.go | 10 +- cmd/frpc/sub/root.go | 7 + conf/frpc_full_example.toml | 26 ++ doc/virtual_net.md | 75 ++++ go.mod | 25 +- go.sum | 65 ++-- pkg/config/v1/client.go | 15 +- pkg/config/v1/{plugin.go => proxy_plugin.go} | 56 +-- pkg/config/v1/validation/client.go | 8 + pkg/config/v1/visitor.go | 3 + pkg/config/v1/visitor_plugin.go | 86 +++++ pkg/featuregate/feature_gate.go | 219 +++++++++++ pkg/plugin/client/http2http.go | 10 +- pkg/plugin/client/http2https.go | 10 +- pkg/plugin/client/http_proxy.go | 8 +- pkg/plugin/client/https2http.go | 14 +- pkg/plugin/client/https2https.go | 14 +- pkg/plugin/client/plugin.go | 26 +- pkg/plugin/client/socks5.go | 11 +- pkg/plugin/client/static_file.go | 10 +- pkg/plugin/client/tls2raw.go | 9 +- pkg/plugin/client/unix_domain_socket.go | 13 +- pkg/plugin/client/virtual_net.go | 71 ++++ pkg/plugin/server/http.go | 2 +- pkg/plugin/server/manager.go | 2 +- pkg/plugin/server/plugin.go | 2 +- pkg/plugin/server/tracer.go | 2 +- pkg/plugin/server/types.go | 2 +- pkg/plugin/visitor/plugin.go | 58 +++ pkg/plugin/visitor/virtual_net.go | 232 ++++++++++++ pkg/util/version/version.go | 2 +- pkg/vnet/controller.go | 360 +++++++++++++++++++ pkg/vnet/message.go | 81 +++++ pkg/vnet/tun.go | 77 ++++ pkg/vnet/tun_darwin.go | 85 +++++ pkg/vnet/tun_linux.go | 84 +++++ pkg/vnet/tun_unsupported.go | 27 ++ 48 files changed, 1821 insertions(+), 179 deletions(-) create mode 100644 doc/virtual_net.md rename pkg/config/v1/{plugin.go => proxy_plugin.go} (96%) create mode 100644 pkg/config/v1/visitor_plugin.go create mode 100644 pkg/featuregate/feature_gate.go create mode 100644 pkg/plugin/client/virtual_net.go create mode 100644 pkg/plugin/visitor/plugin.go create mode 100644 pkg/plugin/visitor/virtual_net.go create mode 100644 pkg/vnet/controller.go create mode 100644 pkg/vnet/message.go create mode 100644 pkg/vnet/tun.go create mode 100644 pkg/vnet/tun_darwin.go create mode 100644 pkg/vnet/tun_linux.go create mode 100644 pkg/vnet/tun_unsupported.go diff --git a/README.md b/README.md index ab9ef37b271..e604d5af114 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,6 @@ frp is an open source project with its ongoing development made possible entirel

-

- - - -

@@ -97,6 +92,11 @@ frp also offers a P2P connect mode. * [Client Plugins](#client-plugins) * [Server Manage Plugins](#server-manage-plugins) * [SSH Tunnel Gateway](#ssh-tunnel-gateway) + * [Virtual Network (VirtualNet)](#virtual-network-virtualnet) +* [Feature Gates](#feature-gates) + * [Available Feature Gates](#available-feature-gates) + * [Enabling Feature Gates](#enabling-feature-gates) + * [Feature Lifecycle](#feature-lifecycle) * [Related Projects](#related-projects) * [Contributing](#contributing) * [Donation](#donation) @@ -1260,6 +1260,41 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information. +### Virtual Network (VirtualNet) + +*Alpha feature added in v0.62.0* + +The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity. + +For detailed information about configuration and usage, please refer to the [VirtualNet documentation](/doc/virtual_net.md). + +## Feature Gates + +frp supports feature gates to enable or disable experimental features. This allows users to try out new features before they're considered stable. + +### Available Feature Gates + +| Name | Stage | Default | Description | +|------|-------|---------|-------------| +| VirtualNet | ALPHA | false | Virtual network capabilities for frp | + +### Enabling Feature Gates + +To enable an experimental feature, add the feature gate to your configuration: + +```toml +featureGates = { + VirtualNet = true +} +``` + +### Feature Lifecycle + +Features typically go through three stages: +1. **ALPHA**: Disabled by default, may be unstable +2. **BETA**: May be enabled by default, more stable but still evolving +3. **GA (Generally Available)**: Enabled by default, ready for production use + ## Related Projects * [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios. diff --git a/README_zh.md b/README_zh.md index ebd3109ba0d..d911e15230f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,11 +20,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

-

- - - -

diff --git a/Release.md b/Release.md index 11b788a62bb..b3edf41090c 100644 --- a/Release.md +++ b/Release.md @@ -1,7 +1,8 @@ -### Features +### Notes -* Support metadatas and annotations in frpc proxy commands. +* **Feature Gates Introduced:** This version introduces a new experimental mechanism called Feature Gates. This allows users to enable or disable specific experimental features before they become generally available. Feature gates can be configured in the `featureGates` map within the configuration file. +* **VirtualNet Feature Gate:** The first available feature gate is `VirtualNet`, which enables the experimental Virtual Network functionality (currently in Alpha stage). -### Fixes +### Features -* Properly release resources in service.Close() to prevent resource leaks when used as a library. +* **Virtual Network (VirtualNet):** Introduce experimental virtual network capabilities (Alpha). This allows creating a TUN device managed by frp, enabling Layer 3 connectivity between different clients within the frp network. Requires root/admin privileges and is currently supported on Linux and macOS. Configuration is done via the `virtualNet` section and the `virtual_net` plugin. Enable with feature gate `VirtualNet`. **Note: As an Alpha feature, configuration details may change in future releases.** \ No newline at end of file diff --git a/client/control.go b/client/control.go index 3e20c312822..157b4aef913 100644 --- a/client/control.go +++ b/client/control.go @@ -29,6 +29,7 @@ import ( netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) type SessionContext struct { @@ -46,6 +47,8 @@ type SessionContext struct { AuthSetter auth.Setter // Connector is used to create new connections, which could be real TCP connections or virtual streams. Connector Connector + // Virtual net controller + VnetController *vnet.Controller } type Control struct { @@ -99,8 +102,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro ctl.registerMsgHandlers() ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) - ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter) - ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter) + ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController) + ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, + ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController) return ctl, nil } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 5cb5ccf2ab4..debda9fa2ff 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -36,6 +36,7 @@ import ( "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/limit" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{} @@ -58,6 +59,7 @@ func NewProxy( pxyConf v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig, msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, ) (pxy Proxy) { var limiter *rate.Limiter limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() @@ -70,6 +72,7 @@ func NewProxy( clientCfg: clientCfg, limiter: limiter, msgTransporter: msgTransporter, + vnetController: vnetController, xl: xlog.FromContextSafe(ctx), ctx: ctx, } @@ -85,6 +88,7 @@ type BaseProxy struct { baseCfg *v1.ProxyBaseConfig clientCfg *v1.ClientCommonConfig msgTransporter transport.MessageTransporter + vnetController *vnet.Controller limiter *rate.Limiter // proxyPlugin is used to handle connections instead of dialing to local service. // It's only validate for TCP protocol now. @@ -98,7 +102,10 @@ type BaseProxy struct { func (pxy *BaseProxy) Run() error { if pxy.baseCfg.Plugin.Type != "" { - p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions) + p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{ + Name: pxy.baseCfg.Name, + VnetController: pxy.vnetController, + }, pxy.baseCfg.Plugin.ClientPluginOptions) if err != nil { return err } @@ -157,22 +164,22 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } // check if we need to send proxy protocol info - var extraInfo plugin.ExtraInfo + var connInfo plugin.ConnectionInfo if m.SrcAddr != "" && m.SrcPort != 0 { if m.DstAddr == "" { m.DstAddr = "127.0.0.1" } srcAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.SrcAddr, strconv.Itoa(int(m.SrcPort)))) dstAddr, _ := net.ResolveTCPAddr("tcp", net.JoinHostPort(m.DstAddr, strconv.Itoa(int(m.DstPort)))) - extraInfo.SrcAddr = srcAddr - extraInfo.DstAddr = dstAddr + connInfo.SrcAddr = srcAddr + connInfo.DstAddr = dstAddr } if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { h := &pp.Header{ Command: pp.PROXY, - SourceAddr: extraInfo.SrcAddr, - DestinationAddr: extraInfo.DstAddr, + SourceAddr: connInfo.SrcAddr, + DestinationAddr: connInfo.DstAddr, } if strings.Contains(m.SrcAddr, ".") { @@ -186,13 +193,15 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } else if baseCfg.Transport.ProxyProtocolVersion == "v2" { h.Version = 2 } - extraInfo.ProxyProtocolHeader = h + connInfo.ProxyProtocolHeader = h } + connInfo.Conn = remote + connInfo.UnderlyingConn = workConn if pxy.proxyPlugin != nil { // if plugin is set, let plugin handle connection first xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name()) - pxy.proxyPlugin.Handle(pxy.ctx, remote, workConn, &extraInfo) + pxy.proxyPlugin.Handle(pxy.ctx, &connInfo) xl.Debugf("handle by plugin finished") return } @@ -210,8 +219,8 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) - if extraInfo.ProxyProtocolHeader != nil { - if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { + if connInfo.ProxyProtocolHeader != nil { + if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { workConn.Close() xl.Errorf("write proxy protocol header to local conn error: %v", err) return diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index d42aedc00eb..ea5cc553112 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -28,12 +28,14 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) type Manager struct { proxies map[string]*Wrapper msgTransporter transport.MessageTransporter inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool + vnetController *vnet.Controller closed bool mu sync.RWMutex @@ -47,10 +49,12 @@ func NewManager( ctx context.Context, clientCfg *v1.ClientCommonConfig, msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, ) *Manager { return &Manager{ proxies: make(map[string]*Wrapper), msgTransporter: msgTransporter, + vnetController: vnetController, closed: false, clientCfg: clientCfg, ctx: ctx, @@ -159,7 +163,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) + pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, 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 95048f29907..f3f17e2b10c 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -31,6 +31,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) const ( @@ -73,6 +74,8 @@ type Wrapper struct { handler event.Handler msgTransporter transport.MessageTransporter + // vnet controller + vnetController *vnet.Controller health uint32 lastSendStartMsg time.Time @@ -91,6 +94,7 @@ func NewWrapper( clientCfg *v1.ClientCommonConfig, eventHandler event.Handler, msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, ) *Wrapper { baseInfo := cfg.GetBaseConfig() xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) @@ -105,6 +109,7 @@ func NewWrapper( healthNotifyCh: make(chan struct{}), handler: eventHandler, msgTransporter: msgTransporter, + vnetController: vnetController, xl: xl, ctx: xlog.NewContext(ctx, xl), } @@ -117,7 +122,7 @@ func NewWrapper( xl.Tracef("enable health check monitor") } - pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter) + pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController) return pw } diff --git a/client/service.go b/client/service.go index b06706a6a48..57eb483559e 100644 --- a/client/service.go +++ b/client/service.go @@ -37,6 +37,7 @@ import ( "github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) func init() { @@ -110,6 +111,8 @@ type Service struct { // web server for admin UI and apis webServer *httppkg.Server + vnetController *vnet.Controller + cfgMu sync.RWMutex common *v1.ClientCommonConfig proxyCfgs []v1.ProxyConfigurer @@ -156,6 +159,9 @@ func NewService(options ServiceOptions) (*Service, error) { if webServer != nil { webServer.RouteRegister(s.registerRouteHandlers) } + if options.Common.VirtualNet.Address != "" { + s.vnetController = vnet.NewController(options.Common.VirtualNet) + } return s, nil } @@ -169,6 +175,19 @@ func (svr *Service) Run(ctx context.Context) error { netpkg.SetDefaultDNSAddress(svr.common.DNSServer) } + if svr.vnetController != nil { + if err := svr.vnetController.Init(); err != nil { + log.Errorf("init virtual network controller error: %v", err) + return err + } + go func() { + log.Infof("virtual network controller start...") + if err := svr.vnetController.Run(); err != nil { + log.Warnf("virtual network controller exit with error: %v", err) + } + }() + } + if svr.webServer != nil { go func() { log.Infof("admin server listen on %s", svr.webServer.Address()) @@ -311,12 +330,13 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE connEncrypted = false } sessionCtx := &SessionContext{ - Common: svr.common, - RunID: svr.runID, - Conn: conn, - ConnEncrypted: connEncrypted, - AuthSetter: svr.authSetter, - Connector: connector, + Common: svr.common, + RunID: svr.runID, + Conn: conn, + ConnEncrypted: connEncrypted, + AuthSetter: svr.authSetter, + Connector: connector, + VnetController: svr.vnetController, } ctl, err := NewControl(svr.ctx, sessionCtx) if err != nil { diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index b26faf5200c..124202eb8d5 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -44,6 +44,10 @@ func (sv *STCPVisitor) Run() (err error) { } go sv.internalConnWorker() + + if sv.plugin != nil { + sv.plugin.Start() + } return } diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index d520f735ddc..fb2b3e118a1 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -20,9 +20,11 @@ import ( "sync" v1 "github.com/fatedier/frp/pkg/config/v1" + plugin "github.com/fatedier/frp/pkg/plugin/visitor" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) // Helper wraps some functions for visitor to use. @@ -34,6 +36,8 @@ type Helper interface { // MsgTransporter returns the message transporter that is used to send and receive messages // to the frp server through the controller. MsgTransporter() transport.MessageTransporter + // VNetController returns the vnet controller that is used to manage the virtual network. + VNetController() *vnet.Controller // RunID returns the run id of current controller. RunID() string } @@ -50,14 +54,34 @@ func NewVisitor( cfg v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig, helper Helper, -) (visitor Visitor) { +) (Visitor, error) { xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name) + ctx = xlog.NewContext(ctx, xl) + var visitor Visitor baseVisitor := BaseVisitor{ clientCfg: clientCfg, helper: helper, - ctx: xlog.NewContext(ctx, xl), + ctx: ctx, internalLn: netpkg.NewInternalListener(), } + if cfg.GetBaseConfig().Plugin.Type != "" { + p, err := plugin.Create( + cfg.GetBaseConfig().Plugin.Type, + plugin.PluginContext{ + Name: cfg.GetBaseConfig().Name, + Ctx: ctx, + VnetController: helper.VNetController(), + HandleConn: func(conn net.Conn) { + _ = baseVisitor.AcceptConn(conn) + }, + }, + cfg.GetBaseConfig().Plugin.VisitorPluginOptions, + ) + if err != nil { + return nil, err + } + baseVisitor.plugin = p + } switch cfg := cfg.(type) { case *v1.STCPVisitorConfig: visitor = &STCPVisitor{ @@ -77,7 +101,7 @@ func NewVisitor( checkCloseCh: make(chan struct{}), } } - return + return visitor, nil } type BaseVisitor struct { @@ -85,6 +109,7 @@ type BaseVisitor struct { helper Helper l net.Listener internalLn *netpkg.InternalListener + plugin plugin.Plugin mu sync.RWMutex ctx context.Context @@ -101,4 +126,7 @@ func (v *BaseVisitor) Close() { if v.internalLn != nil { v.internalLn.Close() } + if v.plugin != nil { + v.plugin.Close() + } } diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go index 6ff65dabae0..b3539c69295 100644 --- a/client/visitor/visitor_manager.go +++ b/client/visitor/visitor_manager.go @@ -27,6 +27,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" ) type Manager struct { @@ -50,6 +51,7 @@ func NewManager( clientCfg *v1.ClientCommonConfig, connectServer func() (net.Conn, error), msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, ) *Manager { m := &Manager{ clientCfg: clientCfg, @@ -62,6 +64,7 @@ func NewManager( m.helper = &visitorHelperImpl{ connectServerFn: connectServer, msgTransporter: msgTransporter, + vnetController: vnetController, transferConnFn: m.TransferConn, runID: runID, } @@ -112,7 +115,11 @@ func (vm *Manager) Close() { func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { xl := xlog.FromContextSafe(vm.ctx) name := cfg.GetBaseConfig().Name - visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper) + visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper) + if err != nil { + xl.Warnf("new visitor error: %v", err) + return + } err = visitor.Run() if err != nil { xl.Warnf("start error: %v", err) @@ -187,6 +194,7 @@ func (vm *Manager) TransferConn(name string, conn net.Conn) error { type visitorHelperImpl struct { connectServerFn func() (net.Conn, error) msgTransporter transport.MessageTransporter + vnetController *vnet.Controller transferConnFn func(name string, conn net.Conn) error runID string } @@ -203,6 +211,10 @@ func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter { return v.msgTransporter } +func (v *visitorHelperImpl) VNetController() *vnet.Controller { + return v.vnetController +} + func (v *visitorHelperImpl) RunID() string { return v.runID } diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index a1efd72b62b..51f29ad2a94 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -73,6 +73,10 @@ func (sv *XTCPVisitor) Run() (err error) { sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour) go sv.keepTunnelOpenWorker() } + + if sv.plugin != nil { + sv.plugin.Start() + } return } @@ -157,9 +161,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) - isConnTrasfered := false + isConnTransfered := false defer func() { - if !isConnTrasfered { + if !isConnTransfered { userConn.Close() } }() @@ -187,7 +191,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) return } - isConnTrasfered = true + isConnTransfered = true return } diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index b844ddfd5e9..ee89c48947a 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -31,6 +31,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/featuregate" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) @@ -120,6 +121,12 @@ func runClient(cfgFilePath string) error { "please use yaml/json/toml format instead!\n") } + if len(cfg.FeatureGates) > 0 { + if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil { + return err + } + } + warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs) if warning != nil { fmt.Printf("WARNING: %v\n", warning) diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index eb44739239d..7d4838cd78f 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -129,6 +129,15 @@ transport.tls.enable = true # It affects the udp and sudp proxy. udpPacketSize = 1500 +# Feature gates allows you to enable or disable experimental features +# Format is a map of feature names to boolean values +# You can enable specific features: +#featureGates = { VirtualNet = true } + +# VirtualNet settings for experimental virtual network capabilities +# The virtual network feature requires enabling the VirtualNet feature gate above +# virtualNet.address = "100.86.1.1/24" + # Additional metadatas for client. metadatas.var1 = "abc" metadatas.var2 = "123" @@ -358,6 +367,13 @@ localPort = 22 # Otherwise, visitors from same user can connect. '*' means allow all users. allowUsers = ["user1", "user2"] +[[proxies]] +name = "vnet-server" +type = "stcp" +secretKey = "your-secret-key" +[proxies.plugin] +type = "virtual_net" + # frpc role visitor -> frps -> frpc role server [[visitors]] name = "secret_tcp_visitor" @@ -389,3 +405,13 @@ maxRetriesAnHour = 8 minRetryInterval = 90 # fallbackTo = "stcp_visitor" # fallbackTimeoutMs = 500 + +[[visitors]] +name = "vnet-visitor" +type = "stcp" +serverName = "vnet-server" +secretKey = "your-secret-key" +bindPort = -1 +[visitors.plugin] +type = "virtual_net" +destinationIP = "100.86.0.1" diff --git a/doc/virtual_net.md b/doc/virtual_net.md new file mode 100644 index 00000000000..62653e9f188 --- /dev/null +++ b/doc/virtual_net.md @@ -0,0 +1,75 @@ +# Virtual Network (VirtualNet) + +*Alpha feature added in v0.62.0* + +The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity. + +> **Note**: VirtualNet is an Alpha stage feature and is currently unstable. Its configuration methods and functionality may be adjusted and changed at any time in subsequent versions. Do not use this feature in production environments; it is only recommended for testing and evaluation purposes. + +## Enabling VirtualNet + +Since VirtualNet is currently an alpha feature, you need to enable it with feature gates in your configuration: + +```toml +# frpc.toml +featureGates = { VirtualNet = true } +``` + +## Basic Configuration + +To use the virtual network capabilities: + +1. First, configure your frpc with a virtual network address: + +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +featureGates = { VirtualNet = true } + +# Configure the virtual network interface +virtualNet.address = "100.86.0.1/24" +``` + +2. For client proxies, use the `virtual_net` plugin: + +```toml +# frpc.toml (server side) +[[proxies]] +name = "vnet-server" +type = "stcp" +secretKey = "your-secret-key" +[proxies.plugin] +type = "virtual_net" +``` + +3. For visitor connections, configure the `virtual_net` visitor plugin: + +```toml +# frpc.toml (client side) +serverAddr = "x.x.x.x" +serverPort = 7000 +featureGates = { + VirtualNet = true +} + +# Configure the virtual network interface +virtualNet.address = "100.86.0.2/24" + +[[visitors]] +name = "vnet-visitor" +type = "stcp" +serverName = "vnet-server" +secretKey = "your-secret-key" +bindPort = -1 +[visitors.plugin] +type = "virtual_net" +destinationIP = "100.86.0.1" +``` + +## Requirements and Limitations + +- **Permissions**: Creating a TUN interface requires elevated permissions (root/admin) +- **Platform Support**: Currently supported on Linux and macOS +- **Default Status**: As an alpha feature, VirtualNet is disabled by default +- **Configuration**: A valid IP/CIDR must be provided for each endpoint in the virtual network diff --git a/go.mod b/go.mod index ecf202114f6..0b8ee2a5e8a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 - github.com/coreos/go-oidc/v3 v3.10.0 + github.com/coreos/go-oidc/v3 v3.14.1 github.com/fatedier/golib v0.5.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 @@ -19,16 +19,19 @@ require ( github.com/quic-go/quic-go v0.48.2 github.com/rodaine/table v1.2.0 github.com/samber/lo v1.47.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 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.30.0 - golang.org/x/net v0.32.0 - golang.org/x/oauth2 v0.16.0 - golang.org/x/sync v0.10.0 + golang.org/x/crypto v0.37.0 + golang.org/x/net v0.39.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sync v0.13.0 golang.org/x/time v0.5.0 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 gopkg.in/ini.v1 v1.67.0 k8s.io/apimachinery v0.28.8 k8s.io/client-go v0.28.8 @@ -39,10 +42,9 @@ 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/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect @@ -64,13 +66,14 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/vishvananda/netns v0.0.4 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.28.0 // indirect - google.golang.org/appengine v1.6.8 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e1053561e04..a39d174edae 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= -github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,8 +25,8 @@ github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= 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/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -42,17 +42,14 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8= @@ -117,6 +114,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -129,8 +128,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= @@ -143,6 +143,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= @@ -156,8 +160,8 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh 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.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= @@ -181,18 +185,18 @@ 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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.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= @@ -201,29 +205,30 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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= @@ -238,10 +243,12 @@ golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 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= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -254,8 +261,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -268,6 +273,8 @@ 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= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ= diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index d43ec1bcee3..d616fc0a300 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -58,9 +58,14 @@ type ClientCommonConfig struct { // set. Start []string `json:"start,omitempty"` - Log LogConfig `json:"log,omitempty"` - WebServer WebServerConfig `json:"webServer,omitempty"` - Transport ClientTransportConfig `json:"transport,omitempty"` + Log LogConfig `json:"log,omitempty"` + WebServer WebServerConfig `json:"webServer,omitempty"` + Transport ClientTransportConfig `json:"transport,omitempty"` + VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"` + + // FeatureGates specifies a set of feature gates to enable or disable. + // This can be used to enable alpha/beta features or disable default features. + FeatureGates map[string]bool `json:"featureGates,omitempty"` // UDPPacketSize specifies the udp packet size // By default, this value is 1500 @@ -204,3 +209,7 @@ type AuthOIDCClientConfig struct { // this field will be transfer to map[string][]string in OIDC token generator. AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` } + +type VirtualNetConfig struct { + Address string `json:"address,omitempty"` +} diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/proxy_plugin.go similarity index 96% rename from pkg/config/v1/plugin.go rename to pkg/config/v1/proxy_plugin.go index cdf3cf263e9..128ccae629c 100644 --- a/pkg/config/v1/plugin.go +++ b/pkg/config/v1/proxy_plugin.go @@ -26,6 +26,32 @@ import ( "github.com/fatedier/frp/pkg/util/util" ) +const ( + PluginHTTP2HTTPS = "http2https" + PluginHTTPProxy = "http_proxy" + PluginHTTPS2HTTP = "https2http" + PluginHTTPS2HTTPS = "https2https" + PluginHTTP2HTTP = "http2http" + PluginSocks5 = "socks5" + PluginStaticFile = "static_file" + PluginUnixDomainSocket = "unix_domain_socket" + PluginTLS2Raw = "tls2raw" + PluginVirtualNet = "virtual_net" +) + +var clientPluginOptionsTypeMap = map[string]reflect.Type{ + PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), + PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), + PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), + PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), + PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), + PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), + PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), + PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), + PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), + PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}), +} + type ClientPluginOptions interface { Complete() } @@ -74,30 +100,6 @@ func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) { return json.Marshal(c.ClientPluginOptions) } -const ( - PluginHTTP2HTTPS = "http2https" - PluginHTTPProxy = "http_proxy" - PluginHTTPS2HTTP = "https2http" - PluginHTTPS2HTTPS = "https2https" - PluginHTTP2HTTP = "http2http" - PluginSocks5 = "socks5" - PluginStaticFile = "static_file" - PluginUnixDomainSocket = "unix_domain_socket" - PluginTLS2Raw = "tls2raw" -) - -var clientPluginOptionsTypeMap = map[string]reflect.Type{ - PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), - PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), - PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), - PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), - PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}), - PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), - PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), - PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), - PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}), -} - type HTTP2HTTPSPluginOptions struct { Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` @@ -185,3 +187,9 @@ type TLS2RawPluginOptions struct { } func (o *TLS2RawPluginOptions) Complete() {} + +type VirtualNetPluginOptions struct { + Type string `json:"type,omitempty"` +} + +func (o *VirtualNetPluginOptions) Complete() {} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index cc46607cb9c..bae21fda40b 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -23,6 +23,7 @@ import ( "github.com/samber/lo" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/featuregate" ) func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { @@ -30,6 +31,13 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { warnings Warning errs error ) + // validate feature gates + 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") + } + } + if !slices.Contains(SupportedAuthMethods, c.Auth.Method) { errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) } diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 51fe88a6300..f00391c34e4 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -44,6 +44,9 @@ type VisitorBaseConfig struct { // 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"` + + // Plugin specifies what plugin should be used. + Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"` } func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go new file mode 100644 index 00000000000..5a4909bdb84 --- /dev/null +++ b/pkg/config/v1/visitor_plugin.go @@ -0,0 +1,86 @@ +// 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 v1 + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "reflect" +) + +const ( + VisitorPluginVirtualNet = "virtual_net" +) + +var visitorPluginOptionsTypeMap = map[string]reflect.Type{ + VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}), +} + +type VisitorPluginOptions interface { + Complete() +} + +type TypedVisitorPluginOptions struct { + Type string `json:"type"` + VisitorPluginOptions +} + +func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { + if len(b) == 4 && string(b) == "null" { + return nil + } + + typeStruct := struct { + Type string `json:"type"` + }{} + if err := json.Unmarshal(b, &typeStruct); err != nil { + return err + } + + c.Type = typeStruct.Type + if c.Type == "" { + return errors.New("visitor plugin type is empty") + } + + v, ok := visitorPluginOptionsTypeMap[typeStruct.Type] + if !ok { + return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type) + } + options := reflect.New(v).Interface().(VisitorPluginOptions) + + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if DisallowUnknownFields { + decoder.DisallowUnknownFields() + } + + if err := decoder.Decode(options); err != nil { + return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err) + } + c.VisitorPluginOptions = options + return nil +} + +func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) { + return json.Marshal(c.VisitorPluginOptions) +} + +type VirtualNetVisitorPluginOptions struct { + Type string `json:"type"` + DestinationIP string `json:"destinationIP"` +} + +func (o *VirtualNetVisitorPluginOptions) Complete() {} diff --git a/pkg/featuregate/feature_gate.go b/pkg/featuregate/feature_gate.go new file mode 100644 index 00000000000..c5fd684bbd2 --- /dev/null +++ b/pkg/featuregate/feature_gate.go @@ -0,0 +1,219 @@ +// 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 featuregate + +import ( + "fmt" + "sort" + "strings" + "sync" + "sync/atomic" +) + +// Feature represents a feature gate name +type Feature string + +// FeatureStage represents the maturity level of a feature +type FeatureStage string + +const ( + // Alpha means the feature is experimental and disabled by default + Alpha FeatureStage = "ALPHA" + // Beta means the feature is more stable but still might change and is disabled by default + Beta FeatureStage = "BETA" + // GA means the feature is generally available and enabled by default + GA FeatureStage = "" +) + +// FeatureSpec describes a feature and its properties +type FeatureSpec struct { + // Default is the default enablement state for the feature + Default bool + // LockToDefault indicates the feature cannot be changed from its default + LockToDefault bool + // Stage indicates the maturity level of the feature + Stage FeatureStage +} + +// Define all available features here +var ( + VirtualNet = Feature("VirtualNet") +) + +// defaultFeatures defines default features with their specifications +var defaultFeatures = map[Feature]FeatureSpec{ + // Actual features + VirtualNet: {Default: false, Stage: Alpha}, +} + +// FeatureGate indicates whether a given feature is enabled or not +type FeatureGate interface { + // Enabled returns true if the key is enabled + Enabled(key Feature) bool + // KnownFeatures returns a slice of strings describing the known features + KnownFeatures() []string +} + +// MutableFeatureGate allows for dynamic feature gate configuration +type MutableFeatureGate interface { + FeatureGate + + // SetFromMap sets feature gate values from a map[string]bool + SetFromMap(m map[string]bool) error + // Add adds features to the feature gate + Add(features map[Feature]FeatureSpec) error + // String returns a string representing the feature gate configuration + String() string +} + +// featureGate implements the FeatureGate and MutableFeatureGate interfaces +type featureGate struct { + // lock guards writes to known, enabled, and reads/writes of closed + lock sync.Mutex + // known holds a map[Feature]FeatureSpec + known atomic.Value + // enabled holds a map[Feature]bool + enabled atomic.Value + // closed is set to true once the feature gates are considered immutable + closed bool +} + +// NewFeatureGate creates a new feature gate with the default features +func NewFeatureGate() MutableFeatureGate { + known := map[Feature]FeatureSpec{} + for k, v := range defaultFeatures { + known[k] = v + } + + f := &featureGate{} + f.known.Store(known) + f.enabled.Store(map[Feature]bool{}) + return f +} + +// SetFromMap sets feature gate values from a map[string]bool +func (f *featureGate) SetFromMap(m map[string]bool) error { + f.lock.Lock() + defer f.lock.Unlock() + + // Copy existing state + known := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + known[k] = v + } + enabled := map[Feature]bool{} + for k, v := range f.enabled.Load().(map[Feature]bool) { + enabled[k] = v + } + + // Apply the new settings + for k, v := range m { + k := Feature(k) + featureSpec, ok := known[k] + if !ok { + return fmt.Errorf("unrecognized feature gate: %s", k) + } + if featureSpec.LockToDefault && featureSpec.Default != v { + return fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default) + } + enabled[k] = v + } + + // Persist the changes + f.known.Store(known) + f.enabled.Store(enabled) + return nil +} + +// Add adds features to the feature gate +func (f *featureGate) Add(features map[Feature]FeatureSpec) error { + f.lock.Lock() + defer f.lock.Unlock() + + if f.closed { + return fmt.Errorf("cannot add feature gates after the feature gate is closed") + } + + // Copy existing state + known := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + known[k] = v + } + + // Add new features + for name, spec := range features { + if existingSpec, found := known[name]; found { + if existingSpec == spec { + continue + } + return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec) + } + known[name] = spec + } + + // Persist changes + f.known.Store(known) + + return nil +} + +// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..." +func (f *featureGate) String() string { + pairs := []string{} + for k, v := range f.enabled.Load().(map[Feature]bool) { + pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} + +// Enabled returns true if the key is enabled +func (f *featureGate) Enabled(key Feature) bool { + if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok { + return v + } + if v, ok := f.known.Load().(map[Feature]FeatureSpec)[key]; ok { + return v.Default + } + return false +} + +// KnownFeatures returns a slice of strings describing the FeatureGate's known features +// GA features are hidden from the list +func (f *featureGate) KnownFeatures() []string { + knownFeatures := f.known.Load().(map[Feature]FeatureSpec) + known := make([]string, 0, len(knownFeatures)) + for k, v := range knownFeatures { + if v.Stage == GA { + continue + } + known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v.Stage, v.Default)) + } + sort.Strings(known) + return known +} + +// Default feature gates instance +var DefaultFeatureGates = NewFeatureGate() + +// Enabled checks if a feature is enabled in the default feature gates +func Enabled(name Feature) bool { + return DefaultFeatureGates.Enabled(name) +} + +// SetFromMap sets feature gate values from a map in the default feature gates +func SetFromMap(featureMap map[string]bool) error { + return DefaultFeatureGates.SetFromMap(featureMap) +} diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go index d7c4c7e492a..889a10f6643 100644 --- a/pkg/plugin/client/http2http.go +++ b/pkg/plugin/client/http2http.go @@ -14,13 +14,11 @@ //go:build !frps -package plugin +package client import ( "context" - "io" stdlog "log" - "net" "net/http" "net/http/httputil" @@ -42,7 +40,7 @@ type HTTP2HTTPPlugin struct { s *http.Server } -func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTP2HTTPPluginOptions) listener := NewProxyListener() @@ -80,8 +78,8 @@ func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTP2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) +func (p *HTTP2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index 66f9098912c..538f2850f1f 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -14,14 +14,12 @@ //go:build !frps -package plugin +package client import ( "context" "crypto/tls" - "io" stdlog "log" - "net" "net/http" "net/http/httputil" @@ -43,7 +41,7 @@ type HTTP2HTTPSPlugin struct { s *http.Server } -func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTP2HTTPSPluginOptions) listener := NewProxyListener() @@ -89,8 +87,8 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) +func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go index b7491bd1082..0f6b55f4438 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -14,7 +14,7 @@ //go:build !frps -package plugin +package client import ( "bufio" @@ -45,7 +45,7 @@ type HTTPProxy struct { s *http.Server } -func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTPProxyPluginOptions) listener := NewProxyListener() @@ -69,8 +69,8 @@ func (hp *HTTPProxy) Name() string { return v1.PluginHTTPProxy } -func (hp *HTTPProxy) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) +func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) sc, rd := libnet.NewSharedConn(wrapConn) firstBytes := make([]byte, 7) diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index 9632a6fbc80..963b9d2e037 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -14,15 +14,13 @@ //go:build !frps -package plugin +package client import ( "context" "crypto/tls" "fmt" - "io" stdlog "log" - "net" "net/http" "net/http/httputil" "time" @@ -48,7 +46,7 @@ type HTTPS2HTTPPlugin struct { s *http.Server } -func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTPS2HTTPPluginOptions) listener := NewProxyListener() @@ -106,10 +104,10 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) - if extra.SrcAddr != nil { - wrapConn.SetRemoteAddr(extra.SrcAddr) +func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + if connInfo.SrcAddr != nil { + wrapConn.SetRemoteAddr(connInfo.SrcAddr) } _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index 8121e0943de..5c669d367da 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -14,15 +14,13 @@ //go:build !frps -package plugin +package client import ( "context" "crypto/tls" "fmt" - "io" stdlog "log" - "net" "net/http" "net/http/httputil" "time" @@ -48,7 +46,7 @@ type HTTPS2HTTPSPlugin struct { s *http.Server } -func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.HTTPS2HTTPSPluginOptions) listener := NewProxyListener() @@ -112,10 +110,10 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) - if extra.SrcAddr != nil { - wrapConn.SetRemoteAddr(extra.SrcAddr) +func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + if connInfo.SrcAddr != nil { + wrapConn.SetRemoteAddr(connInfo.SrcAddr) } _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go index 3dce8592629..7bcd04893f5 100644 --- a/pkg/plugin/client/plugin.go +++ b/pkg/plugin/client/plugin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package client import ( "context" @@ -25,13 +25,18 @@ import ( pp "github.com/pires/go-proxyproto" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/vnet" ) +type PluginContext struct { + Name string + VnetController *vnet.Controller +} + // Creators is used for create plugins to handle connections. var creators = make(map[string]CreatorFn) -// params has prefix "plugin_" -type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error) +type CreatorFn func(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) func Register(name string, fn CreatorFn) { if _, exist := creators[name]; exist { @@ -40,16 +45,19 @@ func Register(name string, fn CreatorFn) { creators[name] = fn } -func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) { - if fn, ok := creators[name]; ok { - p, err = fn(options) +func Create(pluginName string, pluginCtx PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { + if fn, ok := creators[pluginName]; ok { + p, err = fn(pluginCtx, options) } else { - err = fmt.Errorf("plugin [%s] is not registered", name) + err = fmt.Errorf("plugin [%s] is not registered", pluginName) } return } -type ExtraInfo struct { +type ConnectionInfo struct { + Conn io.ReadWriteCloser + UnderlyingConn net.Conn + ProxyProtocolHeader *pp.Header SrcAddr net.Addr DstAddr net.Addr @@ -58,7 +66,7 @@ type ExtraInfo struct { type Plugin interface { Name() string - Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) + Handle(ctx context.Context, connInfo *ConnectionInfo) Close() error } diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go index 7478ebe1414..752dd1e198e 100644 --- a/pkg/plugin/client/socks5.go +++ b/pkg/plugin/client/socks5.go @@ -14,13 +14,12 @@ //go:build !frps -package plugin +package client import ( "context" "io" "log" - "net" gosocks5 "github.com/armon/go-socks5" @@ -36,7 +35,7 @@ type Socks5Plugin struct { Server *gosocks5.Server } -func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { +func NewSocks5Plugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { opts := options.(*v1.Socks5PluginOptions) cfg := &gosocks5.Config{ @@ -51,9 +50,9 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { return } -func (sp *Socks5Plugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - defer conn.Close() - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) +func (sp *Socks5Plugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + defer connInfo.Conn.Close() + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) _ = sp.Server.ServeConn(wrapConn) } diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go index 02cb9930728..0bab120aa22 100644 --- a/pkg/plugin/client/static_file.go +++ b/pkg/plugin/client/static_file.go @@ -14,12 +14,10 @@ //go:build !frps -package plugin +package client import ( "context" - "io" - "net" "net/http" "time" @@ -40,7 +38,7 @@ type StaticFilePlugin struct { s *http.Server } -func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewStaticFilePlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.StaticFilePluginOptions) listener := NewProxyListener() @@ -70,8 +68,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { return sp, nil } -func (sp *StaticFilePlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) +func (sp *StaticFilePlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) _ = sp.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/tls2raw.go b/pkg/plugin/client/tls2raw.go index adcc77417d8..445b6c91dfc 100644 --- a/pkg/plugin/client/tls2raw.go +++ b/pkg/plugin/client/tls2raw.go @@ -14,12 +14,11 @@ //go:build !frps -package plugin +package client import ( "context" "crypto/tls" - "io" "net" libio "github.com/fatedier/golib/io" @@ -40,7 +39,7 @@ type TLS2RawPlugin struct { tlsConfig *tls.Config } -func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) { +func NewTLS2RawPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { opts := options.(*v1.TLS2RawPluginOptions) p := &TLS2RawPlugin{ @@ -55,10 +54,10 @@ func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) { return p, nil } -func (p *TLS2RawPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { +func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { xl := xlog.FromContextSafe(ctx) - wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) tlsConn := tls.Server(wrapConn, p.tlsConfig) if err := tlsConn.Handshake(); err != nil { diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index b6aa6075040..52d9c6525be 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -14,11 +14,10 @@ //go:build !frps -package plugin +package client import ( "context" - "io" "net" libio "github.com/fatedier/golib/io" @@ -35,7 +34,7 @@ type UnixDomainSocketPlugin struct { UnixAddr *net.UnixAddr } -func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err error) { +func NewUnixDomainSocketPlugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { opts := options.(*v1.UnixDomainSocketPluginOptions) unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath) @@ -50,20 +49,20 @@ func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err er return } -func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) { +func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { xl := xlog.FromContextSafe(ctx) localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) if err != nil { xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) return } - if extra.ProxyProtocolHeader != nil { - if _, err := extra.ProxyProtocolHeader.WriteTo(localConn); err != nil { + if connInfo.ProxyProtocolHeader != nil { + if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { return } } - libio.Join(localConn, conn) + libio.Join(localConn, connInfo.Conn) } func (uds *UnixDomainSocketPlugin) Name() string { diff --git a/pkg/plugin/client/virtual_net.go b/pkg/plugin/client/virtual_net.go new file mode 100644 index 00000000000..e1b29fdc843 --- /dev/null +++ b/pkg/plugin/client/virtual_net.go @@ -0,0 +1,71 @@ +// 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. + +//go:build !frps + +package client + +import ( + "context" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func init() { + Register(v1.PluginVirtualNet, NewVirtualNetPlugin) +} + +type VirtualNetPlugin struct { + pluginCtx PluginContext + opts *v1.VirtualNetPluginOptions +} + +func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.VirtualNetPluginOptions) + + p := &VirtualNetPlugin{ + pluginCtx: pluginCtx, + opts: opts, + } + return p, nil +} + +func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { + xl := xlog.FromContextSafe(ctx) + + // Verify if virtual network controller is available + if p.pluginCtx.VnetController == nil { + return + } + + // Register the connection with the controller + routeName := p.pluginCtx.Name + err := p.pluginCtx.VnetController.RegisterServerConn(ctx, routeName, connInfo.Conn) + if err != nil { + xl.Errorf("virtual net failed to register server connection: %v", err) + return + } +} + +func (p *VirtualNetPlugin) Name() string { + return v1.PluginVirtualNet +} + +func (p *VirtualNetPlugin) Close() error { + if p.pluginCtx.VnetController != nil { + p.pluginCtx.VnetController.UnregisterServerConn(p.pluginCtx.Name) + } + return nil +} diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 6046c38a848..196993ef8b6 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "bytes" diff --git a/pkg/plugin/server/manager.go b/pkg/plugin/server/manager.go index 8f23b529fd8..dabfb46cbd0 100644 --- a/pkg/plugin/server/manager.go +++ b/pkg/plugin/server/manager.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" diff --git a/pkg/plugin/server/plugin.go b/pkg/plugin/server/plugin.go index 9456ee9b039..3d3c8cfdd65 100644 --- a/pkg/plugin/server/plugin.go +++ b/pkg/plugin/server/plugin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" diff --git a/pkg/plugin/server/tracer.go b/pkg/plugin/server/tracer.go index 2f4f2ccc25d..5b6ede182ec 100644 --- a/pkg/plugin/server/tracer.go +++ b/pkg/plugin/server/tracer.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go index c9fc4a40225..4a5b7527185 100644 --- a/pkg/plugin/server/types.go +++ b/pkg/plugin/server/types.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "github.com/fatedier/frp/pkg/msg" diff --git a/pkg/plugin/visitor/plugin.go b/pkg/plugin/visitor/plugin.go new file mode 100644 index 00000000000..94adce093b0 --- /dev/null +++ b/pkg/plugin/visitor/plugin.go @@ -0,0 +1,58 @@ +// 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 visitor + +import ( + "context" + "fmt" + "net" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/vnet" +) + +type PluginContext struct { + Name string + Ctx context.Context + VnetController *vnet.Controller + HandleConn func(net.Conn) +} + +// Creators is used for create plugins to handle connections. +var creators = make(map[string]CreatorFn) + +type CreatorFn func(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error) + +func Register(name string, fn CreatorFn) { + if _, exist := creators[name]; exist { + panic(fmt.Sprintf("plugin [%s] is already registered", name)) + } + creators[name] = fn +} + +func Create(pluginName string, pluginCtx PluginContext, options v1.VisitorPluginOptions) (p Plugin, err error) { + if fn, ok := creators[pluginName]; ok { + p, err = fn(pluginCtx, options) + } else { + err = fmt.Errorf("plugin [%s] is not registered", pluginName) + } + return +} + +type Plugin interface { + Name() string + Start() + Close() error +} diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go new file mode 100644 index 00000000000..e452e14f810 --- /dev/null +++ b/pkg/plugin/visitor/virtual_net.go @@ -0,0 +1,232 @@ +// 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. + +//go:build !frps + +package visitor + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "time" + + v1 "github.com/fatedier/frp/pkg/config/v1" + netutil "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func init() { + Register(v1.VisitorPluginVirtualNet, NewVirtualNetPlugin) +} + +type VirtualNetPlugin struct { + pluginCtx PluginContext + + routes []net.IPNet + + mu sync.Mutex + controllerConn net.Conn + closeSignal chan struct{} + + ctx context.Context + cancel context.CancelFunc +} + +func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error) { + opts := options.(*v1.VirtualNetVisitorPluginOptions) + + p := &VirtualNetPlugin{ + pluginCtx: pluginCtx, + routes: make([]net.IPNet, 0), + } + + p.ctx, p.cancel = context.WithCancel(pluginCtx.Ctx) + + if opts.DestinationIP == "" { + return nil, errors.New("destinationIP is required") + } + + // Parse DestinationIP as a single IP and create a host route + ip := net.ParseIP(opts.DestinationIP) + if ip == nil { + return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP) + } + + var mask net.IPMask + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // /32 for IPv4 + } else { + mask = net.CIDRMask(128, 128) // /128 for IPv6 + } + p.routes = append(p.routes, net.IPNet{IP: ip, Mask: mask}) + + return p, nil +} + +func (p *VirtualNetPlugin) Name() string { + return v1.VisitorPluginVirtualNet +} + +func (p *VirtualNetPlugin) Start() { + xl := xlog.FromContextSafe(p.pluginCtx.Ctx) + if p.pluginCtx.VnetController == nil { + return + } + + routeStr := "unknown" + if len(p.routes) > 0 { + routeStr = p.routes[0].String() + } + xl.Infof("Starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr) + + go p.run() +} + +func (p *VirtualNetPlugin) run() { + xl := xlog.FromContextSafe(p.ctx) + reconnectDelay := 10 * time.Second + + for { + // Create a signal channel for this connection attempt + currentCloseSignal := make(chan struct{}) + + // Store the signal channel under lock + p.mu.Lock() + p.closeSignal = currentCloseSignal + p.mu.Unlock() + + select { + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name) + // Ensure controllerConn from previous loop is cleaned up if necessary + p.cleanupControllerConn(xl) + return + default: + } + + controllerConn, pluginConn := net.Pipe() + + // Store controllerConn under lock for cleanup purposes + p.mu.Lock() + p.controllerConn = controllerConn + p.mu.Unlock() + + // Wrap pluginConn using CloseNotifyConn + pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() { + close(currentCloseSignal) // Signal the run loop + }) + + xl.Infof("Attempting to register client route for visitor [%s]", p.pluginCtx.Name) + err := p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn) + if err != nil { + xl.Errorf("Failed to register client route for visitor [%s]: %v. Retrying after %v", p.pluginCtx.Name, err, reconnectDelay) + p.cleanupPipePair(xl, controllerConn, pluginConn) // Close both ends on registration failure + + // Wait before retrying registration, unless context is cancelled + select { + case <-time.After(reconnectDelay): + continue // Retry the loop + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin registration retry wait interrupted for visitor [%s]", p.pluginCtx.Name) + return // Exit loop if context is cancelled during wait + } + } + + 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) + + // Wait for either the plugin context to be cancelled or the wrapper's Close() to be called via the signal channel. + select { + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name) + // Context cancelled, ensure controller side is closed if HandleConn didn't close its side yet. + p.cleanupControllerConn(xl) + return + case <-currentCloseSignal: + xl.Infof("Detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name) + // HandleConn closed the plugin side (pluginNotifyConn). The closeFn was called, closing currentCloseSignal. + // We still need to close the controller side. + p.cleanupControllerConn(xl) + + // Add a delay before attempting to reconnect, respecting context cancellation. + xl.Infof("Waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name) + select { + case <-time.After(reconnectDelay): + // Delay completed, loop will continue. + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name) + return // Exit loop if context is cancelled during wait + } + // Loop will continue to reconnect. + } + + // Loop will restart, context check at the beginning of the loop is sufficient. + xl.Infof("Re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name) + } +} + +// cleanupControllerConn closes the current controllerConn (if it exists) under lock. +func (p *VirtualNetPlugin) cleanupControllerConn(xl *xlog.Logger) { + p.mu.Lock() + defer p.mu.Unlock() + if p.controllerConn != nil { + xl.Debugf("Cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name) + p.controllerConn.Close() + p.controllerConn = nil + } + // Also clear the closeSignal reference for the completed/cancelled connection attempt + p.closeSignal = nil +} + +// cleanupPipePair closes both ends of a pipe, used typically when registration fails. +func (p *VirtualNetPlugin) cleanupPipePair(xl *xlog.Logger, controllerConn, pluginConn net.Conn) { + xl.Debugf("Cleaning up pipe pair for visitor [%s] after registration failure", p.pluginCtx.Name) + controllerConn.Close() + pluginConn.Close() + p.mu.Lock() + p.controllerConn = nil // Ensure field is nil if it was briefly set + p.closeSignal = nil // Ensure field is nil if it was briefly set + p.mu.Unlock() +} + +// Close initiates the plugin shutdown. +func (p *VirtualNetPlugin) Close() error { + xl := xlog.FromContextSafe(p.pluginCtx.Ctx) // Use base context for close logging + xl.Infof("Closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name) + + // 1. Signal the run loop goroutine to stop via context cancellation. + p.cancel() + + // 2. Unregister the route from the controller. + // This might implicitly cause the VnetController to close its end of the pipe (controllerConn). + if p.pluginCtx.VnetController != nil { + p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name) + xl.Infof("Unregistered client route for visitor [%s]", p.pluginCtx.Name) + } else { + xl.Warnf("VnetController is nil during close for visitor [%s], cannot unregister route", p.pluginCtx.Name) + } + + // 3. Explicitly close the controller side of the pipe managed by this plugin. + // This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end. + p.cleanupControllerConn(xl) + xl.Infof("Finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) + + return nil +} diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index f56e1ec072c..e74233c7083 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.61.2" +var version = "0.62.0" func Full() string { return version diff --git a/pkg/vnet/controller.go b/pkg/vnet/controller.go new file mode 100644 index 00000000000..d43147a3076 --- /dev/null +++ b/pkg/vnet/controller.go @@ -0,0 +1,360 @@ +// 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 vnet + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net" + "sync" + + "github.com/fatedier/golib/pool" + "github.com/songgao/water/waterutil" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/xlog" +) + +const ( + maxPacketSize = 1420 +) + +type Controller struct { + addr string + + tun io.ReadWriteCloser + clientRouter *clientRouter // Route based on destination IP (client mode) + serverRouter *serverRouter // Route based on source IP (server mode) +} + +func NewController(cfg v1.VirtualNetConfig) *Controller { + return &Controller{ + addr: cfg.Address, + clientRouter: newClientRouter(), + serverRouter: newServerRouter(), + } +} + +func (c *Controller) Init() error { + tunDevice, err := OpenTun(context.Background(), c.addr) + if err != nil { + return err + } + c.tun = tunDevice + return nil +} + +func (c *Controller) Run() error { + conn := c.tun + + for { + buf := pool.GetBuf(maxPacketSize) + n, err := conn.Read(buf) + if err != nil { + pool.PutBuf(buf) + log.Warnf("vnet read from tun error: %v", err) + return err + } + + c.handlePacket(buf[:n]) + pool.PutBuf(buf) + } +} + +// handlePacket processes a single packet. The caller is responsible for managing the buffer. +func (c *Controller) handlePacket(buf []byte) { + log.Tracef("vnet read from tun [%d]: %s", len(buf), base64.StdEncoding.EncodeToString(buf)) + + var src, dst net.IP + switch { + case waterutil.IsIPv4(buf): + header, err := ipv4.ParseHeader(buf) + if err != nil { + log.Warnf("parse ipv4 header error:", err) + return + } + src = header.Src + dst = header.Dst + log.Tracef("%s >> %s %d/%-4d %-4x %d", + header.Src, header.Dst, + header.Len, header.TotalLen, header.ID, header.Flags) + case waterutil.IsIPv6(buf): + header, err := ipv6.ParseHeader(buf) + if err != nil { + log.Warnf("parse ipv6 header error:", err) + return + } + src = header.Src + dst = header.Dst + log.Tracef("%s >> %s %d %d", + header.Src, header.Dst, + header.PayloadLen, header.TrafficClass) + default: + log.Tracef("unknown packet, discarded(%d)", len(buf)) + return + } + + targetConn, err := c.clientRouter.findConn(dst) + if err == nil { + if err := WriteMessage(targetConn, buf); err != nil { + log.Warnf("write to client target conn error: %v", err) + } + return + } + + targetConn, err = c.serverRouter.findConnBySrc(dst) + if err == nil { + if err := WriteMessage(targetConn, buf); err != nil { + log.Warnf("write to server target conn error: %v", err) + } + return + } + + log.Tracef("no route found for packet from %s to %s", src, dst) +} + +func (c *Controller) Stop() error { + return c.tun.Close() +} + +// Client connection read loop +func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) { + xl := xlog.FromContextSafe(ctx) + for { + data, err := ReadMessage(conn) + if err != nil { + xl.Warnf("client read error: %v", err) + return + } + + if len(data) == 0 { + continue + } + + switch { + case waterutil.IsIPv4(data): + header, err := ipv4.ParseHeader(data) + if err != nil { + xl.Warnf("parse ipv4 header error: %v", err) + continue + } + xl.Tracef("%s >> %s %d/%-4d %-4x %d", + header.Src, header.Dst, + header.Len, header.TotalLen, header.ID, header.Flags) + case waterutil.IsIPv6(data): + header, err := ipv6.ParseHeader(data) + if err != nil { + xl.Warnf("parse ipv6 header error: %v", err) + continue + } + xl.Tracef("%s >> %s %d %d", + header.Src, header.Dst, + header.PayloadLen, header.TrafficClass) + default: + xl.Tracef("unknown packet, discarded(%d)", len(data)) + continue + } + + xl.Tracef("vnet write to tun (client) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data)) + _, err = c.tun.Write(data) + if err != nil { + xl.Warnf("client write tun error: %v", err) + } + } +} + +// Server connection read loop +func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser) { + xl := xlog.FromContextSafe(ctx) + for { + data, err := ReadMessage(conn) + if err != nil { + xl.Warnf("server read error: %v", err) + return + } + + if len(data) == 0 { + continue + } + + // Register source IP to connection mapping + if waterutil.IsIPv4(data) || waterutil.IsIPv6(data) { + var src net.IP + if waterutil.IsIPv4(data) { + header, err := ipv4.ParseHeader(data) + if err == nil { + src = header.Src + c.serverRouter.registerSrcIP(src, conn) + } + } else { + header, err := ipv6.ParseHeader(data) + if err == nil { + src = header.Src + c.serverRouter.registerSrcIP(src, conn) + } + } + } + + xl.Tracef("vnet write to tun (server) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data)) + _, err = c.tun.Write(data) + if err != nil { + xl.Warnf("server write tun error: %v", err) + } + } +} + +// RegisterClientRoute Register client route (based on destination IP CIDR) +func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) error { + if err := c.clientRouter.addRoute(name, routes, conn); err != nil { + return err + } + go c.readLoopClient(ctx, conn) + return nil +} + +// RegisterServerConn Register server connection (dynamically associates with source IPs) +func (c *Controller) RegisterServerConn(ctx context.Context, name string, conn io.ReadWriteCloser) error { + if err := c.serverRouter.addConn(name, conn); err != nil { + return err + } + go c.readLoopServer(ctx, conn) + return nil +} + +// UnregisterServerConn Remove server connection from routing table +func (c *Controller) UnregisterServerConn(name string) { + c.serverRouter.delConn(name) +} + +// UnregisterClientRoute Remove client route from routing table +func (c *Controller) UnregisterClientRoute(name string) { + c.clientRouter.delRoute(name) +} + +// ParseRoutes Convert route strings to IPNet objects +func ParseRoutes(routeStrings []string) ([]net.IPNet, error) { + routes := make([]net.IPNet, 0, len(routeStrings)) + for _, r := range routeStrings { + _, ipNet, err := net.ParseCIDR(r) + if err != nil { + return nil, fmt.Errorf("parse route %s error: %v", r, err) + } + routes = append(routes, *ipNet) + } + return routes, nil +} + +// Client router (based on destination IP routing) +type clientRouter struct { + routes map[string]*routeElement + mu sync.RWMutex +} + +func newClientRouter() *clientRouter { + return &clientRouter{ + routes: make(map[string]*routeElement), + } +} + +func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error { + r.mu.Lock() + defer r.mu.Unlock() + r.routes[name] = &routeElement{ + name: name, + routes: routes, + conn: conn, + } + return nil +} + +func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) { + r.mu.RLock() + defer r.mu.RUnlock() + for _, re := range r.routes { + for _, route := range re.routes { + if route.Contains(dst) { + return re.conn, nil + } + } + } + return nil, fmt.Errorf("no route found for destination %s", dst) +} + +func (r *clientRouter) delRoute(name string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.routes, name) +} + +// Server router (based on source IP routing) +type serverRouter struct { + namedConns map[string]io.ReadWriteCloser // Name to connection mapping + srcIPConns map[string]io.Writer // Source IP string to connection mapping + mu sync.RWMutex +} + +func newServerRouter() *serverRouter { + return &serverRouter{ + namedConns: make(map[string]io.ReadWriteCloser), + srcIPConns: make(map[string]io.Writer), + } +} + +func (r *serverRouter) addConn(name string, conn io.ReadWriteCloser) error { + r.mu.Lock() + original, ok := r.namedConns[name] + r.namedConns[name] = conn + r.mu.Unlock() + if ok { + // Close the original connection if it exists + _ = original.Close() + } + return nil +} + +func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) { + r.mu.RLock() + defer r.mu.RUnlock() + conn, exists := r.srcIPConns[src.String()] + if !exists { + return nil, fmt.Errorf("no route found for source %s", src) + } + return conn, nil +} + +func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) { + r.mu.Lock() + defer r.mu.Unlock() + r.srcIPConns[src.String()] = conn +} + +func (r *serverRouter) delConn(name string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.namedConns, name) + // Note: We don't delete mappings from srcIPConns because we don't know which source IPs are associated with this connection + // This might cause dangling references, but they will be overwritten on new connections or restart +} + +type routeElement struct { + name string + routes []net.IPNet + conn io.ReadWriteCloser +} diff --git a/pkg/vnet/message.go b/pkg/vnet/message.go new file mode 100644 index 00000000000..002b090ab4a --- /dev/null +++ b/pkg/vnet/message.go @@ -0,0 +1,81 @@ +// 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 vnet + +import ( + "encoding/binary" + "fmt" + "io" +) + +// Maximum message size +const ( + maxMessageSize = 1024 * 1024 // 1MB +) + +// Format: [length(4 bytes)][data(length bytes)] + +// ReadMessage reads a framed message from the reader +func ReadMessage(r io.Reader) ([]byte, error) { + // Read length (4 bytes) + var length uint32 + err := binary.Read(r, binary.LittleEndian, &length) + if err != nil { + return nil, fmt.Errorf("read message length error: %v", err) + } + + // Check length to prevent DoS + if length == 0 { + return nil, fmt.Errorf("message length is 0") + } + if length > maxMessageSize { + return nil, fmt.Errorf("message too large: %d > %d", length, maxMessageSize) + } + + // Read message data + data := make([]byte, length) + _, err = io.ReadFull(r, data) + if err != nil { + return nil, fmt.Errorf("read message data error: %v", err) + } + + return data, nil +} + +// WriteMessage writes a framed message to the writer +func WriteMessage(w io.Writer, data []byte) error { + // Get data length + length := uint32(len(data)) + if length == 0 { + return fmt.Errorf("message data length is 0") + } + if length > maxMessageSize { + return fmt.Errorf("message too large: %d > %d", length, maxMessageSize) + } + + // Write length + err := binary.Write(w, binary.LittleEndian, length) + if err != nil { + return fmt.Errorf("write message length error: %v", err) + } + + // Write message data + _, err = w.Write(data) + if err != nil { + return fmt.Errorf("write message data error: %v", err) + } + + return nil +} diff --git a/pkg/vnet/tun.go b/pkg/vnet/tun.go new file mode 100644 index 00000000000..bafc639271d --- /dev/null +++ b/pkg/vnet/tun.go @@ -0,0 +1,77 @@ +// 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 vnet + +import ( + "context" + "io" + + "github.com/fatedier/golib/pool" + "golang.zx2c4.com/wireguard/tun" +) + +const ( + offset = 16 +) + +type TunDevice interface { + io.ReadWriteCloser +} + +func OpenTun(ctx context.Context, addr string) (TunDevice, error) { + td, err := openTun(ctx, addr) + if err != nil { + return nil, err + } + return &tunDeviceWrapper{dev: td}, nil +} + +type tunDeviceWrapper struct { + dev tun.Device +} + +func (d *tunDeviceWrapper) Read(p []byte) (int, error) { + buf := pool.GetBuf(len(p) + offset) + defer pool.PutBuf(buf) + + sz := make([]int, 1) + + n, err := d.dev.Read([][]byte{buf}, sz, offset) + if err != nil { + return 0, err + } + if n == 0 { + return 0, io.EOF + } + + dataSize := sz[0] + if dataSize > len(p) { + dataSize = len(p) + } + copy(p, buf[offset:offset+dataSize]) + return dataSize, nil +} + +func (d *tunDeviceWrapper) Write(p []byte) (int, error) { + buf := pool.GetBuf(len(p) + offset) + defer pool.PutBuf(buf) + + copy(buf[offset:], p) + return d.dev.Write([][]byte{buf}, offset) +} + +func (d *tunDeviceWrapper) Close() error { + return d.dev.Close() +} diff --git a/pkg/vnet/tun_darwin.go b/pkg/vnet/tun_darwin.go new file mode 100644 index 00000000000..9fa7d42c58f --- /dev/null +++ b/pkg/vnet/tun_darwin.go @@ -0,0 +1,85 @@ +// 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 vnet + +import ( + "context" + "fmt" + "net" + "os/exec" + + "golang.zx2c4.com/wireguard/tun" +) + +const ( + defaultTunName = "utun" + defaultMTU = 1420 +) + +func openTun(_ context.Context, addr string) (tun.Device, error) { + dev, err := tun.CreateTUN(defaultTunName, defaultMTU) + if err != nil { + return nil, err + } + + name, err := dev.Name() + if err != nil { + return nil, err + } + + ip, ipNet, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + + // Calculate a peer IP for the point-to-point tunnel + peerIP := generatePeerIP(ip) + + // Configure the interface with proper point-to-point addressing + if err = exec.Command("ifconfig", name, "inet", ip.String(), peerIP.String(), "mtu", fmt.Sprint(defaultMTU), "up").Run(); err != nil { + return nil, err + } + + // Add default route for the tunnel subnet + routes := []net.IPNet{*ipNet} + if err = addRoutes(name, routes); err != nil { + return nil, err + } + return dev, nil +} + +// generatePeerIP creates a peer IP for the point-to-point tunnel +// by incrementing the last octet of the IP +func generatePeerIP(ip net.IP) net.IP { + // Make a copy to avoid modifying the original + peerIP := make(net.IP, len(ip)) + copy(peerIP, ip) + + // Increment the last octet + peerIP[len(peerIP)-1]++ + + return peerIP +} + +// addRoutes configures system routes for the TUN interface +func addRoutes(ifName string, routes []net.IPNet) error { + for _, route := range routes { + routeStr := route.String() + if err := exec.Command("route", "add", "-net", routeStr, "-interface", ifName).Run(); err != nil { + return err + } + } + return nil +} diff --git a/pkg/vnet/tun_linux.go b/pkg/vnet/tun_linux.go new file mode 100644 index 00000000000..7c9c684cc97 --- /dev/null +++ b/pkg/vnet/tun_linux.go @@ -0,0 +1,84 @@ +// 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 vnet + +import ( + "context" + "fmt" + "net" + + "github.com/vishvananda/netlink" + "golang.zx2c4.com/wireguard/tun" +) + +const ( + defaultTunName = "utun" + defaultMTU = 1420 +) + +func openTun(_ context.Context, addr string) (tun.Device, error) { + dev, err := tun.CreateTUN(defaultTunName, defaultMTU) + if err != nil { + return nil, err + } + + name, err := dev.Name() + if err != nil { + return nil, err + } + + ifn, err := net.InterfaceByName(name) + if err != nil { + return nil, err + } + + link, err := netlink.LinkByName(name) + if err != nil { + return nil, err + } + + ip, cidr, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + if err := netlink.AddrAdd(link, &netlink.Addr{ + IPNet: &net.IPNet{ + IP: ip, + Mask: cidr.Mask, + }, + }); err != nil { + return nil, err + } + + if err := netlink.LinkSetUp(link); err != nil { + return nil, err + } + + if err = addRoutes(ifn, cidr); err != nil { + return nil, err + } + return dev, nil +} + +func addRoutes(ifn *net.Interface, cidr *net.IPNet) error { + r := netlink.Route{ + Dst: cidr, + LinkIndex: ifn.Index, + } + if err := netlink.RouteReplace(&r); err != nil { + return fmt.Errorf("add route to %v error: %v", r.Dst, err) + } + return nil +} diff --git a/pkg/vnet/tun_unsupported.go b/pkg/vnet/tun_unsupported.go new file mode 100644 index 00000000000..0e2a811441f --- /dev/null +++ b/pkg/vnet/tun_unsupported.go @@ -0,0 +1,27 @@ +// 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. + +//go:build !darwin && !linux + +package vnet + +import ( + "context" + "fmt" + "runtime" +) + +func openTun(ctx context.Context) (TunDevice, error) { + return nil, fmt.Errorf("virtual net is not supported on this platform (%s/%s)", runtime.GOOS, runtime.GOARCH) +} From e208043323d0ffb5d495c32e14787f509572174c Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 16 Apr 2025 16:17:26 +0800 Subject: [PATCH 37/83] vnet: update tun_unsupported function (#4752) --- pkg/vnet/tun_unsupported.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/vnet/tun_unsupported.go b/pkg/vnet/tun_unsupported.go index 0e2a811441f..731a06cda9e 100644 --- a/pkg/vnet/tun_unsupported.go +++ b/pkg/vnet/tun_unsupported.go @@ -20,8 +20,10 @@ import ( "context" "fmt" "runtime" + + "golang.zx2c4.com/wireguard/tun" ) -func openTun(ctx context.Context) (TunDevice, error) { +func openTun(_ context.Context, _ string) (tun.Device, error) { return nil, fmt.Errorf("virtual net is not supported on this platform (%s/%s)", runtime.GOOS, runtime.GOARCH) } From 27f66baf547ce88f45e2b9d68427410c729427c9 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 17 Apr 2025 01:34:14 +0800 Subject: [PATCH 38/83] update feature gates doc (#4755) --- README.md | 4 +--- doc/virtual_net.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e604d5af114..25537220e94 100644 --- a/README.md +++ b/README.md @@ -1283,9 +1283,7 @@ frp supports feature gates to enable or disable experimental features. This allo To enable an experimental feature, add the feature gate to your configuration: ```toml -featureGates = { - VirtualNet = true -} +featureGates = { VirtualNet = true } ``` ### Feature Lifecycle diff --git a/doc/virtual_net.md b/doc/virtual_net.md index 62653e9f188..6f135dfc2fc 100644 --- a/doc/virtual_net.md +++ b/doc/virtual_net.md @@ -49,9 +49,7 @@ type = "virtual_net" # frpc.toml (client side) serverAddr = "x.x.x.x" serverPort = 7000 -featureGates = { - VirtualNet = true -} +featureGates = { VirtualNet = true } # Configure the virtual network interface virtualNet.address = "100.86.0.2/24" From 3c8d648ddc4541f45bdc8d171c564834d8832abc Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 27 Apr 2025 15:22:28 +0800 Subject: [PATCH 39/83] vnet: fix issues (#4771) --- client/admin_api.go | 12 +-- client/control.go | 2 +- client/service.go | 2 +- pkg/plugin/client/virtual_net.go | 43 +++++++--- pkg/plugin/visitor/virtual_net.go | 80 +++++-------------- pkg/util/vhost/http.go | 2 +- pkg/util/vhost/vhost.go | 2 +- pkg/vnet/controller.go | 118 +++++++++++++++++----------- pkg/vnet/message.go | 8 +- pkg/vnet/tun.go | 62 +++++++++++---- pkg/vnet/tun_linux.go | 63 +++++++++++++-- server/control.go | 2 +- server/dashboard_api.go | 20 ++--- server/service.go | 18 ++--- test/e2e/e2e.go | 2 +- test/e2e/framework/request.go | 6 +- test/e2e/legacy/features/real_ip.go | 4 +- test/e2e/v1/features/real_ip.go | 4 +- 18 files changed, 268 insertions(+), 182 deletions(-) diff --git a/client/admin_api.go b/client/admin_api.go index 708b2cbd9e6..f161d588b4d 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -165,9 +165,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) { res StatusResp = make(map[string][]ProxyStatusResp) ) - log.Infof("Http request [/api/status]") + log.Infof("http request [/api/status]") defer func() { - log.Infof("Http response [/api/status]") + log.Infof("http response [/api/status]") buf, _ = json.Marshal(&res) _, _ = w.Write(buf) }() @@ -198,9 +198,9 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { res := GeneralResponse{Code: 200} - log.Infof("Http get request [/api/config]") + log.Infof("http get request [/api/config]") defer func() { - log.Infof("Http get response [/api/config], code [%d]", res.Code) + log.Infof("http get response [/api/config], code [%d]", res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) @@ -228,9 +228,9 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} - log.Infof("Http put request [/api/config]") + log.Infof("http put request [/api/config]") defer func() { - log.Infof("Http put response [/api/config], code [%d]", res.Code) + log.Infof("http put response [/api/config], code [%d]", res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) diff --git a/client/control.go b/client/control.go index 157b4aef913..0dd70b8c72f 100644 --- a/client/control.go +++ b/client/control.go @@ -189,7 +189,7 @@ func (ctl *Control) handlePong(m msg.Message) { inMsg := m.(*msg.Pong) if inMsg.Error != "" { - xl.Errorf("Pong message contains error: %s", inMsg.Error) + xl.Errorf("pong message contains error: %s", inMsg.Error) ctl.closeSession() return } diff --git a/client/service.go b/client/service.go index 57eb483559e..e163cac4554 100644 --- a/client/service.go +++ b/client/service.go @@ -341,7 +341,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE ctl, err := NewControl(svr.ctx, sessionCtx) if err != nil { conn.Close() - xl.Errorf("NewControl error: %v", err) + xl.Errorf("new control error: %v", err) return false, err } ctl.SetInWorkConnCallback(svr.handleWorkConnCb) diff --git a/pkg/plugin/client/virtual_net.go b/pkg/plugin/client/virtual_net.go index e1b29fdc843..53570035c55 100644 --- a/pkg/plugin/client/virtual_net.go +++ b/pkg/plugin/client/virtual_net.go @@ -18,9 +18,10 @@ package client import ( "context" + "io" + "sync" v1 "github.com/fatedier/frp/pkg/config/v1" - "github.com/fatedier/frp/pkg/util/xlog" ) func init() { @@ -30,6 +31,8 @@ func init() { type VirtualNetPlugin struct { pluginCtx PluginContext opts *v1.VirtualNetPluginOptions + mu sync.Mutex + conns map[io.ReadWriteCloser]struct{} } func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) { @@ -43,19 +46,32 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions } func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { - xl := xlog.FromContextSafe(ctx) - // Verify if virtual network controller is available if p.pluginCtx.VnetController == nil { return } - // Register the connection with the controller - routeName := p.pluginCtx.Name - err := p.pluginCtx.VnetController.RegisterServerConn(ctx, routeName, connInfo.Conn) - if err != nil { - xl.Errorf("virtual net failed to register server connection: %v", err) - return + // Add the connection before starting the read loop to avoid race condition + // where RemoveConn might be called before the connection is added. + p.mu.Lock() + if p.conns == nil { + p.conns = make(map[io.ReadWriteCloser]struct{}) + } + p.conns[connInfo.Conn] = struct{}{} + p.mu.Unlock() + + // Register the connection with the controller and pass the cleanup function + p.pluginCtx.VnetController.StartServerConnReadLoop(ctx, connInfo.Conn, func() { + p.RemoveConn(connInfo.Conn) + }) +} + +func (p *VirtualNetPlugin) RemoveConn(conn io.ReadWriteCloser) { + p.mu.Lock() + defer p.mu.Unlock() + // Check if the map exists, as Close might have set it to nil concurrently + if p.conns != nil { + delete(p.conns, conn) } } @@ -64,8 +80,13 @@ func (p *VirtualNetPlugin) Name() string { } func (p *VirtualNetPlugin) Close() error { - if p.pluginCtx.VnetController != nil { - p.pluginCtx.VnetController.UnregisterServerConn(p.pluginCtx.Name) + p.mu.Lock() + defer p.mu.Unlock() + + // Close any remaining connections + for conn := range p.conns { + _ = conn.Close() } + p.conns = nil return nil } diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go index e452e14f810..f660c0c89e5 100644 --- a/pkg/plugin/visitor/virtual_net.go +++ b/pkg/plugin/visitor/virtual_net.go @@ -60,7 +60,7 @@ func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOption return nil, errors.New("destinationIP is required") } - // Parse DestinationIP as a single IP and create a host route + // Parse DestinationIP and create a host route. ip := net.ParseIP(opts.DestinationIP) if ip == nil { return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP) @@ -91,7 +91,7 @@ func (p *VirtualNetPlugin) Start() { if len(p.routes) > 0 { routeStr = p.routes[0].String() } - xl.Infof("Starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr) + xl.Infof("starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr) go p.run() } @@ -101,10 +101,8 @@ func (p *VirtualNetPlugin) run() { reconnectDelay := 10 * time.Second for { - // Create a signal channel for this connection attempt currentCloseSignal := make(chan struct{}) - // Store the signal channel under lock p.mu.Lock() p.closeSignal = currentCloseSignal p.mu.Unlock() @@ -112,7 +110,6 @@ func (p *VirtualNetPlugin) run() { select { case <-p.ctx.Done(): xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name) - // Ensure controllerConn from previous loop is cleaned up if necessary p.cleanupControllerConn(xl) return default: @@ -120,65 +117,43 @@ func (p *VirtualNetPlugin) run() { controllerConn, pluginConn := net.Pipe() - // Store controllerConn under lock for cleanup purposes p.mu.Lock() p.controllerConn = controllerConn p.mu.Unlock() - // Wrap pluginConn using CloseNotifyConn pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() { - close(currentCloseSignal) // Signal the run loop + close(currentCloseSignal) // Signal the run loop on close. }) - xl.Infof("Attempting to register client route for visitor [%s]", p.pluginCtx.Name) - err := p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn) - if err != nil { - xl.Errorf("Failed to register client route for visitor [%s]: %v. Retrying after %v", p.pluginCtx.Name, err, reconnectDelay) - p.cleanupPipePair(xl, controllerConn, pluginConn) // Close both ends on registration failure - - // Wait before retrying registration, unless context is cancelled - select { - case <-time.After(reconnectDelay): - continue // Retry the loop - case <-p.ctx.Done(): - xl.Infof("VirtualNetPlugin registration retry wait interrupted for visitor [%s]", p.pluginCtx.Name) - return // Exit loop if context is cancelled during wait - } - } - - xl.Infof("Successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name) + xl.Infof("attempting to register client route for visitor [%s]", p.pluginCtx.Name) + 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) - // Wait for either the plugin context to be cancelled or the wrapper's Close() to be called via the signal channel. + // Wait for context cancellation or connection close. select { case <-p.ctx.Done(): xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name) - // Context cancelled, ensure controller side is closed if HandleConn didn't close its side yet. p.cleanupControllerConn(xl) return case <-currentCloseSignal: - xl.Infof("Detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name) - // HandleConn closed the plugin side (pluginNotifyConn). The closeFn was called, closing currentCloseSignal. - // We still need to close the controller side. + xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name) + // HandleConn closed the plugin side. Close the controller side. p.cleanupControllerConn(xl) - // Add a delay before attempting to reconnect, respecting context cancellation. - xl.Infof("Waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name) + xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name) select { case <-time.After(reconnectDelay): - // Delay completed, loop will continue. case <-p.ctx.Done(): xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name) - return // Exit loop if context is cancelled during wait + return } - // Loop will continue to reconnect. } - // Loop will restart, context check at the beginning of the loop is sufficient. - xl.Infof("Re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name) + xl.Infof("re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name) } } @@ -187,46 +162,31 @@ func (p *VirtualNetPlugin) cleanupControllerConn(xl *xlog.Logger) { p.mu.Lock() defer p.mu.Unlock() if p.controllerConn != nil { - xl.Debugf("Cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name) + xl.Debugf("cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name) p.controllerConn.Close() p.controllerConn = nil } - // Also clear the closeSignal reference for the completed/cancelled connection attempt p.closeSignal = nil } -// cleanupPipePair closes both ends of a pipe, used typically when registration fails. -func (p *VirtualNetPlugin) cleanupPipePair(xl *xlog.Logger, controllerConn, pluginConn net.Conn) { - xl.Debugf("Cleaning up pipe pair for visitor [%s] after registration failure", p.pluginCtx.Name) - controllerConn.Close() - pluginConn.Close() - p.mu.Lock() - p.controllerConn = nil // Ensure field is nil if it was briefly set - p.closeSignal = nil // Ensure field is nil if it was briefly set - p.mu.Unlock() -} - // Close initiates the plugin shutdown. func (p *VirtualNetPlugin) Close() error { - xl := xlog.FromContextSafe(p.pluginCtx.Ctx) // Use base context for close logging - xl.Infof("Closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name) + xl := xlog.FromContextSafe(p.pluginCtx.Ctx) + xl.Infof("closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name) - // 1. Signal the run loop goroutine to stop via context cancellation. + // Signal the run loop goroutine to stop. p.cancel() - // 2. Unregister the route from the controller. - // This might implicitly cause the VnetController to close its end of the pipe (controllerConn). + // Unregister the route from the controller. if p.pluginCtx.VnetController != nil { p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name) - xl.Infof("Unregistered client route for visitor [%s]", p.pluginCtx.Name) - } else { - xl.Warnf("VnetController is nil during close for visitor [%s], cannot unregister route", p.pluginCtx.Name) + xl.Infof("unregistered client route for visitor [%s]", p.pluginCtx.Name) } - // 3. Explicitly close the controller side of the pipe managed by this plugin. + // 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. p.cleanupControllerConn(xl) - xl.Infof("Finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) + xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) return nil } diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index bc458a5c48a..46ebc3c7d7f 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -162,7 +162,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) { func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig { vr, ok := rp.getVhost(domain, location, routeByHTTPUser) if ok { - log.Debugf("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) + log.Debugf("get new http request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) return vr.payload.(*RouteConfig) } return nil diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 75b472b6ee4..66dd577d8ec 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -275,7 +275,7 @@ func (l *Listener) Accept() (net.Conn, error) { xl := xlog.FromContextSafe(l.ctx) conn, ok := <-l.accept if !ok { - return nil, fmt.Errorf("Listener closed") + return nil, fmt.Errorf("listener closed") } // if rewriteHost func is exist diff --git a/pkg/vnet/controller.go b/pkg/vnet/controller.go index d43147a3076..ca71a8c3abd 100644 --- a/pkg/vnet/controller.go +++ b/pkg/vnet/controller.go @@ -87,7 +87,7 @@ func (c *Controller) handlePacket(buf []byte) { case waterutil.IsIPv4(buf): header, err := ipv4.ParseHeader(buf) if err != nil { - log.Warnf("parse ipv4 header error:", err) + log.Warnf("parse ipv4 header error: %v", err) return } src = header.Src @@ -98,7 +98,7 @@ func (c *Controller) handlePacket(buf []byte) { case waterutil.IsIPv6(buf): header, err := ipv6.ParseHeader(buf) if err != nil { - log.Warnf("parse ipv6 header error:", err) + log.Warnf("parse ipv6 header error: %v", err) return } src = header.Src @@ -137,6 +137,12 @@ func (c *Controller) Stop() error { // Client connection read loop func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) { xl := xlog.FromContextSafe(ctx) + defer func() { + // Remove the route when read loop ends (connection closed) + c.clientRouter.removeConnRoute(conn) + conn.Close() + }() + for { data, err := ReadMessage(conn) if err != nil { @@ -181,8 +187,18 @@ func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser } // Server connection read loop -func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser) { +func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser, onClose func()) { xl := xlog.FromContextSafe(ctx) + defer func() { + // Clean up all IP mappings associated with this connection when it closes + c.serverRouter.cleanupConnIPs(conn) + // Call the provided callback upon closure + if onClose != nil { + onClose() + } + conn.Close() + }() + for { data, err := ReadMessage(conn) if err != nil { @@ -220,27 +236,11 @@ func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser } } -// RegisterClientRoute Register client route (based on destination IP CIDR) -func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) error { - if err := c.clientRouter.addRoute(name, routes, conn); err != nil { - return err - } +// RegisterClientRoute registers a client route (based on destination IP CIDR) +// and starts the read loop +func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) { + c.clientRouter.addRoute(name, routes, conn) go c.readLoopClient(ctx, conn) - return nil -} - -// RegisterServerConn Register server connection (dynamically associates with source IPs) -func (c *Controller) RegisterServerConn(ctx context.Context, name string, conn io.ReadWriteCloser) error { - if err := c.serverRouter.addConn(name, conn); err != nil { - return err - } - go c.readLoopServer(ctx, conn) - return nil -} - -// UnregisterServerConn Remove server connection from routing table -func (c *Controller) UnregisterServerConn(name string) { - c.serverRouter.delConn(name) } // UnregisterClientRoute Remove client route from routing table @@ -248,6 +248,12 @@ func (c *Controller) UnregisterClientRoute(name string) { c.clientRouter.delRoute(name) } +// StartServerConnReadLoop starts the read loop for a server connection +// (dynamically associates with source IPs) +func (c *Controller) StartServerConnReadLoop(ctx context.Context, conn io.ReadWriteCloser, onClose func()) { + go c.readLoopServer(ctx, conn, onClose) +} + // ParseRoutes Convert route strings to IPNet objects func ParseRoutes(routeStrings []string) ([]net.IPNet, error) { routes := make([]net.IPNet, 0, len(routeStrings)) @@ -273,7 +279,7 @@ func newClientRouter() *clientRouter { } } -func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) error { +func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) { r.mu.Lock() defer r.mu.Unlock() r.routes[name] = &routeElement{ @@ -281,7 +287,6 @@ func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWri routes: routes, conn: conn, } - return nil } func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) { @@ -303,32 +308,29 @@ func (r *clientRouter) delRoute(name string) { delete(r.routes, name) } -// Server router (based on source IP routing) +func (r *clientRouter) removeConnRoute(conn io.Writer) { + r.mu.Lock() + defer r.mu.Unlock() + for name, re := range r.routes { + if re.conn == conn { + delete(r.routes, name) + return + } + } +} + +// Server router (based solely on source IP routing) type serverRouter struct { - namedConns map[string]io.ReadWriteCloser // Name to connection mapping - srcIPConns map[string]io.Writer // Source IP string to connection mapping + srcIPConns map[string]io.Writer // Source IP string to connection mapping mu sync.RWMutex } func newServerRouter() *serverRouter { return &serverRouter{ - namedConns: make(map[string]io.ReadWriteCloser), srcIPConns: make(map[string]io.Writer), } } -func (r *serverRouter) addConn(name string, conn io.ReadWriteCloser) error { - r.mu.Lock() - original, ok := r.namedConns[name] - r.namedConns[name] = conn - r.mu.Unlock() - if ok { - // Close the original connection if it exists - _ = original.Close() - } - return nil -} - func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) { r.mu.RLock() defer r.mu.RUnlock() @@ -340,17 +342,41 @@ func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) { } func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) { + key := src.String() + + r.mu.RLock() + existingConn, ok := r.srcIPConns[key] + r.mu.RUnlock() + + // If the entry exists and the connection is the same, no need to do anything. + if ok && existingConn == conn { + return + } + + // Acquire write lock to update the map. r.mu.Lock() defer r.mu.Unlock() - r.srcIPConns[src.String()] = conn + + // Double-check after acquiring the write lock to handle potential race conditions. + existingConn, ok = r.srcIPConns[key] + if ok && existingConn == conn { + return + } + + r.srcIPConns[key] = conn } -func (r *serverRouter) delConn(name string) { +// cleanupConnIPs removes all IP mappings associated with the specified connection +func (r *serverRouter) cleanupConnIPs(conn io.Writer) { r.mu.Lock() defer r.mu.Unlock() - delete(r.namedConns, name) - // Note: We don't delete mappings from srcIPConns because we don't know which source IPs are associated with this connection - // This might cause dangling references, but they will be overwritten on new connections or restart + + // Find and delete all IP mappings pointing to this connection + for ip, mappedConn := range r.srcIPConns { + if mappedConn == conn { + delete(r.srcIPConns, ip) + } + } } type routeElement struct { diff --git a/pkg/vnet/message.go b/pkg/vnet/message.go index 002b090ab4a..68ac7704c12 100644 --- a/pkg/vnet/message.go +++ b/pkg/vnet/message.go @@ -33,7 +33,7 @@ func ReadMessage(r io.Reader) ([]byte, error) { var length uint32 err := binary.Read(r, binary.LittleEndian, &length) if err != nil { - return nil, fmt.Errorf("read message length error: %v", err) + return nil, fmt.Errorf("read message length error: %w", err) } // Check length to prevent DoS @@ -48,7 +48,7 @@ func ReadMessage(r io.Reader) ([]byte, error) { data := make([]byte, length) _, err = io.ReadFull(r, data) if err != nil { - return nil, fmt.Errorf("read message data error: %v", err) + return nil, fmt.Errorf("read message data error: %w", err) } return data, nil @@ -68,13 +68,13 @@ func WriteMessage(w io.Writer, data []byte) error { // Write length err := binary.Write(w, binary.LittleEndian, length) if err != nil { - return fmt.Errorf("write message length error: %v", err) + return fmt.Errorf("write message length error: %w", err) } // Write message data _, err = w.Write(data) if err != nil { - return fmt.Errorf("write message data error: %v", err) + return fmt.Errorf("write message data error: %w", err) } return nil diff --git a/pkg/vnet/tun.go b/pkg/vnet/tun.go index bafc639271d..d26314d053e 100644 --- a/pkg/vnet/tun.go +++ b/pkg/vnet/tun.go @@ -23,7 +23,8 @@ import ( ) const ( - offset = 16 + offset = 16 + defaultPacketSize = 1420 ) type TunDevice interface { @@ -35,20 +36,45 @@ func OpenTun(ctx context.Context, addr string) (TunDevice, error) { if err != nil { return nil, err } - return &tunDeviceWrapper{dev: td}, nil + + mtu, err := td.MTU() + if err != nil { + mtu = defaultPacketSize + } + + bufferSize := max(mtu, defaultPacketSize) + batchSize := td.BatchSize() + + device := &tunDeviceWrapper{ + dev: td, + bufferSize: bufferSize, + readBuffers: make([][]byte, batchSize), + sizeBuffer: make([]int, batchSize), + } + + for i := range device.readBuffers { + device.readBuffers[i] = make([]byte, offset+bufferSize) + } + + return device, nil } type tunDeviceWrapper struct { - dev tun.Device + dev tun.Device + bufferSize int + readBuffers [][]byte + packetBuffers [][]byte + sizeBuffer []int } func (d *tunDeviceWrapper) Read(p []byte) (int, error) { - buf := pool.GetBuf(len(p) + offset) - defer pool.PutBuf(buf) - - sz := make([]int, 1) + if len(d.packetBuffers) > 0 { + n := copy(p, d.packetBuffers[0]) + d.packetBuffers = d.packetBuffers[1:] + return n, nil + } - n, err := d.dev.Read([][]byte{buf}, sz, offset) + n, err := d.dev.Read(d.readBuffers, d.sizeBuffer, offset) if err != nil { return 0, err } @@ -56,20 +82,26 @@ func (d *tunDeviceWrapper) Read(p []byte) (int, error) { return 0, io.EOF } - dataSize := sz[0] - if dataSize > len(p) { - dataSize = len(p) + for i := range n { + if d.sizeBuffer[i] <= 0 { + continue + } + d.packetBuffers = append(d.packetBuffers, d.readBuffers[i][offset:offset+d.sizeBuffer[i]]) } - copy(p, buf[offset:offset+dataSize]) + + dataSize := copy(p, d.packetBuffers[0]) + d.packetBuffers = d.packetBuffers[1:] + return dataSize, nil } func (d *tunDeviceWrapper) Write(p []byte) (int, error) { - buf := pool.GetBuf(len(p) + offset) + buf := pool.GetBuf(offset + d.bufferSize) defer pool.PutBuf(buf) - copy(buf[offset:], p) - return d.dev.Write([][]byte{buf}, offset) + n := copy(buf[offset:], p) + _, err := d.dev.Write([][]byte{buf[:offset+n]}, offset) + return n, err } func (d *tunDeviceWrapper) Close() error { diff --git a/pkg/vnet/tun_linux.go b/pkg/vnet/tun_linux.go index 7c9c684cc97..2e0cc56b234 100644 --- a/pkg/vnet/tun_linux.go +++ b/pkg/vnet/tun_linux.go @@ -16,35 +16,44 @@ package vnet import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" "net" + "strconv" + "strings" "github.com/vishvananda/netlink" "golang.zx2c4.com/wireguard/tun" ) const ( - defaultTunName = "utun" - defaultMTU = 1420 + baseTunName = "utun" + defaultMTU = 1420 ) func openTun(_ context.Context, addr string) (tun.Device, error) { - dev, err := tun.CreateTUN(defaultTunName, defaultMTU) + name, err := findNextTunName(baseTunName) if err != nil { - return nil, err + name = getFallbackTunName(baseTunName, addr) + } + + tunDevice, err := tun.CreateTUN(name, defaultMTU) + if err != nil { + return nil, fmt.Errorf("failed to create TUN device '%s': %w", name, err) } - name, err := dev.Name() + actualName, err := tunDevice.Name() if err != nil { return nil, err } - ifn, err := net.InterfaceByName(name) + ifn, err := net.InterfaceByName(actualName) if err != nil { return nil, err } - link, err := netlink.LinkByName(name) + link, err := netlink.LinkByName(actualName) if err != nil { return nil, err } @@ -69,7 +78,34 @@ func openTun(_ context.Context, addr string) (tun.Device, error) { if err = addRoutes(ifn, cidr); err != nil { return nil, err } - return dev, nil + return tunDevice, nil +} + +func findNextTunName(basename string) (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("failed to get network interfaces: %w", err) + } + maxSuffix := -1 + + for _, iface := range interfaces { + name := iface.Name + if strings.HasPrefix(name, basename) { + suffix := name[len(basename):] + if suffix == "" { + continue + } + + numSuffix, err := strconv.Atoi(suffix) + if err == nil && numSuffix > maxSuffix { + maxSuffix = numSuffix + } + } + } + + nextSuffix := maxSuffix + 1 + name := fmt.Sprintf("%s%d", basename, nextSuffix) + return name, nil } func addRoutes(ifn *net.Interface, cidr *net.IPNet) error { @@ -82,3 +118,14 @@ func addRoutes(ifn *net.Interface, cidr *net.IPNet) error { } return nil } + +// getFallbackTunName generates a deterministic fallback TUN device name +// based on the base name and the provided address string using a hash. +func getFallbackTunName(baseName, addr string) string { + hasher := sha256.New() + hasher.Write([]byte(addr)) + hashBytes := hasher.Sum(nil) + // Use first 4 bytes -> 8 hex chars for brevity, respecting IFNAMSIZ limit. + shortHash := hex.EncodeToString(hashBytes[:4]) + return fmt.Sprintf("%s%s", baseName, shortHash) +} diff --git a/server/control.go b/server/control.go index 0b6b31741b0..b70d8d1268a 100644 --- a/server/control.go +++ b/server/control.go @@ -224,7 +224,7 @@ func (ctl *Control) Close() error { func (ctl *Control) Replaced(newCtl *Control) { xl := ctl.xl - xl.Infof("Replaced by client [%s]", newCtl.runID) + xl.Infof("replaced by client [%s]", newCtl.runID) ctl.runID = "" ctl.conn.Close() } diff --git a/server/dashboard_api.go b/server/dashboard_api.go index a29433a6235..54e5d9e97a8 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -97,14 +97,14 @@ func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) { func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} defer func() { - log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code) + log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) } }() - log.Infof("Http request: [%s]", r.URL.Path) + log.Infof("http request: [%s]", r.URL.Path) serverStats := mem.StatsCollector.GetServer() svrResp := serverInfoResp{ Version: version.Full(), @@ -218,13 +218,13 @@ func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) { proxyType := params["type"] defer func() { - log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code) + log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) } }() - log.Infof("Http request: [%s]", r.URL.Path) + log.Infof("http request: [%s]", r.URL.Path) proxyInfoResp := GetProxyInfoResp{} proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType) @@ -290,13 +290,13 @@ func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request name := params["name"] defer func() { - log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code) + log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) } }() - log.Infof("Http request: [%s]", r.URL.Path) + log.Infof("http request: [%s]", r.URL.Path) var proxyStatsResp GetProxyStatsResp proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name) @@ -358,13 +358,13 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) { name := params["name"] defer func() { - log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code) + log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) } }() - log.Infof("Http request: [%s]", r.URL.Path) + log.Infof("http request: [%s]", r.URL.Path) trafficResp := GetProxyTrafficResp{} trafficResp.Name = name @@ -386,9 +386,9 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) { func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} - log.Infof("Http request: [%s]", r.URL.Path) + log.Infof("http request: [%s]", r.URL.Path) defer func() { - log.Infof("Http response [%s]: code [%d]", r.URL.Path, res.Code) + log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) w.WriteHeader(res.Code) if len(res.Msg) > 0 { _, _ = w.Write([]byte(res.Msg)) diff --git a/server/service.go b/server/service.go index d1dd68a1c90..b9abaa80099 100644 --- a/server/service.go +++ b/server/service.go @@ -427,7 +427,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna _ = conn.SetReadDeadline(time.Now().Add(connReadTimeout)) if rawMsg, err = msg.ReadMsg(conn); err != nil { - log.Tracef("Failed to read message: %v", err) + log.Tracef("failed to read message: %v", err) conn.Close() return } @@ -475,7 +475,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna }) } default: - log.Warnf("Error message type for the new connection [%s]", conn.RemoteAddr().String()) + log.Warnf("error message type for the new connection [%s]", conn.RemoteAddr().String()) conn.Close() } } @@ -488,7 +488,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { for { c, err := l.Accept() if err != nil { - log.Warnf("Listener for incoming connections from client closed") + log.Warnf("listener for incoming connections from client closed") return } // inject xlog object into net.Conn context @@ -504,7 +504,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { var isTLS, custom bool c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout) if err != nil { - log.Warnf("CheckAndEnableTLSServerConnWithTimeout error: %v", err) + log.Warnf("checkAndEnableTLSServerConnWithTimeout error: %v", err) originConn.Close() continue } @@ -520,7 +520,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Server(frpConn, fmuxCfg) if err != nil { - log.Warnf("Failed to create mux connection: %v", err) + log.Warnf("failed to create mux connection: %v", err) frpConn.Close() return } @@ -528,7 +528,7 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { for { stream, err := session.AcceptStream() if err != nil { - log.Debugf("Accept new mux stream error: %v", err) + log.Debugf("accept new mux stream error: %v", err) session.Close() return } @@ -546,7 +546,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) { for { c, err := l.Accept(context.Background()) if err != nil { - log.Warnf("QUICListener for incoming connections from client closed") + log.Warnf("quic listener for incoming connections from client closed") return } // Start a new goroutine to handle connection. @@ -554,7 +554,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) { for { stream, err := frpConn.AcceptStream(context.Background()) if err != nil { - log.Debugf("Accept new quic mux stream error: %v", err) + log.Debugf("accept new quic mux stream error: %v", err) _ = frpConn.CloseWithError(0, "") return } @@ -620,7 +620,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) xl := netpkg.NewLogFromConn(workConn) ctl, exist := svr.ctlManager.GetByID(newMsg.RunID) if !exist { - xl.Warnf("No client control found for run id [%s]", newMsg.RunID) + xl.Warnf("no client control found for run id [%s]", newMsg.RunID) return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID) } // server plugin hook diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index 994ac59deef..ee00e1031e2 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -38,7 +38,7 @@ func RunE2ETests(t *testing.T) { // Randomize specs as well as suites suiteConfig.RandomizeAllSpecs = true - log.Infof("Starting e2e run %q on Ginkgo node %d of total %d", + log.Infof("starting e2e run %q on Ginkgo node %d of total %d", framework.RunID, suiteConfig.ParallelProcess, suiteConfig.ParallelTotal) ginkgo.RunSpecs(t, "frp e2e suite", suiteConfig, reporterConfig) } diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go index f56fc973a9f..599ff11b26a 100644 --- a/test/e2e/framework/request.go +++ b/test/e2e/framework/request.go @@ -20,7 +20,7 @@ func ExpectResponseCode(code int) EnsureFunc { if resp.Code == code { return true } - flog.Warnf("Expect code %d, but got %d", code, resp.Code) + flog.Warnf("expect code %d, but got %d", code, resp.Code) return false } } @@ -111,14 +111,14 @@ func (e *RequestExpect) Ensure(fns ...EnsureFunc) { if len(fns) == 0 { if !bytes.Equal(e.expectResp, ret.Content) { - flog.Tracef("Response info: %+v", ret) + flog.Tracef("response info: %+v", ret) } ExpectEqualValuesWithOffset(1, string(ret.Content), string(e.expectResp), e.explain...) } else { for _, fn := range fns { ok := fn(ret) if !ok { - flog.Tracef("Response info: %+v", ret) + flog.Tracef("response info: %+v", ret) } ExpectTrueWithOffset(1, ok, e.explain...) } diff --git a/test/e2e/legacy/features/real_ip.go b/test/e2e/legacy/features/real_ip.go index f74c62d2d68..a79afb45b1d 100644 --- a/test/e2e/legacy/features/real_ip.go +++ b/test/e2e/legacy/features/real_ip.go @@ -93,7 +93,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { f.RunProcesses([]string{serverConf}, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { - log.Tracef("ProxyProtocol get SourceAddr: %s", string(resp.Content)) + log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) addr, err := net.ResolveTCPAddr("tcp", string(resp.Content)) if err != nil { return false @@ -142,7 +142,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { r.HTTP().HTTPHost("normal.example.com") }).Ensure(framework.ExpectResponseCode(404)) - log.Tracef("ProxyProtocol get SourceAddr: %s", srcAddrRecord) + log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord) addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord) framework.ExpectNoError(err, srcAddrRecord) framework.ExpectEqualValues("127.0.0.1", addr.IP.String()) diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go index 94508f7da9a..216f531de92 100644 --- a/test/e2e/v1/features/real_ip.go +++ b/test/e2e/v1/features/real_ip.go @@ -215,7 +215,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { f.RunProcesses([]string{serverConf}, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { - log.Tracef("ProxyProtocol get SourceAddr: %s", string(resp.Content)) + log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) addr, err := net.ResolveTCPAddr("tcp", string(resp.Content)) if err != nil { return false @@ -265,7 +265,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { r.HTTP().HTTPHost("normal.example.com") }).Ensure(framework.ExpectResponseCode(404)) - log.Tracef("ProxyProtocol get SourceAddr: %s", srcAddrRecord) + log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord) addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord) framework.ExpectNoError(err, srcAddrRecord) framework.ExpectEqualValues("127.0.0.1", addr.IP.String()) From b41d8f8e4074c4f633fb67d7d31b97db59472674 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 27 Apr 2025 15:41:13 +0800 Subject: [PATCH 40/83] update release notes (#4772) --- Release.md | 9 ++------- pkg/util/version/version.go | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Release.md b/Release.md index b3edf41090c..94cba964084 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,3 @@ -### Notes +### Bug Fixes -* **Feature Gates Introduced:** This version introduces a new experimental mechanism called Feature Gates. This allows users to enable or disable specific experimental features before they become generally available. Feature gates can be configured in the `featureGates` map within the configuration file. -* **VirtualNet Feature Gate:** The first available feature gate is `VirtualNet`, which enables the experimental Virtual Network functionality (currently in Alpha stage). - -### Features - -* **Virtual Network (VirtualNet):** Introduce experimental virtual network capabilities (Alpha). This allows creating a TUN device managed by frp, enabling Layer 3 connectivity between different clients within the frp network. Requires root/admin privileges and is currently supported on Linux and macOS. Configuration is done via the `virtualNet` section and the `virtual_net` plugin. Enable with feature gate `VirtualNet`. **Note: As an Alpha feature, configuration details may change in future releases.** \ No newline at end of file +* **VirtualNet:** Resolved various issues related to connection handling, TUN device management, and stability in the virtual network feature. \ No newline at end of file diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index e74233c7083..8fa6bc193ed 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.62.0" +var version = "0.62.1" func Full() string { return version From c99986fa28bf12b4e46e32d599ddac98b0f87eb0 Mon Sep 17 00:00:00 2001 From: CrynTox <88785107+CrynTox@users.noreply.github.com> Date: Tue, 6 May 2025 07:11:20 +0300 Subject: [PATCH 41/83] build: add x64 openbsd (#4780) Co-authored-by: CrynTox <> --- Makefile.cross-compiles | 2 +- package.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index b75792a798c..d084bbef44a 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -2,7 +2,7 @@ export PATH := $(PATH):`go env GOPATH`/bin export GO111MODULE=on LDFLAGS := -s -w -os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64 +os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64 all: build diff --git a/package.sh b/package.sh index 4699f59977d..16dfcee03ea 100755 --- a/package.sh +++ b/package.sh @@ -17,7 +17,7 @@ make -f ./Makefile.cross-compiles rm -rf ./release/packages mkdir -p ./release/packages -os_all='linux windows darwin freebsd android' +os_all='linux windows darwin freebsd openbsd android' arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64' extra_all='_ hf' From 077ba80ba35d8ad257f99bbb819fb4cbeaa9e1ba Mon Sep 17 00:00:00 2001 From: scientificworld <30764166+scientificworld@users.noreply.github.com> Date: Mon, 19 May 2025 11:39:35 +0800 Subject: [PATCH 42/83] fix: type error in server_plugin doc (#4799) --- doc/server_plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/server_plugin.md b/doc/server_plugin.md index b8fa161907e..6ddef827470 100644 --- a/doc/server_plugin.md +++ b/doc/server_plugin.md @@ -121,7 +121,7 @@ Create new proxy // http and https only "custom_domains": [], "subdomain": , - "locations": , + "locations": [], "http_user": , "http_pwd": , "host_header_rewrite": , From 8eb525a64853aa15fb2bd21865ccaad3092baf29 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 23 May 2025 19:25:34 +0800 Subject: [PATCH 43/83] feat: support YAML merge in strict configuration mode (#4809) --- Release.md | 4 +- pkg/config/load.go | 34 ++++++++++- pkg/config/load_test.go | 119 ++++++++++++++++++++++++++++++++++++ pkg/util/version/version.go | 2 +- 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/Release.md b/Release.md index 94cba964084..07c58d4a736 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,3 @@ -### Bug Fixes +## Features -* **VirtualNet:** Resolved various issues related to connection handling, TUN device management, and stability in the virtual network feature. \ No newline at end of file +* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter. \ No newline at end of file diff --git a/pkg/config/load.go b/pkg/config/load.go index fa394dda115..bb050b406b5 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -111,6 +111,33 @@ func LoadConfigureFromFile(path string, c any, strict bool) error { return LoadConfigure(content, c, strict) } +// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling +// This function handles both cases efficiently: with or without dot fields +func parseYAMLWithDotFieldsHandling(content []byte, target any) error { + var temp any + if err := yaml.Unmarshal(content, &temp); err != nil { + return err + } + + // Remove dot fields if it's a map + if tempMap, ok := temp.(map[string]any); ok { + for key := range tempMap { + if strings.HasPrefix(key, ".") { + delete(tempMap, key) + } + } + } + + // Convert to JSON and decode with strict validation + jsonBytes, err := json.Marshal(temp) + if err != nil { + return err + } + decoder := json.NewDecoder(bytes.NewReader(jsonBytes)) + decoder.DisallowUnknownFields() + return decoder.Decode(target) +} + // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. func LoadConfigure(b []byte, c any, strict bool) error { @@ -134,10 +161,13 @@ func LoadConfigure(b []byte, c any, strict bool) error { } return decoder.Decode(c) } - // It wasn't JSON. Unmarshal as YAML. + + // Handle YAML content if strict { - return yaml.UnmarshalStrict(b, c) + // In strict mode, always use our custom handler to support YAML merge + return parseYAMLWithDotFieldsHandling(b, c) } + // Non-strict mode, parse normally return yaml.Unmarshal(b, c) } diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index 980a332ad1b..95d6101e0ba 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -187,3 +187,122 @@ unixPath = "/tmp/uds.sock" err = LoadConfigure([]byte(pluginStr), &clientCfg, true) require.Error(err) } + +// TestYAMLMergeInStrictMode tests that YAML merge functionality works +// even in strict mode by properly handling dot-prefixed fields +func TestYAMLMergeInStrictMode(t *testing.T) { + require := require.New(t) + + yamlContent := ` +serverAddr: "127.0.0.1" +serverPort: 7000 + +.common: &common + type: stcp + secretKey: "test-secret" + localIP: 127.0.0.1 + transport: + useEncryption: true + useCompression: true + +proxies: +- name: ssh + localPort: 22 + <<: *common +- name: web + localPort: 80 + <<: *common +` + + clientCfg := v1.ClientConfig{} + // This should work in strict mode + err := LoadConfigure([]byte(yamlContent), &clientCfg, true) + require.NoError(err) + + // Verify the merge worked correctly + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) + require.Len(clientCfg.Proxies, 2) + + // Check first proxy + sshProxy := clientCfg.Proxies[0].ProxyConfigurer + require.Equal("ssh", sshProxy.GetBaseConfig().Name) + require.Equal("stcp", sshProxy.GetBaseConfig().Type) + + // Check second proxy + webProxy := clientCfg.Proxies[1].ProxyConfigurer + require.Equal("web", webProxy.GetBaseConfig().Name) + require.Equal("stcp", webProxy.GetBaseConfig().Type) +} + +// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing +func TestOptimizedYAMLProcessing(t *testing.T) { + require := require.New(t) + + yamlWithDotFields := []byte(` +serverAddr: "127.0.0.1" +.common: &common + type: stcp +proxies: +- name: test + <<: *common +`) + + yamlWithoutDotFields := []byte(` +serverAddr: "127.0.0.1" +proxies: +- name: test + type: tcp + localPort: 22 +`) + + // Test that YAML without dot fields works in strict mode + clientCfg := v1.ClientConfig{} + err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + + // Test that YAML with dot fields still works in strict mode + err = LoadConfigure(yamlWithDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type) +} + +// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types +func TestYAMLEdgeCases(t *testing.T) { + require := require.New(t) + + // Test array at root (should fail for frp config) + arrayYAML := []byte(` +- item1 +- item2 +`) + clientCfg := v1.ClientConfig{} + err := LoadConfigure(arrayYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test scalar at root (should fail for frp config) + scalarYAML := []byte(`"just a string"`) + err = LoadConfigure(scalarYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test empty object (should work) + emptyYAML := []byte(`{}`) + err = LoadConfigure(emptyYAML, &clientCfg, true) + require.NoError(err) + + // Test nested structure without dots (should work) + nestedYAML := []byte(` +serverAddr: "127.0.0.1" +serverPort: 7000 +`) + err = LoadConfigure(nestedYAML, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) +} diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 8fa6bc193ed..966a942fe62 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.62.1" +var version = "0.63.0" func Full() string { return version From 3fa76b72f3e6b2b9d8b0093ea755d5d1eb0d6b92 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 23 May 2025 21:39:47 +0800 Subject: [PATCH 44/83] add proxy protocol support for UDP proxies (#4810) --- README.md | 2 +- Release.md | 3 +- client/proxy/proxy.go | 24 +--- client/proxy/sudp.go | 2 +- client/proxy/udp.go | 4 +- pkg/proto/udp/udp.go | 16 ++- pkg/util/net/proxyprotocol.go | 45 ++++++++ pkg/util/net/proxyprotocol_test.go | 178 +++++++++++++++++++++++++++++ test/e2e/v1/features/real_ip.go | 50 ++++++++ 9 files changed, 299 insertions(+), 25 deletions(-) create mode 100644 pkg/util/net/proxyprotocol.go create mode 100644 pkg/util/net/proxyprotocol_test.go diff --git a/README.md b/README.md index 25537220e94..f0ab4273096 100644 --- a/README.md +++ b/README.md @@ -1025,7 +1025,7 @@ You can get user's real IP from HTTP request headers `X-Forwarded-For`. #### Proxy Protocol -frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP. +frp supports Proxy Protocol to send user's real IP to local services. Here is an example for https service: diff --git a/Release.md b/Release.md index 07c58d4a736..19b79d6478b 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,4 @@ ## Features -* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter. \ No newline at end of file +* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter. +* Support for proxy protocol in UDP proxies to preserve real client IP addresses. \ No newline at end of file diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index debda9fa2ff..876ca579d38 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -20,13 +20,11 @@ import ( "net" "reflect" "strconv" - "strings" "sync" "time" libio "github.com/fatedier/golib/io" libnet "github.com/fatedier/golib/net" - pp "github.com/pires/go-proxyproto" "golang.org/x/time/rate" "github.com/fatedier/frp/pkg/config/types" @@ -35,6 +33,7 @@ import ( plugin "github.com/fatedier/frp/pkg/plugin/client" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/limit" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/vnet" ) @@ -176,24 +175,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { - h := &pp.Header{ - Command: pp.PROXY, - SourceAddr: connInfo.SrcAddr, - DestinationAddr: connInfo.DstAddr, - } - - if strings.Contains(m.SrcAddr, ".") { - h.TransportProtocol = pp.TCPv4 - } else { - h.TransportProtocol = pp.TCPv6 - } - - if baseCfg.Transport.ProxyProtocolVersion == "v1" { - h.Version = 1 - } else if baseCfg.Transport.ProxyProtocolVersion == "v2" { - h.Version = 2 - } - connInfo.ProxyProtocolHeader = h + // Use the common proxy protocol builder function + header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion) + connInfo.ProxyProtocolHeader = header } connInfo.Conn = remote connInfo.UnderlyingConn = workConn diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index ad9db89a940..13741d0ddc7 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -205,5 +205,5 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { go workConnReaderFn(workConn, readCh) go heartbeatFn(sendCh) - udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize)) + udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion) } diff --git a/client/proxy/udp.go b/client/proxy/udp.go index b08fe1608ad..b70ffe4a0c8 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -171,5 +171,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { go workConnSenderFn(pxy.workConn, pxy.sendCh) go workConnReaderFn(pxy.workConn, pxy.readCh) go heartbeatFn(pxy.sendCh) - udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize)) + + // Call Forwarder with proxy protocol version (empty string means no proxy protocol) + udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion) } diff --git a/pkg/proto/udp/udp.go b/pkg/proto/udp/udp.go index 7a11984b2e4..f97b3b439cd 100644 --- a/pkg/proto/udp/udp.go +++ b/pkg/proto/udp/udp.go @@ -24,6 +24,7 @@ import ( "github.com/fatedier/golib/pool" "github.com/fatedier/frp/pkg/msg" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket { @@ -69,7 +70,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh } } -func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) { +func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) { var mu sync.RWMutex udpConnMap := make(map[string]*net.UDPConn) @@ -110,6 +111,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- if err != nil { continue } + mu.Lock() udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()] if !ok { @@ -122,6 +124,18 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- } mu.Unlock() + // Add proxy protocol header if configured + if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil { + ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion) + if err == nil { + // Prepend proxy protocol header to the UDP payload + finalBuf := make([]byte, len(ppBuf)+len(buf)) + copy(finalBuf, ppBuf) + copy(finalBuf[len(ppBuf):], buf) + buf = finalBuf + } + } + _, err = udpConn.Write(buf) if err != nil { udpConn.Close() diff --git a/pkg/util/net/proxyprotocol.go b/pkg/util/net/proxyprotocol.go new file mode 100644 index 00000000000..5f0cd51fab8 --- /dev/null +++ b/pkg/util/net/proxyprotocol.go @@ -0,0 +1,45 @@ +// 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 net + +import ( + "bytes" + "fmt" + "net" + + pp "github.com/pires/go-proxyproto" +) + +func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header { + var versionByte byte + if version == "v1" { + versionByte = 1 + } else { + versionByte = 2 // default to v2 + } + return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr) +} + +func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) { + h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version) + + // Convert header to bytes using a buffer + var buf bytes.Buffer + _, err := h.WriteTo(&buf) + if err != nil { + return nil, fmt.Errorf("failed to write proxy protocol header: %v", err) + } + return buf.Bytes(), nil +} diff --git a/pkg/util/net/proxyprotocol_test.go b/pkg/util/net/proxyprotocol_test.go new file mode 100644 index 00000000000..187801f67aa --- /dev/null +++ b/pkg/util/net/proxyprotocol_test.go @@ -0,0 +1,178 @@ +package net + +import ( + "net" + "testing" + + pp "github.com/pires/go-proxyproto" + "github.com/stretchr/testify/require" +) + +func TestBuildProxyProtocolHeader(t *testing.T) { + require := require.New(t) + + tests := []struct { + name string + srcAddr net.Addr + dstAddr net.Addr + version string + expectError bool + }{ + { + name: "UDP IPv4 v2", + srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "TCP IPv4 v1", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + version: "v1", + expectError: false, + }, + { + name: "UDP IPv6 v2", + srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "TCP IPv6 v1", + srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + version: "v1", + expectError: false, + }, + { + name: "nil source address", + srcAddr: nil, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "nil destination address", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: nil, + version: "v2", + expectError: false, + }, + { + name: "unsupported address type", + srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + } + + for _, tt := range tests { + header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version) + + if tt.expectError { + require.Error(err, "test case: %s", tt.name) + continue + } + + require.NoError(err, "test case: %s", tt.name) + require.NotEmpty(header, "test case: %s", tt.name) + } +} + +func TestBuildProxyProtocolHeaderStruct(t *testing.T) { + require := require.New(t) + + tests := []struct { + name string + srcAddr net.Addr + dstAddr net.Addr + version string + expectedProtocol pp.AddressFamilyAndProtocol + expectedVersion byte + expectedCommand pp.ProtocolVersionAndCommand + expectedSourceAddr net.Addr + expectedDestAddr net.Addr + }{ + { + name: "TCP IPv4 v2", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + version: "v2", + expectedProtocol: pp.TCPv4, + expectedVersion: 2, + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + }, + { + name: "UDP IPv6 v1", + srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + version: "v1", + expectedProtocol: pp.UDPv6, + expectedVersion: 1, + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + }, + { + name: "TCP IPv6 default version", + srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + version: "", + expectedProtocol: pp.TCPv6, + expectedVersion: 2, // default to v2 + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + }, + { + name: "nil source address", + srcAddr: nil, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil + expectedDestAddr: nil, + }, + { + name: "nil destination address", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: nil, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil + expectedDestAddr: nil, + }, + { + name: "unsupported address type", + srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types + expectedDestAddr: nil, + }, + } + + for _, tt := range tests { + header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version) + + require.NotNil(header, "test case: %s", tt.name) + + require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name) + require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name) + require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name) + require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name) + require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name) + } +} diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go index 216f531de92..a52cf0a2fbe 100644 --- a/test/e2e/v1/features/real_ip.go +++ b/test/e2e/v1/features/real_ip.go @@ -227,6 +227,56 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { }) }) + ginkgo.It("UDP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.UDP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Errorf("read proxy protocol error: %v", err) + return + } + + // Read the actual UDP content after proxy protocol header + if _, err := rpc.ReadBytes(rd); err != nil { + return + } + + buf := []byte(ppHeader.SourceAddr.String()) + _, _ = rpc.WriteBytes(c, buf) + })) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp" + type = "udp" + localPort = %d + remotePort = %d + transport.proxyProtocolVersion = "v2" + `, localPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Protocol("udp").Port(remotePort).Ensure(func(resp *request.Response) bool { + log.Tracef("udp proxy protocol get SourceAddr: %s", string(resp.Content)) + addr, err := net.ResolveUDPAddr("udp", string(resp.Content)) + if err != nil { + return false + } + if addr.IP.String() != "127.0.0.1" { + return false + } + return true + }) + }) + ginkgo.It("HTTP", func() { vhostHTTPPort := f.AllocPort() serverConf := consts.DefaultServerConfig + fmt.Sprintf(` From 720c09c06b31b44295a90dab9428a910e3c1b222 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 26 May 2025 14:54:03 +0800 Subject: [PATCH 45/83] update test package (#4814) --- .github/workflows/stale.yml | 2 +- pkg/proto/udp/udp_test.go | 8 +++---- pkg/util/metric/counter_test.go | 12 +++++----- pkg/util/metric/date_counter_test.go | 18 +++++++------- pkg/util/util/util_test.go | 36 +++++++++++++--------------- 5 files changed, 36 insertions(+), 40 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 784053e8f96..8f10d6415d8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -name: "Close stale issues" +name: "Close stale issues and PRs" on: schedule: - cron: "20 0 * * *" diff --git a/pkg/proto/udp/udp_test.go b/pkg/proto/udp/udp_test.go index 0e61d9e65a5..1a7f009117c 100644 --- a/pkg/proto/udp/udp_test.go +++ b/pkg/proto/udp/udp_test.go @@ -3,16 +3,16 @@ package udp import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUdpPacket(t *testing.T) { - assert := assert.New(t) + require := require.New(t) buf := []byte("hello world") udpMsg := NewUDPPacket(buf, nil, nil) newBuf, err := GetContent(udpMsg) - assert.NoError(err) - assert.EqualValues(buf, newBuf) + require.NoError(err) + require.EqualValues(buf, newBuf) } diff --git a/pkg/util/metric/counter_test.go b/pkg/util/metric/counter_test.go index 4925c25b523..dca72052ed3 100644 --- a/pkg/util/metric/counter_test.go +++ b/pkg/util/metric/counter_test.go @@ -3,21 +3,21 @@ package metric import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCounter(t *testing.T) { - assert := assert.New(t) + require := require.New(t) c := NewCounter() c.Inc(10) - assert.EqualValues(10, c.Count()) + require.EqualValues(10, c.Count()) c.Dec(5) - assert.EqualValues(5, c.Count()) + require.EqualValues(5, c.Count()) cTmp := c.Snapshot() - assert.EqualValues(5, cTmp.Count()) + require.EqualValues(5, cTmp.Count()) c.Clear() - assert.EqualValues(0, c.Count()) + require.EqualValues(0, c.Count()) } diff --git a/pkg/util/metric/date_counter_test.go b/pkg/util/metric/date_counter_test.go index c9997c70fe7..8752f1985a8 100644 --- a/pkg/util/metric/date_counter_test.go +++ b/pkg/util/metric/date_counter_test.go @@ -3,25 +3,25 @@ package metric import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDateCounter(t *testing.T) { - assert := assert.New(t) + require := require.New(t) dc := NewDateCounter(3) dc.Inc(10) - assert.EqualValues(10, dc.TodayCount()) + require.EqualValues(10, dc.TodayCount()) dc.Dec(5) - assert.EqualValues(5, dc.TodayCount()) + require.EqualValues(5, dc.TodayCount()) counts := dc.GetLastDaysCount(3) - assert.EqualValues(3, len(counts)) - assert.EqualValues(5, counts[0]) - assert.EqualValues(0, counts[1]) - assert.EqualValues(0, counts[2]) + require.EqualValues(3, len(counts)) + require.EqualValues(5, counts[0]) + require.EqualValues(0, counts[1]) + require.EqualValues(0, counts[2]) dcTmp := dc.Snapshot() - assert.EqualValues(5, dcTmp.TodayCount()) + require.EqualValues(5, dcTmp.TodayCount()) } diff --git a/pkg/util/util/util_test.go b/pkg/util/util/util_test.go index 0061861119e..0a63ba6d086 100644 --- a/pkg/util/util/util_test.go +++ b/pkg/util/util/util_test.go @@ -3,45 +3,41 @@ package util import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRandId(t *testing.T) { - assert := assert.New(t) + require := require.New(t) id, err := RandID() - assert.NoError(err) + require.NoError(err) t.Log(id) - assert.Equal(16, len(id)) + require.Equal(16, len(id)) } func TestGetAuthKey(t *testing.T) { - assert := assert.New(t) + require := require.New(t) key := GetAuthKey("1234", 1488720000) - assert.Equal("6df41a43725f0c770fd56379e12acf8c", key) + require.Equal("6df41a43725f0c770fd56379e12acf8c", key) } func TestParseRangeNumbers(t *testing.T) { - assert := assert.New(t) + require := require.New(t) numbers, err := ParseRangeNumbers("2-5") - if assert.NoError(err) { - assert.Equal([]int64{2, 3, 4, 5}, numbers) - } + require.NoError(err) + require.Equal([]int64{2, 3, 4, 5}, numbers) numbers, err = ParseRangeNumbers("1") - if assert.NoError(err) { - assert.Equal([]int64{1}, numbers) - } + require.NoError(err) + require.Equal([]int64{1}, numbers) numbers, err = ParseRangeNumbers("3-5,8") - if assert.NoError(err) { - assert.Equal([]int64{3, 4, 5, 8}, numbers) - } + require.NoError(err) + require.Equal([]int64{3, 4, 5, 8}, numbers) numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ") - if assert.NoError(err) { - assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers) - } + require.NoError(err) + require.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers) _, err = ParseRangeNumbers("3-a") - assert.Error(err) + require.Error(err) } From 43cf1688e483bc210f334b6e4c664fea0e3324d8 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 27 May 2025 16:46:15 +0800 Subject: [PATCH 46/83] update golangci-lint version (#4817) --- .github/workflows/golangci-lint.yml | 10 +- .golangci.yml | 212 +++++++++++-------------- client/service.go | 7 +- go.mod | 16 +- go.sum | 32 ++-- hack/run-e2e.sh | 6 +- pkg/config/legacy/client.go | 7 +- pkg/config/legacy/conversion.go | 44 ++--- pkg/config/legacy/proxy.go | 2 +- pkg/config/v1/proxy.go | 2 +- pkg/metrics/mem/server.go | 2 +- pkg/util/net/conn.go | 2 +- pkg/util/vhost/http.go | 6 +- pkg/util/vhost/vhost.go | 6 +- test/e2e/legacy/basic/client_server.go | 8 +- test/e2e/legacy/plugin/server.go | 4 +- test/e2e/v1/basic/client_server.go | 8 +- test/e2e/v1/plugin/server.go | 4 +- 18 files changed, 170 insertions(+), 208 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8058592853a..40aba1ce409 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,10 +20,10 @@ jobs: go-version: '1.23' cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v8 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.61 + version: v2.1 # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 @@ -34,9 +34,3 @@ jobs: # Optional: if set to true then the all caching functionality will be complete disabled, # takes precedence over all other caching options. # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true diff --git a/.golangci.yml b/.golangci.yml index a45e4ba3a2d..be0a717e74c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,139 +1,111 @@ -service: - golangci-lint-version: 1.61.x # use the fixed version to not introduce new linters unexpectedly - +version: "2" run: concurrency: 4 - # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 20m build-tags: - integ - integfuzz - linters: - disable-all: true + default: none enable: - - unused - - errcheck + - asciicheck - copyloopvar + - errcheck - gocritic - - gofumpt - - goimports - - revive - - gosimple + - gosec - govet - ineffassign - lll + - makezero - misspell + - prealloc + - predeclared + - revive - staticcheck - - stylecheck - - typecheck - unconvert - unparam + - unused + settings: + errcheck: + check-type-assertions: false + check-blank: false + gocritic: + disabled-checks: + - exitAfterDefer + gosec: + excludes: + - G401 + - G402 + - G404 + - G501 + - G115 + severity: low + confidence: low + govet: + disable: + - shadow + lll: + line-length: 160 + tab-width: 1 + misspell: + locale: US + ignore-rules: + - cancelled + - marshalled + unparam: + check-exported: false + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - errcheck + - maligned + path: _test\.go$|^tests/|^samples/ + - linters: + - revive + - staticcheck + text: use underscores in Go names + - linters: + - revive + text: unused-parameter + - linters: + - unparam + text: is always false + paths: + - .*\.pb\.go + - .*\.gen\.go + - genfiles$ + - vendor$ + - bin$ + - third_party$ + - builtin$ + - examples$ +formatters: + enable: - gci - - gosec - - asciicheck - - prealloc - - predeclared - - makezero - fast: false - -linters-settings: - errcheck: - # report about not checking of errors in type assetions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - govet: - # report about shadowed variables - disable: - - shadow - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - ignore-words: - - cancelled - - marshalled - lll: - # max line length, lines longer will be reported. Default is 120. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option - line-length: 160 - # tab width in spaces. Default to 1. - tab-width: 1 - gocritic: - disabled-checks: - - exitAfterDefer - unused: - check-exported: false - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - gci: - sections: - - standard - - default - - prefix(github.com/fatedier/frp/) - gosec: - severity: "low" - confidence: "low" - excludes: - - G401 - - G402 - - G404 - - G501 - - G115 # integer overflow conversion - + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/fatedier/frp/) + exclusions: + generated: lax + paths: + - .*\.pb\.go + - .*\.gen\.go + - genfiles$ + - vendor$ + - bin$ + - third_party$ + - builtin$ + - examples$ issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - # exclude: - # - composite literal uses unkeyed fields - - exclude-rules: - # 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" - - linters: - - unparam - text: "is always false" - - exclude-dirs: - - genfiles$ - - vendor$ - - bin$ - exclude-files: - - ".*\\.pb\\.go" - - ".*\\.gen\\.go" - - # Independently from option `exclude` we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute `golangci-lint run --help`. - # Default value for this option is true. - exclude-use-default: true - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. - max-per-linter: 0 - - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-issues-per-linter: 0 max-same-issues: 0 diff --git a/client/service.go b/client/service.go index e163cac4554..d6a12970c6c 100644 --- a/client/service.go +++ b/client/service.go @@ -325,10 +325,9 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE proxyCfgs := svr.proxyCfgs visitorCfgs := svr.visitorCfgs svr.cfgMu.RUnlock() - connEncrypted := true - if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" { - connEncrypted = false - } + + connEncrypted := svr.clientSpec == nil || svr.clientSpec.Type != "ssh-tunnel" + sessionCtx := &SessionContext{ Common: svr.common, RunID: svr.runID, diff --git a/go.mod b/go.mod index 0b8ee2a5e8a..e3bdc711cc0 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/yamux v0.1.1 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.34.2 + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.36.3 github.com/pelletier/go-toml/v2 v2.2.0 github.com/pion/stun/v2 v2.0.0 github.com/pires/go-proxyproto v0.7.0 @@ -46,12 +46,11 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20241206021119-61a79c692802 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/transport/v2 v2.2.1 // indirect @@ -67,14 +66,15 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect 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/exp v0.0.0-20241204233417-43b7b7cde48d // indirect - golang.org/x/mod v0.22.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.28.0 // indirect + golang.org/x/tools v0.31.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/protobuf v1.34.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect diff --git a/go.sum b/go.sum index a39d174edae..a65c303320c 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -50,10 +49,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20241206021119-61a79c692802 h1:US08AXzP0bLurpzFUV3Poa9ZijrRdd1zAIOVtoHEiS8= -github.com/google/pprof v0.0.0-20241206021119-61a79c692802/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -72,10 +72,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= +github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -94,6 +94,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -152,6 +154,8 @@ github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= 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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -170,8 +174,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx 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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +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/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= @@ -239,8 +243,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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +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/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= @@ -261,8 +265,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/hack/run-e2e.sh b/hack/run-e2e.sh index 8009359c14a..d1b20b527f3 100755 --- a/hack/run-e2e.sh +++ b/hack/run-e2e.sh @@ -3,10 +3,10 @@ SCRIPT=$(readlink -f "$0") ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) -ginkgo_command=$(which ginkgo 2>/dev/null) -if [ -z "$ginkgo_command" ]; then +# Check if ginkgo is available +if ! command -v ginkgo >/dev/null 2>&1; then echo "ginkgo not found, try to install..." - go install github.com/onsi/ginkgo/v2/ginkgo@v2.17.1 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 fi debug=false diff --git a/pkg/config/legacy/client.go b/pkg/config/legacy/client.go index 0d677d9cc56..8cc02614798 100644 --- a/pkg/config/legacy/client.go +++ b/pkg/config/legacy/client.go @@ -194,7 +194,7 @@ func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) { } common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_") - common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") + common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") return common, nil } @@ -229,10 +229,7 @@ func LoadAllProxyConfsFromIni( startProxy[s] = struct{}{} } - startAll := true - if len(startProxy) > 0 { - startAll = false - } + startAll := len(startProxy) == 0 // Build template sections from range section And append to ini.File. rangeSections := make([]*ini.Section, 0) diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go index dd8c4a11d91..4ae54f88ecb 100644 --- a/pkg/config/legacy/conversion.go +++ b/pkg/config/legacy/conversion.go @@ -26,20 +26,20 @@ import ( func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig { out := &v1.ClientCommonConfig{} out.User = conf.User - out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod) - out.Auth.Token = conf.ClientConfig.Token - if conf.ClientConfig.AuthenticateHeartBeats { + out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod) + out.Auth.Token = conf.Token + if conf.AuthenticateHeartBeats { out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) } - if conf.ClientConfig.AuthenticateNewWorkConns { + if conf.AuthenticateNewWorkConns { out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) } - out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID - out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret - out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience - out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope - out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL - out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams + out.Auth.OIDC.ClientID = conf.OidcClientID + out.Auth.OIDC.ClientSecret = conf.OidcClientSecret + out.Auth.OIDC.Audience = conf.OidcAudience + out.Auth.OIDC.Scope = conf.OidcScope + out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL + out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams out.ServerAddr = conf.ServerAddr out.ServerPort = conf.ServerPort @@ -59,10 +59,10 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf 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.Transport.TLS.CertFile = conf.TLSCertFile + out.Transport.TLS.KeyFile = conf.TLSKeyFile + out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile + out.Transport.TLS.ServerName = conf.TLSServerName out.Log.To = conf.LogFile out.Log.Level = conf.LogLevel @@ -87,18 +87,18 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { out := &v1.ServerConfig{} - out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod) - out.Auth.Token = conf.ServerConfig.Token - if conf.ServerConfig.AuthenticateHeartBeats { + out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod) + out.Auth.Token = conf.Token + if conf.AuthenticateHeartBeats { out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) } - if conf.ServerConfig.AuthenticateNewWorkConns { + if conf.AuthenticateNewWorkConns { out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) } - out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience - out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer - out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck - out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck + out.Auth.OIDC.Audience = conf.OidcAudience + out.Auth.OIDC.Issuer = conf.OidcIssuer + out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck + out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck out.BindAddr = conf.BindAddr out.BindPort = conf.BindPort diff --git a/pkg/config/legacy/proxy.go b/pkg/config/legacy/proxy.go index e6653ee9787..0c461a1a63f 100644 --- a/pkg/config/legacy/proxy.go +++ b/pkg/config/legacy/proxy.go @@ -206,7 +206,7 @@ func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section) } // plugin_xxx - cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") + cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") return nil } diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index d53d05e3cbd..34bd712592c 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -129,7 +129,7 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) { c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) if c.Plugin.ClientPluginOptions != nil { - c.Plugin.ClientPluginOptions.Complete() + c.Plugin.Complete() } } diff --git a/pkg/metrics/mem/server.go b/pkg/metrics/mem/server.go index d92546c4c58..70cfc1c1486 100644 --- a/pkg/metrics/mem/server.go +++ b/pkg/metrics/mem/server.go @@ -109,7 +109,7 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) { m.info.ProxyTypeCounts[proxyType] = counter proxyStats, ok := m.info.ProxyStatistics[name] - if !(ok && proxyStats.ProxyType == proxyType) { + if !ok || proxyStats.ProxyType != proxyType { proxyStats = &ProxyStatistics{ Name: name, ProxyType: proxyType, diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index 20ac73ed1c6..ff7d1c37606 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -223,7 +223,7 @@ func (conn *wrapQuicStream) RemoteAddr() net.Addr { } func (conn *wrapQuicStream) Close() error { - conn.Stream.CancelRead(0) + conn.CancelRead(0) return conn.Stream.Close() } diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 46ebc3c7d7f..05ec174bff0 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -225,11 +225,7 @@ func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) ( // *.example.com // *.com domainSplit := strings.Split(domain, ".") - for { - if len(domainSplit) < 3 { - break - } - + for len(domainSplit) >= 3 { domainSplit[0] = "*" domain = strings.Join(domainSplit, ".") vr, ok = findRouter(domain, location, routeByHTTPUser) diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 66dd577d8ec..007751d7f3e 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -169,11 +169,7 @@ func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) { } domainSplit := strings.Split(name, ".") - for { - if len(domainSplit) < 3 { - break - } - + for len(domainSplit) >= 3 { domainSplit[0] = "*" name = strings.Join(domainSplit, ".") diff --git a/test/e2e/legacy/basic/client_server.go b/test/e2e/legacy/basic/client_server.go index 03bca0c54e2..d85b5accfa3 100644 --- a/test/e2e/legacy/basic/client_server.go +++ b/test/e2e/legacy/basic/client_server.go @@ -24,12 +24,14 @@ type generalTestConfigures struct { } func renderBindPortConfig(protocol string) string { - if protocol == "kcp" { + switch protocol { + case "kcp": return fmt.Sprintf(`kcp_bind_port = {{ .%s }}`, consts.PortServerName) - } else if protocol == "quic" { + case "quic": return fmt.Sprintf(`quic_bind_port = {{ .%s }}`, consts.PortServerName) + default: + return "" } - return "" } func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { diff --git a/test/e2e/legacy/plugin/server.go b/test/e2e/legacy/plugin/server.go index cf600be2ee0..9120b7d77a1 100644 --- a/test/e2e/legacy/plugin/server.go +++ b/test/e2e/legacy/plugin/server.go @@ -223,7 +223,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.PingContent) - record = content.Ping.PrivilegeKey + record = content.PrivilegeKey ret.Unchange = true return &ret } @@ -273,7 +273,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.NewWorkConnContent) - record = content.NewWorkConn.RunID + record = content.RunID ret.Unchange = true return &ret } diff --git a/test/e2e/v1/basic/client_server.go b/test/e2e/v1/basic/client_server.go index 16270781855..85dd1227dd7 100644 --- a/test/e2e/v1/basic/client_server.go +++ b/test/e2e/v1/basic/client_server.go @@ -24,12 +24,14 @@ type generalTestConfigures struct { } func renderBindPortConfig(protocol string) string { - if protocol == "kcp" { + switch protocol { + case "kcp": return fmt.Sprintf(`kcpBindPort = {{ .%s }}`, consts.PortServerName) - } else if protocol == "quic" { + case "quic": return fmt.Sprintf(`quicBindPort = {{ .%s }}`, consts.PortServerName) + default: + return "" } - return "" } func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go index b043c57f13f..6637650d46a 100644 --- a/test/e2e/v1/plugin/server.go +++ b/test/e2e/v1/plugin/server.go @@ -232,7 +232,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.PingContent) - record = content.Ping.PrivilegeKey + record = content.PrivilegeKey ret.Unchange = true return &ret } @@ -284,7 +284,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.NewWorkConnContent) - record = content.NewWorkConn.RunID + record = content.RunID ret.Unchange = true return &ret } From c777891f7589e3491f6e5c375b91fd637680aa04 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 25 Jun 2025 11:16:48 +0800 Subject: [PATCH 47/83] update .golangci.yml (#4848) --- .gitignore | 3 +++ .golangci.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 0f69b089a2b..c6480f59a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ client.key # Cache *.swp + +# AI +CLAUDE.md diff --git a/.golangci.yml b/.golangci.yml index be0a717e74c..09848bc7211 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,7 @@ version: "2" run: concurrency: 4 + timeout: 20m build-tags: - integ - integfuzz From 61330d4d794180c38d1f8ff7e9024b7f0f69d717 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 1 Jul 2025 18:56:46 +0800 Subject: [PATCH 48/83] Update quic-go dependency from v0.48.2 to v0.53.0 (#4862) - Update go.mod to use github.com/quic-go/quic-go v0.53.0 - Replace quic.Connection interface with *quic.Conn struct - Replace quic.Stream interface with *quic.Stream struct - Update all affected files to use new API: - pkg/util/net/conn.go: Update QuicStreamToNetConn function and wrapQuicStream struct - server/service.go: Update HandleQUICListener function parameter - client/visitor/xtcp.go: Update QUICTunnelSession struct field - client/connector.go: Update defaultConnectorImpl struct field Fixes #4852 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- client/connector.go | 2 +- client/visitor/xtcp.go | 2 +- go.mod | 3 +-- go.sum | 6 ++---- pkg/util/net/conn.go | 6 +++--- server/service.go | 2 +- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/client/connector.go b/client/connector.go index 64aa71c06e1..ab7c2fdda42 100644 --- a/client/connector.go +++ b/client/connector.go @@ -48,7 +48,7 @@ type defaultConnectorImpl struct { cfg *v1.ClientCommonConfig muxSession *fmux.Session - quicConn quic.Connection + quicConn *quic.Conn closeOnce sync.Once } diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 51f29ad2a94..99d25d8ac81 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -398,7 +398,7 @@ func (ks *KCPTunnelSession) Close() { } type QUICTunnelSession struct { - session quic.Connection + session *quic.Conn listenConn *net.UDPConn mu sync.RWMutex diff --git a/go.mod b/go.mod index e3bdc711cc0..46e753e2a14 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.48.2 + github.com/quic-go/quic-go v0.53.0 github.com/rodaine/table v1.2.0 github.com/samber/lo v1.47.0 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 @@ -68,7 +68,6 @@ require ( 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/exp v0.0.0-20241204233417-43b7b7cde48d // 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 diff --git a/go.sum b/go.sum index a65c303320c..bd044c39cbb 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.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= -github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +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/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= @@ -167,8 +167,6 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y 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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 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= diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index ff7d1c37606..20468f1b8c5 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -197,11 +197,11 @@ func (statsConn *StatsConn) Close() (err error) { } type wrapQuicStream struct { - quic.Stream - c quic.Connection + *quic.Stream + c *quic.Conn } -func QuicStreamToNetConn(s quic.Stream, c quic.Connection) net.Conn { +func QuicStreamToNetConn(s *quic.Stream, c *quic.Conn) net.Conn { return &wrapQuicStream{ Stream: s, c: c, diff --git a/server/service.go b/server/service.go index b9abaa80099..514afb518e3 100644 --- a/server/service.go +++ b/server/service.go @@ -550,7 +550,7 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) { return } // Start a new goroutine to handle connection. - go func(ctx context.Context, frpConn quic.Connection) { + go func(ctx context.Context, frpConn *quic.Conn) { for { stream, err := frpConn.AcceptStream(context.Background()) if err != nil { From f9065a6a78f91f31ca9522209194346755ac4d87 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 3 Jul 2025 13:17:21 +0800 Subject: [PATCH 49/83] add tokenSource support for auth configuration (#4865) --- README.md | 15 ++ Release.md | 3 +- client/service.go | 11 +- cmd/frpc/sub/nathole.go | 5 +- cmd/frpc/sub/proxy.go | 10 +- cmd/frps/root.go | 5 +- conf/frpc_full_example.toml | 5 + conf/frps_full_example.toml | 5 + pkg/config/load.go | 8 +- pkg/config/v1/client.go | 30 +++- pkg/config/v1/client_test.go | 72 ++++++++- pkg/config/v1/server.go | 25 ++- pkg/config/v1/server_test.go | 72 ++++++++- pkg/config/v1/validation/client.go | 12 ++ pkg/config/v1/validation/server.go | 12 ++ pkg/config/v1/value_source.go | 93 +++++++++++ pkg/config/v1/value_source_test.go | 246 +++++++++++++++++++++++++++++ pkg/ssh/server.go | 5 +- pkg/virtual/client.go | 4 +- test/e2e/v1/basic/token_source.go | 217 +++++++++++++++++++++++++ 20 files changed, 832 insertions(+), 23 deletions(-) create mode 100644 pkg/config/v1/value_source.go create mode 100644 pkg/config/v1/value_source_test.go create mode 100644 test/e2e/v1/basic/token_source.go diff --git a/README.md b/README.md index f0ab4273096..38bafab4891 100644 --- a/README.md +++ b/README.md @@ -612,6 +612,21 @@ When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token b Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation +##### Token Source + +frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported. + +**File-based token source:** + +```toml +# frpc.toml +auth.method = "token" +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "/path/to/token/file" +``` + +The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons. + #### OIDC Authentication When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. diff --git a/Release.md b/Release.md index 19b79d6478b..7ee50ea07c9 100644 --- a/Release.md +++ b/Release.md @@ -1,4 +1,3 @@ ## Features -* Support for YAML merge functionality (anchors and references with dot-prefixed fields) in strict configuration mode without requiring `--strict-config=false` parameter. -* Support for proxy protocol in UDP proxies to preserve real client IP addresses. \ No newline at end of file +* Support tokenSource for loading authentication tokens from files \ No newline at end of file diff --git a/client/service.go b/client/service.go index d6a12970c6c..337f8f2b8d0 100644 --- a/client/service.go +++ b/client/service.go @@ -88,13 +88,16 @@ type ServiceOptions struct { } // setServiceOptionsDefault sets the default values for ServiceOptions. -func setServiceOptionsDefault(options *ServiceOptions) { +func setServiceOptionsDefault(options *ServiceOptions) error { if options.Common != nil { - options.Common.Complete() + if err := options.Common.Complete(); err != nil { + return err + } } if options.ConnectorCreator == nil { options.ConnectorCreator = NewConnector } + return nil } // Service is the client service that connects to frps and provides proxy services. @@ -134,7 +137,9 @@ type Service struct { } func NewService(options ServiceOptions) (*Service, error) { - setServiceOptionsDefault(&options) + if err := setServiceOptionsDefault(&options); err != nil { + return nil, err + } var webServer *httppkg.Server if options.Common.WebServer.Port > 0 { diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index fb5b08078c2..a07d6852305 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -51,7 +51,10 @@ var natholeDiscoveryCmd = &cobra.Command{ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { cfg = &v1.ClientCommonConfig{} - cfg.Complete() + if err := cfg.Complete(); err != nil { + fmt.Printf("failed to complete config: %v\n", err) + os.Exit(1) + } } if natHoleSTUNServer != "" { cfg.NatHoleSTUNServer = natHoleSTUNServer diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index c5d76b1e3b3..67bd774f7a3 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -73,7 +73,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm Use: name, Short: fmt.Sprintf("Run frpc with a single %s proxy", name), Run: func(cmd *cobra.Command, args []string) { - clientCfg.Complete() + if err := clientCfg.Complete(); err != nil { + fmt.Println(err) + os.Exit(1) + } if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) @@ -99,7 +102,10 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client Use: "visitor", Short: fmt.Sprintf("Run frpc with a single %s visitor", name), Run: func(cmd *cobra.Command, args []string) { - clientCfg.Complete() + if err := clientCfg.Complete(); err != nil { + fmt.Println(err) + os.Exit(1) + } if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frps/root.go b/cmd/frps/root.go index fff487d1074..c1bfc880624 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -70,7 +70,10 @@ var rootCmd = &cobra.Command{ "please use yaml/json/toml format instead!\n") } } else { - serverCfg.Complete() + if err := serverCfg.Complete(); err != nil { + fmt.Printf("failed to complete server config: %v\n", err) + os.Exit(1) + } svrCfg = &serverCfg } diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 7d4838cd78f..d8d93a3fe25 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -32,6 +32,11 @@ auth.method = "token" # auth token auth.token = "12345678" +# alternatively, you can use tokenSource to load the token from a file +# this is mutually exclusive with auth.token +# auth.tokenSource.type = "file" +# auth.tokenSource.file.path = "/etc/frp/token" + # oidc.clientID specifies the client ID to use to get a token in OIDC authentication. # auth.oidc.clientID = "" # oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication. diff --git a/conf/frps_full_example.toml b/conf/frps_full_example.toml index a4fc2736a74..aba37435ffc 100644 --- a/conf/frps_full_example.toml +++ b/conf/frps_full_example.toml @@ -105,6 +105,11 @@ auth.method = "token" # auth token auth.token = "12345678" +# alternatively, you can use tokenSource to load the token from a file +# this is mutually exclusive with auth.token +# auth.tokenSource.type = "file" +# auth.tokenSource.file.path = "/etc/frp/token" + # oidc issuer specifies the issuer to verify OIDC tokens with. auth.oidc.issuer = "" # oidc audience specifies the audience OIDC tokens should contain when validated. diff --git a/pkg/config/load.go b/pkg/config/load.go index bb050b406b5..3852af9a886 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -212,7 +212,9 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) } } if svrCfg != nil { - svrCfg.Complete() + if err := svrCfg.Complete(); err != nil { + return nil, isLegacyFormat, err + } } return svrCfg, isLegacyFormat, nil } @@ -280,7 +282,9 @@ func LoadClientConfig(path string, strict bool) ( } if cliCfg != nil { - cliCfg.Complete() + if err := cliCfg.Complete(); err != nil { + return nil, nil, nil, isLegacyFormat, err + } } for _, c := range proxyCfgs { c.Complete(cliCfg.User) diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index d616fc0a300..a830df994a5 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -15,6 +15,8 @@ package v1 import ( + "context" + "fmt" "os" "github.com/samber/lo" @@ -77,18 +79,21 @@ type ClientCommonConfig struct { IncludeConfigFiles []string `json:"includes,omitempty"` } -func (c *ClientCommonConfig) Complete() { +func (c *ClientCommonConfig) Complete() error { 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.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478") - c.Auth.Complete() + if err := c.Auth.Complete(); err != nil { + return err + } c.Log.Complete() c.Transport.Complete() c.WebServer.Complete() c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) + return nil } type ClientTransportConfig struct { @@ -184,12 +189,27 @@ type AuthClientConfig 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 `json:"token,omitempty"` - OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` + Token string `json:"token,omitempty"` + // TokenSource specifies a dynamic source for the authorization token. + // This is mutually exclusive with Token field. + TokenSource *ValueSource `json:"tokenSource,omitempty"` + OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` } -func (c *AuthClientConfig) Complete() { +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 } type AuthOIDCClientConfig struct { diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go index 9ff7c287164..120c4fd42a3 100644 --- a/pkg/config/v1/client_test.go +++ b/pkg/config/v1/client_test.go @@ -15,6 +15,8 @@ package v1 import ( + "os" + "path/filepath" "testing" "github.com/samber/lo" @@ -24,7 +26,8 @@ import ( func TestClientConfigComplete(t *testing.T) { require := require.New(t) c := &ClientConfig{} - c.Complete() + err := c.Complete() + require.NoError(err) require.EqualValues("token", c.Auth.Method) require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) @@ -33,3 +36,70 @@ func TestClientConfigComplete(t *testing.T) { require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte)) require.NotEmpty(c.NatHoleSTUNServer) } + +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") + } + }) + } +} diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 3108cd34f40..54aac080bd0 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -15,6 +15,9 @@ package v1 import ( + "context" + "fmt" + "github.com/samber/lo" "github.com/fatedier/frp/pkg/config/types" @@ -98,8 +101,10 @@ type ServerConfig struct { HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"` } -func (c *ServerConfig) Complete() { - c.Auth.Complete() +func (c *ServerConfig) Complete() error { + if err := c.Auth.Complete(); err != nil { + return err + } c.Log.Complete() c.Transport.Complete() c.WebServer.Complete() @@ -120,17 +125,31 @@ func (c *ServerConfig) Complete() { c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24) + return nil } type AuthServerConfig struct { Method AuthMethod `json:"method,omitempty"` AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` Token string `json:"token,omitempty"` + TokenSource *ValueSource `json:"tokenSource,omitempty"` OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` } -func (c *AuthServerConfig) Complete() { +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 } type AuthOIDCServerConfig struct { diff --git a/pkg/config/v1/server_test.go b/pkg/config/v1/server_test.go index 3100fc4b12f..21d18fb7653 100644 --- a/pkg/config/v1/server_test.go +++ b/pkg/config/v1/server_test.go @@ -15,6 +15,8 @@ package v1 import ( + "os" + "path/filepath" "testing" "github.com/samber/lo" @@ -24,9 +26,77 @@ import ( func TestServerConfigComplete(t *testing.T) { require := require.New(t) c := &ServerConfig{} - c.Complete() + err := c.Complete() + require.NoError(err) require.EqualValues("token", c.Auth.Method) require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient)) } + +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") + } + }) + } +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index bae21fda40b..0c8575c990c 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -45,6 +45,18 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { 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 { + 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 { + errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) + } + } + if err := validateLogConfig(&c.Log); err != nil { errs = AppendError(errs, err) } diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go index cdb80ea3851..5694227299f 100644 --- a/pkg/config/v1/validation/server.go +++ b/pkg/config/v1/validation/server.go @@ -35,6 +35,18 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { 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 { + 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 { + errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) + } + } + if err := validateLogConfig(&c.Log); err != nil { errs = AppendError(errs, err) } diff --git a/pkg/config/v1/value_source.go b/pkg/config/v1/value_source.go new file mode 100644 index 00000000000..624a2658965 --- /dev/null +++ b/pkg/config/v1/value_source.go @@ -0,0 +1,93 @@ +// 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 v1 + +import ( + "context" + "errors" + "fmt" + "os" + "strings" +) + +// ValueSource provides a way to dynamically resolve configuration values +// from various sources like files, environment variables, or external services. +type ValueSource struct { + Type string `json:"type"` + File *FileSource `json:"file,omitempty"` +} + +// FileSource specifies how to load a value from a file. +type FileSource struct { + Path string `json:"path"` +} + +// Validate validates the ValueSource configuration. +func (v *ValueSource) Validate() error { + if v == nil { + return errors.New("valueSource cannot be nil") + } + + switch v.Type { + case "file": + if v.File == nil { + return errors.New("file configuration is required when type is 'file'") + } + return v.File.Validate() + default: + return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type) + } +} + +// Resolve resolves the value from the configured source. +func (v *ValueSource) Resolve(ctx context.Context) (string, error) { + if err := v.Validate(); err != nil { + return "", err + } + + switch v.Type { + case "file": + return v.File.Resolve(ctx) + default: + return "", fmt.Errorf("unsupported value source type: %s", v.Type) + } +} + +// Validate validates the FileSource configuration. +func (f *FileSource) Validate() error { + if f == nil { + return errors.New("fileSource cannot be nil") + } + + if f.Path == "" { + return errors.New("file path cannot be empty") + } + return nil +} + +// Resolve reads and returns the content from the specified file. +func (f *FileSource) Resolve(_ context.Context) (string, error) { + if err := f.Validate(); err != nil { + return "", err + } + + content, err := os.ReadFile(f.Path) + if err != nil { + return "", fmt.Errorf("failed to read file %s: %v", f.Path, err) + } + + // Trim whitespace, which is important for file-based tokens + return strings.TrimSpace(string(content)), nil +} diff --git a/pkg/config/v1/value_source_test.go b/pkg/config/v1/value_source_test.go new file mode 100644 index 00000000000..685151f44ad --- /dev/null +++ b/pkg/config/v1/value_source_test.go @@ -0,0 +1,246 @@ +// 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 v1 + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestValueSource_Validate(t *testing.T) { + tests := []struct { + name string + vs *ValueSource + wantErr bool + }{ + { + name: "nil valueSource", + vs: nil, + wantErr: true, + }, + { + name: "unsupported type", + vs: &ValueSource{ + Type: "unsupported", + }, + wantErr: true, + }, + { + name: "file type without file config", + vs: &ValueSource{ + Type: "file", + File: nil, + }, + wantErr: true, + }, + { + name: "valid file type with absolute path", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "/tmp/test", + }, + }, + wantErr: false, + }, + { + name: "valid file type with relative path", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "configs/token", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.vs.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFileSource_Validate(t *testing.T) { + tests := []struct { + name string + fs *FileSource + wantErr bool + }{ + { + name: "nil fileSource", + fs: nil, + wantErr: true, + }, + { + name: "empty path", + fs: &FileSource{ + Path: "", + }, + wantErr: true, + }, + { + name: "relative path (allowed)", + fs: &FileSource{ + Path: "relative/path", + }, + wantErr: false, + }, + { + name: "absolute path", + fs: &FileSource{ + Path: "/absolute/path", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.fs.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFileSource_Resolve(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test_token") + testContent := "test-token-value\n\t " + expectedContent := "test-token-value" + + err := os.WriteFile(testFile, []byte(testContent), 0o600) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + tests := []struct { + name string + fs *FileSource + want string + wantErr bool + }{ + { + name: "valid file path", + fs: &FileSource{ + Path: testFile, + }, + want: expectedContent, + wantErr: false, + }, + { + name: "non-existent file", + fs: &FileSource{ + Path: "/non/existent/file", + }, + want: "", + wantErr: true, + }, + { + name: "path traversal attempt (should fail validation)", + fs: &FileSource{ + Path: "../../../etc/passwd", + }, + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fs.Resolve(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValueSource_Resolve(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test_token") + testContent := "test-token-value" + + err := os.WriteFile(testFile, []byte(testContent), 0o600) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + tests := []struct { + name string + vs *ValueSource + want string + wantErr bool + }{ + { + name: "valid file type", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: testFile, + }, + }, + want: testContent, + wantErr: false, + }, + { + name: "unsupported type", + vs: &ValueSource{ + Type: "unsupported", + }, + want: "", + wantErr: true, + }, + { + name: "file type with path traversal", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "../../../etc/passwd", + }, + }, + want: "", + wantErr: true, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.vs.Resolve(ctx) + if (err != nil) != tt.wantErr { + t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go index 84b744fb9a4..378c609847b 100644 --- a/pkg/ssh/server.go +++ b/pkg/ssh/server.go @@ -105,7 +105,10 @@ func (s *TunnelServer) Run() error { s.writeToClient(err.Error()) return fmt.Errorf("parse flags from ssh client error: %v", err) } - clientCfg.Complete() + if err := clientCfg.Complete(); err != nil { + s.writeToClient(fmt.Sprintf("failed to complete client config: %v", err)) + return fmt.Errorf("complete client config error: %v", err) + } if sshConn.Permissions != nil { clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) } diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go index 96835a48c7f..8fec28c858d 100644 --- a/pkg/virtual/client.go +++ b/pkg/virtual/client.go @@ -37,7 +37,9 @@ type Client struct { func NewClient(options ClientOptions) (*Client, error) { if options.Common != nil { - options.Common.Complete() + if err := options.Common.Complete(); err != nil { + return nil, err + } } ln := netpkg.NewInternalListener() diff --git a/test/e2e/v1/basic/token_source.go b/test/e2e/v1/basic/token_source.go new file mode 100644 index 00000000000..95bb8dd4b0c --- /dev/null +++ b/test/e2e/v1/basic/token_source.go @@ -0,0 +1,217 @@ +// 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 basic + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: TokenSource]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("File-based token loading", func() { + ginkgo.It("should work with file tokenSource", func() { + // Create a temporary token file + tmpDir := f.TempDirectory + tokenFile := filepath.Join(tmpDir, "test_token") + tokenContent := "test-token-123" + + err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, tokenFile) + + // Client config with matching token + clientConf += fmt.Sprintf(` +auth.token = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, tokenContent, framework.TCPEchoServerPort, portName) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should work with client tokenSource", func() { + // Create a temporary token file + tmpDir := f.TempDirectory + tokenFile := filepath.Join(tmpDir, "client_token") + tokenContent := "client-token-456" + + err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with matching token + serverConf += fmt.Sprintf(` +auth.token = "%s" +`, tokenContent) + + // Client config with tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, tokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should work with both server and client tokenSource", func() { + // Create temporary token files + tmpDir := f.TempDirectory + serverTokenFile := filepath.Join(tmpDir, "server_token") + clientTokenFile := filepath.Join(tmpDir, "client_token") + tokenContent := "shared-token-789" + + err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, serverTokenFile) + + // Client config with tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, clientTokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should fail with mismatched tokens", func() { + // Create temporary token files with different content + tmpDir := f.TempDirectory + serverTokenFile := filepath.Join(tmpDir, "server_token") + clientTokenFile := filepath.Join(tmpDir, "client_token") + + err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600) + framework.ExpectNoError(err) + + err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, serverTokenFile) + + // Client config with different tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, clientTokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // This should fail due to token mismatch - the client should not be able to connect + // We expect the request to fail because the proxy tunnel is not established + framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure() + }) + + ginkgo.It("should fail with non-existent token file", func() { + // This test verifies that server fails to start when tokenSource points to non-existent file + // We'll verify this by checking that the configuration loading itself fails + + // Create a config that references a non-existent file + tmpDir := f.TempDirectory + nonExistentFile := filepath.Join(tmpDir, "non_existent_token") + + serverConf := consts.DefaultServerConfig + + // Server config with non-existent tokenSource file + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, nonExistentFile) + + // The test expectation is that this will fail during the RunProcesses call + // because the server cannot load the configuration due to missing token file + defer func() { + if r := recover(); r != nil { + // Expected: server should fail to start due to missing file + ginkgo.By(fmt.Sprintf("Server correctly failed to start: %v", r)) + } + }() + + // This should cause a panic or error during server startup + f.RunProcesses([]string{serverConf}, []string{}) + }) + }) +}) From c3bf952d8f49dfb23ef9dfc430cec1d9dab1c967 Mon Sep 17 00:00:00 2001 From: maguowei Date: Thu, 24 Jul 2025 10:16:44 +0800 Subject: [PATCH 50/83] fix webserver port not being released on frpc svr.Close() (#4896) --- client/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/service.go b/client/service.go index 337f8f2b8d0..9e1833b972c 100644 --- a/client/service.go +++ b/client/service.go @@ -403,6 +403,10 @@ func (svr *Service) stop() { svr.ctl.GracefulClose(svr.gracefulShutdownDuration) svr.ctl = nil } + if svr.webServer != nil { + svr.webServer.Close() + svr.webServer = nil + } } func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { From 7fe295f4f4f8edf7f5b1a8b7452a9f8dec4b85f0 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 25 Jul 2025 17:10:32 +0800 Subject: [PATCH 51/83] update golangci-lint version (#4897) --- .github/workflows/golangci-lint.yml | 12 +----------- .golangci.yml | 3 +++ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 40aba1ce409..d4faac70a7e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -23,14 +23,4 @@ jobs: uses: golangci/golangci-lint-action@v8 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v2.1 - - # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true + version: v2.3 diff --git a/.golangci.yml b/.golangci.yml index 09848bc7211..3ba2c60fe63 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,6 +73,9 @@ linters: - linters: - revive text: unused-parameter + - linters: + - revive + text: "avoid meaningless package names" - linters: - unparam text: is always false From e6dacf3a67c127c005f5e7416bf87ec887f47e15 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 28 Jul 2025 15:19:56 +0800 Subject: [PATCH 52/83] Fix SSH tunnel gateway binding address issue #4900 (#4902) - Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr - This caused external connections to fail when proxyBindAddr was set to 127.0.0.1 - SSH tunnel gateway now correctly binds to bindAddr for external accessibility - Update Release.md with bug fix description Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- Release.md | 6 +++++- server/service.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Release.md b/Release.md index 7ee50ea07c9..9e1ce466a8e 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,7 @@ ## Features -* Support tokenSource for loading authentication tokens from files \ No newline at end of file +* Support tokenSource for loading authentication tokens from files + +## Fixes + +* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1 diff --git a/server/service.go b/server/service.go index 514afb518e3..fad0e1432c8 100644 --- a/server/service.go +++ b/server/service.go @@ -262,7 +262,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) { } if cfg.SSHTunnelGateway.BindPort > 0 { - sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.sshTunnelListener) + sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.BindAddr, svr.sshTunnelListener) if err != nil { return nil, fmt.Errorf("create ssh gateway error: %v", err) } From dc3bc9182c403c897830c0366c720c691df75545 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 8 Aug 2025 22:28:17 +0800 Subject: [PATCH 53/83] update sponsor info (#4917) --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 38bafab4891..baf9c734cfd 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,36 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + +
+ Warp, the intelligent terminal +
+ Available for macOS, Linux and Windows +
+

+
+ The complete IDE crafted for professional Go developers

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

+
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy

From 024e4f5f1dd0a408f588ce53eecbc0851613f134 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 10 Aug 2025 22:59:28 +0800 Subject: [PATCH 54/83] improve random TLS certificate generation (#4923) --- Release.md | 4 ++-- pkg/transport/tls.go | 36 +++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Release.md b/Release.md index 9e1ce466a8e..5b2724d80dd 100644 --- a/Release.md +++ b/Release.md @@ -1,7 +1,7 @@ ## Features -* Support tokenSource for loading authentication tokens from files +* Support tokenSource for loading authentication tokens from files. ## Fixes -* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1 +* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1. diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index 5bc75921cbd..e8d2bf483a5 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -22,6 +22,7 @@ import ( "encoding/pem" "math/big" "os" + "time" ) func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { @@ -32,12 +33,30 @@ func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { return &tlsCert, nil } -func newRandomTLSKeyPair() *tls.Certificate { +func newRandomTLSKeyPair() (*tls.Certificate, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - panic(err) + return nil, err + } + + // Generate a random positive serial number with 128 bits of entropy. + // RFC 5280 requires serial numbers to be positive integers (not zero). + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + // Ensure serial number is positive (not zero) + if serialNumber.Sign() == 0 { + serialNumber = big.NewInt(1) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour * 10), } - template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate( rand.Reader, &template, @@ -45,16 +64,16 @@ func newRandomTLSKeyPair() *tls.Certificate { &key.PublicKey, key) if err != nil { - panic(err) + return nil, err } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { - panic(err) + return nil, err } - return &tlsCert + return &tlsCert, nil } // Only support one ca file to add @@ -76,7 +95,10 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { if certPath == "" || keyPath == "" { // server will generate tls conf by itself - cert := newRandomTLSKeyPair() + cert, err := newRandomTLSKeyPair() + if err != nil { + return nil, err + } base.Certificates = []tls.Certificate{*cert} } else { cert, err := newCustomTLSKeyPair(certPath, keyPath) From f795950742a9edb8174cf0cdf97e53eb49865914 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 10 Aug 2025 23:11:50 +0800 Subject: [PATCH 55/83] bump version to v0.64.0 (#4924) --- pkg/util/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 966a942fe62..c6497e145f6 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.63.0" +var version = "0.64.0" func Full() string { return version From 024c334d9dcf47ac4d69099c1ef658b6468d6b48 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 12 Aug 2025 01:48:26 +0800 Subject: [PATCH 56/83] Merge pull request #4928 from fatedier/xtcp improve context and polling logic in xtcp visitor --- client/visitor/xtcp.go | 49 ++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index 99d25d8ac81..cb374b68e08 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -145,7 +145,7 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { return case <-ticker.C: xl.Debugf("keepTunnelOpenWorker try to check tunnel...") - conn, err := sv.getTunnelConn() + conn, err := sv.getTunnelConn(sv.ctx) if err != nil { xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err) _ = sv.retryLimiter.Wait(sv.ctx) @@ -161,9 +161,9 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() { func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl := xlog.FromContextSafe(sv.ctx) - isConnTransfered := false + isConnTransferred := false defer func() { - if !isConnTransfered { + if !isConnTransferred { userConn.Close() } }() @@ -172,7 +172,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { // Open a tunnel connection to the server. If there is already a successful hole-punching connection, // it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout. - ctx := context.Background() + ctx := sv.ctx if sv.cfg.FallbackTo != "" { timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond) defer cancel() @@ -191,7 +191,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) return } - isConnTransfered = true + isConnTransferred = true return } @@ -219,40 +219,37 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { // openTunnel will open a tunnel connection to the target server. func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) { xl := xlog.FromContextSafe(sv.ctx) - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() - timeoutC := time.After(20 * time.Second) - immediateTrigger := make(chan struct{}, 1) - defer close(immediateTrigger) - immediateTrigger <- struct{}{} + timer := time.NewTimer(0) + defer timer.Stop() for { select { case <-sv.ctx.Done(): return nil, sv.ctx.Err() case <-ctx.Done(): + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, fmt.Errorf("open tunnel timeout") + } return nil, ctx.Err() - case <-immediateTrigger: - conn, err = sv.getTunnelConn() - case <-ticker.C: - conn, err = sv.getTunnelConn() - case <-timeoutC: - return nil, fmt.Errorf("open tunnel timeout") - } - - if err != nil { - if err != ErrNoTunnelSession { - xl.Warnf("get tunnel connection error: %v", err) + case <-timer.C: + conn, err = sv.getTunnelConn(ctx) + if err != nil { + if !errors.Is(err, ErrNoTunnelSession) { + xl.Warnf("get tunnel connection error: %v", err) + } + timer.Reset(500 * time.Millisecond) + continue } - continue + return conn, nil } - return conn, nil } } -func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) { - conn, err := sv.session.OpenConn(sv.ctx) +func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) { + conn, err := sv.session.OpenConn(ctx) if err == nil { return conn, nil } From 14253afe2f5deb66cddbd9a0aac37dd31ace7d29 Mon Sep 17 00:00:00 2001 From: immomo808 <4128219+immomo808@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:11:06 +0900 Subject: [PATCH 57/83] remove quotes (#4938) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baf9c734cfd..9a1db495ebc 100644 --- a/README.md +++ b/README.md @@ -519,7 +519,7 @@ name = "ssh" type = "tcp" localIP = "127.0.0.1" localPort = 22 -remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}" +remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }} ``` With the config above, variables can be passed into `frpc` program like this: From 80d3f332e184331172f94567b6f5f5606aa4fe18 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 25 Aug 2025 15:52:52 +0800 Subject: [PATCH 58/83] xtcp: add configuration to disable assisted addresses in NAT traversal (#4951) --- Release.md | 6 +----- client/proxy/xtcp.go | 10 +++++++++- client/visitor/xtcp.go | 10 +++++++++- conf/frpc_full_example.toml | 15 +++++++++++++++ pkg/config/v1/common.go | 8 ++++++++ pkg/config/v1/proxy.go | 3 +++ pkg/config/v1/visitor.go | 3 +++ pkg/nathole/nathole.go | 19 +++++++++++++++---- 8 files changed, 63 insertions(+), 11 deletions(-) diff --git a/Release.md b/Release.md index 5b2724d80dd..e0f5084e9ee 100644 --- a/Release.md +++ b/Release.md @@ -1,7 +1,3 @@ ## Features -* Support tokenSource for loading authentication tokens from files. - -## Fixes - -* Fix SSH tunnel gateway incorrectly binding to proxyBindAddr instead of bindAddr, which caused external connections to fail when proxyBindAddr was set to 127.0.0.1. +* Add NAT traversal configuration options for XTCP proxies and visitors. Support disabling assisted addresses to avoid using slow VPN connections during NAT hole punching. diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 31f9ac89734..6e1deac39d3 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -64,11 +64,19 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}) + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts) if err != nil { xl.Warnf("nathole prepare error: %v", err) return } + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) defer prepareResult.ListenConn.Close() diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index cb374b68e08..353577db659 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -276,11 +276,19 @@ func (sv *XTCPVisitor) makeNatHole() { } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}) + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts) if err != nil { xl.Warnf("nathole prepare error: %v", err) return } + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index d8d93a3fe25..dfae95734d3 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -372,6 +372,14 @@ localPort = 22 # Otherwise, visitors from same user can connect. '*' means allow all users. allowUsers = ["user1", "user2"] +# NAT traversal configuration (optional) +[proxies.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# This can improve performance when you have slow VPN connections. +# Default: false +disableAssistedAddrs = false + [[proxies]] name = "vnet-server" type = "stcp" @@ -411,6 +419,13 @@ minRetryInterval = 90 # fallbackTo = "stcp_visitor" # fallbackTimeoutMs = 500 +# NAT traversal configuration (optional) +[visitors.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# Default: false +disableAssistedAddrs = false + [[visitors]] name = "vnet-visitor" type = "stcp" diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index ddb23356823..38965a3cbcc 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -96,6 +96,14 @@ type TLSConfig struct { ServerName string `json:"serverName,omitempty"` } +// NatTraversalConfig defines configuration options for NAT traversal +type NatTraversalConfig struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal. When enabled, + // only STUN-discovered public addresses will be used. + DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"` +} + type LogConfig struct { // This is destination where frp should write the logs. // If "console" is used, logs will be printed to stdout, otherwise, diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 34bd712592c..37701b6d785 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -422,6 +422,9 @@ type XTCPProxyConfig struct { Secretkey string `json:"secretKey,omitempty"` AllowUsers []string `json:"allowUsers,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` } func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index f00391c34e4..7629875a6f2 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -160,6 +160,9 @@ type XTCPVisitorConfig struct { MinRetryInterval int `json:"minRetryInterval,omitempty"` FallbackTo string `json:"fallbackTo,omitempty"` FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` } func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index bdd0ee83cb2..72522fac0ed 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -68,6 +68,13 @@ var ( DetectRoleReceiver = "receiver" ) +// PrepareOptions defines options for NAT traversal preparation +type PrepareOptions struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal + DisableAssistedAddrs bool +} + type PrepareResult struct { Addrs []string AssistedAddrs []string @@ -108,7 +115,7 @@ func PreCheck( } // Prepare is used to do some preparation work before penetration. -func Prepare(stunServers []string) (*PrepareResult, error) { +func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) { // discover for Nat type addrs, localAddr, err := Discover(stunServers, "") if err != nil { @@ -133,9 +140,13 @@ func Prepare(stunServers []string) (*PrepareResult, error) { return nil, fmt.Errorf("listen local udp addr error: %v", err) } - assistedAddrs := make([]string, 0, len(localIPs)) - for _, ip := range localIPs { - assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) + // Apply NAT traversal options + var assistedAddrs []string + if !opts.DisableAssistedAddrs { + assistedAddrs = make([]string, 0, len(localIPs)) + for _, ip := range localIPs { + assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) + } } return &PrepareResult{ Addrs: addrs, From 610e5ed4798db5452cc0b62bf4a89ab50d1ee65d Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 25 Aug 2025 17:52:58 +0800 Subject: [PATCH 59/83] improve yamux logging (#4952) --- client/connector.go | 4 +-- client/control.go | 2 ++ go.mod | 2 +- go.sum | 4 +-- pkg/util/xlog/log_writer.go | 65 +++++++++++++++++++++++++++++++++++++ server/service.go | 4 +-- 6 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 pkg/util/xlog/log_writer.go diff --git a/client/connector.go b/client/connector.go index ab7c2fdda42..03d7fc29d97 100644 --- a/client/connector.go +++ b/client/connector.go @@ -17,7 +17,6 @@ package client import ( "context" "crypto/tls" - "io" "net" "strconv" "strings" @@ -115,7 +114,8 @@ func (c *defaultConnectorImpl) Open() error { fmuxCfg := fmux.DefaultConfig() fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xl) fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Client(conn, fmuxCfg) if err != nil { diff --git a/client/control.go b/client/control.go index 0dd70b8c72f..4bd6a2f737a 100644 --- a/client/control.go +++ b/client/control.go @@ -276,10 +276,12 @@ func (ctl *Control) heartbeatWorker() { } func (ctl *Control) worker() { + xl := ctl.xl go ctl.heartbeatWorker() go ctl.msgDispatcher.Run() <-ctl.msgDispatcher.Done() + xl.Debugf("control message dispatcher exited") ctl.closeSession() ctl.pm.Close() diff --git a/go.mod b/go.mod index 46e753e2a14..06bfb6ecc0e 100644 --- a/go.mod +++ b/go.mod @@ -82,4 +82,4 @@ require ( ) // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. -replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d +replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 diff --git a/go.sum b/go.sum index bd044c39cbb..a411ff30581 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= -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/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= +github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= diff --git a/pkg/util/xlog/log_writer.go b/pkg/util/xlog/log_writer.go new file mode 100644 index 00000000000..3fff73240a8 --- /dev/null +++ b/pkg/util/xlog/log_writer.go @@ -0,0 +1,65 @@ +// 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 xlog + +import "strings" + +// LogWriter forwards writes to frp's logger at configurable level. +// It is safe for concurrent use as long as the underlying Logger is thread-safe. +type LogWriter struct { + xl *Logger + logFunc func(string) +} + +func (w LogWriter) Write(p []byte) (n int, err error) { + msg := strings.TrimSpace(string(p)) + w.logFunc(msg) + return len(p), nil +} + +func NewTraceWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Tracef("%s", msg) }, + } +} + +func NewDebugWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Debugf("%s", msg) }, + } +} + +func NewInfoWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Infof("%s", msg) }, + } +} + +func NewWarnWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Warnf("%s", msg) }, + } +} + +func NewErrorWriter(xl *Logger) LogWriter { + return LogWriter{ + xl: xl, + logFunc: func(msg string) { xl.Errorf("%s", msg) }, + } +} diff --git a/server/service.go b/server/service.go index fad0e1432c8..7ca80dc89ba 100644 --- a/server/service.go +++ b/server/service.go @@ -19,7 +19,6 @@ import ( "context" "crypto/tls" "fmt" - "io" "net" "net/http" "os" @@ -516,7 +515,8 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) { if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal { fmuxCfg := fmux.DefaultConfig() fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xlog.FromContextSafe(ctx)) fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Server(frpConn, fmuxCfg) if err != nil { From 604700cea5e56b0a607f3e243bd85850824b495b Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 27 Aug 2025 11:07:18 +0800 Subject: [PATCH 60/83] update README (#4957) --- README.md | 2 +- README_zh.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a1db495ebc..c1afcae0477 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ frp is an open source project with its ongoing development made possible entirel
- Warp, the intelligent terminal + Warp, built for collaborating with AI Agents
Available for macOS, Linux and Windows
diff --git a/README_zh.md b/README_zh.md index d911e15230f..c21be1c0356 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,19 +15,36 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者

Gold Sponsors

+

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

+
+ The complete IDE crafted for professional Go developers

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

+
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy

From 0a798a7a69adf3ccad352644b01c97763567be70 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 27 Aug 2025 15:10:36 +0800 Subject: [PATCH 61/83] update go version to 1.24 (#4960) --- .circleci/config.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/goreleaser.yml | 2 +- dockerfiles/Dockerfile-for-frpc | 2 +- dockerfiles/Dockerfile-for-frps | 2 +- go.mod | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ede1f5fef64..017d75c9e73 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: go-version-latest: docker: - - image: cimg/go:1.23-node + - image: cimg/go:1.24-node resource_class: large steps: - checkout diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d4faac70a7e..eb631c240b3 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v8 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index d55913fda6c..652d156ce59 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - name: Make All run: | diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index d8d4437ad22..7d77a26de3d 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -1,4 +1,4 @@ -FROM golang:1.23 AS building +FROM golang:1.24 AS building COPY . /building WORKDIR /building diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 00fb51a89d8..489ce0f5282 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -1,4 +1,4 @@ -FROM golang:1.23 AS building +FROM golang:1.24 AS building COPY . /building WORKDIR /building diff --git a/go.mod b/go.mod index 06bfb6ecc0e..af633af4135 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.23.0 +go 1.24.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 From 7cfa546b55ee485240c3a713b01a4923ee6b714c Mon Sep 17 00:00:00 2001 From: Charlie Blevins Date: Mon, 22 Sep 2025 11:18:49 -0500 Subject: [PATCH 62/83] add proxy name label to the proxy_count prometheus metric (#4985) * add proxy name label to the proxy_count metric * undo label addition in favor of a new metric - this change should not break existing queries * also register this new metric * add type label to proxy_counts_detailed * Update pkg/metrics/prometheus/server.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/metrics/prometheus/server.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/metrics/prometheus/server.go b/pkg/metrics/prometheus/server.go index 56dea6e82fe..a99bb1d5efe 100644 --- a/pkg/metrics/prometheus/server.go +++ b/pkg/metrics/prometheus/server.go @@ -14,11 +14,12 @@ const ( var ServerMetrics metrics.ServerMetrics = newServerMetrics() type serverMetrics struct { - clientCount prometheus.Gauge - proxyCount *prometheus.GaugeVec - connectionCount *prometheus.GaugeVec - trafficIn *prometheus.CounterVec - trafficOut *prometheus.CounterVec + clientCount prometheus.Gauge + proxyCount *prometheus.GaugeVec + proxyCountDetailed *prometheus.GaugeVec + connectionCount *prometheus.GaugeVec + trafficIn *prometheus.CounterVec + trafficOut *prometheus.CounterVec } func (m *serverMetrics) NewClient() { @@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() { m.clientCount.Dec() } -func (m *serverMetrics) NewProxy(_ string, proxyType string) { +func (m *serverMetrics) NewProxy(name string, proxyType string) { m.proxyCount.WithLabelValues(proxyType).Inc() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc() } -func (m *serverMetrics) CloseProxy(_ string, proxyType string) { +func (m *serverMetrics) CloseProxy(name string, proxyType string) { m.proxyCount.WithLabelValues(proxyType).Dec() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec() } func (m *serverMetrics) OpenConnection(name string, proxyType string) { @@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics { Name: "proxy_counts", Help: "The current proxy counts", }, []string{"type"}), + proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "proxy_counts_detailed", + Help: "The current number of proxies grouped by type and name", + }, []string{"type", "name"}), connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: serverSubsystem, @@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics { } prometheus.MustRegister(m.clientCount) prometheus.MustRegister(m.proxyCount) + prometheus.MustRegister(m.proxyCountDetailed) prometheus.MustRegister(m.connectionCount) prometheus.MustRegister(m.trafficIn) prometheus.MustRegister(m.trafficOut) From abf4942e8a48dacf3689982d41c23a0bfc3470a9 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Sep 2025 10:19:19 +0800 Subject: [PATCH 63/83] auth: enhance OIDC client with TLS and proxy configuration options (#4990) --- Release.md | 1 + client/service.go | 8 ++++- conf/frpc_full_example.toml | 14 ++++++++ pkg/auth/auth.go | 11 +++--- pkg/auth/oidc.go | 71 +++++++++++++++++++++++++++++++++++-- pkg/config/v1/client.go | 11 ++++++ 6 files changed, 108 insertions(+), 8 deletions(-) diff --git a/Release.md b/Release.md index e0f5084e9ee..742c9847aec 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,4 @@ ## 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. diff --git a/client/service.go b/client/service.go index 9e1833b972c..f906e4d0a82 100644 --- a/client/service.go +++ b/client/service.go @@ -149,9 +149,15 @@ func NewService(options ServiceOptions) (*Service, error) { } webServer = ws } + + authSetter, err := auth.NewAuthSetter(options.Common.Auth) + if err != nil { + return nil, err + } + s := &Service{ ctx: context.Background(), - authSetter: auth.NewAuthSetter(options.Common.Auth), + authSetter: authSetter, webServer: webServer, common: options.Common, configFilePath: options.ConfigFilePath, diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index dfae95734d3..6b86907e459 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -55,6 +55,20 @@ auth.token = "12345678" # auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" # auth.oidc.additionalEndpointParams.var1 = "foobar" +# OIDC TLS and proxy configuration +# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate. +# This is useful when the OIDC provider uses a self-signed certificate or a custom CA. +# auth.oidc.trustedCaFile = "/path/to/ca.crt" + +# Skip TLS certificate verification for the OIDC token endpoint. +# INSECURE: Only use this for debugging purposes, not recommended for production. +# auth.oidc.insecureSkipVerify = false + +# Specify a proxy server for OIDC token endpoint connections. +# Supports http, https, socks5, and socks5h proxy protocols. +# If not specified, no proxy is used for OIDC connections. +# auth.oidc.proxyURL = "http://proxy.example.com:8080" + # Set admin address for control frpc's action by http api such as reload webServer.addr = "127.0.0.1" webServer.port = 7400 diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index ae706986708..b954fc80eaa 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -27,16 +27,19 @@ type Setter interface { SetNewWorkConn(*msg.NewWorkConn) error } -func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { +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 = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + if err != nil { + return nil, err + } default: - panic(fmt.Sprintf("wrong method: '%s'", cfg.Method)) + return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method) } - return authProvider + return authProvider, nil } type Verifier interface { diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 40ce060faa9..c5f636402c8 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -16,23 +16,72 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" + "net/url" + "os" "slices" "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) +// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests +func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) { + // Clone the default transport to get all reasonable defaults + transport := http.DefaultTransport.(*http.Transport).Clone() + + // Configure TLS settings + if trustedCAFile != "" || insecureSkipVerify { + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + + if trustedCAFile != "" && !insecureSkipVerify { + caCert, err := os.ReadFile(trustedCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile) + } + + tlsConfig.RootCAs = caCertPool + } + transport.TLSClientConfig = tlsConfig + } + + // Configure proxy settings + if proxyURL != "" { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err) + } + transport.Proxy = http.ProxyURL(parsedURL) + } else { + // Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment + transport.Proxy = nil + } + + return &http.Client{Transport: transport}, nil +} + type OidcAuthProvider struct { additionalAuthScopes []v1.AuthScope tokenGenerator *clientcredentials.Config + httpClient *http.Client } -func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider { +func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) { eps := make(map[string][]string) for k, v := range cfg.AdditionalEndpointParams { eps[k] = []string{v} @@ -50,14 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien EndpointParams: eps, } + // Create custom HTTP client if needed + var httpClient *http.Client + if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" { + var err error + httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) + if err != nil { + return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err) + } + } + return &OidcAuthProvider{ additionalAuthScopes: additionalAuthScopes, tokenGenerator: tokenGenerator, - } + httpClient: httpClient, + }, nil } func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { - tokenObj, err := auth.tokenGenerator.Token(context.Background()) + ctx := context.Background() + if auth.httpClient != nil { + ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient) + } + + tokenObj, err := auth.tokenGenerator.Token(ctx) if err != nil { return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) } diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index a830df994a5..c6cf97a69be 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -228,6 +228,17 @@ type AuthOIDCClientConfig struct { // 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"` + + // TrustedCaFile specifies the path to a custom CA certificate file + // for verifying the OIDC token endpoint's TLS certificate. + TrustedCaFile string `json:"trustedCaFile,omitempty"` + // InsecureSkipVerify disables TLS certificate verification for the + // OIDC token endpoint. Only use this for debugging, not recommended for production. + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + // ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint. + // Supports http, https, socks5, and socks5h proxy protocols. + // If empty, no proxy is used for OIDC connections. + ProxyURL string `json:"proxyURL,omitempty"` } type VirtualNetConfig struct { From 6561107945b8bbfaccfa2dc0beab0444ce6a1afe Mon Sep 17 00:00:00 2001 From: juejinyuxitu Date: Thu, 25 Sep 2025 16:47:33 +0800 Subject: [PATCH 64/83] chore: fix struct field name in comment (#4993) Signed-off-by: juejinyuxitu --- pkg/config/v1/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index 38965a3cbcc..8989855485b 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -85,9 +85,9 @@ func (c *WebServerConfig) Complete() { } type TLSConfig struct { - // CertPath specifies the path of the cert file that client will load. + // CertFile 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 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"` From b642a6323cf504ed555cdb984cc1ce731cfba42f Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Sep 2025 16:50:14 +0800 Subject: [PATCH 65/83] update sponsors info (#4997) --- README.md | 7 +++++++ README_zh.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index c1afcae0477..00a3498cb63 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ 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. +
+

diff --git a/README_zh.md b/README_zh.md index c21be1c0356..ea63d7261cb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,6 +15,13 @@ 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. +
+

From b5e90c03a1d578b9cf5205cc619dd1570ef46a25 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Sep 2025 20:11:17 +0800 Subject: [PATCH 66/83] bump version to v0.65.0 and update release notes (#4998) --- Release.md | 1 + pkg/util/version/version.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Release.md b/Release.md index 742c9847aec..2ea047fa5de 100644 --- a/Release.md +++ b/Release.md @@ -2,3 +2,4 @@ * 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. diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index c6497e145f6..0f4ec433898 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.64.0" +var version = "0.65.0" func Full() string { return version From e382676659cc8d5974ea2e3fa3c4fb94eda1fb76 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 26 Sep 2025 12:26:08 +0800 Subject: [PATCH 67/83] update README (#5001) --- README.md | 16 +++++++++------- README_zh.md | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 00a3498cb63..07b14473f9d 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,15 @@ 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. -
-

+
+ +## 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. + +

diff --git a/README_zh.md b/README_zh.md index ea63d7261cb..174a648063d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,13 +15,15 @@ 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. -
-

+
+ +## 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. + +

From ee3cc4b14ea2f8c9581fa06b7151593b94f4ddcc Mon Sep 17 00:00:00 2001 From: Zachary Whaley Date: Thu, 16 Oct 2025 21:53:43 -0500 Subject: [PATCH 68/83] Fix CloseNotifyConn.Close function (#5022) The CloseNotifyConn.Close() function calls itself if the closeFlag is equal to 0 which would mean it would immediately swap the closeFlag value to 1 and never call closeFn. It is unclear what the intent of this call to cc.Close() was but I assume it was meant to be a call to close the Conn object instead. --- pkg/util/net/conn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index 20468f1b8c5..6946b1c2f14 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -149,7 +149,7 @@ 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() } From 2def23bb0b506efd0ed66c8aaa73abe0d631d48f Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Oct 2025 15:44:03 +0800 Subject: [PATCH 69/83] update sponsor (#5030) --- README.md | 9 +++++++++ README_zh.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index 07b14473f9d..fc822a0ea8c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ 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. +

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

diff --git a/README_zh.md b/README_zh.md index 174a648063d..d3ec8aafbe7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -24,6 +24,15 @@ 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. +

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

From 469097a54971a3f1026ceb32c440d618da950e2d Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Oct 2025 16:08:29 +0800 Subject: [PATCH 70/83] update sponsor pic (#5031) --- doc/pic/sponsor_daytona.png | Bin 42315 -> 14455 bytes doc/pic/sponsor_lokal.png | Bin 56387 -> 0 bytes doc/pic/sponsor_workos.png | Bin 37517 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/pic/sponsor_lokal.png delete mode 100644 doc/pic/sponsor_workos.png diff --git a/doc/pic/sponsor_daytona.png b/doc/pic/sponsor_daytona.png index a5271cc9502b575014fb52ba55cfa2ef0d41eb72..e36b6bba4aff4a5396ef46a09023e97b7e107673 100644 GIT binary patch literal 14455 zcmbVyWl$V2*Y2{oFYd576pBO9vb#kVDNd2%?k)vNSzvML;_mM51&S3dT4<31#i6*i z=vr-dpy|u zi_ko^6k&kcN&5YN88!`79r>rHr=z2zm6e~Pqhrl2E!jD_;o*_)9-eyohLTb;U{)?t zQqlkozySatj;16B)p@scxCB)Ono|#(hc0nayR?w$=YICs>@;d7a^_F}?NRVz

1d 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 K literal 42315 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∈g!Z5 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 diff --git a/doc/pic/sponsor_lokal.png b/doc/pic/sponsor_lokal.png deleted file mode 100644 index 82386356fca673b930634f86435841177e481d91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56387 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 BFwy`3 diff --git a/doc/pic/sponsor_workos.png b/doc/pic/sponsor_workos.png deleted file mode 100644 index 5bc5e627d33020a565aad61ad496dbbf0d8bbf38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37517 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 Date: Tue, 28 Oct 2025 17:37:18 +0800 Subject: [PATCH 71/83] https: add load balancing group support (#5032) --- README.md | 2 +- README_zh.md | 2 +- Release.md | 4 +- server/controller/resource.go | 3 + server/group/https.go | 197 ++++++++++++++++++++++++++++++++++ server/proxy/https.go | 41 ++++--- server/service.go | 3 + test/e2e/framework/process.go | 4 +- test/e2e/v1/features/group.go | 76 +++++++++++++ 9 files changed, 311 insertions(+), 21 deletions(-) create mode 100644 server/group/https.go diff --git a/README.md b/README.md index fc822a0ea8c..299f9a3ccdc 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and

- +
Requestly - Free & Open-Source alternative to Postman diff --git a/README_zh.md b/README_zh.md index d3ec8aafbe7..82ac8a5004e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -25,7 +25,7 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and

- +
Requestly - Free & Open-Source alternative to Postman diff --git a/Release.md b/Release.md index 2ea047fa5de..e237538b4ad 100644 --- a/Release.md +++ b/Release.md @@ -1,5 +1,3 @@ ## 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. 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/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/service.go b/server/service.go index 7ca80dc89ba..1fe882d293a 100644 --- a/server/service.go +++ b/server/service.go @@ -322,6 +322,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 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() { From a75320ef2fcbb6103c06bbd5b4ad85f9c35b4cc3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 28 Oct 2025 17:52:34 +0800 Subject: [PATCH 72/83] update quic-go dependency from v0.53.0 to v0.55.0 (#5033) --- go.mod | 17 ++++++++--------- go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 29 deletions(-) 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= From e025843d3c14a1a6801d3a277882cb43b6548e2b Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 29 Oct 2025 01:08:48 +0800 Subject: [PATCH 73/83] vnet: add exponential backoff for failed reconnections (#5035) --- Release.md | 4 +++ client/visitor/stcp.go | 18 ++++++++++++- client/visitor/visitor.go | 2 +- client/visitor/xtcp.go | 11 ++++++++ pkg/plugin/visitor/plugin.go | 15 ++++++++--- pkg/plugin/visitor/virtual_net.go | 44 +++++++++++++++++++++++++------ pkg/util/net/conn.go | 21 ++++++++++++--- pkg/util/net/websocket.go | 2 +- 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/Release.md b/Release.md index e237538b4ad..18aa0182935 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,7 @@ ## Features * 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. + +## 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. 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/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/util/net/conn.go b/pkg/util/net/conn.go index 6946b1c2f14..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, @@ -151,12 +151,25 @@ func (cc *CloseNotifyConn) Close() (err error) { if pflag == 0 { 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 From b27b846971cd9f7b08023314437fdafc939f6664 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 6 Nov 2025 14:05:03 +0800 Subject: [PATCH 74/83] config: add enabled field for individual proxy and visitor (#5048) --- Release.md | 1 + conf/frpc_full_example.toml | 9 +++++++++ pkg/config/load.go | 11 +++++++++++ pkg/config/v1/proxy.go | 7 +++++-- pkg/config/v1/visitor.go | 7 +++++-- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Release.md b/Release.md index 18aa0182935..c346193f056 100644 --- a/Release.md +++ b/Release.md @@ -1,6 +1,7 @@ ## Features * 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. ## Improvements 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/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/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/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 From f736d171ac98db09061d78271edbfd3012c61834 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 18 Nov 2025 00:09:37 +0800 Subject: [PATCH 75/83] rotate gold sponsor order periodically (#5067) --- README.md | 19 ++++++++++--------- README_zh.md | 18 +++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 299f9a3ccdc..0b88eb14b18 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + +
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy +
+

+
## Recall.ai - API for meeting recordings @@ -54,15 +64,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and Secure and Elastic Infrastructure for Running Your AI-Generated Code

-

- - -
- The sovereign cloud that puts you in control -
- An open source, self-hosted alternative to public clouds, built for data ownership and privacy -
-

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

Gold Sponsors

+

+ + +
+ The sovereign cloud that puts you in control +
+ An open source, self-hosted alternative to public clouds, built for data ownership and privacy +
+

## Recall.ai - API for meeting recordings @@ -56,15 +65,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and Secure and Elastic Infrastructure for Running Your AI-Generated Code

-

- - -
- The sovereign cloud that puts you in control -
- An open source, self-hosted alternative to public clouds, built for data ownership and privacy -
-

## 为什么使用 frp ? From 66973a03dbe8e300f2607ad3ada8d515948bac93 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 17 Nov 2025 17:20:21 +0100 Subject: [PATCH 76/83] Add `exec` value source type (#5050) * config: introduce ExecSource value source * auth: introduce OidcTokenSourceAuthProvider * auth: use OidcTokenSourceAuthProvider if tokenSource config is present on the client * cmd: allow exec token source only if CLI flag was passed --- client/admin_api.go | 2 +- client/service.go | 5 +++ cmd/frpc/sub/proxy.go | 11 +++-- cmd/frpc/sub/root.go | 27 ++++++++---- cmd/frpc/sub/verify.go | 5 ++- pkg/auth/auth.go | 10 +++-- pkg/auth/oidc.go | 45 ++++++++++++++++++++ pkg/config/v1/client.go | 8 ++++ pkg/config/v1/validation/client.go | 19 +++++++-- pkg/config/v1/value_source.go | 67 +++++++++++++++++++++++++++++- 10 files changed, 179 insertions(+), 20 deletions(-) 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/service.go b/client/service.go index f906e4d0a82..60eb2d9030d 100644 --- a/client/service.go +++ b/client/service.go @@ -64,6 +64,8 @@ type ServiceOptions struct { ProxyCfgs []v1.ProxyConfigurer VisitorCfgs []v1.VisitorConfigurer + UnsafeFeatures v1.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. @@ -122,6 +124,8 @@ type Service struct { visitorCfgs []v1.VisitorConfigurer clientSpec *msg.ClientSpec + unsafeFeatures v1.UnsafeFeatures + // The configuration file used to initialize this client, or an empty // string if no configuration file was used. configFilePath string @@ -161,6 +165,7 @@ func NewService(options ServiceOptions) (*Service, error) { webServer: webServer, common: options.Common, configFilePath: options.ConfigFilePath, + unsafeFeatures: options.UnsafeFeatures, proxyCfgs: options.ProxyCfgs, visitorCfgs: options.VisitorCfgs, clientSpec: options.ClientSpec, diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 67bd774f7a3..64c0aade486 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -77,7 +77,9 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm fmt.Println(err) os.Exit(1) } - if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + + unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) } @@ -88,7 +90,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 +108,8 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) } @@ -117,7 +120,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..b9a373b16b6 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -21,6 +21,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "sync" "syscall" "time" @@ -36,11 +37,18 @@ import ( "github.com/fatedier/frp/pkg/util/version" ) +type UnsafeFeature = string + +const ( + TokenSourceExec UnsafeFeature = "TokenSourceExec" +) + var ( cfgFile string cfgDir string showVersion bool strictConfigMode bool + allowUnsafe []UnsafeFeature ) func init() { @@ -48,6 +56,7 @@ 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{}, "allowed unsafe features, one or more of: TokenSourceExec") } var rootCmd = &cobra.Command{ @@ -59,15 +68,17 @@ var rootCmd = &cobra.Command{ return nil } + unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + // 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 +87,7 @@ var rootCmd = &cobra.Command{ }, } -func runMultipleClients(cfgDir string) error { +func runMultipleClients(cfgDir string, unsafeFeatures v1.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 +97,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 +122,7 @@ func handleTermSignal(svr *client.Service) { svr.GracefulClose(500 * time.Millisecond) } -func runClient(cfgFilePath string) error { +func runClient(cfgFilePath string, unsafeFeatures v1.UnsafeFeatures) error { cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) if err != nil { return err @@ -127,20 +138,21 @@ 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 v1.UnsafeFeatures, cfgFile string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) @@ -153,6 +165,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..2198114cbd7 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -17,10 +17,12 @@ package sub import ( "fmt" "os" + "slices" "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" ) @@ -42,7 +44,8 @@ var verifyCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs) + unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index b954fc80eaa..64462a2085c 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -32,9 +32,13 @@ func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) { 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) 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/v1/client.go b/pkg/config/v1/client.go index c6cf97a69be..72a19fb3c43 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -239,8 +239,16 @@ 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 { Address string `json:"address,omitempty"` } + +type UnsafeFeatures struct { + TokenSourceExec bool +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 0c8575c990c..7734a5a4a94 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -26,7 +26,7 @@ import ( "github.com/fatedier/frp/pkg/featuregate" ) -func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { +func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.UnsafeFeatures) (Warning, error) { var ( warnings Warning errs error @@ -52,11 +52,24 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { // Validate tokenSource if specified if c.Auth.TokenSource != nil { + if c.Auth.TokenSource.Type == "exec" && !unsafeFeatures.TokenSourceExec { + errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.tokenSource.type")) + } if err := c.Auth.TokenSource.Validate(); err != nil { errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) } } + if c.Auth.OIDC.TokenSource != nil { + // Validate oidc.tokenSource mutual exclusivity with other fields of oidc + if c.Auth.OIDC.ClientID != "" || c.Auth.OIDC.ClientSecret != "" || c.Auth.OIDC.Audience != "" || c.Auth.OIDC.Scope != "" || c.Auth.OIDC.TokenEndpointURL != "" || len(c.Auth.OIDC.AdditionalEndpointParams) > 0 || c.Auth.OIDC.TrustedCaFile != "" || c.Auth.OIDC.InsecureSkipVerify || c.Auth.OIDC.ProxyURL != "" { + errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) + } + if c.Auth.OIDC.TokenSource.Type == "exec" && !unsafeFeatures.TokenSourceExec { + errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.oidc.tokenSource.type")) + } + } + if err := validateLogConfig(&c.Log); err != nil { errs = AppendError(errs, err) } @@ -101,10 +114,10 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { return warnings, 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 v1.UnsafeFeatures) (Warning, error) { var warnings Warning if c != nil { - warning, err := ValidateClientCommonConfig(c) + warning, err := ValidateClientCommonConfig(c, unsafeFeatures) warnings = AppendError(warnings, warning) if err != nil { return warnings, err 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 +} From 15fd19a16db3348b495716a5a5b41f1646e38838 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 18 Nov 2025 01:11:44 +0800 Subject: [PATCH 77/83] fix lint (#5068) --- .golangci.yml | 1 + cmd/frpc/sub/proxy.go | 4 ++-- cmd/frpc/sub/root.go | 13 +++---------- cmd/frpc/sub/verify.go | 3 +-- pkg/config/v1/client.go | 18 +++++++++++++++++- pkg/config/v1/validation/client.go | 15 +++++++++++---- 6 files changed, 35 insertions(+), 19 deletions(-) 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/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 64c0aade486..0b0251a2650 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -78,7 +78,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm os.Exit(1) } - unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) @@ -108,7 +108,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index b9a373b16b6..68d69158e6d 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -21,7 +21,6 @@ import ( "os" "os/signal" "path/filepath" - "slices" "sync" "syscall" "time" @@ -37,18 +36,12 @@ import ( "github.com/fatedier/frp/pkg/util/version" ) -type UnsafeFeature = string - -const ( - TokenSourceExec UnsafeFeature = "TokenSourceExec" -) - var ( cfgFile string cfgDir string showVersion bool strictConfigMode bool - allowUnsafe []UnsafeFeature + allowUnsafe []string ) func init() { @@ -56,7 +49,7 @@ 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{}, "allowed unsafe features, one or more of: TokenSourceExec") + rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, "allowed unsafe features, one or more of: TokenSourceExec") } var rootCmd = &cobra.Command{ @@ -68,7 +61,7 @@ var rootCmd = &cobra.Command{ return nil } - unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + unsafeFeatures := v1.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. diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index 2198114cbd7..c0c9d0e275f 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -17,7 +17,6 @@ package sub import ( "fmt" "os" - "slices" "github.com/spf13/cobra" @@ -44,7 +43,7 @@ var verifyCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - unsafeFeatures := v1.UnsafeFeatures{TokenSourceExec: slices.Contains(allowUnsafe, TokenSourceExec)} + unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index 72a19fb3c43..8528ff7e149 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -249,6 +249,22 @@ type VirtualNetConfig struct { Address string `json:"address,omitempty"` } +const ( + UnsafeFeatureTokenSourceExec = "TokenSourceExec" +) + type UnsafeFeatures struct { - TokenSourceExec bool + 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 { + return u.features[feature] } diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 7734a5a4a94..1c21f01d427 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -52,7 +52,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.Unsa // Validate tokenSource if specified if c.Auth.TokenSource != nil { - if c.Auth.TokenSource.Type == "exec" && !unsafeFeatures.TokenSourceExec { + if c.Auth.TokenSource.Type == "exec" && !unsafeFeatures.IsEnabled(v1.UnsafeFeatureTokenSourceExec) { errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.tokenSource.type")) } if err := c.Auth.TokenSource.Validate(); err != nil { @@ -62,10 +62,12 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.Unsa if c.Auth.OIDC.TokenSource != nil { // Validate oidc.tokenSource mutual exclusivity with other fields of oidc - if c.Auth.OIDC.ClientID != "" || c.Auth.OIDC.ClientSecret != "" || c.Auth.OIDC.Audience != "" || c.Auth.OIDC.Scope != "" || c.Auth.OIDC.TokenEndpointURL != "" || len(c.Auth.OIDC.AdditionalEndpointParams) > 0 || c.Auth.OIDC.TrustedCaFile != "" || c.Auth.OIDC.InsecureSkipVerify || c.Auth.OIDC.ProxyURL != "" { + if c.Auth.OIDC.ClientID != "" || c.Auth.OIDC.ClientSecret != "" || c.Auth.OIDC.Audience != "" || + c.Auth.OIDC.Scope != "" || c.Auth.OIDC.TokenEndpointURL != "" || len(c.Auth.OIDC.AdditionalEndpointParams) > 0 || + c.Auth.OIDC.TrustedCaFile != "" || c.Auth.OIDC.InsecureSkipVerify || c.Auth.OIDC.ProxyURL != "" { errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) } - if c.Auth.OIDC.TokenSource.Type == "exec" && !unsafeFeatures.TokenSourceExec { + if c.Auth.OIDC.TokenSource.Type == "exec" && !unsafeFeatures.IsEnabled(v1.UnsafeFeatureTokenSourceExec) { errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.oidc.tokenSource.type")) } } @@ -114,7 +116,12 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.Unsa return warnings, errs } -func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer, unsafeFeatures v1.UnsafeFeatures) (Warning, error) { +func ValidateAllClientConfig( + c *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures v1.UnsafeFeatures, +) (Warning, error) { var warnings Warning if c != nil { warning, err := ValidateClientCommonConfig(c, unsafeFeatures) From c3821202b1b0c3931a9ecbe03368716a6ca792dc Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 26 Nov 2025 23:55:54 +0800 Subject: [PATCH 78/83] docs: remove zsxq section (#5077) --- README_zh.md | 6 ------ doc/pic/zsxq.jpg | Bin 12647 -> 0 bytes 2 files changed, 6 deletions(-) delete mode 100644 doc/pic/zsxq.jpg diff --git a/README_zh.md b/README_zh.md index 4426813a80a..1c3879fd67f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -137,9 +137,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 - -### 知识星球 - -如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: - -![zsxq](/doc/pic/zsxq.jpg) diff --git a/doc/pic/zsxq.jpg b/doc/pic/zsxq.jpg deleted file mode 100644 index 0bb1f1d516de893fb25d971fa164ff6deb8c93dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12647 zcmb_?cU%)s*YAde7Ni&HB0V5T@4XX33jw8A=!8xv3Q7?X={59#2!swQ(gjpRse&|> zA_7tb1f(g7!d>uJp8LMfANP;D`Rwd>&zw1JW@qLM9KAo91?Z4^hI#-50ss*32OQ1Q zRvT(-Uok_Q>KUSRPbz2$_P)NJBy<4aud2`w6@_P{{-@r0F(e5AZ739@2hEM zh62Fr@8>c8_xkqsSULcViXF@Pm;V2R=$)MX9YF)lKyC*oe@8D6*8>17%F!1W003kP zAf4}8fbTK312Gd0G!VqT$Jq4`jylE;e{l8*4+gCb@>Bx=#VLDxoGSp(G=p@WAV*hF z4$XTIOL#kBy#auh0>o;LZuU+fHURNCFE5{C4DtcO4*wy?^*^w^y~95;?d@Iv!GC-K zTY`#pu>QWD_Q5AF|33>a&pZ($ARb%e^;P~MdHI9r#5QP*8`ki+Eodh+H^2>Z+{edqsvYf5>}LgWv$MD6UonARcJ{w` zY}<)nPR=^IAO?MhzH#=p{L5eHtZ#tUv99B`+rFMB-v-DJgFAcbAJch3%;_Iw@mE`z zS^y4nOb6|RnfL~n9mf!~6XxJ1sJ z(F5!O9N-E#17d$s{%&#Mq{bU8Er1YU9q{x?k%^a=%PJ?0AhTP_dn z-<)K+WIAL9WO`%>7&}Z2rVK-Yr3QFQ!4zPMe_Q)6Kj{+bFzGz$IO#0u#1(8L?r)C2 zea+9;6)WJRS%B5>*l_62JaP32?q10FGNg+s8K;hjn!e;L!wUerFy-Z%6TSJW>*p ziU4rDjvVsEIRs^u#*` z;)4FjgB?Fe0XRSleo<@yH^2u7gZ@YXa)2_R0q6oqz!bm$*1%y7$Izs zGY|oYC`1aP08xYJLQoJi#2RuH;tKJG1VU~?ZbRZBX^>pVeMkl55u^q39MTIJhD<=_ zAS;k<$WIa|$te;h5-t)!lJg`AB$^~fBp8xQB(5ZWBq1bGBuOOMBt<0EB#k7`N%~2~ zNoGmbNcNxrlnTlMJqs0sDnPZNrcfKG3ls+pgT_HKp+(SIXe-#yqtIFC7w8Wd8H^Fe z0}}&BumQ{x<_PnHg~1YFxv+9rBdiNH3Y&v{g&mSolX8-Zk}8oJl3pZrCA~%(O`1ts zO4>mB68xGLNxzYik+F~ok;#K!qcxd3*$uJ;vV5`!WY5Tk$v%M`1(ZO%X|vNl{7BK`}zHOmRR-M=3z5L}^Ot zL>Ww(Oj$zNN;yQiNO=IKhYP_~;TX6({1!YLUJHK-pN8*HQBmnJVW}dhvZ)?Y z^-#@G{W!&NO5~LGDZ5jFr;<*UoqBd^;?xc`HMJnM2DJ@!0Cf^|1@#N+8R~r+MjCM% zLmDTVaGE@tMw$_t4O&WC0a{Jk%d{c1nY52-hiJdhQPK(0Y17%$h0*2Fwa~qz+oNZs zm!vnP_o7drucYs#|I9$nAjqK0;KUHcP|Wax;S(d2k&jWE(UI{sV+rF+#ziJFCLtz6 zCU>TIrW&R}rmxHl%reYY%-5OonA@3WS)eR}EQTzeEXgc&EN@wUp5{8OdD{7O?CILm zBd5Q!aP7+QLPBiBY&SK61&OI(}E`2Uvt{koxTwl0ZxHY)lx$)c`+$(1o�Zjp244a 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 Date: Tue, 2 Dec 2025 11:22:48 +0800 Subject: [PATCH 79/83] refactor: use MessageSender interface for message transporter (#5083) --- Release.md | 4 ++++ client/control.go | 2 +- pkg/msg/handler.go | 4 ---- pkg/transport/message.go | 14 ++++++++------ server/control.go | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Release.md b/Release.md index c346193f056..3775da513f4 100644 --- a/Release.md +++ b/Release.md @@ -6,3 +6,7 @@ ## 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/control.go b/client/control.go index 4bd6a2f737a..c18ae07c358 100644 --- a/client/control.go +++ b/client/control.go @@ -100,7 +100,7 @@ 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.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, 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/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/server/control.go b/server/control.go index b70d8d1268a..65d52062996 100644 --- a/server/control.go +++ b/server/control.go @@ -195,7 +195,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 } From 0fe8f7a0b6bef8ea97172bfa21ff85c5e429ea40 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 5 Dec 2025 16:26:09 +0800 Subject: [PATCH 80/83] refactor: reorganize security policy into dedicated packag (#5088) --- client/service.go | 5 +- cmd/frpc/sub/proxy.go | 5 +- cmd/frpc/sub/root.go | 16 ++- cmd/frpc/sub/verify.go | 4 +- pkg/config/v1/client.go | 20 ---- pkg/config/v1/validation/client.go | 117 +++++++++++++------ pkg/{ => policy}/featuregate/feature_gate.go | 0 pkg/policy/security/unsafe.go | 34 ++++++ 8 files changed, 133 insertions(+), 68 deletions(-) rename pkg/{ => policy}/featuregate/feature_gate.go (100%) create mode 100644 pkg/policy/security/unsafe.go diff --git a/client/service.go b/client/service.go index 60eb2d9030d..819a2bc5be6 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,7 +65,7 @@ type ServiceOptions struct { ProxyCfgs []v1.ProxyConfigurer VisitorCfgs []v1.VisitorConfigurer - UnsafeFeatures v1.UnsafeFeatures + 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. @@ -124,7 +125,7 @@ type Service struct { visitorCfgs []v1.VisitorConfigurer clientSpec *msg.ClientSpec - unsafeFeatures v1.UnsafeFeatures + unsafeFeatures *security.UnsafeFeatures // The configuration file used to initialize this client, or an empty // string if no configuration file was used. diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 0b0251a2650..0748a8b166f 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{ @@ -78,7 +79,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm os.Exit(1) } - unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) @@ -108,7 +109,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 68d69158e6d..0750562b351 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" ) @@ -49,7 +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{}, "allowed unsafe features, one or more of: TokenSourceExec") + + 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{ @@ -61,7 +65,7 @@ var rootCmd = &cobra.Command{ return nil } - unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) + 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. @@ -80,7 +84,7 @@ var rootCmd = &cobra.Command{ }, } -func runMultipleClients(cfgDir string, unsafeFeatures v1.UnsafeFeatures) 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() { @@ -115,7 +119,7 @@ func handleTermSignal(svr *client.Service) { svr.GracefulClose(500 * time.Millisecond) } -func runClient(cfgFilePath string, unsafeFeatures v1.UnsafeFeatures) error { +func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error { cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) if err != nil { return err @@ -145,7 +149,7 @@ func startService( cfg *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer, - unsafeFeatures v1.UnsafeFeatures, + unsafeFeatures *security.UnsafeFeatures, cfgFile string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index c0c9d0e275f..830f7bf1118 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -21,8 +21,8 @@ import ( "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" ) func init() { @@ -43,7 +43,7 @@ var verifyCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - unsafeFeatures := v1.NewUnsafeFeatures(allowUnsafe) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) if warning != nil { fmt.Printf("WARNING: %v\n", warning) diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index 8528ff7e149..61bc6ac67cd 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -248,23 +248,3 @@ type AuthOIDCClientConfig struct { type VirtualNetConfig struct { Address string `json:"address,omitempty"` } - -const ( - UnsafeFeatureTokenSourceExec = "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 { - return u.features[feature] -} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 1c21f01d427..b55fece7185 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -23,70 +23,111 @@ 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, unsafeFeatures v1.UnsafeFeatures) (Warning, error) { +func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) { var ( warnings Warning errs error ) - // validate feature gates + + validators := []func() (Warning, error){ + func() (Warning, error) { return validateFeatureGates(c) }, + func() (Warning, error) { return validateAuthConfig(&c.Auth, unsafeFeatures) }, + 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 _, v := range validators { + w, err := v() + 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 validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeFeatures) (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 c.Auth.TokenSource.Type == "exec" && !unsafeFeatures.IsEnabled(v1.UnsafeFeatureTokenSourceExec) { - errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.tokenSource.type")) + if c.TokenSource != nil { + if c.TokenSource.Type == "exec" { + if !unsafeFeatures.IsEnabled(security.TokenSourceExec) { + errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+ + "To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec)) + } } - if err := c.Auth.TokenSource.Validate(); err != nil { + if err := c.TokenSource.Validate(); err != nil { errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) } } - if c.Auth.OIDC.TokenSource != nil { - // Validate oidc.tokenSource mutual exclusivity with other fields of oidc - if c.Auth.OIDC.ClientID != "" || c.Auth.OIDC.ClientSecret != "" || c.Auth.OIDC.Audience != "" || - c.Auth.OIDC.Scope != "" || c.Auth.OIDC.TokenEndpointURL != "" || len(c.Auth.OIDC.AdditionalEndpointParams) > 0 || - c.Auth.OIDC.TrustedCaFile != "" || c.Auth.OIDC.InsecureSkipVerify || c.Auth.OIDC.ProxyURL != "" { - errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) - } - if c.Auth.OIDC.TokenSource.Type == "exec" && !unsafeFeatures.IsEnabled(v1.UnsafeFeatureTokenSourceExec) { - errs = AppendError(errs, fmt.Errorf("unsafe 'exec' not allowed for auth.oidc.tokenSource.type")) - } - } - - if err := validateLogConfig(&c.Log); err != nil { + if err := validateOIDCConfig(&c.OIDC, unsafeFeatures); err != nil { errs = AppendError(errs, err) } + return nil, errs +} - if err := validateWebServerConfig(&c.WebServer); err != nil { - errs = AppendError(errs, err) +func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.UnsafeFeatures) 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 !unsafeFeatures.IsEnabled(security.TokenSourceExec) { + errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+ + "To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec)) + } } + if err := c.TokenSource.Validate(); err != nil { + errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err)) + } + return errs +} + +func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) - if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 { - if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval { + 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) @@ -94,16 +135,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.Unsa 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)) @@ -113,14 +158,14 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures v1.Unsa 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, - unsafeFeatures v1.UnsafeFeatures, + unsafeFeatures *security.UnsafeFeatures, ) (Warning, error) { var warnings Warning if c != nil { 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] +} From 2bdf25bae68160230bbe7047e3fef09a9553ef90 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 11 Dec 2025 12:48:08 +0800 Subject: [PATCH 81/83] rotate gold sponsor order periodically (#5094) --- README.md | 22 ++++++++-------------- README_zh.md | 22 ++++++++-------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0b88eb14b18..a99b2277094 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,14 @@ frp is an open source project with its ongoing development made possible entirel

Gold Sponsors

+

+ + +
+ The complete IDE crafted for professional Go developers +
+

+

@@ -50,20 +58,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and Available for macOS, Linux and Windows

-

- - -
- The complete IDE crafted for professional Go developers -
-

-

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

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

Gold Sponsors

+

+ + +
+ The complete IDE crafted for professional Go developers +
+

+

@@ -51,20 +59,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and Available for macOS, Linux and Windows

-

- - -
- The complete IDE crafted for professional Go developers -
-

-

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

## 为什么使用 frp ? From 7526d7a69af9de505f96ccec5ef47b68666a9201 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Dec 2025 00:53:08 +0800 Subject: [PATCH 82/83] refactor: separate auth config from runtime and defer token resolution (#5105) --- client/control.go | 12 ++--- client/proxy/proxy.go | 5 +- client/proxy/proxy_manager.go | 7 ++- client/proxy/proxy_wrapper.go | 3 +- client/proxy/sudp.go | 2 +- client/proxy/udp.go | 2 +- client/service.go | 12 ++--- cmd/frpc/sub/proxy.go | 6 ++- cmd/frpc/sub/root.go | 1 + cmd/frps/root.go | 9 +++- cmd/frps/verify.go | 5 +- pkg/auth/auth.go | 63 ++++++++++++++++++++++++ pkg/config/v1/client.go | 13 ----- pkg/config/v1/client_test.go | 71 ++------------------------- pkg/config/v1/server.go | 14 ------ pkg/config/v1/server_test.go | 71 ++------------------------- pkg/config/v1/validation/client.go | 27 +++++----- pkg/config/v1/validation/server.go | 8 ++- pkg/config/v1/validation/validator.go | 28 +++++++++++ server/control.go | 7 ++- server/proxy/http.go | 2 +- server/proxy/proxy.go | 6 ++- server/proxy/udp.go | 2 +- server/service.go | 15 ++++-- 24 files changed, 185 insertions(+), 206 deletions(-) create mode 100644 pkg/config/v1/validation/validator.go diff --git a/client/control.go b/client/control.go index c18ae07c358..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 } @@ -102,7 +102,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro ctl.registerMsgHandlers() 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 819a2bc5be6..b282163e220 100644 --- a/client/service.go +++ b/client/service.go @@ -111,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 @@ -155,14 +155,14 @@ 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, @@ -296,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 } @@ -350,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/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 0748a8b166f..ef7fe67fddb 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -80,7 +80,8 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm } unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) - if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) } @@ -110,7 +111,8 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client os.Exit(1) } unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) - if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil { + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { fmt.Println(err) os.Exit(1) } diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 0750562b351..1c2d8d5eaae 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -142,6 +142,7 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro if err != nil { return err } + return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath) } 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/pkg/auth/auth.go b/pkg/auth/auth.go index 64462a2085c..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,6 +28,39 @@ 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: @@ -52,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/config/v1/client.go b/pkg/config/v1/client.go index 61bc6ac67cd..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 } 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/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 b55fece7185..eb4a0253dbf 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -27,7 +27,7 @@ import ( "github.com/fatedier/frp/pkg/policy/security" ) -func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) { +func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { var ( warnings Warning errs error @@ -35,15 +35,15 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *securi validators := []func() (Warning, error){ func() (Warning, error) { return validateFeatureGates(c) }, - func() (Warning, error) { return validateAuthConfig(&c.Auth, unsafeFeatures) }, + 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 _, v := range validators { - w, err := v() + for _, validator := range validators { + w, err := validator() warnings = AppendError(warnings, w) errs = AppendError(errs, err) } @@ -59,7 +59,7 @@ func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) { return nil, nil } -func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) { +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)) @@ -76,9 +76,8 @@ func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeF // Validate tokenSource if specified if c.TokenSource != nil { if c.TokenSource.Type == "exec" { - if !unsafeFeatures.IsEnabled(security.TokenSourceExec) { - errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+ - "To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec)) + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) } } if err := c.TokenSource.Validate(); err != nil { @@ -86,13 +85,13 @@ func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeF } } - if err := validateOIDCConfig(&c.OIDC, unsafeFeatures); err != nil { + if err := v.validateOIDCConfig(&c.OIDC); err != nil { errs = AppendError(errs, err) } return nil, errs } -func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.UnsafeFeatures) error { +func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error { if c.TokenSource == nil { return nil } @@ -104,9 +103,8 @@ func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.Uns errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) } if c.TokenSource.Type == "exec" { - if !unsafeFeatures.IsEnabled(security.TokenSourceExec) { - errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+ - "To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec)) + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) } } if err := c.TokenSource.Validate(); err != nil { @@ -167,9 +165,10 @@ func ValidateAllClientConfig( visitorCfgs []v1.VisitorConfigurer, unsafeFeatures *security.UnsafeFeatures, ) (Warning, error) { + validator := NewConfigValidator(unsafeFeatures) var warnings Warning if c != nil { - warning, err := ValidateClientCommonConfig(c, unsafeFeatures) + 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/server/control.go b/server/control.go index 65d52062996..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 } @@ -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/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/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 1fe882d293a..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, @@ -586,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 } @@ -595,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 From ef96481f58122ec00d2aa168cc3203532197b014 Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Dec 2025 10:15:40 +0800 Subject: [PATCH 83/83] update version and release notes (#5106) --- Release.md | 1 + pkg/util/version/version.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Release.md b/Release.md index 3775da513f4..c1861c58839 100644 --- a/Release.md +++ b/Release.md @@ -2,6 +2,7 @@ * 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 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