diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 98583c77129..9517af535fa 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,7 +22,7 @@ jobs: uses: golangci/golangci-lint-action@v3 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.53 + version: v1.55 # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 diff --git a/.gitignore b/.gitignore index f6df315b1e4..c9480d52d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ lastversion/ dist/ .idea/ .vscode/ +.autogen_ssh_key # Cache *.swp diff --git a/.golangci.yml b/.golangci.yml index 18cbaf0be8a..f166e9def62 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ service: - golangci-lint-version: 1.51.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.55.x # use the fixed version to not introduce new linters unexpectedly run: concurrency: 4 diff --git a/Makefile b/Makefile index d94e7c36543..f8326891de6 100644 --- a/Makefile +++ b/Makefile @@ -26,10 +26,10 @@ vet: go vet ./... frps: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps frpc: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc test: gotest diff --git a/README.md b/README.md index fb1592879e6..4bebb8ab607 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ +   + + +

@@ -42,7 +46,7 @@ frp also offers a P2P connect mode. * [Using Environment Variables](#using-environment-variables) * [Split Configures Into Different Files](#split-configures-into-different-files) * [Server Dashboard](#server-dashboard) - * [Admin UI](#admin-ui) + * [Client Admin UI](#client-admin-ui) * [Monitor](#monitor) * [Prometheus](#prometheus) * [Authenticating the Client](#authenticating-the-client) @@ -71,9 +75,10 @@ frp also offers a P2P connect mode. * [Custom Subdomain Names](#custom-subdomain-names) * [URL Routing](#url-routing) * [TCP Port Multiplexing](#tcp-port-multiplexing) - * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) + * [Connecting to frps via PROXY](#connecting-to-frps-via-proxy) * [Client Plugins](#client-plugins) * [Server Manage Plugins](#server-manage-plugins) + * [SSH Tunnel Gateway](#ssh-tunnel-gateway) * [Contributing](#contributing) * [Donation](#donation) * [GitHub Sponsors](#github-sponsors) @@ -505,6 +510,7 @@ includes = ["./confd/*.toml"] ```toml # ./confd/test.toml + [[proxies]] name = "ssh" type = "tcp" @@ -616,6 +622,7 @@ The features are off by default. You can turn on encryption and/or compression: ```toml # frpc.toml + [[proxies]] name = "ssh" type = "tcp" @@ -771,6 +778,7 @@ We would like to try to allow multiple proxies bind a same remote port with diff ```toml # frpc.toml + [[proxies]] name = "ssh" type = "tcp" @@ -876,6 +884,7 @@ This feature is only available for types `tcp`, `http`, `tcpmux` now. ```toml # frpc.toml + [[proxies]] name = "test1" type = "tcp" @@ -911,6 +920,7 @@ With health check type **tcp**, the service port will be pinged (TCPing): ```toml # frpc.toml + [[proxies]] name = "test1" type = "tcp" @@ -930,6 +940,7 @@ With health check type **http**, an HTTP request will be sent to the service and ```toml # frpc.toml + [[proxies]] name = "web" type = "http" @@ -954,6 +965,7 @@ However, speaking of web servers and HTTP requests, your web server might rely o ```toml # frpc.toml + [[proxies]] name = "web" type = "http" @@ -970,6 +982,7 @@ Similar to `Host`, You can override other HTTP request headers with proxy type ` ```toml # frpc.toml + [[proxies]] name = "web" type = "http" @@ -997,6 +1010,7 @@ Here is an example for https service: ```toml # frpc.toml + [[proxies]] name = "web" type = "https" @@ -1019,6 +1033,7 @@ It can only be enabled when proxy type is http. ```toml # frpc.toml + [[proxies]] name = "web" type = "http" @@ -1043,6 +1058,7 @@ Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard ```toml # frpc.toml + [[proxies]] name = "web" type = "http" @@ -1062,6 +1078,7 @@ frp supports forwarding HTTP requests to different backend web services by url r ```toml # frpc.toml + [[proxies]] name = "web01" type = "http" @@ -1147,6 +1164,7 @@ Using plugin **http_proxy**: ```toml # frpc.toml + [[proxies]] name = "http_proxy" type = "tcp" @@ -1165,6 +1183,44 @@ Read the [document](/doc/server_plugin.md). Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin). +### SSH Tunnel Gateway + +*added in v0.53.0* + +frp supports listening to an SSH port on the frps side and achieves TCP protocol proxying through the SSH -R protocol, without relying on frpc. + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +``` + +When running `./frps -c frps.toml`, a private key file named `.autogen_ssh_key` will be automatically created in the current working directory. This generated private key file will be used by the SSH server in frps. + +Executing the command + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090 +``` + +sets up a proxy on frps that forwards the local 8080 service to the port 9090. + +```bash +frp (via SSH) (Ctrl+C to quit) + +User: +ProxyName: test-tcp +Type: tcp +RemoteAddress: :9090 +``` + +This is equivalent to: + +```bash +frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090 +``` + +Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information. + ## Contributing Interested in getting involved? We would like to help you! diff --git a/README_zh.md b/README_zh.md index 77cf797467e..c54bdce784c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,6 +13,10 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP +   + + +

@@ -84,7 +88,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 ### 知识星球 -如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: +如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: ![zsxq](/doc/pic/zsxq.jpg) diff --git a/Release.md b/Release.md index 1660f1e19e2..8e1ea863ea7 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,11 @@ +### Features + +* The new command line parameter `--strict_config` has been added to enable strict configuration validation mode. It will throw an error for unknown fields instead of ignoring them. In future versions, we will set the default value of this parameter to true to avoid misconfigurations. +* Support `SSH reverse tunneling`. With this feature, you can expose your local service without running frpc, only using SSH. The SSH reverse tunnel agent has many functional limitations compared to the frpc agent. The currently supported proxy types are tcp, http, https, tcpmux, and stcp. +* The frpc tcpmux command line parameters have been updated to support configuring `http_user` and `http_pwd`. +* The frpc stcp/sudp/xtcp command line parameters have been updated to support configuring `allow_users`. + ### Fixes -* `admin_user` is not effective in the INI configuration. +* frpc: Return code 1 when the first login attempt fails and exits. +* When auth.method is `oidc` and auth.additionalScopes contains `HeartBeats`, if obtaining AccessToken fails, the application will be unresponsive. diff --git a/client/admin.go b/client/admin.go deleted file mode 100644 index da8bab1bd5c..00000000000 --- a/client/admin.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2017 fatedier, fatedier@gmail.com -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package client - -import ( - "net" - "net/http" - "net/http/pprof" - "time" - - "github.com/gorilla/mux" - - "github.com/fatedier/frp/assets" - utilnet "github.com/fatedier/frp/pkg/util/net" -) - -var ( - httpServerReadTimeout = 60 * time.Second - httpServerWriteTimeout = 60 * time.Second -) - -func (svr *Service) RunAdminServer(address string) (err error) { - // url router - router := mux.NewRouter() - - router.HandleFunc("/healthz", svr.healthz) - - // debug - if svr.cfg.WebServer.PprofEnable { - router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - router.HandleFunc("/debug/pprof/profile", pprof.Profile) - router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - router.HandleFunc("/debug/pprof/trace", pprof.Trace) - router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) - } - - subRouter := router.NewRoute().Subrouter() - user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password - subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware) - - // api, see admin_api.go - subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET") - subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST") - subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET") - subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET") - subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT") - - // view - subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET") - subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET") - subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/static/", http.StatusMovedPermanently) - }) - - server := &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: httpServerReadTimeout, - WriteTimeout: httpServerWriteTimeout, - } - if address == "" { - address = ":http" - } - ln, err := net.Listen("tcp", address) - if err != nil { - return err - } - - go func() { - _ = server.Serve(ln) - }() - return -} diff --git a/client/admin_api.go b/client/admin_api.go index a348e8dd332..5e4d67c60fa 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -31,7 +31,9 @@ import ( "github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config/v1/validation" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" ) type GeneralResponse struct { @@ -39,14 +41,42 @@ type GeneralResponse struct { Msg string } +func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { + helper.Router.HandleFunc("/healthz", svr.healthz) + subRouter := helper.Router.NewRoute().Subrouter() + + subRouter.Use(helper.AuthMiddleware.Middleware) + + // api, see admin_api.go + subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET") + subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST") + subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET") + subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET") + subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT") + + // view + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") + subRouter.PathPrefix("/static/").Handler( + netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), + ).Methods("GET") + subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/", http.StatusMovedPermanently) + }) +} + // /healthz func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) } // GET /api/reload -func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) { +func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} + strictConfigMode := false + strictStr := r.URL.Query().Get("strictConfig") + if strictStr != "" { + strictConfigMode, _ = strconv.ParseBool(strictStr) + } log.Info("api request [/api/reload]") defer func() { @@ -57,21 +87,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) { } }() - cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile) + cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode) if err != nil { res.Code = 400 res.Msg = err.Error() log.Warn("reload frpc proxy config error: %s", res.Msg) return } - if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil { + if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil { res.Code = 400 res.Msg = err.Error() log.Warn("reload frpc proxy config error: %s", res.Msg) return } - if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { + if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil { res.Code = 500 res.Msg = err.Error() log.Warn("reload frpc proxy config error: %s", res.Msg) @@ -144,9 +174,16 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(buf) }() - ps := svr.ctl.pm.GetAllProxyStatus() + svr.ctlMu.RLock() + ctl := svr.ctl + svr.ctlMu.RUnlock() + if ctl == nil { + return + } + + ps := ctl.pm.GetAllProxyStatus() for _, status := range ps { - res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr)) + res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr)) } for _, arrs := range res { @@ -172,14 +209,14 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { } }() - if svr.cfgFile == "" { + if svr.configFilePath == "" { res.Code = 400 res.Msg = "frpc has no config file path" log.Warn("%s", res.Msg) return } - content, err := os.ReadFile(svr.cfgFile) + content, err := os.ReadFile(svr.configFilePath) if err != nil { res.Code = 400 res.Msg = err.Error() @@ -218,7 +255,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { return } - if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil { + if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil { res.Code = 500 res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err) log.Warn("%s", res.Msg) diff --git a/client/connector.go b/client/connector.go new file mode 100644 index 00000000000..ba1441468ee --- /dev/null +++ b/client/connector.go @@ -0,0 +1,227 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +import ( + "context" + "crypto/tls" + "io" + "net" + "strconv" + "strings" + "sync" + "time" + + libdial "github.com/fatedier/golib/net/dial" + fmux "github.com/hashicorp/yamux" + quic "github.com/quic-go/quic-go" + "github.com/samber/lo" + + 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" +) + +// Connector is a interface for establishing connections to the server. +type Connector interface { + Open() error + Connect() (net.Conn, error) + Close() error +} + +// defaultConnectorImpl is the default implementation of Connector for normal frpc. +type defaultConnectorImpl struct { + ctx context.Context + cfg *v1.ClientCommonConfig + + muxSession *fmux.Session + quicConn quic.Connection + closeOnce sync.Once +} + +func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector { + return &defaultConnectorImpl{ + ctx: ctx, + cfg: cfg, + } +} + +// Open opens a underlying connection to the server. +// The underlying connection is either a TCP connection or a QUIC connection. +// After the underlying connection is established, you can call Connect() to get a stream. +// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect(). +func (c *defaultConnectorImpl) Open() error { + xl := xlog.FromContextSafe(c.ctx) + + // special for quic + if strings.EqualFold(c.cfg.Transport.Protocol, "quic") { + var tlsConfig *tls.Config + var err error + sn := c.cfg.Transport.TLS.ServerName + if sn == "" { + sn = c.cfg.ServerAddr + } + if lo.FromPtr(c.cfg.Transport.TLS.Enable) { + tlsConfig, err = transport.NewClientTLSConfig( + c.cfg.Transport.TLS.CertFile, + c.cfg.Transport.TLS.KeyFile, + c.cfg.Transport.TLS.TrustedCaFile, + sn) + } else { + tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn) + } + if err != nil { + xl.Warn("fail to build tls configuration, err: %v", err) + return err + } + tlsConfig.NextProtos = []string{"frp"} + + conn, err := quic.DialAddr( + c.ctx, + net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)), + tlsConfig, &quic.Config{ + MaxIdleTimeout: time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second, + }) + if err != nil { + return err + } + c.quicConn = conn + return nil + } + + if !lo.FromPtr(c.cfg.Transport.TCPMux) { + return nil + } + + conn, err := c.realConnect() + if err != nil { + return err + } + + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second + fmuxCfg.LogOutput = io.Discard + fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 + session, err := fmux.Client(conn, fmuxCfg) + if err != nil { + return err + } + c.muxSession = session + return nil +} + +// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled. +func (c *defaultConnectorImpl) Connect() (net.Conn, error) { + if c.quicConn != nil { + stream, err := c.quicConn.OpenStreamSync(context.Background()) + if err != nil { + return nil, err + } + return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil + } else if c.muxSession != nil { + stream, err := c.muxSession.OpenStream() + if err != nil { + return nil, err + } + return stream, nil + } + + return c.realConnect() +} + +func (c *defaultConnectorImpl) realConnect() (net.Conn, error) { + xl := xlog.FromContextSafe(c.ctx) + var tlsConfig *tls.Config + var err error + tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable) + if c.cfg.Transport.Protocol == "wss" { + tlsEnable = true + } + if tlsEnable { + sn := c.cfg.Transport.TLS.ServerName + if sn == "" { + sn = c.cfg.ServerAddr + } + + tlsConfig, err = transport.NewClientTLSConfig( + c.cfg.Transport.TLS.CertFile, + c.cfg.Transport.TLS.KeyFile, + c.cfg.Transport.TLS.TrustedCaFile, + sn) + if err != nil { + xl.Warn("fail to build tls configuration, err: %v", err) + return nil, err + } + } + + proxyType, addr, auth, err := libdial.ParseProxyURL(c.cfg.Transport.ProxyURL) + if err != nil { + xl.Error("fail to parse proxy url") + return nil, err + } + dialOptions := []libdial.DialOption{} + protocol := c.cfg.Transport.Protocol + switch protocol { + case "websocket": + protocol = "tcp" + dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")})) + dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{ + Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)), + })) + dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) + case "wss": + protocol = "tcp" + dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig)) + // Make sure that if it is wss, the websocket hook is executed after the tls hook. + dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110})) + default: + dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{ + Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)), + })) + dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) + } + + if c.cfg.Transport.ConnectServerLocalIP != "" { + dialOptions = append(dialOptions, libdial.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP)) + } + dialOptions = append(dialOptions, + libdial.WithProtocol(protocol), + libdial.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second), + libdial.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second), + libdial.WithProxy(proxyType, addr), + libdial.WithProxyAuth(auth), + ) + conn, err := libdial.DialContext( + c.ctx, + net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)), + dialOptions..., + ) + return conn, err +} + +func (c *defaultConnectorImpl) Close() error { + c.closeOnce.Do(func() { + if c.quicConn != nil { + _ = c.quicConn.CloseWithError(0, "") + } + if c.muxSession != nil { + _ = c.muxSession.Close() + } + }) + return nil +} diff --git a/client/control.go b/client/control.go index 63c6c331fc9..e4b01ae8c21 100644 --- a/client/control.go +++ b/client/control.go @@ -16,13 +16,10 @@ package client import ( "context" - "io" "net" - "runtime/debug" + "sync/atomic" "time" - "github.com/fatedier/golib/control/shutdown" - "github.com/fatedier/golib/crypto" "github.com/samber/lo" "github.com/fatedier/frp/client/proxy" @@ -31,101 +28,99 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" ) +type SessionContext struct { + // The client common configuration. + Common *v1.ClientCommonConfig + + // Unique ID obtained from frps. + // It should be attached to the login message when reconnecting. + RunID string + // Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit. + Conn net.Conn + // Indicates whether the connection is encrypted. + ConnEncrypted bool + // Sets authentication based on selected method + AuthSetter auth.Setter + // Connector is used to create new connections, which could be real TCP connections or virtual streams. + Connector Connector +} + type Control struct { // service context ctx context.Context xl *xlog.Logger - // Unique ID obtained from frps. - // It should be attached to the login message when reconnecting. - runID string + // session context + sessionCtx *SessionContext // manage all proxies - pxyCfgs []v1.ProxyConfigurer - pm *proxy.Manager + pm *proxy.Manager // manage all visitors vm *visitor.Manager - // control connection - conn net.Conn - - cm *ConnectionManager + doneCh chan struct{} - // put a message in this channel to send it over control connection to server - sendCh chan (msg.Message) - - // read from this channel to get the next message sent by server - readCh chan (msg.Message) - - // goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed - closedCh chan struct{} - - closedDoneCh chan struct{} - - // last time got the Pong message - lastPong time.Time - - // The client configuration - clientCfg *v1.ClientCommonConfig - - readerShutdown *shutdown.Shutdown - writerShutdown *shutdown.Shutdown - msgHandlerShutdown *shutdown.Shutdown - - // sets authentication based on selected method - authSetter auth.Setter + // of time.Time, last time got the Pong message + lastPong atomic.Value + // The role of msgTransporter is similar to HTTP2. + // It allows multiple messages to be sent simultaneously on the same control connection. + // The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type. msgTransporter transport.MessageTransporter + + // msgDispatcher is a wrapper for control connection. + // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types. + msgDispatcher *msg.Dispatcher } -func NewControl( - ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager, - clientCfg *v1.ClientCommonConfig, - pxyCfgs []v1.ProxyConfigurer, - visitorCfgs []v1.VisitorConfigurer, - authSetter auth.Setter, -) *Control { +func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { // new xlog instance ctl := &Control{ - ctx: ctx, - xl: xlog.FromContextSafe(ctx), - runID: runID, - conn: conn, - cm: cm, - pxyCfgs: pxyCfgs, - sendCh: make(chan msg.Message, 100), - readCh: make(chan msg.Message, 100), - closedCh: make(chan struct{}), - closedDoneCh: make(chan struct{}), - clientCfg: clientCfg, - readerShutdown: shutdown.New(), - writerShutdown: shutdown.New(), - msgHandlerShutdown: shutdown.New(), - authSetter: authSetter, + ctx: ctx, + xl: xlog.FromContextSafe(ctx), + sessionCtx: sessionCtx, + doneCh: make(chan struct{}), } - ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) - ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter) + ctl.lastPong.Store(time.Now()) - ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter) - ctl.vm.Reload(visitorCfgs) - return ctl + if sessionCtx.ConnEncrypted { + cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token)) + if err != nil { + return nil, err + } + ctl.msgDispatcher = msg.NewDispatcher(cryptoRW) + } else { + ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) + } + 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) + return ctl, nil } -func (ctl *Control) Run() { +func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) { go ctl.worker() // start all proxies - ctl.pm.Reload(ctl.pxyCfgs) + ctl.pm.UpdateAll(proxyCfgs) // start all visitors - go ctl.vm.Run() + ctl.vm.UpdateAll(visitorCfgs) +} + +func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + ctl.pm.SetInWorkConnCallback(cb) } -func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) { +func (ctl *Control) handleReqWorkConn(_ msg.Message) { xl := ctl.xl workConn, err := ctl.connectServer() if err != nil { @@ -134,9 +129,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) { } m := &msg.NewWorkConn{ - RunID: ctl.runID, + RunID: ctl.sessionCtx.RunID, } - if err = ctl.authSetter.SetNewWorkConn(m); err != nil { + if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil { xl.Warn("error during NewWorkConn authentication: %v", err) return } @@ -162,8 +157,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) { ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) } -func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { +func (ctl *Control) handleNewProxyResp(m msg.Message) { xl := ctl.xl + inMsg := m.(*msg.NewProxyResp) // Server will return NewProxyResp message to each NewProxy message. // Start a new proxy handler if no error got err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) @@ -174,8 +170,9 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { } } -func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) { +func (ctl *Control) handleNatHoleResp(m msg.Message) { xl := ctl.xl + inMsg := m.(*msg.NatHoleResp) // Dispatch the NatHoleResp message to the related proxy. ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID) @@ -184,6 +181,25 @@ func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) { } } +func (ctl *Control) handlePong(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.Pong) + + if inMsg.Error != "" { + xl.Error("Pong message contains error: %s", inMsg.Error) + ctl.closeSession() + return + } + ctl.lastPong.Store(time.Now()) + xl.Debug("receive heartbeat from server") +} + +// closeSession closes the control connection. +func (ctl *Control) closeSession() { + ctl.sessionCtx.Conn.Close() + ctl.sessionCtx.Connector.Close() +} + func (ctl *Control) Close() error { return ctl.GracefulClose(0) } @@ -194,170 +210,86 @@ func (ctl *Control) GracefulClose(d time.Duration) error { time.Sleep(d) - ctl.conn.Close() - ctl.cm.Close() + ctl.closeSession() return nil } -// ClosedDoneCh returns a channel that will be closed after all resources are released -func (ctl *Control) ClosedDoneCh() <-chan struct{} { - return ctl.closedDoneCh +// Done returns a channel that will be closed after all resources are released +func (ctl *Control) Done() <-chan struct{} { + return ctl.doneCh } // connectServer return a new connection to frps -func (ctl *Control) connectServer() (conn net.Conn, err error) { - return ctl.cm.Connect() +func (ctl *Control) connectServer() (net.Conn, error) { + return ctl.sessionCtx.Connector.Connect() } -// reader read all messages from frps and send to readCh -func (ctl *Control) reader() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - defer ctl.readerShutdown.Done() - defer close(ctl.closedCh) - - encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token)) - for { - m, err := msg.ReadMsg(encReader) - if err != nil { - if err == io.EOF { - xl.Debug("read from control connection EOF") - return - } - xl.Warn("read error: %v", err) - ctl.conn.Close() - return - } - ctl.readCh <- m - } +func (ctl *Control) registerMsgHandlers() { + ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn)) + ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp) + ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong) } -// writer writes messages got from sendCh to frps -func (ctl *Control) writer() { +// headerWorker sends heartbeat to server and check heartbeat timeout. +func (ctl *Control) heartbeatWorker() { xl := ctl.xl - defer ctl.writerShutdown.Done() - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token)) - if err != nil { - xl.Error("crypto new writer error: %v", err) - ctl.conn.Close() - return - } - for { - m, ok := <-ctl.sendCh - if !ok { - xl.Info("control writer is closing") - return - } - if err := msg.WriteMsg(encWriter, m); err != nil { - xl.Warn("write message to control connection error: %v", err) - return + // TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled. + // Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value. + if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 { + // send heartbeat to server + sendHeartBeat := func() error { + xl.Debug("send heartbeat to server") + pingMsg := &msg.Ping{} + if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil { + xl.Warn("error during ping authentication: %v, skip sending ping message", err) + return err + } + _ = ctl.msgDispatcher.Send(pingMsg) + return nil } - } -} -// msgHandler handles all channel events and performs corresponding operations. -func (ctl *Control) msgHandler() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - defer ctl.msgHandlerShutdown.Done() - - var hbSendCh <-chan time.Time - // TODO(fatedier): disable heartbeat if TCPMux is enabled. - // Just keep it here to keep compatible with old version frps. - if ctl.clientCfg.Transport.HeartbeatInterval > 0 { - hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second) - defer hbSend.Stop() - hbSendCh = hbSend.C + go wait.BackoffUntil(sendHeartBeat, + wait.NewFastBackoffManager(wait.FastBackoffOptions{ + Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second, + InitDurationIfFail: time.Second, + Factor: 2.0, + Jitter: 0.1, + MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second, + }), + true, ctl.doneCh, + ) } - var hbCheckCh <-chan time.Time // Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature. - if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 && - !lo.FromPtr(ctl.clientCfg.Transport.TCPMux) { - hbCheck := time.NewTicker(time.Second) - defer hbCheck.Stop() - hbCheckCh = hbCheck.C - } + if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 && + !lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) { - ctl.lastPong = time.Now() - for { - select { - case <-hbSendCh: - // send heartbeat to server - xl.Debug("send heartbeat to server") - pingMsg := &msg.Ping{} - if err := ctl.authSetter.SetPing(pingMsg); err != nil { - xl.Warn("error during ping authentication: %v", err) - return - } - ctl.sendCh <- pingMsg - case <-hbCheckCh: - if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second { + go wait.Until(func() { + if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second { xl.Warn("heartbeat timeout") - // let reader() stop - ctl.conn.Close() + ctl.closeSession() return } - case rawMsg, ok := <-ctl.readCh: - if !ok { - return - } - - switch m := rawMsg.(type) { - case *msg.ReqWorkConn: - go ctl.HandleReqWorkConn(m) - case *msg.NewProxyResp: - ctl.HandleNewProxyResp(m) - case *msg.NatHoleResp: - ctl.HandleNatHoleResp(m) - case *msg.Pong: - if m.Error != "" { - xl.Error("Pong contains error: %s", m.Error) - ctl.conn.Close() - return - } - ctl.lastPong = time.Now() - xl.Debug("receive heartbeat from server") - } - } + }, time.Second, ctl.doneCh) } } -// If controler is notified by closedCh, reader and writer and handler will exit func (ctl *Control) worker() { - go ctl.msgHandler() - go ctl.reader() - go ctl.writer() - - <-ctl.closedCh - // close related channels and wait until other goroutines done - close(ctl.readCh) - ctl.readerShutdown.WaitDone() - ctl.msgHandlerShutdown.WaitDone() + go ctl.heartbeatWorker() + go ctl.msgDispatcher.Run() - close(ctl.sendCh) - ctl.writerShutdown.WaitDone() + <-ctl.msgDispatcher.Done() + ctl.closeSession() ctl.pm.Close() ctl.vm.Close() - - close(ctl.closedDoneCh) - ctl.cm.Close() + close(ctl.doneCh) } -func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { - ctl.vm.Reload(visitorCfgs) - ctl.pm.Reload(pxyCfgs) +func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { + ctl.vm.UpdateAll(visitorCfgs) + ctl.pm.UpdateAll(proxyCfgs) return nil } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 5ba63f94cce..396539c0837 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -47,10 +47,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v // Proxy defines how to handle work connections for different proxy type. type Proxy interface { Run() error - // InWorkConn accept work connections registered to server. InWorkConn(net.Conn, *msg.StartWorkConn) - + SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool) Close() } @@ -89,7 +88,8 @@ type BaseProxy struct { limiter *rate.Limiter // proxyPlugin is used to handle connections instead of dialing to local service. // It's only validate for TCP protocol now. - proxyPlugin plugin.Plugin + proxyPlugin plugin.Plugin + inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool mu sync.RWMutex xl *xlog.Logger @@ -113,7 +113,16 @@ func (pxy *BaseProxy) Close() { } } +func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pxy.inWorkConnCallback = cb +} + func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { + if pxy.inWorkConnCallback != nil { + if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) { + return + } + } pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token)) } @@ -132,7 +141,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor }) } - xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t", + xl.Trace("handle tcp work connection, useEncryption: %t, useCompression: %t", baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) if baseCfg.Transport.UseEncryption { remote, err = libio.WithEncryption(remote, encKey) diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index db66cb26397..12e2f6cfee8 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -31,8 +31,9 @@ import ( ) type Manager struct { - proxies map[string]*Wrapper - msgTransporter transport.MessageTransporter + proxies map[string]*Wrapper + msgTransporter transport.MessageTransporter + inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool closed bool mu sync.RWMutex @@ -71,6 +72,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri return nil } +func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pm.inWorkConnCallback = cb +} + func (pm *Manager) Close() { pm.mu.Lock() defer pm.mu.Unlock() @@ -115,9 +120,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus { return ps } -func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) { +func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) { + pm.mu.RLock() + defer pm.mu.RUnlock() + if pxy, ok := pm.proxies[name]; ok { + return pxy.GetStatus(), true + } + return nil, false +} + +func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { xl := xlog.FromContextSafe(pm.ctx) - pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string { + proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string { return c.GetBaseConfig().Name }) pm.mu.Lock() @@ -126,7 +140,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) { delPxyNames := make([]string, 0) for name, pxy := range pm.proxies { del := false - cfg, ok := pxyCfgsMap[name] + cfg, ok := proxyCfgsMap[name] if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) { del = true } @@ -142,10 +156,13 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) { } addPxyNames := make([]string, 0) - for _, cfg := range pxyCfgs { + 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) + if pm.inWorkConnCallback != nil { + pxy.SetInWorkConnCallback(pm.inWorkConnCallback) + } pm.proxies[name] = pxy addPxyNames = append(addPxyNames, name) diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 346c6d076a8..84f24abb668 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -121,6 +121,10 @@ func NewWrapper( return pw } +func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pw.pxy.SetInWorkConnCallback(cb) +} + func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error { pw.mu.Lock() defer pw.mu.Unlock() diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index e67a33974f0..4d06170dc90 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package proxy import ( @@ -29,7 +31,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -99,7 +101,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } - conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) + conn = netpkg.WrapReadWriteCloserToConn(rwc, conn) workConn := conn readCh := make(chan *msg.UDPPacket, 1024) diff --git a/client/proxy/udp.go b/client/proxy/udp.go index 0a5cefcc291..38d14ff598b 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package proxy import ( @@ -28,7 +30,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -89,7 +91,7 @@ func (pxy *UDPProxy) Close() { func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { xl := pxy.xl xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) - // close resources releated with old workConn + // close resources related with old workConn pxy.Close() var rwc io.ReadWriteCloser = conn @@ -110,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } - conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) + conn = netpkg.WrapReadWriteCloserToConn(rwc, conn) pxy.mu.Lock() pxy.workConn = conn diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 8271099bb50..e5e5d47e280 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package proxy import ( @@ -27,7 +29,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -131,7 +133,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s } defer lConn.Close() - remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String()) + remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) if err != nil { xl.Warn("create kcp connection from udp connection error: %v", err) return @@ -192,6 +194,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star _ = c.CloseWithError(0, "") return } - go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey)) + go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey)) } } diff --git a/client/service.go b/client/service.go index 184a87a3305..c43f8f60497 100644 --- a/client/service.go +++ b/client/service.go @@ -16,32 +16,25 @@ package client import ( "context" - "crypto/tls" + "errors" "fmt" - "io" "net" "runtime" - "strconv" - "strings" "sync" - "sync/atomic" "time" "github.com/fatedier/golib/crypto" - libdial "github.com/fatedier/golib/net/dial" - fmux "github.com/hashicorp/yamux" - quic "github.com/quic-go/quic-go" "github.com/samber/lo" - "github.com/fatedier/frp/assets" + "github.com/fatedier/frp/client/proxy" "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/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" - utilnet "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/version" + "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -49,212 +42,197 @@ func init() { crypto.DefaultSalt = "frp" } -// Service is a client service. -type Service struct { - // uniq id got from frps, attach it in loginMsg - runID string +type cancelErr struct { + Err error +} - // manager control connection with server - ctl *Control +func (e cancelErr) Error() string { + return e.Err.Error() +} + +// ServiceOptions contains options for creating a new client service. +type ServiceOptions struct { + Common *v1.ClientCommonConfig + ProxyCfgs []v1.ProxyConfigurer + VisitorCfgs []v1.VisitorConfigurer + + // 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. + ConfigFilePath string + + // ClientSpec is the client specification that control the client behavior. + ClientSpec *msg.ClientSpec + + // ConnectorCreator is a function that creates a new connector to make connections to the server. + // The Connector shields the underlying connection details, whether it is through TCP or QUIC connection, + // and regardless of whether multiplexing is used. + // + // If it is not set, the default frpc connector will be used. + // By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps + // through a pipe instead of a real physical connection. + ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector + + // HandleWorkConnCb is a callback function that is called when a new work connection is created. + // + // If it is not set, the default frpc implementation will be used. + HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool +} + +// setServiceOptionsDefault sets the default values for ServiceOptions. +func setServiceOptionsDefault(options *ServiceOptions) { + if options.Common != nil { + options.Common.Complete() + } + if options.ConnectorCreator == nil { + options.ConnectorCreator = NewConnector + } +} + +// Service is the client service that connects to frps and provides proxy services. +type Service struct { ctlMu sync.RWMutex + // manager control connection with server + ctl *Control + // Uniq id got from frps, it will be attached to loginMsg. + runID string // Sets authentication based on selected method authSetter auth.Setter - cfg *v1.ClientCommonConfig - pxyCfgs []v1.ProxyConfigurer - visitorCfgs []v1.VisitorConfigurer + // web server for admin UI and apis + webServer *httppkg.Server + cfgMu sync.RWMutex + common *v1.ClientCommonConfig + proxyCfgs []v1.ProxyConfigurer + visitorCfgs []v1.VisitorConfigurer + clientSpec *msg.ClientSpec // The configuration file used to initialize this client, or an empty // string if no configuration file was used. - cfgFile string - - exit uint32 // 0 means not exit + configFilePath string // service context ctx context.Context // call cancel to stop service - cancel context.CancelFunc -} + cancel context.CancelCauseFunc + gracefulShutdownDuration time.Duration -func NewService( - cfg *v1.ClientCommonConfig, - pxyCfgs []v1.ProxyConfigurer, - visitorCfgs []v1.VisitorConfigurer, - cfgFile string, -) (svr *Service, err error) { - svr = &Service{ - authSetter: auth.NewAuthSetter(cfg.Auth), - cfg: cfg, - cfgFile: cfgFile, - pxyCfgs: pxyCfgs, - visitorCfgs: visitorCfgs, - ctx: context.Background(), - exit: 0, - } - return + connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector + handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool } -func (svr *Service) GetController() *Control { - svr.ctlMu.RLock() - defer svr.ctlMu.RUnlock() - return svr.ctl +func NewService(options ServiceOptions) (*Service, error) { + setServiceOptionsDefault(&options) + + var webServer *httppkg.Server + if options.Common.WebServer.Port > 0 { + ws, err := httppkg.NewServer(options.Common.WebServer) + if err != nil { + return nil, err + } + webServer = ws + } + s := &Service{ + ctx: context.Background(), + authSetter: auth.NewAuthSetter(options.Common.Auth), + webServer: webServer, + common: options.Common, + configFilePath: options.ConfigFilePath, + proxyCfgs: options.ProxyCfgs, + visitorCfgs: options.VisitorCfgs, + clientSpec: options.ClientSpec, + connectorCreator: options.ConnectorCreator, + handleWorkConnCb: options.HandleWorkConnCb, + } + if webServer != nil { + webServer.RouteRegister(s.registerRouteHandlers) + } + return s, nil } func (svr *Service) Run(ctx context.Context) error { - ctx, cancel := context.WithCancel(ctx) - svr.ctx = xlog.NewContext(ctx, xlog.New()) + ctx, cancel := context.WithCancelCause(ctx) + svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx)) svr.cancel = cancel - xl := xlog.FromContextSafe(svr.ctx) - // set custom DNSServer - if svr.cfg.DNSServer != "" { - dnsAddr := svr.cfg.DNSServer - if _, _, err := net.SplitHostPort(dnsAddr); err != nil { - dnsAddr = net.JoinHostPort(dnsAddr, "53") - } - // Change default dns server for frpc - net.DefaultResolver = &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - return net.Dial("udp", dnsAddr) - }, - } + if svr.common.DNSServer != "" { + netpkg.SetDefaultDNSAddress(svr.common.DNSServer) } - // login to frps - for { - conn, cm, err := svr.login() - if err != nil { - xl.Warn("login to server failed: %v", err) - - // if login_fail_exit is true, just exit this program - // otherwise sleep a while and try again to connect to server - if lo.FromPtr(svr.cfg.LoginFailExit) { - return err - } - util.RandomSleep(5*time.Second, 0.9, 1.1) - } else { - // login success - ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter) - ctl.Run() - svr.ctlMu.Lock() - svr.ctl = ctl - svr.ctlMu.Unlock() - break - } + // first login to frps + svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit)) + if svr.ctl == nil { + cancelCause := cancelErr{} + _ = errors.As(context.Cause(svr.ctx), &cancelCause) + return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err) } go svr.keepControllerWorking() - if svr.cfg.WebServer.Port != 0 { - // Init admin server assets - assets.Load(svr.cfg.WebServer.AssetsDir) - - address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port)) - err := svr.RunAdminServer(address) - if err != nil { - log.Warn("run admin server error: %v", err) - } - log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port) + if svr.webServer != nil { + go func() { + log.Info("admin server listen on %s", svr.webServer.Address()) + if err := svr.webServer.Run(); err != nil { + log.Warn("admin server exit with error: %v", err) + } + }() } <-svr.ctx.Done() - // service context may not be canceled by svr.Close(), we should call it here to release resources - if atomic.LoadUint32(&svr.exit) == 0 { - svr.Close() - } + svr.stop() return nil } func (svr *Service) keepControllerWorking() { - xl := xlog.FromContextSafe(svr.ctx) - maxDelayTime := 20 * time.Second - delayTime := time.Second - - // if frpc reconnect frps, we need to limit retry times in 1min - // current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ... - // when exceed 1min, we will reset delay and counts - cutoffTime := time.Now().Add(time.Minute) - reconnectDelay := time.Second - reconnectCounts := 1 - - for { - <-svr.ctl.ClosedDoneCh() - if atomic.LoadUint32(&svr.exit) != 0 { - return - } - - // the first three attempts with a low delay - if reconnectCounts > 3 { - util.RandomSleep(reconnectDelay, 0.9, 1.1) - xl.Info("wait %v to reconnect", reconnectDelay) - reconnectDelay *= 2 - } else { - util.RandomSleep(time.Second, 0, 0.5) - } - reconnectCounts++ - - now := time.Now() - if now.After(cutoffTime) { - // reset - cutoffTime = now.Add(time.Minute) - reconnectDelay = time.Second - reconnectCounts = 1 - } - - for { - if atomic.LoadUint32(&svr.exit) != 0 { - return - } - - xl.Info("try to reconnect to server...") - conn, cm, err := svr.login() - if err != nil { - xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime) - util.RandomSleep(delayTime, 0.9, 1.1) - - delayTime *= 2 - if delayTime > maxDelayTime { - delayTime = maxDelayTime - } - continue - } - // reconnect success, init delayTime - delayTime = time.Second - - ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter) - ctl.Run() - svr.ctlMu.Lock() - if svr.ctl != nil { - svr.ctl.Close() - } - svr.ctl = ctl - svr.ctlMu.Unlock() - break + <-svr.ctl.Done() + + // There is a situation where the login is successful but due to certain reasons, + // the control immediately exits. It is necessary to limit the frequency of reconnection in this case. + // The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially. + // The maximum interval is 20 seconds. + wait.BackoffUntil(func() error { + // loopLoginUntilSuccess is another layer of loop that will continuously attempt to + // login to the server until successful. + svr.loopLoginUntilSuccess(20*time.Second, false) + if svr.ctl != nil { + <-svr.ctl.Done() + return errors.New("control is closed and try another loop") } - } + // If the control is nil, it means that the login failed and the service is also closed. + return nil + }, wait.NewFastBackoffManager( + wait.FastBackoffOptions{ + Duration: time.Second, + Factor: 2, + Jitter: 0.1, + MaxDuration: 20 * time.Second, + FastRetryCount: 3, + FastRetryDelay: 200 * time.Millisecond, + FastRetryWindow: time.Minute, + FastRetryJitter: 0.5, + }, + ), true, svr.ctx.Done()) } // login creates a connection to frps and registers it self as a client // conn: control connection // session: if it's not nil, using tcp mux -func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { +func (svr *Service) login() (conn net.Conn, connector Connector, err error) { xl := xlog.FromContextSafe(svr.ctx) - cm = NewConnectionManager(svr.ctx, svr.cfg) - - if err = cm.OpenConnection(); err != nil { + connector = svr.connectorCreator(svr.ctx, svr.common) + if err = connector.Open(); err != nil { return nil, nil, err } defer func() { if err != nil { - cm.Close() + connector.Close() } }() - conn, err = cm.Connect() + conn, err = connector.Connect() if err != nil { return } @@ -262,12 +240,15 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { loginMsg := &msg.Login{ Arch: runtime.GOARCH, Os: runtime.GOOS, - PoolCount: svr.cfg.Transport.PoolCount, - User: svr.cfg.User, + PoolCount: svr.common.Transport.PoolCount, + User: svr.common.User, Version: version.Full(), Timestamp: time.Now().Unix(), RunID: svr.runID, - Metas: svr.cfg.Metadatas, + Metas: svr.common.Metadatas, + } + if svr.clientSpec != nil { + loginMsg.ClientSpec = *svr.clientSpec } // Add auth @@ -293,16 +274,79 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { } svr.runID = loginRespMsg.RunID - xl.ResetPrefixes() - xl.AppendPrefix(svr.runID) + xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID}) xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID) return } -func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { +func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) { + xl := xlog.FromContextSafe(svr.ctx) + successCh := make(chan struct{}) + + loginFunc := func() error { + xl.Info("try to connect to server...") + conn, connector, err := svr.login() + if err != nil { + xl.Warn("connect to server error: %v", err) + if firstLoginExit { + svr.cancel(cancelErr{Err: err}) + } + return err + } + + svr.cfgMu.RLock() + proxyCfgs := svr.proxyCfgs + visitorCfgs := svr.visitorCfgs + svr.cfgMu.RUnlock() + connEncrypted := true + if svr.clientSpec != nil && svr.clientSpec.Type == "ssh-tunnel" { + connEncrypted = false + } + sessionCtx := &SessionContext{ + Common: svr.common, + RunID: svr.runID, + Conn: conn, + ConnEncrypted: connEncrypted, + AuthSetter: svr.authSetter, + Connector: connector, + } + ctl, err := NewControl(svr.ctx, sessionCtx) + if err != nil { + conn.Close() + xl.Error("NewControl error: %v", err) + return err + } + ctl.SetInWorkConnCallback(svr.handleWorkConnCb) + + ctl.Run(proxyCfgs, visitorCfgs) + // close and replace previous control + svr.ctlMu.Lock() + if svr.ctl != nil { + svr.ctl.Close() + } + svr.ctl = ctl + svr.ctlMu.Unlock() + + close(successCh) + return nil + } + + // try to reconnect to server until success + wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager( + wait.FastBackoffOptions{ + Duration: time.Second, + Factor: 2, + Jitter: 0.1, + MaxDuration: maxInterval, + }), + true, + wait.MergeAndCloseOnAnyStopChannel(svr.ctx.Done(), successCh)) +} + +func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { svr.cfgMu.Lock() - svr.pxyCfgs = pxyCfgs + svr.proxyCfgs = proxyCfgs svr.visitorCfgs = visitorCfgs svr.cfgMu.Unlock() @@ -311,7 +355,7 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi svr.ctlMu.RUnlock() if ctl != nil { - return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs) + return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs) } return nil } @@ -321,191 +365,31 @@ func (svr *Service) Close() { } func (svr *Service) GracefulClose(d time.Duration) { - atomic.StoreUint32(&svr.exit, 1) + svr.gracefulShutdownDuration = d + svr.cancel(nil) +} - svr.ctlMu.RLock() +func (svr *Service) stop() { + svr.ctlMu.Lock() + defer svr.ctlMu.Unlock() if svr.ctl != nil { - svr.ctl.GracefulClose(d) + svr.ctl.GracefulClose(svr.gracefulShutdownDuration) svr.ctl = nil } - svr.ctlMu.RUnlock() - - if svr.cancel != nil { - svr.cancel() - } -} - -type ConnectionManager struct { - ctx context.Context - cfg *v1.ClientCommonConfig - - muxSession *fmux.Session - quicConn quic.Connection -} - -func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager { - return &ConnectionManager{ - ctx: ctx, - cfg: cfg, - } -} - -func (cm *ConnectionManager) OpenConnection() error { - xl := xlog.FromContextSafe(cm.ctx) - - // special for quic - if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") { - var tlsConfig *tls.Config - var err error - sn := cm.cfg.Transport.TLS.ServerName - if sn == "" { - sn = cm.cfg.ServerAddr - } - if lo.FromPtr(cm.cfg.Transport.TLS.Enable) { - tlsConfig, err = transport.NewClientTLSConfig( - cm.cfg.Transport.TLS.CertFile, - cm.cfg.Transport.TLS.KeyFile, - cm.cfg.Transport.TLS.TrustedCaFile, - sn) - } else { - tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn) - } - if err != nil { - xl.Warn("fail to build tls configuration, err: %v", err) - return err - } - tlsConfig.NextProtos = []string{"frp"} - - conn, err := quic.DialAddr( - cm.ctx, - net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)), - tlsConfig, &quic.Config{ - MaxIdleTimeout: time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, - MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams), - KeepAlivePeriod: time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second, - }) - if err != nil { - return err - } - cm.quicConn = conn - return nil - } - - if !lo.FromPtr(cm.cfg.Transport.TCPMux) { - return nil - } - - conn, err := cm.realConnect() - if err != nil { - return err - } - - fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard - fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 - session, err := fmux.Client(conn, fmuxCfg) - if err != nil { - return err - } - cm.muxSession = session - return nil -} - -func (cm *ConnectionManager) Connect() (net.Conn, error) { - if cm.quicConn != nil { - stream, err := cm.quicConn.OpenStreamSync(context.Background()) - if err != nil { - return nil, err - } - return utilnet.QuicStreamToNetConn(stream, cm.quicConn), nil - } else if cm.muxSession != nil { - stream, err := cm.muxSession.OpenStream() - if err != nil { - return nil, err - } - return stream, nil - } - - return cm.realConnect() } -func (cm *ConnectionManager) realConnect() (net.Conn, error) { - xl := xlog.FromContextSafe(cm.ctx) - var tlsConfig *tls.Config - var err error - tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable) - if cm.cfg.Transport.Protocol == "wss" { - tlsEnable = true - } - if tlsEnable { - sn := cm.cfg.Transport.TLS.ServerName - if sn == "" { - sn = cm.cfg.ServerAddr - } - - tlsConfig, err = transport.NewClientTLSConfig( - cm.cfg.Transport.TLS.CertFile, - cm.cfg.Transport.TLS.KeyFile, - cm.cfg.Transport.TLS.TrustedCaFile, - sn) - if err != nil { - xl.Warn("fail to build tls configuration, err: %v", err) - return nil, err - } - } - - proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL) - if err != nil { - xl.Error("fail to parse proxy url") - return nil, err - } - dialOptions := []libdial.DialOption{} - protocol := cm.cfg.Transport.Protocol - switch protocol { - case "websocket": - protocol = "tcp" - dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")})) - dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{ - Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)), - })) - dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) - case "wss": - protocol = "tcp" - dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig)) - // Make sure that if it is wss, the websocket hook is executed after the tls hook. - dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110})) - default: - dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{ - Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)), - })) - dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) - } - - if cm.cfg.Transport.ConnectServerLocalIP != "" { - dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP)) - } - dialOptions = append(dialOptions, - libdial.WithProtocol(protocol), - libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second), - libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second), - libdial.WithProxy(proxyType, addr), - libdial.WithProxyAuth(auth), - ) - conn, err := libdial.DialContext( - cm.ctx, - net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)), - dialOptions..., - ) - return conn, err -} +// TODO(fatedier): Use StatusExporter to provide query interfaces instead of directly using methods from the Service. +func (svr *Service) GetProxyStatus(name string) (*proxy.WorkingStatus, error) { + svr.ctlMu.RLock() + ctl := svr.ctl + svr.ctlMu.RUnlock() -func (cm *ConnectionManager) Close() error { - if cm.quicConn != nil { - _ = cm.quicConn.CloseWithError(0, "") + if ctl == nil { + return nil, fmt.Errorf("control is not running") } - if cm.muxSession != nil { - _ = cm.muxSession.Close() + ws, ok := ctl.pm.GetProxyStatus(name) + if !ok { + return nil, fmt.Errorf("proxy [%s] is not found", name) } - return nil + return ws, nil } diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 159f46ee074..1d489bec42b 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -28,7 +28,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -242,7 +242,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { if sv.cfg.Transport.UseCompression { remote = libio.WithCompression(remote) } - return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil + return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil } func (sv *SUDPVisitor) Close() { diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index dcd1f7b3047..d520f735ddc 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -21,11 +21,11 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" ) -// Helper wrapps some functions for visitor to use. +// Helper wraps some functions for visitor to use. type Helper interface { // ConnectServer directly connects to the frp server. ConnectServer() (net.Conn, error) @@ -56,7 +56,7 @@ func NewVisitor( clientCfg: clientCfg, helper: helper, ctx: xlog.NewContext(ctx, xl), - internalLn: utilnet.NewInternalListener(), + internalLn: netpkg.NewInternalListener(), } switch cfg := cfg.(type) { case *v1.STCPVisitorConfig: @@ -84,7 +84,7 @@ type BaseVisitor struct { clientCfg *v1.ClientCommonConfig helper Helper l net.Listener - internalLn *utilnet.InternalListener + internalLn *netpkg.InternalListener mu sync.RWMutex ctx context.Context diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go index 4b235cdb113..4f31f2706ed 100644 --- a/client/visitor/visitor_manager.go +++ b/client/visitor/visitor_manager.go @@ -35,7 +35,8 @@ type Manager struct { visitors map[string]Visitor helper Helper - checkInterval time.Duration + checkInterval time.Duration + keepVisitorsRunningOnce sync.Once mu sync.RWMutex ctx context.Context @@ -67,7 +68,9 @@ func NewManager( return m } -func (vm *Manager) Run() { +// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it. +// It will only start after Reload is called and a new visitor is added. +func (vm *Manager) keepVisitorsRunning() { xl := xlog.FromContextSafe(vm.ctx) ticker := time.NewTicker(vm.checkInterval) @@ -76,7 +79,7 @@ func (vm *Manager) Run() { for { select { case <-vm.stopCh: - xl.Info("gracefully shutdown visitor manager") + xl.Trace("gracefully shutdown visitor manager") return case <-ticker.C: vm.mu.Lock() @@ -120,7 +123,14 @@ func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { return } -func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) { +func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) { + if len(cfgs) > 0 { + // Only start keepVisitorsRunning goroutine once and only when there is at least one visitor. + vm.keepVisitorsRunningOnce.Do(func() { + go vm.keepVisitorsRunning() + }) + } + xl := xlog.FromContextSafe(vm.ctx) cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string { return c.GetBaseConfig().Name diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index c180621c29e..ad773503e03 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -33,7 +33,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -349,7 +349,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er if err != nil { return fmt.Errorf("dial udp error: %v", err) } - remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String()) + remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) if err != nil { return fmt.Errorf("create kcp connection from udp connection error: %v", err) } @@ -440,7 +440,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) { if err != nil { return nil, err } - return utilnet.QuicStreamToNetConn(stream, session), nil + return netpkg.QuicStreamToNetConn(stream, session), nil } func (qs *QUICTunnelSession) Close() { diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go index 2a5f2830a15..5d478d44a58 100644 --- a/cmd/frpc/sub/admin.go +++ b/cmd/frpc/sub/admin.go @@ -52,7 +52,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er Use: name, Short: short, Run: func(cmd *cobra.Command, args []string) { - cfg, _, _, _, err := config.LoadClientConfig(cfgFile) + cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) @@ -73,7 +73,7 @@ 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(); err != nil { + if err := client.Reload(strictConfigMode); err != nil { return err } fmt.Println("reload success") diff --git a/cmd/frpc/sub/flags.go b/cmd/frpc/sub/flags.go deleted file mode 100644 index eb3cc010629..00000000000 --- a/cmd/frpc/sub/flags.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 The frp Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sub - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config/types" - v1 "github.com/fatedier/frp/pkg/config/v1" - "github.com/fatedier/frp/pkg/config/v1/validation" -) - -type BandwidthQuantityFlag struct { - V *types.BandwidthQuantity -} - -func (f *BandwidthQuantityFlag) Set(s string) error { - return f.V.UnmarshalString(s) -} - -func (f *BandwidthQuantityFlag) String() string { - return f.V.String() -} - -func (f *BandwidthQuantityFlag) Type() string { - return "string" -} - -func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) { - registerProxyBaseConfigFlags(cmd, c.GetBaseConfig()) - switch cc := c.(type) { - case *v1.TCPProxyConfig: - cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") - case *v1.UDPProxyConfig: - cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") - case *v1.HTTPProxyConfig: - registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) - cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations") - cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") - cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") - cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") - case *v1.HTTPSProxyConfig: - registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) - case *v1.TCPMuxProxyConfig: - registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) - cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer") - case *v1.STCPProxyConfig: - cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") - case *v1.SUDPProxyConfig: - cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") - case *v1.XTCPProxyConfig: - cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") - } -} - -func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) { - if c == nil { - return - } - cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name") - cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip") - cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port") - cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") - cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") - cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode") - cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)") -} - -func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) { - if c == nil { - return - } - cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains") - cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain") -} - -func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) { - registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig()) - - // add visitor flags if exist -} - -func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig) { - if c == nil { - return - } - cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name") - cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") - 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.BindAddr, "bind_addr", "", "", "bind addr") - cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") -} - -func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig) { - cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address") - cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port") - cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user") - cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp", - fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols)) - cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") - cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") - cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path") - cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days") - cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") - cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") - cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one") - - c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls") -} diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index 72b635f1bf1..fb5b08078c2 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -48,9 +48,10 @@ var natholeDiscoveryCmd = &cobra.Command{ Short: "Discover nathole information from stun server", RunE: func(cmd *cobra.Command, args []string) error { // ignore error here, because we can use command line pameters - cfg, _, _, _, err := config.LoadClientConfig(cfgFile) + cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { cfg = &v1.ClientCommonConfig{} + cfg.Complete() } if natHoleSTUNServer != "" { cfg.NatHoleSTUNServer = natHoleSTUNServer diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index 7ae8d353b39..960509433ca 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -21,6 +21,7 @@ import ( "github.com/samber/lo" "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" ) @@ -50,8 +51,8 @@ func init() { } clientCfg := v1.ClientCommonConfig{} cmd := NewProxyCommand(string(typ), c, &clientCfg) - RegisterClientCommonConfigFlags(cmd, &clientCfg) - RegisterProxyFlags(cmd, c) + config.RegisterClientCommonConfigFlags(cmd, &clientCfg) + config.RegisterProxyFlags(cmd, c) // add sub command for visitor if lo.Contains(visitorTypes, v1.VisitorType(typ)) { @@ -60,7 +61,7 @@ func init() { panic("visitor type: " + typ + " not support") } visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg) - RegisterVisitorFlags(visitorCmd, vc) + config.RegisterVisitorFlags(visitorCmd, vc) cmd.AddCommand(visitorCmd) } rootCmd.AddCommand(cmd) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 915e6b35174..fffc49850de 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -36,15 +36,17 @@ import ( ) var ( - cfgFile string - cfgDir string - showVersion bool + cfgFile string + cfgDir string + showVersion bool + strictConfigMode bool ) func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") 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", "", false, "strict config parsing mode, unknown fields will cause an error") } var rootCmd = &cobra.Command{ @@ -108,7 +110,7 @@ func handleTermSignal(svr *client.Service) { } func runClient(cfgFilePath string) error { - cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath) + cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode) if err != nil { return err } @@ -117,19 +119,19 @@ func runClient(cfgFilePath string) error { "please use yaml/json/toml format instead!\n") } - warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs) + warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } if err != nil { return err } - return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) + return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath) } func startService( cfg *v1.ClientCommonConfig, - pxyCfgs []v1.ProxyConfigurer, + proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer, cfgFile string, ) error { @@ -139,7 +141,12 @@ func startService( log.Info("start frpc service for config file [%s]", cfgFile) defer log.Info("frpc service for config file [%s] stopped", cfgFile) } - svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) + svr, err := client.NewService(client.ServiceOptions{ + Common: cfg, + ProxyCfgs: proxyCfgs, + VisitorCfgs: visitorCfgs, + ConfigFilePath: cfgFile, + }) if err != nil { return err } @@ -149,7 +156,5 @@ func startService( if shouldGracefulClose { go handleTermSignal(svr) } - - _ = svr.Run(context.Background()) - return nil + return svr.Run(context.Background()) } diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index a84f54f2a8f..4b971f531c7 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -37,12 +37,12 @@ var verifyCmd = &cobra.Command{ return nil } - cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile) + cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) } - warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs) + warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs) if warning != nil { fmt.Printf("WARNING: %v\n", warning) } diff --git a/cmd/frps/flags.go b/cmd/frps/flags.go deleted file mode 100644 index 50170684734..00000000000 --- a/cmd/frps/flags.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2023 The frp Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "strconv" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config/types" - v1 "github.com/fatedier/frp/pkg/config/v1" -) - -type PortsRangeSliceFlag struct { - V *[]types.PortsRange -} - -func (f *PortsRangeSliceFlag) String() string { - if f.V == nil { - return "" - } - return types.PortsRangeSlice(*f.V).String() -} - -func (f *PortsRangeSliceFlag) Set(s string) error { - slice, err := types.NewPortsRangeSliceFromString(s) - if err != nil { - return err - } - *f.V = slice - return nil -} - -func (f *PortsRangeSliceFlag) Type() string { - return "string" -} - -type BoolFuncFlag struct { - TrueFunc func() - FalseFunc func() - - v bool -} - -func (f *BoolFuncFlag) String() string { - return strconv.FormatBool(f.v) -} - -func (f *BoolFuncFlag) Set(s string) error { - f.v = strconv.FormatBool(f.v) == "true" - - if !f.v { - if f.FalseFunc != nil { - f.FalseFunc() - } - return nil - } - - if f.TrueFunc != nil { - f.TrueFunc() - } - return nil -} - -func (f *BoolFuncFlag) Type() string { - return "bool" -} - -func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) { - 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().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") - cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") - cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address") - cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port") - cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user") - cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password") - cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") - cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file") - cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") - cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days") - cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") - cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") - cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host") - cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports") - cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") - cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only") - - webServerTLS := v1.TLSConfig{} - cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") - cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") - cmd.PersistentFlags().VarP(&BoolFuncFlag{ - TrueFunc: func() { c.WebServer.TLS = &webServerTLS }, - }, "dashboard_tls_mode", "", "if enable dashboard tls mode") -} diff --git a/cmd/frps/root.go b/cmd/frps/root.go index 4a6f0117620..0cf8e4e79d7 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -30,8 +30,9 @@ import ( ) var ( - cfgFile string - showVersion bool + cfgFile string + showVersion bool + strictConfigMode bool serverCfg v1.ServerConfig ) @@ -39,8 +40,9 @@ var ( 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", "", false, "strict config parsing mode, unknown fields will cause error") - RegisterServerConfigFlags(rootCmd, &serverCfg) + config.RegisterServerConfigFlags(rootCmd, &serverCfg) } var rootCmd = &cobra.Command{ @@ -58,7 +60,7 @@ var rootCmd = &cobra.Command{ err error ) if cfgFile != "" { - svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile) + svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go index 4f0cefb18fb..33ad3f63229 100644 --- a/cmd/frps/verify.go +++ b/cmd/frps/verify.go @@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{ fmt.Println("frps: the configuration file is not specified") return nil } - svrCfg, _, err := config.LoadServerConfig(cfgFile) + svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index bdfc5643031..247d0a6af4f 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -38,7 +38,7 @@ auth.token = "12345678" # auth.oidc.clientSecret = "" # oidc.audience specifies the audience of the token in OIDC authentication. # auth.oidc.audience = "" -# oidc.scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". # auth.oidc.scope = "" # oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint. # It will be used to get an OIDC token. diff --git a/conf/frps_full_example.toml b/conf/frps_full_example.toml index d25f6473b35..88cf60ebc66 100644 --- a/conf/frps_full_example.toml +++ b/conf/frps_full_example.toml @@ -143,6 +143,14 @@ udpPacketSize = 1500 # Retention time for NAT hole punching strategy data. natholeAnalysisDataReserveHours = 168 +# ssh tunnel gateway +# If you want to enable this feature, the bindPort parameter is required, while others are optional. +# By default, this feature is disabled. It will be enabled if bindPort is greater than 0. +# sshTunnelGateway.bindPort = 2200 +# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa" +# sshTunnelGateway.autoGenPrivateKeyPath = "" +# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys" + [[httpPlugins]] name = "user-manager" addr = "127.0.0.1:9000" diff --git a/conf/legacy/frpc_legacy_full.ini b/conf/legacy/frpc_legacy_full.ini index f8eca6b774c..51ac9c47725 100644 --- a/conf/legacy/frpc_legacy_full.ini +++ b/conf/legacy/frpc_legacy_full.ini @@ -56,7 +56,7 @@ oidc_client_secret = # oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". oidc_audience = -# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". oidc_scope = # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint. diff --git a/doc/pic/donate-alipay.png b/doc/pic/donate-alipay.png deleted file mode 100644 index f717145ca67..00000000000 Binary files a/doc/pic/donate-alipay.png and /dev/null differ diff --git a/doc/pic/sponsor_asocks.jpg b/doc/pic/sponsor_asocks.jpg deleted file mode 100644 index c970decf817..00000000000 Binary files a/doc/pic/sponsor_asocks.jpg and /dev/null differ diff --git a/doc/pic/sponsor_nango.png b/doc/pic/sponsor_nango.png new file mode 100644 index 00000000000..4b83565698c Binary files /dev/null and b/doc/pic/sponsor_nango.png differ diff --git a/doc/ssh_tunnel_gateway.md b/doc/ssh_tunnel_gateway.md new file mode 100644 index 00000000000..b3dd4c34376 --- /dev/null +++ b/doc/ssh_tunnel_gateway.md @@ -0,0 +1,160 @@ +### SSH Tunnel Gateway + +*Added in v0.53.0* + +### Concept + +SSH supports reverse proxy capabilities [rfc](https://www.rfc-editor.org/rfc/rfc4254#page-16). + +frp supports listening on an SSH port on the frps side to achieve TCP protocol proxying using the SSH -R protocol. This mode does not rely on frpc. + +SSH reverse tunneling proxying and proxying SSH ports through frp are two different concepts. SSH reverse tunneling proxying is essentially a basic reverse proxying accomplished by connecting to frps via an SSH client when you don't want to use frpc. + +```toml +# frps.toml +sshTunnelGateway.bindPort = 0 +sshTunnelGateway.privateKeyFile = "" +sshTunnelGateway.autoGenPrivateKeyPath = "" +sshTunnelGateway.authorizedKeysFile = "" +``` + +| Field | Type | Description | Required | +| :--- | :--- | :--- | :--- | +| bindPort| int | The ssh server port that frps listens on.| Yes | +| privateKeyFile | string | Default value is empty. The private key file used by the ssh server. If it is empty, frps will read the private key file under the autoGenPrivateKeyPath path. It can reuse the /home/user/.ssh/id_rsa file on the local machine, or a custom path can be specified.| No | +| autoGenPrivateKeyPath | string |Default value is ./.autogen_ssh_key. If the file does not exist or its content is empty, frps will automatically generate RSA private key file content and store it in this file.|No| +| authorizedKeysFile | string |Default value is empty. If it is empty, ssh client authentication is not authenticated. If it is not empty, it can implement ssh password-free login authentication. It can reuse the local /home/user/.ssh/authorized_keys file or a custom path can be specified.| No | + +### Basic Usage + +#### Server-side frps + +Minimal configuration: + +```toml +sshTunnelGateway.bindPort = 2200 +``` + +Place the above configuration in frps.toml and run `./frps -c frps.toml`. It will listen on port 2200 and accept SSH reverse proxy requests. + +Note: + +1. When using the minimal configuration, a `.autogen_ssh_key` private key file will be automatically created in the current working directory. The SSH server of frps will use this private key file for encryption and decryption. Alternatively, you can reuse an existing private key file on your local machine, such as `/home/user/.ssh/id_rsa`. + +2. When running frps in the minimal configuration mode, connecting to frps via SSH does not require authentication. It is strongly recommended to configure a token in frps and specify the token in the SSH command line. + +#### Client-side SSH + +The command format is: + +```bash +ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token} +``` + +1. `--proxy_name` is optional, and if left empty, a random one will be generated. +2. The username for logging in to frps is always "v0" and currently has no significance, i.e., `v0@{frps_address}`. +3. The server-side proxy listens on the port determined by `--remote_port`. +4. `{tcp|http|https|stcp|tcpmux}` supports the complete command parameters, which can be obtained by using `--help`. For example: `ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help`. +5. The token is optional, but for security reasons, it is strongly recommended to configure the token in frps. + +#### TCP Proxy + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090 +``` + +This sets up a proxy on frps that listens on port 9090 and proxies local service on port 8080. + +```bash +frp (via SSH) (Ctrl+C to quit) + +User: +ProxyName: test-tcp +Type: tcp +RemoteAddress: :9090 +``` + +Equivalent to: + +```bash +frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090 +``` + +More parameters can be obtained by executing `--help`. + +#### HTTP Proxy + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 http --proxy_name "test-http" --custom_domain test-http.frps.com +``` + +Equivalent to: +```bash +frpc http --proxy_name "test-http" --custom_domain test-http.frps.com +``` + +You can access the HTTP service using the following command: + +curl 'http://test-http.frps.com' + +More parameters can be obtained by executing --help. + +#### HTTPS/STCP/TCPMUX Proxy + +To obtain the usage instructions, use the following command: + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 {https|stcp|tcpmux} --help +``` + +### Advanced Usage + +#### Reusing the id_rsa File on the Local Machine + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa" +``` + +During the SSH protocol handshake, public keys are exchanged for data encryption. Therefore, the SSH server on the frps side needs to specify a private key file, which can be reused from an existing file on the local machine. If the privateKeyFile field is empty, frps will automatically create an RSA private key file. + +#### Specifying the Auto-Generated Private Key File Path + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file" +``` + +frps will automatically create a private key file and store it at the specified path. + +Note: Changing the private key file in frps can cause SSH client login failures. If you need to log in successfully, you can delete the old records from the `/home/user/.ssh/known_hosts` file. + +#### Using an Existing authorized_keys File for SSH Public Key Authentication + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys" +``` + +The authorizedKeysFile is the file used for SSH public key authentication, which contains the public key information for users, with one key per line. + +If authorizedKeysFile is empty, frps won't perform any authentication for SSH clients. Frps does not support SSH username and password authentication. + +You can reuse an existing `authorized_keys` file on your local machine for client authentication. + +Note: authorizedKeysFile is for user authentication during the SSH login phase, while the token is for frps authentication. These two authentication methods are independent. SSH authentication comes first, followed by frps token authentication. It is strongly recommended to enable at least one of them. If authorizedKeysFile is empty, it is highly recommended to enable token authentication in frps to avoid security risks. + +#### Using a Custom authorized_keys File for SSH Public Key Authentication + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file" +``` + +Specify the path to a custom `authorized_keys` file. + +Note that changes to the authorizedKeysFile file may result in SSH authentication failures. You may need to re-add the public key information to the authorizedKeysFile. diff --git a/go.mod b/go.mod index f7996399484..4e178fb5393 100644 --- a/go.mod +++ b/go.mod @@ -21,9 +21,11 @@ require ( github.com/quic-go/quic-go v0.37.4 github.com/rodaine/table v1.1.0 github.com/samber/lo v1.38.1 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.12.0 + golang.org/x/crypto v0.15.0 + golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.10.0 golang.org/x/sync v0.3.0 golang.org/x/time v0.3.0 @@ -60,15 +62,13 @@ require ( github.com/prometheus/procfs v0.10.1 // indirect github.com/quic-go/qtls-go1-20 v0.3.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect github.com/tjfoc/gmsm v1.4.1 // indirect - golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index 4cab567e29b..56966be2f0d 100644 --- a/go.sum +++ b/go.sum @@ -16,7 +16,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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= @@ -128,8 +128,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +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= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -157,8 +157,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh 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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 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= @@ -183,8 +183,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= @@ -210,20 +210,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.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.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/auth/pass.go b/pkg/auth/pass.go new file mode 100644 index 00000000000..2eaf3f0bd70 --- /dev/null +++ b/pkg/auth/pass.go @@ -0,0 +1,31 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "github.com/fatedier/frp/pkg/msg" +) + +var AlwaysPassVerifier = &alwaysPass{} + +var _ Verifier = &alwaysPass{} + +type alwaysPass struct{} + +func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil } + +func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil } + +func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil } diff --git a/pkg/config/flags.go b/pkg/config/flags.go new file mode 100644 index 00000000000..712e3d3fba3 --- /dev/null +++ b/pkg/config/flags.go @@ -0,0 +1,244 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +type RegisterFlagOption func(*registerFlagOptions) + +type registerFlagOptions struct { + sshMode bool +} + +func WithSSHMode() RegisterFlagOption { + return func(o *registerFlagOptions) { + o.sshMode = true + } +} + +type BandwidthQuantityFlag struct { + V *types.BandwidthQuantity +} + +func (f *BandwidthQuantityFlag) Set(s string) error { + return f.V.UnmarshalString(s) +} + +func (f *BandwidthQuantityFlag) String() string { + return f.V.String() +} + +func (f *BandwidthQuantityFlag) Type() string { + return "string" +} + +func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer, opts ...RegisterFlagOption) { + registerProxyBaseConfigFlags(cmd, c.GetBaseConfig(), opts...) + + switch cc := c.(type) { + case *v1.TCPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.UDPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.HTTPProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations") + cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") + cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") + cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") + case *v1.HTTPSProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + case *v1.TCPMuxProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer") + cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") + cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") + case *v1.STCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + case *v1.SUDPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + case *v1.XTCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + } +} + +func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) { + if c == nil { + return + } + options := ®isterFlagOptions{} + for _, opt := range opts { + opt(options) + } + + cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name") + + if !options.sshMode { + cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip") + cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") + cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode") + cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)") + } +} + +func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) { + if c == nil { + return + } + cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains") + cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain") +} + +func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer, opts ...RegisterFlagOption) { + registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig(), opts...) + + // add visitor flags if exist +} + +func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) { + if c == nil { + return + } + cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + 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.BindAddr, "bind_addr", "", "", "bind addr") + cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") +} + +func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig, opts ...RegisterFlagOption) { + options := ®isterFlagOptions{} + for _, opt := range opts { + opt(options) + } + + if !options.sshMode { + cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address") + cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port") + cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp", + fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols)) + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") + cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one") + c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls") + } + cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user") + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") +} + +type PortsRangeSliceFlag struct { + V *[]types.PortsRange +} + +func (f *PortsRangeSliceFlag) String() string { + if f.V == nil { + return "" + } + return types.PortsRangeSlice(*f.V).String() +} + +func (f *PortsRangeSliceFlag) Set(s string) error { + slice, err := types.NewPortsRangeSliceFromString(s) + if err != nil { + return err + } + *f.V = slice + return nil +} + +func (f *PortsRangeSliceFlag) Type() string { + return "string" +} + +type BoolFuncFlag struct { + TrueFunc func() + FalseFunc func() + + v bool +} + +func (f *BoolFuncFlag) String() string { + return strconv.FormatBool(f.v) +} + +func (f *BoolFuncFlag) Set(s string) error { + f.v = strconv.FormatBool(f.v) == "true" + + if !f.v { + if f.FalseFunc != nil { + f.FalseFunc() + } + return nil + } + + if f.TrueFunc != nil { + f.TrueFunc() + } + return nil +} + +func (f *BoolFuncFlag) Type() string { + return "bool" +} + +func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...RegisterFlagOption) { + 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().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") + cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") + cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address") + cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port") + cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user") + cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password") + cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file") + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host") + cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports") + cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") + cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only") + + webServerTLS := v1.TLSConfig{} + cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") + cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") + cmd.PersistentFlags().VarP(&BoolFuncFlag{ + TrueFunc: func() { c.WebServer.TLS = &webServerTLS }, + }, "dashboard_tls_mode", "", "if enable dashboard tls mode") +} diff --git a/pkg/config/legacy/client.go b/pkg/config/legacy/client.go index f7257cb55f2..50f62bef1bc 100644 --- a/pkg/config/legacy/client.go +++ b/pkg/config/legacy/client.go @@ -99,7 +99,7 @@ type ClientCommonConf struct { // the server must have TCP multiplexing enabled as well. By default, this // value is true. TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` // User specifies a prefix for proxy names to distinguish them from other diff --git a/pkg/config/legacy/parse.go b/pkg/config/legacy/parse.go index 637783bc73b..80850f6f957 100644 --- a/pkg/config/legacy/parse.go +++ b/pkg/config/legacy/parse.go @@ -23,7 +23,7 @@ import ( func ParseClientConfig(filePath string) ( cfg ClientCommonConf, - pxyCfgs map[string]ProxyConf, + proxyCfgs map[string]ProxyConf, visitorCfgs map[string]VisitorConf, err error, ) { @@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) ( configBuffer.Write(buf) // Parse all proxy and visitor configs. - pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) + proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) if err != nil { return } diff --git a/pkg/config/legacy/server.go b/pkg/config/legacy/server.go index 797770a3a94..1279a499057 100644 --- a/pkg/config/legacy/server.go +++ b/pkg/config/legacy/server.go @@ -139,7 +139,7 @@ type ServerCommonConf struct { // from a client to share a single TCP connection. By default, this value // is true. TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. diff --git a/pkg/config/load.go b/pkg/config/load.go index af2c3e80704..cdbb8e916f4 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -100,26 +100,42 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) { return RenderWithTemplate(b, values) } -func LoadConfigureFromFile(path string, c any) error { +func LoadConfigureFromFile(path string, c any, strict bool) error { content, err := LoadFileContentWithTemplate(path, GetValues()) if err != nil { return err } - return LoadConfigure(content, c) + return LoadConfigure(content, c, strict) } // LoadConfigure loads configuration from bytes and unmarshal into c. // Now it supports json, yaml and toml format. -func LoadConfigure(b []byte, c any) error { +func LoadConfigure(b []byte, c any, strict bool) error { + v1.DisallowUnknownFieldsMu.Lock() + defer v1.DisallowUnknownFieldsMu.Unlock() + v1.DisallowUnknownFields = strict + var tomlObj interface{} + // 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) if err != nil { return err } } - decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096) - return decoder.Decode(c) + // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. + if yaml.IsJSONBuffer(b) { + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if strict { + decoder.DisallowUnknownFields() + } + return decoder.Decode(c) + } + // It wasn't JSON. Unmarshal as YAML. + if strict { + return yaml.UnmarshalStrict(b, c) + } + return yaml.Unmarshal(b, c) } func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { @@ -139,7 +155,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1. return configurer, nil } -func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { +func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) { var ( svrCfg *v1.ServerConfig isLegacyFormat bool @@ -158,7 +174,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { isLegacyFormat = true } else { svrCfg = &v1.ServerConfig{} - if err := LoadConfigureFromFile(path, svrCfg); err != nil { + if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil { return nil, false, err } } @@ -168,7 +184,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { return svrCfg, isLegacyFormat, nil } -func LoadClientConfig(path string) ( +func LoadClientConfig(path string, strict bool) ( *v1.ClientCommonConfig, []v1.ProxyConfigurer, []v1.VisitorConfigurer, @@ -176,19 +192,19 @@ func LoadClientConfig(path string) ( ) { var ( cliCfg *v1.ClientCommonConfig - pxyCfgs = make([]v1.ProxyConfigurer, 0) + proxyCfgs = make([]v1.ProxyConfigurer, 0) visitorCfgs = make([]v1.VisitorConfigurer, 0) isLegacyFormat bool ) if DetectLegacyINIFormatFromFile(path) { - legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) + legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) if err != nil { return nil, nil, nil, true, err } cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) - for _, c := range legacyPxyCfgs { - pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) + for _, c := range legacyProxyCfgs { + proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) } for _, c := range legacyVisitorCfgs { visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c)) @@ -196,12 +212,12 @@ func LoadClientConfig(path string) ( isLegacyFormat = true } else { allCfg := v1.ClientConfig{} - if err := LoadConfigureFromFile(path, &allCfg); err != nil { + if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil { return nil, nil, nil, false, err } cliCfg = &allCfg.ClientCommonConfig for _, c := range allCfg.Proxies { - pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) + proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) } for _, c := range allCfg.Visitors { visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) @@ -209,20 +225,20 @@ func LoadClientConfig(path string) ( } // Load additional config from includes. - // legacy ini format alredy handle this in ParseClientConfig. + // legacy ini format already handle this in ParseClientConfig. if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat { - extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat) + extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict) if err != nil { return nil, nil, nil, isLegacyFormat, err } - pxyCfgs = append(pxyCfgs, extPxyCfgs...) + proxyCfgs = append(proxyCfgs, extProxyCfgs...) visitorCfgs = append(visitorCfgs, extVisitorCfgs...) } // Filter by start if len(cliCfg.Start) > 0 { startSet := sets.New(cliCfg.Start...) - pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { + proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { return startSet.Has(c.GetBaseConfig().Name) }) visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { @@ -233,17 +249,17 @@ func LoadClientConfig(path string) ( if cliCfg != nil { cliCfg.Complete() } - for _, c := range pxyCfgs { + for _, c := range proxyCfgs { c.Complete(cliCfg.User) } for _, c := range visitorCfgs { c.Complete(cliCfg) } - return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil + return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil } -func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { - pxyCfgs := make([]v1.ProxyConfigurer, 0) +func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + proxyCfgs := make([]v1.ProxyConfigurer, 0) visitorCfgs := make([]v1.VisitorConfigurer, 0) for _, path := range paths { absDir, err := filepath.Abs(filepath.Dir(path)) @@ -265,11 +281,11 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched { // support yaml/json/toml cfg := v1.ClientConfig{} - if err := LoadConfigureFromFile(absFile, &cfg); err != nil { + if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil { return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err) } for _, c := range cfg.Proxies { - pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) + proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) } for _, c := range cfg.Visitors { visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) @@ -277,5 +293,5 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox } } } - return pxyCfgs, visitorCfgs, nil + return proxyCfgs, visitorCfgs, nil } diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index eab4ba96b7d..b3f77800449 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -15,6 +15,8 @@ package config import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -22,9 +24,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" ) -func TestLoadConfigure(t *testing.T) { - require := require.New(t) - content := ` +const tomlServerContent = ` bindAddr = "127.0.0.1" kcpBindPort = 7000 quicBindPort = 7001 @@ -33,13 +33,134 @@ custom404Page = "/abc.html" transport.tcpKeepalive = 10 ` - svrCfg := v1.ServerConfig{} - err := LoadConfigure([]byte(content), &svrCfg) +const yamlServerContent = ` +bindAddr: 127.0.0.1 +kcpBindPort: 7000 +quicBindPort: 7001 +tcpmuxHTTPConnectPort: 7005 +custom404Page: /abc.html +transport: + tcpKeepalive: 10 +` + +const jsonServerContent = ` +{ + "bindAddr": "127.0.0.1", + "kcpBindPort": 7000, + "quicBindPort": 7001, + "tcpmuxHTTPConnectPort": 7005, + "custom404Page": "/abc.html", + "transport": { + "tcpKeepalive": 10 + } +} +` + +func TestLoadServerConfig(t *testing.T) { + tests := []struct { + name string + content string + }{ + {"toml", tomlServerContent}, + {"yaml", yamlServerContent}, + {"json", jsonServerContent}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + svrCfg := v1.ServerConfig{} + err := LoadConfigure([]byte(test.content), &svrCfg, true) + require.NoError(err) + require.EqualValues("127.0.0.1", svrCfg.BindAddr) + require.EqualValues(7000, svrCfg.KCPBindPort) + require.EqualValues(7001, svrCfg.QUICBindPort) + require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) + require.EqualValues("/abc.html", svrCfg.Custom404Page) + require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) + }) + } +} + +// Test that loading in strict mode fails when the config is invalid. +func TestLoadServerConfigStrictMode(t *testing.T) { + tests := []struct { + name string + content string + }{ + {"toml", tomlServerContent}, + {"yaml", yamlServerContent}, + {"json", jsonServerContent}, + } + + for _, strict := range []bool{false, true} { + for _, test := range tests { + t.Run(fmt.Sprintf("%s-strict-%t", test.name, strict), func(t *testing.T) { + require := require.New(t) + // Break the content with an innocent typo + brokenContent := strings.Replace(test.content, "bindAddr", "bindAdur", 1) + svrCfg := v1.ServerConfig{} + err := LoadConfigure([]byte(brokenContent), &svrCfg, strict) + if strict { + require.ErrorContains(err, "bindAdur") + } else { + require.NoError(err) + // BindAddr didn't get parsed because of the typo. + require.EqualValues("", svrCfg.BindAddr) + } + }) + } + } +} + +func TestCustomStructStrictMode(t *testing.T) { + require := require.New(t) + + proxyStr := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +` + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(proxyStr), &clientCfg, true) + require.NoError(err) + + proxyStr += `unknown = "unknown"` + err = LoadConfigure([]byte(proxyStr), &clientCfg, true) + require.Error(err) + + visitorStr := ` +serverPort = 7000 + +[[visitors]] +name = "test" +type = "stcp" +bindPort = 6000 +serverName = "server" +` + err = LoadConfigure([]byte(visitorStr), &clientCfg, true) + require.NoError(err) + + visitorStr += `unknown = "unknown"` + err = LoadConfigure([]byte(visitorStr), &clientCfg, true) + require.Error(err) + + pluginStr := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +[proxies.plugin] +type = "unix_domain_socket" +unixPath = "/tmp/uds.sock" +` + err = LoadConfigure([]byte(pluginStr), &clientCfg, true) require.NoError(err) - require.EqualValues("127.0.0.1", svrCfg.BindAddr) - require.EqualValues(7000, svrCfg.KCPBindPort) - require.EqualValues(7001, svrCfg.QUICBindPort) - require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) - require.EqualValues("/abc.html", svrCfg.Custom404Page) - require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) + pluginStr += `unknown = "unknown"` + err = LoadConfigure([]byte(pluginStr), &clientCfg, true) + require.Error(err) } diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index 9029aa73f40..52b876905d9 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -111,7 +111,7 @@ type ClientTransportConfig struct { // the server must have TCP multiplexing enabled as well. By default, this // value is true. TCPMux *bool `json:"tcpMux,omitempty"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` // QUIC protocol options. diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index 422a8082ede..24ec9b0d825 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -15,9 +15,23 @@ package v1 import ( + "sync" + "github.com/fatedier/frp/pkg/util/util" ) +// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method +// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder. +// Here, a global variable is temporarily used to control whether unknown fields are allowed. +// Once the v2 version is implemented by the community, we can switch to a standardized approach. +// +// https://github.com/golang/go/issues/41144 +// https://github.com/golang/go/discussions/63397 +var ( + DisallowUnknownFields = false + DisallowUnknownFieldsMu sync.Mutex +) + type AuthScope string const ( @@ -83,7 +97,7 @@ type TLSConfig struct { } type LogConfig struct { - // This is destination where frp should wirte the logs. + // This is destination where frp should write the logs. // If "console" is used, logs will be printed to stdout, otherwise, // logs will be written to the specified file. // By default, this value is "console". diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go index bd5ff384a56..db9d0d1a0b2 100644 --- a/pkg/config/v1/plugin.go +++ b/pkg/config/v1/plugin.go @@ -15,6 +15,7 @@ package v1 import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -49,7 +50,13 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { return fmt.Errorf("unknown plugin type: %s", typeStruct.Type) } options := reflect.New(v).Interface().(ClientPluginOptions) - if err := json.Unmarshal(b, options); err != nil { + + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if DisallowUnknownFields { + decoder.DisallowUnknownFields() + } + + if err := decoder.Decode(options); err != nil { return err } c.ClientPluginOptions = options @@ -77,17 +84,20 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{ } type HTTP2HTTPSPluginOptions struct { + Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` } type HTTPProxyPluginOptions struct { + Type string `json:"type,omitempty"` HTTPUser string `json:"httpUser,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"` } type HTTPS2HTTPPluginOptions struct { + Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` @@ -96,6 +106,7 @@ type HTTPS2HTTPPluginOptions struct { } type HTTPS2HTTPSPluginOptions struct { + Type string `json:"type,omitempty"` LocalAddr string `json:"localAddr,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` @@ -104,11 +115,13 @@ type HTTPS2HTTPSPluginOptions struct { } type Socks5PluginOptions struct { + Type string `json:"type,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` } type StaticFilePluginOptions struct { + Type string `json:"type,omitempty"` LocalPath string `json:"localPath,omitempty"` StripPrefix string `json:"stripPrefix,omitempty"` HTTPUser string `json:"httpUser,omitempty"` @@ -116,5 +129,6 @@ type StaticFilePluginOptions struct { } type UnixDomainSocketPluginOptions struct { + Type string `json:"type,omitempty"` UnixPath string `json:"unixPath,omitempty"` } diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 41bb13414b7..8e19d00481c 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -15,6 +15,7 @@ package v1 import ( + "bytes" "encoding/json" "errors" "fmt" @@ -177,7 +178,11 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error { if configurer == nil { return fmt.Errorf("unknown proxy type: %s", typeStruct.Type) } - if err := json.Unmarshal(b, configurer); err != nil { + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if DisallowUnknownFields { + decoder.DisallowUnknownFields() + } + if err := decoder.Decode(configurer); err != nil { return err } c.ProxyConfigurer = configurer @@ -224,7 +229,9 @@ func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer { if !ok { return nil } - return reflect.New(v).Interface().(ProxyConfigurer) + pc := reflect.New(v).Interface().(ProxyConfigurer) + pc.GetBaseConfig().Type = string(proxyType) + return pc } var _ ProxyConfigurer = &TCPProxyConfig{} diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index c42c3ecaaac..03b05d9d043 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -67,6 +67,8 @@ type ServerConfig struct { // value is "", a default page will be displayed. Custom404Page string `json:"custom404Page,omitempty"` + SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"` + WebServer WebServerConfig `json:"webServer,omitempty"` // EnablePrometheus will export prometheus metrics on webserver address // in /metrics api. @@ -101,6 +103,7 @@ func (c *ServerConfig) Complete() { c.Log.Complete() c.Transport.Complete() c.WebServer.Complete() + c.SSHTunnelGateway.Complete() c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0") c.BindPort = util.EmptyOr(c.BindPort, 7000) @@ -152,7 +155,7 @@ type ServerTransportConfig struct { // is true. // $HideFromDoc TCPMux *bool `json:"tcpMux,omitempty"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. @@ -189,3 +192,14 @@ type TLSServerConfig struct { TLSConfig } + +type SSHTunnelGateway struct { + BindPort int `json:"bindPort,omitempty"` + PrivateKeyFile string `json:"privateKeyFile,omitempty"` + AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"` + AuthorizedKeysFile string `json:"authorizedKeysFile,omitempty"` +} + +func (c *SSHTunnelGateway) Complete() { + c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key") +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go index 38123946a59..16fc4ccb48e 100644 --- a/pkg/config/v1/validation/client.go +++ b/pkg/config/v1/validation/client.go @@ -80,7 +80,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { return warnings, errs } -func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { +func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { var warnings Warning if c != nil { warning, err := ValidateClientCommonConfig(c) @@ -90,7 +90,7 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigu } } - for _, c := range pxyCfgs { + for _, c := range proxyCfgs { if err := ValidateProxyConfigurerForClient(c); err != nil { return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err) } diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index 90ecd86d191..a9b2411ab3d 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -15,6 +15,7 @@ package v1 import ( + "bytes" "encoding/json" "errors" "fmt" @@ -108,7 +109,11 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error { if configurer == nil { return fmt.Errorf("unknown visitor type: %s", typeStruct.Type) } - if err := json.Unmarshal(b, configurer); err != nil { + decoder := json.NewDecoder(bytes.NewBuffer(b)) + if DisallowUnknownFields { + decoder.DisallowUnknownFields() + } + if err := decoder.Decode(configurer); err != nil { return err } c.VisitorConfigurer = configurer @@ -120,7 +125,9 @@ func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer { if !ok { return nil } - return reflect.New(v).Interface().(VisitorConfigurer) + vc := reflect.New(v).Interface().(VisitorConfigurer) + vc.GetBaseConfig().Type = string(t) + return vc } var _ VisitorConfigurer = &STCPVisitorConfig{} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 696496a2357..12c388a5229 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -1,3 +1,17 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package metrics import ( diff --git a/pkg/msg/handler.go b/pkg/msg/handler.go new file mode 100644 index 00000000000..cb1eb15a307 --- /dev/null +++ b/pkg/msg/handler.go @@ -0,0 +1,103 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msg + +import ( + "io" + "reflect" +) + +func AsyncHandler(f func(Message)) func(Message) { + return func(m Message) { + go f(m) + } +} + +// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn. +type Dispatcher struct { + rw io.ReadWriter + + sendCh chan Message + doneCh chan struct{} + msgHandlers map[reflect.Type]func(Message) + defaultHandler func(Message) +} + +func NewDispatcher(rw io.ReadWriter) *Dispatcher { + return &Dispatcher{ + rw: rw, + sendCh: make(chan Message, 100), + doneCh: make(chan struct{}), + msgHandlers: make(map[reflect.Type]func(Message)), + } +} + +// Run will block until io.EOF or some error occurs. +func (d *Dispatcher) Run() { + go d.sendLoop() + go d.readLoop() +} + +func (d *Dispatcher) sendLoop() { + for { + select { + case <-d.doneCh: + return + case m := <-d.sendCh: + _ = WriteMsg(d.rw, m) + } + } +} + +func (d *Dispatcher) readLoop() { + for { + m, err := ReadMsg(d.rw) + if err != nil { + close(d.doneCh) + return + } + + if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok { + handler(m) + } else if d.defaultHandler != nil { + d.defaultHandler(m) + } + } +} + +func (d *Dispatcher) Send(m Message) error { + select { + case <-d.doneCh: + return io.EOF + case d.sendCh <- m: + return nil + } +} + +func (d *Dispatcher) SendChannel() chan Message { + return d.sendCh +} + +func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) { + d.msgHandlers[reflect.TypeOf(msg)] = handler +} + +func (d *Dispatcher) RegisterDefaultHandler(handler func(Message)) { + d.defaultHandler = handler +} + +func (d *Dispatcher) Done() chan struct{} { + return d.doneCh +} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 7a865785243..a85fcd5fe5b 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -63,6 +63,15 @@ var msgTypeMap = map[byte]interface{}{ var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name() +type ClientSpec struct { + // Due to the support of VirtualClient, frps needs to know the client type in order to + // differentiate the processing logic. + // Optional values: ssh-tunnel + Type string `json:"type,omitempty"` + // If the value is true, the client will not require authentication. + AlwaysAuthPass bool `json:"always_auth_pass,omitempty"` +} + // When frpc start, client send this message to login to server. type Login struct { Version string `json:"version,omitempty"` @@ -75,6 +84,9 @@ type Login struct { RunID string `json:"run_id,omitempty"` Metas map[string]string `json:"metas,omitempty"` + // Currently only effective for VirtualClient. + ClientSpec ClientSpec `json:"client_spec,omitempty"` + // Some global configures. PoolCount int `json:"pool_count,omitempty"` } diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index 7f093af1b0f..ac54551afbe 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -22,7 +24,7 @@ import ( "net/http/httputil" v1 "github.com/fatedier/frp/pkg/config/v1" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -77,7 +79,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { } func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + 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 06c6296a11d..90a99b09434 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -27,7 +29,7 @@ import ( libnet "github.com/fatedier/golib/net" v1 "github.com/fatedier/frp/pkg/config/v1" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" ) @@ -66,7 +68,7 @@ func (hp *HTTPProxy) Name() string { } func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) sc, rd := libnet.NewSharedConn(wrapConn) firstBytes := make([]byte, 7) diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index aa498f3f1a9..ba66bfaeb93 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -24,7 +26,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -96,7 +98,7 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { } func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index fc38f62b364..a79ea3b106c 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -24,7 +26,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -102,7 +104,7 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { } func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go index c2e253d241f..a230bf55bdb 100644 --- a/pkg/plugin/client/socks5.go +++ b/pkg/plugin/client/socks5.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -22,7 +24,7 @@ import ( gosocks5 "github.com/armon/go-socks5" v1 "github.com/fatedier/frp/pkg/config/v1" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -50,7 +52,7 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { defer conn.Close() - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + 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 20b79a099da..a7db2657e7e 100644 --- a/pkg/plugin/client/static_file.go +++ b/pkg/plugin/client/static_file.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( @@ -23,7 +25,7 @@ import ( "github.com/gorilla/mux" v1 "github.com/fatedier/frp/pkg/config/v1" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func init() { @@ -55,8 +57,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { } router := mux.NewRouter() - router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware) - router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET") + router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware) + router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET") sp.s = &http.Server{ Handler: router, } @@ -67,7 +69,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { } func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { - wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) + wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn) _ = sp.l.PutConn(wrapConn) } diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index f186ec925ea..df68ffb469d 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !frps + package plugin import ( diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go index c9657905f24..57bf77469f0 100644 --- a/pkg/sdk/client/client.go +++ b/pkg/sdk/client/client.go @@ -6,11 +6,12 @@ import ( "io" "net" "net/http" + "net/url" "strconv" "strings" "github.com/fatedier/frp/client" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" ) type Client struct { @@ -69,8 +70,16 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) { return allStatus, nil } -func (c *Client) Reload() error { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil) +func (c *Client) Reload(strictMode bool) error { + v := url.Values{} + if strictMode { + v.Set("strictConfig", "true") + } + queryStr := "" + if len(v) > 0 { + queryStr = "?" + v.Encode() + } + req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload"+queryStr, nil) if err != nil { return err } @@ -106,7 +115,7 @@ func (c *Client) UpdateConfig(content string) error { func (c *Client) setAuthHeader(req *http.Request) { if c.authUser != "" || c.authPwd != "" { - req.Header.Set("Authorization", util.BasicAuth(c.authUser, c.authPwd)) + req.Header.Set("Authorization", httppkg.BasicAuth(c.authUser, c.authPwd)) } } diff --git a/pkg/ssh/gateway.go b/pkg/ssh/gateway.go new file mode 100644 index 00000000000..90f2228ec18 --- /dev/null +++ b/pkg/ssh/gateway.go @@ -0,0 +1,143 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ssh + +import ( + "fmt" + "net" + "os" + "strconv" + "strings" + + "golang.org/x/crypto/ssh" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +type Gateway struct { + bindPort int + ln net.Listener + + peerServerListener *netpkg.InternalListener + + sshConfig *ssh.ServerConfig +} + +func NewGateway( + cfg v1.SSHTunnelGateway, bindAddr string, + peerServerListener *netpkg.InternalListener, +) (*Gateway, error) { + sshConfig := &ssh.ServerConfig{} + + // privateKey + var ( + privateKeyBytes []byte + err error + ) + if cfg.PrivateKeyFile != "" { + privateKeyBytes, err = os.ReadFile(cfg.PrivateKeyFile) + } else { + if cfg.AutoGenPrivateKeyPath != "" { + privateKeyBytes, _ = os.ReadFile(cfg.AutoGenPrivateKeyPath) + } + if len(privateKeyBytes) == 0 { + privateKeyBytes, err = transport.NewRandomPrivateKey() + if err == nil && cfg.AutoGenPrivateKeyPath != "" { + err = os.WriteFile(cfg.AutoGenPrivateKeyPath, privateKeyBytes, 0o600) + } + } + } + if err != nil { + return nil, err + } + privateKey, err := ssh.ParsePrivateKey(privateKeyBytes) + if err != nil { + return nil, err + } + sshConfig.AddHostKey(privateKey) + + sshConfig.NoClientAuth = cfg.AuthorizedKeysFile == "" + sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile) + if err != nil { + log.Error("load authorized keys file error: %v", err) + return nil, fmt.Errorf("internal error") + } + + user, ok := authorizedKeysMap[string(key.Marshal())] + if !ok { + return nil, fmt.Errorf("unknown public key for remoteAddr %q", conn.RemoteAddr()) + } + return &ssh.Permissions{ + Extensions: map[string]string{ + "user": user, + }, + }, nil + } + + ln, err := net.Listen("tcp", net.JoinHostPort(bindAddr, strconv.Itoa(cfg.BindPort))) + if err != nil { + return nil, err + } + return &Gateway{ + bindPort: cfg.BindPort, + ln: ln, + peerServerListener: peerServerListener, + sshConfig: sshConfig, + }, nil +} + +func (g *Gateway) Run() { + for { + conn, err := g.ln.Accept() + if err != nil { + return + } + go g.handleConn(conn) + } +} + +func (g *Gateway) handleConn(conn net.Conn) { + defer conn.Close() + + ts, err := NewTunnelServer(conn, g.sshConfig, g.peerServerListener) + if err != nil { + return + } + if err := ts.Run(); err != nil { + log.Error("ssh tunnel server run error: %v", err) + } +} + +func loadAuthorizedKeysFromFile(path string) (map[string]string, error) { + authorizedKeysMap := make(map[string]string) // value is username + authorizedKeysBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + for len(authorizedKeysBytes) > 0 { + pubKey, comment, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes) + if err != nil { + return nil, err + } + + authorizedKeysMap[string(pubKey.Marshal())] = strings.TrimSpace(comment) + authorizedKeysBytes = rest + } + return authorizedKeysMap, nil +} diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go new file mode 100644 index 00000000000..264669a3ec3 --- /dev/null +++ b/pkg/ssh/server.go @@ -0,0 +1,383 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ssh + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "net" + "strings" + "sync" + "time" + + libio "github.com/fatedier/golib/io" + "github.com/samber/lo" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "golang.org/x/crypto/ssh" + + "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/virtual" +) + +const ( + // https://datatracker.ietf.org/doc/html/rfc4254#page-16 + ChannelTypeServerOpenChannel = "forwarded-tcpip" + RequestTypeForward = "tcpip-forward" +) + +type tcpipForward struct { + Host string + Port uint32 +} + +// https://datatracker.ietf.org/doc/html/rfc4254#page-16 +type forwardedTCPPayload struct { + Addr string + Port uint32 + + OriginAddr string + OriginPort uint32 +} + +type TunnelServer struct { + underlyingConn net.Conn + sshConn *ssh.ServerConn + sc *ssh.ServerConfig + firstChannel ssh.Channel + + vc *virtual.Client + peerServerListener *netpkg.InternalListener + doneCh chan struct{} + closeDoneChOnce sync.Once +} + +func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, peerServerListener *netpkg.InternalListener) (*TunnelServer, error) { + s := &TunnelServer{ + underlyingConn: conn, + sc: sc, + peerServerListener: peerServerListener, + doneCh: make(chan struct{}), + } + return s, nil +} + +func (s *TunnelServer) Run() error { + sshConn, channels, requests, err := ssh.NewServerConn(s.underlyingConn, s.sc) + if err != nil { + return err + } + + s.sshConn = sshConn + + addr, extraPayload, err := s.waitForwardAddrAndExtraPayload(channels, requests, 3*time.Second) + if err != nil { + return err + } + + clientCfg, pc, helpMessage, err := s.parseClientAndProxyConfigurer(addr, extraPayload) + if err != nil { + if errors.Is(err, flag.ErrHelp) { + s.writeToClient(helpMessage) + return nil + } + s.writeToClient(err.Error()) + return fmt.Errorf("parse flags from ssh client error: %v", err) + } + clientCfg.Complete() + if sshConn.Permissions != nil { + clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) + } + pc.Complete(clientCfg.User) + + vc, err := virtual.NewClient(virtual.ClientOptions{ + Common: clientCfg, + Spec: &msg.ClientSpec{ + Type: "ssh-tunnel", + // If ssh does not require authentication, then the virtual client needs to authenticate through a token. + // Otherwise, once ssh authentication is passed, the virtual client does not need to authenticate again. + AlwaysAuthPass: !s.sc.NoClientAuth, + }, + HandleWorkConnCb: func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool { + // join workConn and ssh channel + c, err := s.openConn(addr) + if err != nil { + log.Trace("open conn error: %v", err) + workConn.Close() + return false + } + libio.Join(c, workConn) + return false + }, + }) + if err != nil { + return err + } + s.vc = vc + + // transfer connection from virtual client to server peer listener + go func() { + l := s.vc.PeerListener() + for { + conn, err := l.Accept() + if err != nil { + return + } + _ = s.peerServerListener.PutConn(conn) + } + }() + xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100}) + ctx := xlog.NewContext(context.Background(), xl) + go func() { + vcErr := s.vc.Run(ctx) + if vcErr != nil { + s.writeToClient(vcErr.Error()) + } + + // If vc.Run returns, it means that the virtual client has been closed, and the ssh tunnel connection should be closed. + // One scenario is that the virtual client exits due to login failure. + s.closeDoneChOnce.Do(func() { + _ = sshConn.Close() + close(s.doneCh) + }) + }() + + s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc}) + + if ps, err := s.waitProxyStatusReady(pc.GetBaseConfig().Name, time.Second); err != nil { + s.writeToClient(err.Error()) + log.Warn("wait proxy status ready error: %v", err) + } else { + // success + s.writeToClient(createSuccessInfo(clientCfg.User, pc, ps)) + _ = sshConn.Wait() + } + + s.vc.Close() + log.Trace("ssh tunnel connection from %v closed", sshConn.RemoteAddr()) + s.closeDoneChOnce.Do(func() { + _ = sshConn.Close() + close(s.doneCh) + }) + return nil +} + +func (s *TunnelServer) writeToClient(data string) { + if s.firstChannel == nil { + return + } + _, _ = s.firstChannel.Write([]byte(data + "\n")) +} + +func (s *TunnelServer) waitForwardAddrAndExtraPayload( + channels <-chan ssh.NewChannel, + requests <-chan *ssh.Request, + timeout time.Duration, +) (*tcpipForward, string, error) { + addrCh := make(chan *tcpipForward, 1) + extraPayloadCh := make(chan string, 1) + + // get forward address + go func() { + addrGot := false + for req := range requests { + if req.Type == RequestTypeForward && !addrGot { + payload := tcpipForward{} + if err := ssh.Unmarshal(req.Payload, &payload); err != nil { + return + } + addrGot = true + addrCh <- &payload + } + if req.WantReply { + _ = req.Reply(true, nil) + } + } + }() + + // get extra payload + go func() { + for newChannel := range channels { + // extraPayload will send to extraPayloadCh + go s.handleNewChannel(newChannel, extraPayloadCh) + } + }() + + var ( + addr *tcpipForward + extraPayload string + ) + + timer := time.NewTimer(timeout) + defer timer.Stop() + for { + select { + case v := <-addrCh: + addr = v + case extra := <-extraPayloadCh: + extraPayload = extra + case <-timer.C: + return nil, "", fmt.Errorf("get addr and extra payload timeout") + } + if addr != nil && extraPayload != "" { + break + } + } + return addr, extraPayload, nil +} + +func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPayload string) (*v1.ClientCommonConfig, v1.ProxyConfigurer, string, error) { + helpMessage := "" + cmd := &cobra.Command{ + Use: "ssh v0@{address} [command]", + Short: "ssh v0@{address} [command]", + Run: func(*cobra.Command, []string) {}, + } + args := strings.Split(extraPayload, " ") + if len(args) < 1 { + return nil, nil, helpMessage, fmt.Errorf("invalid extra payload") + } + proxyType := strings.TrimSpace(args[0]) + supportTypes := []string{"tcp", "http", "https", "tcpmux", "stcp"} + if !lo.Contains(supportTypes, proxyType) { + return nil, nil, helpMessage, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes) + } + pc := v1.NewProxyConfigurerByType(v1.ProxyType(proxyType)) + if pc == nil { + return nil, nil, helpMessage, fmt.Errorf("new proxy configurer error") + } + config.RegisterProxyFlags(cmd, pc, config.WithSSHMode()) + + clientCfg := v1.ClientCommonConfig{} + config.RegisterClientCommonConfigFlags(cmd, &clientCfg, config.WithSSHMode()) + + cmd.InitDefaultHelpCmd() + if err := cmd.ParseFlags(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + helpMessage = cmd.UsageString() + } + return nil, nil, helpMessage, err + } + // if name is not set, generate a random one + if pc.GetBaseConfig().Name == "" { + id, err := util.RandIDWithLen(8) + if err != nil { + return nil, nil, helpMessage, fmt.Errorf("generate random id error: %v", err) + } + pc.GetBaseConfig().Name = fmt.Sprintf("sshtunnel-%s-%s", proxyType, id) + } + return &clientCfg, pc, helpMessage, nil +} + +func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) { + ch, reqs, err := channel.Accept() + if err != nil { + return + } + if s.firstChannel == nil { + s.firstChannel = ch + } + go s.keepAlive(ch) + + for req := range reqs { + if req.WantReply { + _ = req.Reply(true, nil) + } + if req.Type != "exec" || len(req.Payload) <= 4 { + continue + } + end := 4 + binary.BigEndian.Uint32(req.Payload[:4]) + if len(req.Payload) < int(end) { + continue + } + extraPayload := string(req.Payload[4:end]) + select { + case extraPayloadCh <- extraPayload: + default: + } + } +} + +func (s *TunnelServer) keepAlive(ch ssh.Channel) { + tk := time.NewTicker(time.Second * 30) + defer tk.Stop() + + for { + select { + case <-tk.C: + _, err := ch.SendRequest("heartbeat", false, nil) + if err != nil { + return + } + case <-s.doneCh: + return + } + } +} + +func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) { + payload := forwardedTCPPayload{ + Addr: addr.Host, + Port: addr.Port, + // Note: Here is just for compatibility, not the real source address. + OriginAddr: addr.Host, + OriginPort: addr.Port, + } + channel, reqs, err := s.sshConn.OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(&payload)) + if err != nil { + return nil, fmt.Errorf("open ssh channel error: %v", err) + } + go ssh.DiscardRequests(reqs) + + conn := netpkg.WrapReadWriteCloserToConn(channel, s.underlyingConn) + return conn, nil +} + +func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) (*proxy.WorkingStatus, error) { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + select { + case <-ticker.C: + ps, err := s.vc.Service().GetProxyStatus(name) + if err != nil { + continue + } + switch ps.Phase { + case proxy.ProxyPhaseRunning: + return ps, nil + case proxy.ProxyPhaseStartErr, proxy.ProxyPhaseClosed: + return ps, errors.New(ps.Err) + } + case <-timer.C: + return nil, fmt.Errorf("wait proxy status ready timeout") + case <-s.doneCh: + return nil, fmt.Errorf("ssh tunnel server closed") + } + } +} diff --git a/pkg/ssh/terminal.go b/pkg/ssh/terminal.go new file mode 100644 index 00000000000..a2e9a6ff362 --- /dev/null +++ b/pkg/ssh/terminal.go @@ -0,0 +1,31 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ssh + +import ( + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func createSuccessInfo(user string, pc v1.ProxyConfigurer, ps *proxy.WorkingStatus) string { + base := pc.GetBaseConfig() + out := "\n" + out += "frp (via SSH) (Ctrl+C to quit)\n\n" + out += "User: " + user + "\n" + out += "ProxyName: " + base.Name + "\n" + out += "Type: " + base.Type + "\n" + out += "RemoteAddress: " + ps.RemoteAddr + "\n" + return out +} diff --git a/pkg/transport/message.go b/pkg/transport/message.go index 6bcd8ce86f7..dd43fbdc0ed 100644 --- a/pkg/transport/message.go +++ b/pkg/transport/message.go @@ -29,7 +29,9 @@ type MessageTransporter interface { // Recv(ctx context.Context, laneKey string, msgType string) (Message, error) // Do will first send msg, then recv msg with the same laneKey and specified msgType. Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) + // Dispatch will dispatch message to related channel registered in Do function by its message type and laneKey. Dispatch(m msg.Message, laneKey string) bool + // Same with Dispatch but with specified message type. DispatchWithType(m msg.Message, msgType, laneKey string) bool } @@ -44,7 +46,7 @@ type transporterImpl struct { sendCh chan msg.Message // First key is message type and second key is lane key. - // Dispatch will dispatch message to releated channel by its message type + // Dispatch will dispatch message to related channel by its message type // and lane key. registry map[string]map[string]chan msg.Message mu sync.RWMutex diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index d92b1a8205c..5bc75921cbd 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -128,3 +128,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf return base, nil } + +func NewRandomPrivateKey() ([]byte, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + return keyPEM, nil +} diff --git a/pkg/util/util/http.go b/pkg/util/http/http.go similarity index 99% rename from pkg/util/util/http.go rename to pkg/util/http/http.go index a6a25a4cbe1..b85a46a328d 100644 --- a/pkg/util/util/http.go +++ b/pkg/util/http/http.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package http import ( "encoding/base64" diff --git a/pkg/util/http/server.go b/pkg/util/http/server.go new file mode 100644 index 00000000000..99bed3640d3 --- /dev/null +++ b/pkg/util/http/server.go @@ -0,0 +1,126 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/pprof" + "strconv" + "time" + + "github.com/gorilla/mux" + + "github.com/fatedier/frp/assets" + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +var ( + defaultReadTimeout = 60 * time.Second + defaultWriteTimeout = 60 * time.Second +) + +type Server struct { + addr string + ln net.Listener + tlsCfg *tls.Config + + router *mux.Router + hs *http.Server + + authMiddleware mux.MiddlewareFunc +} + +func NewServer(cfg v1.WebServerConfig) (*Server, error) { + assets.Load(cfg.AssetsDir) + + addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port)) + if addr == ":" { + addr = ":http" + } + + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + router := mux.NewRouter() + hs := &http.Server{ + Addr: addr, + Handler: router, + ReadTimeout: defaultReadTimeout, + WriteTimeout: defaultWriteTimeout, + } + s := &Server{ + addr: addr, + ln: ln, + hs: hs, + router: router, + } + if cfg.PprofEnable { + s.registerPprofHandlers() + } + if cfg.TLS != nil { + cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile) + if err != nil { + return nil, err + } + s.tlsCfg = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + } + s.authMiddleware = netpkg.NewHTTPAuthMiddleware(cfg.User, cfg.Password).SetAuthFailDelay(200 * time.Millisecond).Middleware + return s, nil +} + +func (s *Server) Address() string { + return s.addr +} + +func (s *Server) Run() error { + ln := s.ln + if s.tlsCfg != nil { + ln = tls.NewListener(ln, s.tlsCfg) + } + return s.hs.Serve(ln) +} + +func (s *Server) Close() error { + return s.hs.Close() +} + +type RouterRegisterHelper struct { + Router *mux.Router + AssetsFS http.FileSystem + AuthMiddleware mux.MiddlewareFunc +} + +func (s *Server) RouteRegister(register func(helper *RouterRegisterHelper)) { + register(&RouterRegisterHelper{ + Router: s.router, + AssetsFS: assets.FileSystem, + AuthMiddleware: s.authMiddleware, + }) +} + +func (s *Server) registerPprofHandlers() { + s.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + s.router.HandleFunc("/debug/pprof/profile", pprof.Profile) + s.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + s.router.HandleFunc("/debug/pprof/trace", pprof.Trace) + s.router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) +} diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index fb2ff677730..a5bbe7371c5 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -22,6 +22,7 @@ import ( "sync/atomic" "time" + "github.com/fatedier/golib/crypto" quic "github.com/quic-go/quic-go" "github.com/fatedier/frp/pkg/util/xlog" @@ -216,3 +217,18 @@ func (conn *wrapQuicStream) Close() error { conn.Stream.CancelRead(0) return conn.Stream.Close() } + +func NewCryptoReadWriter(rw io.ReadWriter, key []byte) (io.ReadWriter, error) { + encReader := crypto.NewReader(rw, key) + encWriter, err := crypto.NewWriter(rw, key) + if err != nil { + return nil, err + } + return struct { + io.Reader + io.Writer + }{ + Reader: encReader, + Writer: encWriter, + }, nil +} diff --git a/pkg/util/net/dns.go b/pkg/util/net/dns.go new file mode 100644 index 00000000000..5e1d5ccbfc1 --- /dev/null +++ b/pkg/util/net/dns.go @@ -0,0 +1,33 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net + +import ( + "context" + "net" +) + +func SetDefaultDNSAddress(dnsAddress string) { + if _, _, err := net.SplitHostPort(dnsAddress); err != nil { + dnsAddress = net.JoinHostPort(dnsAddress, "53") + } + // Change default dns server + net.DefaultResolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return net.Dial("udp", dnsAddress) + }, + } +} diff --git a/pkg/util/net/http.go b/pkg/util/net/http.go index 1a7da23f72f..642d15901e3 100644 --- a/pkg/util/net/http.go +++ b/pkg/util/net/http.go @@ -24,21 +24,21 @@ import ( "github.com/fatedier/frp/pkg/util/util" ) -type HTTPAuthWraper struct { +type HTTPAuthWrapper struct { h http.Handler user string passwd string } -func NewHTTPBasicAuthWraper(h http.Handler, user, passwd string) http.Handler { - return &HTTPAuthWraper{ +func NewHTTPBasicAuthWrapper(h http.Handler, user, passwd string) http.Handler { + return &HTTPAuthWrapper{ h: h, user: user, passwd: passwd, } } -func (aw *HTTPAuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (aw *HTTPAuthWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { user, passwd, hasAuth := r.BasicAuth() if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) { aw.h.ServeHTTP(w, r) @@ -83,11 +83,11 @@ func (authMid *HTTPAuthMiddleware) Middleware(next http.Handler) http.Handler { }) } -type HTTPGzipWraper struct { +type HTTPGzipWrapper struct { h http.Handler } -func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (gw *HTTPGzipWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { gw.h.ServeHTTP(w, r) return @@ -100,7 +100,7 @@ func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func MakeHTTPGzipHandler(h http.Handler) http.Handler { - return &HTTPGzipWraper{ + return &HTTPGzipWrapper{ h: h, } } diff --git a/pkg/util/net/listener.go b/pkg/util/net/listener.go index 6f2d8a56646..c3aebcd6f6b 100644 --- a/pkg/util/net/listener.go +++ b/pkg/util/net/listener.go @@ -52,7 +52,10 @@ func (l *InternalListener) PutConn(conn net.Conn) error { conn.Close() } }) - return err + if err != nil { + return fmt.Errorf("put conn error: listener is closed") + } + return nil } func (l *InternalListener) Close() error { diff --git a/pkg/util/tcpmux/httpconnect.go b/pkg/util/tcpmux/httpconnect.go index 17989adcad5..6be29a4a6a9 100644 --- a/pkg/util/tcpmux/httpconnect.go +++ b/pkg/util/tcpmux/httpconnect.go @@ -24,7 +24,7 @@ import ( libnet "github.com/fatedier/golib/net" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/vhost" ) @@ -59,10 +59,10 @@ func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host, ht return } - host, _ = util.CanonicalHost(req.Host) + host, _ = httppkg.CanonicalHost(req.Host) proxyAuth := req.Header.Get("Proxy-Authorization") if proxyAuth != "" { - httpUser, httpPwd, _ = util.ParseBasicAuth(proxyAuth) + httpUser, httpPwd, _ = httppkg.ParseBasicAuth(proxyAuth) } return } @@ -71,7 +71,7 @@ func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, _ map[string]s if muxer.passthrough { return nil } - res := util.OkResponse() + res := httppkg.OkResponse() if res.Body != nil { defer res.Body.Close() } @@ -85,7 +85,7 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re return true, nil } - resp := util.ProxyUnauthorizedResponse() + resp := httppkg.ProxyUnauthorizedResponse() if resp.Body != nil { defer resp.Body.Close() } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 2dc60eee6fc..ab79a55b709 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version = "0.52.3" +var version = "0.53.0" func Full() string { return version diff --git a/pkg/util/version/version_test.go b/pkg/util/version/version_test.go index 73b96a85f79..2b4077cf33b 100644 --- a/pkg/util/version/version_test.go +++ b/pkg/util/version/version_test.go @@ -47,7 +47,7 @@ func TestVersion(t *testing.T) { proto := Proto(Full()) major := Major(Full()) minor := Minor(Full()) - parseVerion := fmt.Sprintf("%d.%d.%d", proto, major, minor) + parseVersion := fmt.Sprintf("%d.%d.%d", proto, major, minor) version := Full() - assert.Equal(parseVerion, version) + assert.Equal(parseVersion, version) } diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index 7b914ce9b74..72ab4775cbd 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -31,8 +31,8 @@ import ( libio "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" - frpLog "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" + logpkg "github.com/fatedier/frp/pkg/util/log" ) var ErrNoRouteFound = errors.New("no route found") @@ -61,7 +61,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * Director: func(req *http.Request) { req.URL.Scheme = "http" reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo) - oldHost, _ := util.CanonicalHost(reqRouteInfo.Host) + oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) if rc != nil { @@ -74,7 +74,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * // ignore error here, it will use CreateConnFn instead later endpoint, _ = rc.ChooseEndpointFn() reqRouteInfo.Endpoint = endpoint - frpLog.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]", + logpkg.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]", endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) } // Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections. @@ -116,7 +116,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * BufferPool: newWrapPool(), ErrorLog: log.New(newWrapLogger(), "", 0), ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { - frpLog.Warn("do http proxy request [host: %s] error: %v", req.Host, err) + logpkg.Warn("do http proxy request [host: %s] error: %v", req.Host, err) rw.WriteHeader(http.StatusNotFound) _, _ = rw.Write(getNotFoundPageContent()) }, @@ -143,7 +143,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 { - frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) + logpkg.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) return vr.payload.(*RouteConfig) } return nil @@ -159,7 +159,7 @@ func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) // CreateConnection create a new connection by route config func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) { - host, _ := util.CanonicalHost(reqRouteInfo.Host) + host, _ := httppkg.CanonicalHost(reqRouteInfo.Host) vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser) if ok { if byEndpoint { @@ -188,7 +188,7 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, p return true } -// getVhost trys to get vhost router by route policy. +// getVhost tries to get vhost router by route policy. func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) { findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) { vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser) @@ -303,7 +303,7 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ } func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - domain, _ := util.CanonicalHost(req.Host) + domain, _ := httppkg.CanonicalHost(req.Host) location := req.URL.Path user, passwd, _ := req.BasicAuth() if !rp.CheckAuth(domain, location, user, user, passwd) { @@ -333,6 +333,6 @@ type wrapLogger struct{} func newWrapLogger() *wrapLogger { return &wrapLogger{} } func (l *wrapLogger) Write(p []byte) (n int, err error) { - frpLog.Warn("%s", string(bytes.TrimRight(p, "\n"))) + logpkg.Warn("%s", string(bytes.TrimRight(p, "\n"))) return len(p), nil } diff --git a/pkg/util/vhost/resource.go b/pkg/util/vhost/resource.go index d78082b24d0..bf91e13358f 100644 --- a/pkg/util/vhost/resource.go +++ b/pkg/util/vhost/resource.go @@ -20,7 +20,7 @@ import ( "net/http" "os" - frpLog "github.com/fatedier/frp/pkg/util/log" + logpkg "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) @@ -58,7 +58,7 @@ func getNotFoundPageContent() []byte { if NotFoundPagePath != "" { buf, err = os.ReadFile(NotFoundPagePath) if err != nil { - frpLog.Warn("read custom 404 page error: %v", err) + logpkg.Warn("read custom 404 page error: %v", err) buf = []byte(NotFound) } } else { diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 29123b695d9..d529e4249e3 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -22,7 +22,7 @@ import ( "github.com/fatedier/golib/errors" "github.com/fatedier/frp/pkg/util/log" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -284,7 +284,7 @@ func (l *Listener) Accept() (net.Conn, error) { xl.Debug("rewrite host to [%s] success", l.rewriteHost) conn = sConn } - return utilnet.NewContextConn(l.ctx, conn), nil + return netpkg.NewContextConn(l.ctx, conn), nil } func (l *Listener) Close() error { diff --git a/pkg/util/wait/backoff.go b/pkg/util/wait/backoff.go new file mode 100644 index 00000000000..45e0ab68a52 --- /dev/null +++ b/pkg/util/wait/backoff.go @@ -0,0 +1,197 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wait + +import ( + "math/rand" + "time" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/util" +) + +type BackoffFunc func(previousDuration time.Duration, previousConditionError bool) time.Duration + +func (f BackoffFunc) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration { + return f(previousDuration, previousConditionError) +} + +type BackoffManager interface { + Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration +} + +type FastBackoffOptions struct { + Duration time.Duration + Factor float64 + Jitter float64 + MaxDuration time.Duration + InitDurationIfFail time.Duration + + // If FastRetryCount > 0, then within the FastRetryWindow time window, + // the retry will be performed with a delay of FastRetryDelay for the first FastRetryCount calls. + FastRetryCount int + FastRetryDelay time.Duration + FastRetryJitter float64 + FastRetryWindow time.Duration +} + +type fastBackoffImpl struct { + options FastBackoffOptions + + lastCalledTime time.Time + consecutiveErrCount int + + fastRetryCutoffTime time.Time + countsInFastRetryWindow int +} + +func NewFastBackoffManager(options FastBackoffOptions) BackoffManager { + return &fastBackoffImpl{ + options: options, + countsInFastRetryWindow: 1, + } +} + +func (f *fastBackoffImpl) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration { + if f.lastCalledTime.IsZero() { + f.lastCalledTime = time.Now() + return f.options.Duration + } + now := time.Now() + f.lastCalledTime = now + + if previousConditionError { + f.consecutiveErrCount++ + } else { + f.consecutiveErrCount = 0 + } + + if f.options.FastRetryCount > 0 && previousConditionError { + f.countsInFastRetryWindow++ + if f.countsInFastRetryWindow <= f.options.FastRetryCount { + return Jitter(f.options.FastRetryDelay, f.options.FastRetryJitter) + } + if now.After(f.fastRetryCutoffTime) { + // reset + f.fastRetryCutoffTime = now.Add(f.options.FastRetryWindow) + f.countsInFastRetryWindow = 0 + } + } + + if previousConditionError { + var duration time.Duration + if f.consecutiveErrCount == 1 { + duration = util.EmptyOr(f.options.InitDurationIfFail, previousDuration) + } else { + duration = previousDuration + } + + duration = util.EmptyOr(duration, time.Second) + if f.options.Factor != 0 { + duration = time.Duration(float64(duration) * f.options.Factor) + } + if f.options.Jitter > 0 { + duration = Jitter(duration, f.options.Jitter) + } + if f.options.MaxDuration > 0 && duration > f.options.MaxDuration { + duration = f.options.MaxDuration + } + return duration + } + return f.options.Duration +} + +func BackoffUntil(f func() error, backoff BackoffManager, sliding bool, stopCh <-chan struct{}) { + var delay time.Duration + previousError := false + + ticker := time.NewTicker(backoff.Backoff(delay, previousError)) + defer ticker.Stop() + + for { + select { + case <-stopCh: + return + default: + } + + if !sliding { + delay = backoff.Backoff(delay, previousError) + } + + if err := f(); err != nil { + previousError = true + } else { + previousError = false + } + + if sliding { + delay = backoff.Backoff(delay, previousError) + } + + ticker.Reset(delay) + select { + case <-stopCh: + return + default: + } + + select { + case <-stopCh: + return + case <-ticker.C: + } + } +} + +// Jitter returns a time.Duration between duration and duration + maxFactor * +// duration. +// +// This allows clients to avoid converging on periodic behavior. If maxFactor +// is 0.0, a suggested default value will be chosen. +func Jitter(duration time.Duration, maxFactor float64) time.Duration { + if maxFactor <= 0.0 { + maxFactor = 1.0 + } + wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration)) + return wait +} + +func Until(f func(), period time.Duration, stopCh <-chan struct{}) { + ff := func() error { + f() + return nil + } + BackoffUntil(ff, BackoffFunc(func(time.Duration, bool) time.Duration { + return period + }), true, stopCh) +} + +func MergeAndCloseOnAnyStopChannel[T any](upstreams ...<-chan T) <-chan T { + out := make(chan T) + + for _, upstream := range upstreams { + ch := upstream + go lo.Try0(func() { + select { + case <-ch: + close(out) + case <-out: + } + }) + } + return out +} diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index b5746f9dfb8..7b69dcafe0a 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -15,40 +15,81 @@ package xlog import ( + "sort" + "github.com/fatedier/frp/pkg/util/log" ) +type LogPrefix struct { + // Name is the name of the prefix, it won't be displayed in log but used to identify the prefix. + Name string + // Value is the value of the prefix, it will be displayed in log. + Value string + // The prefix with higher priority will be displayed first, default is 10. + Priority int +} + // Logger is not thread safety for operations on prefix type Logger struct { - prefixes []string + prefixes []LogPrefix prefixString string } func New() *Logger { return &Logger{ - prefixes: make([]string, 0), + prefixes: make([]LogPrefix, 0), } } -func (l *Logger) ResetPrefixes() (old []string) { +func (l *Logger) ResetPrefixes() (old []LogPrefix) { old = l.prefixes - l.prefixes = make([]string, 0) + l.prefixes = make([]LogPrefix, 0) l.prefixString = "" return } func (l *Logger) AppendPrefix(prefix string) *Logger { - l.prefixes = append(l.prefixes, prefix) - l.prefixString += "[" + prefix + "] " + return l.AddPrefix(LogPrefix{ + Name: prefix, + Value: prefix, + Priority: 10, + }) +} + +func (l *Logger) AddPrefix(prefix LogPrefix) *Logger { + found := false + if prefix.Priority <= 0 { + prefix.Priority = 10 + } + for _, p := range l.prefixes { + if p.Name == prefix.Name { + found = true + p.Value = prefix.Value + p.Priority = prefix.Priority + } + } + if !found { + l.prefixes = append(l.prefixes, prefix) + } + l.renderPrefixString() return l } -func (l *Logger) Spawn() *Logger { - nl := New() +func (l *Logger) renderPrefixString() { + sort.SliceStable(l.prefixes, func(i, j int) bool { + return l.prefixes[i].Priority < l.prefixes[j].Priority + }) + l.prefixString = "" for _, v := range l.prefixes { - nl.AppendPrefix(v) + l.prefixString += "[" + v.Value + "] " } +} + +func (l *Logger) Spawn() *Logger { + nl := New() + nl.prefixes = append(nl.prefixes, l.prefixes...) + nl.renderPrefixString() return nl } diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go new file mode 100644 index 00000000000..96835a48c7f --- /dev/null +++ b/pkg/virtual/client.go @@ -0,0 +1,107 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package virtual + +import ( + "context" + "net" + + "github.com/fatedier/frp/client" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +type ClientOptions struct { + Common *v1.ClientCommonConfig + Spec *msg.ClientSpec + HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool +} + +type Client struct { + l *netpkg.InternalListener + svr *client.Service +} + +func NewClient(options ClientOptions) (*Client, error) { + if options.Common != nil { + options.Common.Complete() + } + + ln := netpkg.NewInternalListener() + + serviceOptions := client.ServiceOptions{ + Common: options.Common, + ClientSpec: options.Spec, + ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) client.Connector { + return &pipeConnector{ + peerListener: ln, + } + }, + HandleWorkConnCb: options.HandleWorkConnCb, + } + svr, err := client.NewService(serviceOptions) + if err != nil { + return nil, err + } + return &Client{ + l: ln, + svr: svr, + }, nil +} + +func (c *Client) PeerListener() net.Listener { + return c.l +} + +func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) { + _ = c.svr.UpdateAllConfigurer(proxyCfgs, nil) +} + +func (c *Client) Run(ctx context.Context) error { + return c.svr.Run(ctx) +} + +func (c *Client) Service() *client.Service { + return c.svr +} + +func (c *Client) Close() { + c.svr.Close() + c.l.Close() +} + +type pipeConnector struct { + peerListener *netpkg.InternalListener +} + +func (pc *pipeConnector) Open() error { + return nil +} + +func (pc *pipeConnector) Connect() (net.Conn, error) { + c1, c2 := net.Pipe() + if err := pc.peerListener.PutConn(c1); err != nil { + c1.Close() + c2.Close() + return nil, err + } + return c2, nil +} + +func (pc *pipeConnector) Close() error { + pc.peerListener.Close() + return nil +} diff --git a/server/control.go b/server/control.go index f2eaaa56ad7..dbb1af0a038 100644 --- a/server/control.go +++ b/server/control.go @@ -17,15 +17,12 @@ package server import ( "context" "fmt" - "io" "net" "runtime/debug" "sync" + "sync/atomic" "time" - "github.com/fatedier/golib/control/shutdown" - "github.com/fatedier/golib/crypto" - "github.com/fatedier/golib/errors" "github.com/samber/lo" "github.com/fatedier/frp/pkg/auth" @@ -35,8 +32,10 @@ import ( "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" "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/server/controller" "github.com/fatedier/frp/server/metrics" @@ -111,18 +110,16 @@ type Control struct { // other components can use this to communicate with client msgTransporter transport.MessageTransporter + // msgDispatcher is a wrapper for control connection. + // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types. + msgDispatcher *msg.Dispatcher + // login message loginMsg *msg.Login // control connection conn net.Conn - // put a message in this channel to send it over control connection to client - sendCh chan (msg.Message) - - // read from this channel to get the next message sent by client - readCh chan (msg.Message) - // work connections workConnCh chan net.Conn @@ -136,29 +133,24 @@ type Control struct { portsUsedNum int // last time got the Ping message - lastPing time.Time + lastPing atomic.Value // A new run id will be generated when a new client login. // If run id got from login message has same run id, it means it's the same client, so we can // replace old controller instantly. runID string - readerShutdown *shutdown.Shutdown - writerShutdown *shutdown.Shutdown - managerShutdown *shutdown.Shutdown - allShutdown *shutdown.Shutdown - - started bool - mu sync.RWMutex // Server configuration information serverCfg *v1.ServerConfig - xl *xlog.Logger - ctx context.Context + xl *xlog.Logger + ctx context.Context + doneCh chan struct{} } +// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext. func NewControl( ctx context.Context, rc *controller.ResourceController, @@ -166,38 +158,45 @@ func NewControl( pluginManager *plugin.Manager, authVerifier auth.Verifier, ctlConn net.Conn, + ctlConnEncrypted bool, loginMsg *msg.Login, serverCfg *v1.ServerConfig, -) *Control { +) (*Control, error) { poolCount := loginMsg.PoolCount if poolCount > int(serverCfg.Transport.MaxPoolCount) { poolCount = int(serverCfg.Transport.MaxPoolCount) } ctl := &Control{ - rc: rc, - pxyManager: pxyManager, - pluginManager: pluginManager, - authVerifier: authVerifier, - conn: ctlConn, - loginMsg: loginMsg, - sendCh: make(chan msg.Message, 10), - readCh: make(chan msg.Message, 10), - workConnCh: make(chan net.Conn, poolCount+10), - proxies: make(map[string]proxy.Proxy), - poolCount: poolCount, - portsUsedNum: 0, - lastPing: time.Now(), - runID: loginMsg.RunID, - readerShutdown: shutdown.New(), - writerShutdown: shutdown.New(), - managerShutdown: shutdown.New(), - allShutdown: shutdown.New(), - serverCfg: serverCfg, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, + rc: rc, + pxyManager: pxyManager, + pluginManager: pluginManager, + authVerifier: authVerifier, + conn: ctlConn, + loginMsg: loginMsg, + workConnCh: make(chan net.Conn, poolCount+10), + proxies: make(map[string]proxy.Proxy), + poolCount: poolCount, + portsUsedNum: 0, + runID: loginMsg.RunID, + serverCfg: serverCfg, + xl: xlog.FromContextSafe(ctx), + ctx: ctx, + doneCh: make(chan struct{}), + } + ctl.lastPing.Store(time.Now()) + + if ctlConnEncrypted { + cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) + if err != nil { + return nil, err + } + ctl.msgDispatcher = msg.NewDispatcher(cryptoRW) + } else { + ctl.msgDispatcher = msg.NewDispatcher(ctl.conn) } - ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) - return ctl + ctl.registerMsgHandlers() + ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel()) + return ctl, nil } // Start send a login success message to client and start working. @@ -208,27 +207,18 @@ func (ctl *Control) Start() { Error: "", } _ = msg.WriteMsg(ctl.conn, loginRespMsg) - ctl.mu.Lock() - ctl.started = true - ctl.mu.Unlock() - go ctl.writer() go func() { for i := 0; i < ctl.poolCount; i++ { // ignore error here, that means that this control is closed - _ = errors.PanicToError(func() { - ctl.sendCh <- &msg.ReqWorkConn{} - }) + _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{}) } }() - - go ctl.manager() - go ctl.reader() - go ctl.stoper() + go ctl.worker() } func (ctl *Control) Close() error { - ctl.allShutdown.Start() + ctl.conn.Close() return nil } @@ -236,7 +226,7 @@ func (ctl *Control) Replaced(newCtl *Control) { xl := ctl.xl xl.Info("Replaced by client [%s]", newCtl.runID) ctl.runID = "" - ctl.allShutdown.Start() + ctl.conn.Close() } func (ctl *Control) RegisterWorkConn(conn net.Conn) error { @@ -282,9 +272,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { xl.Debug("get work connection from pool") default: // no work connections available in the poll, send message to frpc to get more - if err = errors.PanicToError(func() { - ctl.sendCh <- &msg.ReqWorkConn{} - }); err != nil { + if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil { return nil, fmt.Errorf("control is already closed") } @@ -304,92 +292,40 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { } // When we get a work connection from pool, replace it with a new one. - _ = errors.PanicToError(func() { - ctl.sendCh <- &msg.ReqWorkConn{} - }) + _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{}) return } -func (ctl *Control) writer() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - - defer ctl.allShutdown.Start() - defer ctl.writerShutdown.Done() - - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) - if err != nil { - xl.Error("crypto new writer error: %v", err) - ctl.allShutdown.Start() - return - } - for { - m, ok := <-ctl.sendCh - if !ok { - xl.Info("control writer is closing") - return - } - - if err := msg.WriteMsg(encWriter, m); err != nil { - xl.Warn("write message to control connection error: %v", err) - return - } - } -} - -func (ctl *Control) reader() { +func (ctl *Control) heartbeatWorker() { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - defer ctl.allShutdown.Start() - defer ctl.readerShutdown.Done() - - encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) - for { - m, err := msg.ReadMsg(encReader) - if err != nil { - if err == io.EOF { - xl.Debug("control connection closed") + // Don't need application heartbeat if TCPMux is enabled, + // yamux will do same thing. + // TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it. + if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 { + go wait.Until(func() { + if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second { + xl.Warn("heartbeat timeout") + ctl.conn.Close() return } - xl.Warn("read error: %v", err) - ctl.conn.Close() - return - } - - ctl.readCh <- m + }, time.Second, ctl.doneCh) } } -func (ctl *Control) stoper() { +// block until Control closed +func (ctl *Control) WaitClosed() { + <-ctl.doneCh +} + +func (ctl *Control) worker() { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - ctl.allShutdown.WaitStart() + go ctl.heartbeatWorker() + go ctl.msgDispatcher.Run() + <-ctl.msgDispatcher.Done() ctl.conn.Close() - ctl.readerShutdown.WaitDone() - - close(ctl.readCh) - ctl.managerShutdown.WaitDone() - - close(ctl.sendCh) - ctl.writerShutdown.WaitDone() ctl.mu.Lock() defer ctl.mu.Unlock() @@ -419,136 +355,104 @@ func (ctl *Control) stoper() { }() } - ctl.allShutdown.Done() - xl.Info("client exit success") metrics.Server.CloseClient() + xl.Info("client exit success") + close(ctl.doneCh) } -// block until Control closed -func (ctl *Control) WaitClosed() { - ctl.mu.RLock() - started := ctl.started - ctl.mu.RUnlock() - - if !started { - ctl.allShutdown.Done() - return - } - ctl.allShutdown.WaitDone() +func (ctl *Control) registerMsgHandlers() { + ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy) + ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor)) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient)) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport)) + ctl.msgDispatcher.RegisterHandler(&msg.CloseProxy{}, ctl.handleCloseProxy) } -func (ctl *Control) manager() { +func (ctl *Control) handleNewProxy(m msg.Message) { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() + inMsg := m.(*msg.NewProxy) - defer ctl.allShutdown.Start() - defer ctl.managerShutdown.Done() + content := &plugin.NewProxyContent{ + User: plugin.UserInfo{ + User: ctl.loginMsg.User, + Metas: ctl.loginMsg.Metas, + RunID: ctl.loginMsg.RunID, + }, + NewProxy: *inMsg, + } + var remoteAddr string + retContent, err := ctl.pluginManager.NewProxy(content) + if err == nil { + inMsg = &retContent.NewProxy + remoteAddr, err = ctl.RegisterProxy(inMsg) + } - var heartbeatCh <-chan time.Time - // Don't need application heartbeat if TCPMux is enabled, - // yamux will do same thing. - if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 { - heartbeat := time.NewTicker(time.Second) - defer heartbeat.Stop() - heartbeatCh = heartbeat.C + // register proxy in this control + resp := &msg.NewProxyResp{ + ProxyName: inMsg.ProxyName, + } + if err != nil { + xl.Warn("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err) + resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName), + err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)) + } else { + resp.RemoteAddr = remoteAddr + xl.Info("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType) + metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType) } + _ = ctl.msgDispatcher.Send(resp) +} - for { - select { - case <-heartbeatCh: - if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second { - xl.Warn("heartbeat timeout") - return - } - case rawMsg, ok := <-ctl.readCh: - if !ok { - return - } +func (ctl *Control) handlePing(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.Ping) - switch m := rawMsg.(type) { - case *msg.NewProxy: - content := &plugin.NewProxyContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - NewProxy: *m, - } - var remoteAddr string - retContent, err := ctl.pluginManager.NewProxy(content) - if err == nil { - m = &retContent.NewProxy - remoteAddr, err = ctl.RegisterProxy(m) - } - - // register proxy in this control - resp := &msg.NewProxyResp{ - ProxyName: m.ProxyName, - } - if err != nil { - xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err) - resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), - err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)) - } else { - resp.RemoteAddr = remoteAddr - xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType) - metrics.Server.NewProxy(m.ProxyName, m.ProxyType) - } - ctl.sendCh <- resp - case *msg.NatHoleVisitor: - go ctl.HandleNatHoleVisitor(m) - case *msg.NatHoleClient: - go ctl.HandleNatHoleClient(m) - case *msg.NatHoleReport: - go ctl.HandleNatHoleReport(m) - case *msg.CloseProxy: - _ = ctl.CloseProxy(m) - xl.Info("close proxy [%s] success", m.ProxyName) - case *msg.Ping: - content := &plugin.PingContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - Ping: *m, - } - retContent, err := ctl.pluginManager.Ping(content) - if err == nil { - m = &retContent.Ping - err = ctl.authVerifier.VerifyPing(m) - } - if err != nil { - xl.Warn("received invalid ping: %v", err) - ctl.sendCh <- &msg.Pong{ - Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), - } - return - } - ctl.lastPing = time.Now() - xl.Debug("receive heartbeat") - ctl.sendCh <- &msg.Pong{} - } - } + content := &plugin.PingContent{ + User: plugin.UserInfo{ + User: ctl.loginMsg.User, + Metas: ctl.loginMsg.Metas, + RunID: ctl.loginMsg.RunID, + }, + Ping: *inMsg, + } + retContent, err := ctl.pluginManager.Ping(content) + if err == nil { + inMsg = &retContent.Ping + err = ctl.authVerifier.VerifyPing(inMsg) } + if err != nil { + xl.Warn("received invalid ping: %v", err) + _ = ctl.msgDispatcher.Send(&msg.Pong{ + Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), + }) + return + } + ctl.lastPing.Store(time.Now()) + xl.Debug("receive heartbeat") + _ = ctl.msgDispatcher.Send(&msg.Pong{}) } -func (ctl *Control) HandleNatHoleVisitor(m *msg.NatHoleVisitor) { - ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter, ctl.loginMsg.User) +func (ctl *Control) handleNatHoleVisitor(m msg.Message) { + inMsg := m.(*msg.NatHoleVisitor) + ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User) } -func (ctl *Control) HandleNatHoleClient(m *msg.NatHoleClient) { - ctl.rc.NatHoleController.HandleClient(m, ctl.msgTransporter) +func (ctl *Control) handleNatHoleClient(m msg.Message) { + inMsg := m.(*msg.NatHoleClient) + ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter) } -func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) { - ctl.rc.NatHoleController.HandleReport(m) +func (ctl *Control) handleNatHoleReport(m msg.Message) { + inMsg := m.(*msg.NatHoleReport) + ctl.rc.NatHoleController.HandleReport(inMsg) +} + +func (ctl *Control) handleCloseProxy(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.CloseProxy) + _ = ctl.CloseProxy(inMsg) + xl.Info("close proxy [%s] success", inMsg.ProxyName) } func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) { @@ -658,6 +562,5 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { go func() { _ = ctl.pluginManager.CloseProxy(notifyContent) }() - return } diff --git a/server/dashboard.go b/server/dashboard.go deleted file mode 100644 index 1f290cf9a5a..00000000000 --- a/server/dashboard.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2017 fatedier, fatedier@gmail.com -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "crypto/tls" - "net" - "net/http" - "net/http/pprof" - "time" - - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/fatedier/frp/assets" - utilnet "github.com/fatedier/frp/pkg/util/net" -) - -var ( - httpServerReadTimeout = 60 * time.Second - httpServerWriteTimeout = 60 * time.Second -) - -func (svr *Service) RunDashboardServer(address string) (err error) { - // url router - router := mux.NewRouter() - router.HandleFunc("/healthz", svr.Healthz) - - // debug - if svr.cfg.WebServer.PprofEnable { - router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - router.HandleFunc("/debug/pprof/profile", pprof.Profile) - router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - router.HandleFunc("/debug/pprof/trace", pprof.Trace) - router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) - } - - subRouter := router.NewRoute().Subrouter() - - user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password - subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware) - - // metrics - if svr.cfg.EnablePrometheus { - subRouter.Handle("/metrics", promhttp.Handler()) - } - - // api, see dashboard_api.go - subRouter.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET") - subRouter.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET") - - // view - subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET") - subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET") - - subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/static/", http.StatusMovedPermanently) - }) - - server := &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: httpServerReadTimeout, - WriteTimeout: httpServerWriteTimeout, - } - ln, err := net.Listen("tcp", address) - if err != nil { - return err - } - - if svr.cfg.WebServer.TLS != nil { - cert, err := tls.LoadX509KeyPair(svr.cfg.WebServer.TLS.CertFile, svr.cfg.WebServer.TLS.KeyFile) - if err != nil { - return err - } - tlsCfg := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - ln = tls.NewListener(ln, tlsCfg) - } - go func() { - _ = server.Serve(ln) - }() - return -} diff --git a/server/dashboard_api.go b/server/dashboard_api.go index b5a923f9456..27944b9a803 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -19,19 +19,52 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/fatedier/frp/pkg/config/types" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/metrics/mem" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/version" ) +// TODO(fatedier): add an API to clean status of all offline proxies. + type GeneralResponse struct { Code int Msg string } +func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { + helper.Router.HandleFunc("/healthz", svr.healthz) + subRouter := helper.Router.NewRoute().Subrouter() + + subRouter.Use(helper.AuthMiddleware.Middleware) + + // metrics + if svr.cfg.EnablePrometheus { + subRouter.Handle("/metrics", promhttp.Handler()) + } + + // apis + subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET") + subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET") + + // view + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") + subRouter.PathPrefix("/static/").Handler( + netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), + ).Methods("GET") + + subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/", http.StatusMovedPermanently) + }) +} + type serverInfoResp struct { Version string `json:"version"` BindPort int `json:"bindPort"` @@ -55,12 +88,12 @@ type serverInfoResp struct { } // /healthz -func (svr *Service) Healthz(w http.ResponseWriter, _ *http.Request) { +func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) } // /api/serverinfo -func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) { +func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} defer func() { log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) @@ -177,7 +210,7 @@ type GetProxyInfoResp struct { } // /api/proxy/:type -func (svr *Service) APIProxyByType(w http.ResponseWriter, r *http.Request) { +func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} params := mux.Vars(r) proxyType := params["type"] @@ -245,7 +278,7 @@ type GetProxyStatsResp struct { } // /api/proxy/:type/:name -func (svr *Service) APIProxyByTypeAndName(w http.ResponseWriter, r *http.Request) { +func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} params := mux.Vars(r) proxyType := params["type"] @@ -314,7 +347,7 @@ type GetProxyTrafficResp struct { TrafficOut []int64 `json:"trafficOut"` } -func (svr *Service) APIProxyTraffic(w http.ResponseWriter, r *http.Request) { +func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) { res := GeneralResponse{Code: 200} params := mux.Vars(r) name := params["name"] diff --git a/server/proxy/http.go b/server/proxy/http.go index cafaf8f3d9a..44a462b7ecc 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -24,7 +24,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/limit" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/server/metrics" @@ -180,8 +180,8 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err }) } - workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn) - workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) + workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn) + workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) return } diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index fe6f781b728..f5c850e98af 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -32,7 +32,7 @@ import ( "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" "github.com/fatedier/frp/pkg/util/limit" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/metrics" @@ -130,7 +130,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, } xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String()) xl.Spawn().AppendPrefix(pxy.GetName()) - workConn = utilnet.NewContextConn(pxy.ctx, workConn) + workConn = netpkg.NewContextConn(pxy.ctx, workConn) var ( srcAddr string diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 772c3f0d1b9..ea970818d8e 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -30,7 +30,7 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/server/metrics" ) @@ -222,7 +222,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { }) } - pxy.workConn = utilnet.WrapReadWriteCloserToConn(rwc, workConn) + pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn) ctx, cancel := context.WithCancel(context.Background()) go workConnReaderFn(pxy.workConn) go workConnSenderFn(pxy.workConn, ctx) diff --git a/server/service.go b/server/service.go index 9deffa020f0..c2410b06376 100644 --- a/server/service.go +++ b/server/service.go @@ -30,16 +30,17 @@ import ( quic "github.com/quic-go/quic-go" "github.com/samber/lo" - "github.com/fatedier/frp/assets" "github.com/fatedier/frp/pkg/auth" v1 "github.com/fatedier/frp/pkg/config/v1" modelmetrics "github.com/fatedier/frp/pkg/metrics" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/ssh" "github.com/fatedier/frp/pkg/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/version" @@ -78,6 +79,9 @@ type Service struct { // Accept frp tls connections tlsListener net.Listener + // Accept pipe connections from ssh tunnel gateway + sshTunnelListener *netpkg.InternalListener + // Manage all controllers ctlManager *ControlManager @@ -93,6 +97,11 @@ type Service struct { // All resource managers and controllers rc *controller.ResourceController + // web server for dashboard UI and apis + webServer *httppkg.Server + + sshTunnelGateway *ssh.Gateway + // Verifies authentication based on selected method authVerifier auth.Verifier @@ -106,16 +115,30 @@ type Service struct { cancel context.CancelFunc } -func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { +func NewService(cfg *v1.ServerConfig) (*Service, error) { tlsConfig, err := transport.NewServerTLSConfig( cfg.Transport.TLS.CertFile, cfg.Transport.TLS.KeyFile, cfg.Transport.TLS.TrustedCaFile) if err != nil { - return + return nil, err + } + + var webServer *httppkg.Server + if cfg.WebServer.Port > 0 { + ws, err := httppkg.NewServer(cfg.WebServer) + if err != nil { + return nil, err + } + webServer = ws + + modelmetrics.EnableMem() + if cfg.EnablePrometheus { + modelmetrics.EnablePrometheus() + } } - svr = &Service{ + svr := &Service{ ctlManager: NewControlManager(), pxyManager: proxy.NewManager(), pluginManager: plugin.NewManager(), @@ -124,11 +147,16 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts), UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts), }, - httpVhostRouter: vhost.NewRouters(), - authVerifier: auth.NewAuthVerifier(cfg.Auth), - tlsConfig: tlsConfig, - cfg: cfg, - ctx: context.Background(), + sshTunnelListener: netpkg.NewInternalListener(), + httpVhostRouter: vhost.NewRouters(), + authVerifier: auth.NewAuthVerifier(cfg.Auth), + webServer: webServer, + tlsConfig: tlsConfig, + cfg: cfg, + ctx: context.Background(), + } + if webServer != nil { + webServer.RouteRegister(svr.registerRouteHandlers) } // Create tcpmux httpconnect multiplexer. @@ -137,14 +165,12 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort)) l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout) if err != nil { - err = fmt.Errorf("create vhost tcpMuxer error, %v", err) - return + return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) } log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough) } @@ -185,8 +211,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort)) ln, err := net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } svr.muxer = mux.NewMux(ln) @@ -202,10 +227,9 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { // Listen for accepting connections from client using kcp protocol. if cfg.KCPBindPort > 0 { address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort)) - svr.kcpListener, err = utilnet.ListenKcp(address) + svr.kcpListener, err = netpkg.ListenKcp(address) if err != nil { - err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err) - return + return nil, fmt.Errorf("listen on kcp udp address %s error: %v", address, err) } log.Info("frps kcp listen on udp %s", address) } @@ -220,18 +244,26 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { KeepAlivePeriod: time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second, }) if err != nil { - err = fmt.Errorf("listen on quic udp address %s error: %v", address, err) - return + return nil, fmt.Errorf("listen on quic udp address %s error: %v", address, err) + } + log.Info("frps quic listen on %s", address) + } + + if cfg.SSHTunnelGateway.BindPort > 0 { + sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.sshTunnelListener) + if err != nil { + return nil, fmt.Errorf("create ssh gateway error: %v", err) } - log.Info("frps quic listen on quic %s", address) + svr.sshTunnelGateway = sshGateway + log.Info("frps sshTunnelGateway listen on port %d", cfg.SSHTunnelGateway.BindPort) } // Listen for accepting connections from client using websocket protocol. - websocketPrefix := []byte("GET " + utilnet.FrpWebsocketPath) + websocketPrefix := []byte("GET " + netpkg.FrpWebsocketPath) websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool { return bytes.Equal(data, websocketPrefix) }) - svr.websocketListener = utilnet.NewWebsocketListener(websocketLn) + svr.websocketListener = netpkg.NewWebsocketListener(websocketLn) // Create http vhost muxer. if cfg.VhostHTTPPort > 0 { @@ -251,8 +283,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { } else { l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("create vhost http listener error, %v", err) - return + return nil, fmt.Errorf("create vhost http listener error, %v", err) } } go func() { @@ -270,55 +301,30 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort)) l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } log.Info("https service listen on %s", address) } svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) if err != nil { - err = fmt.Errorf("create vhost httpsMuxer error, %v", err) - return + return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err) } } // frp tls listener svr.tlsListener = svr.muxer.Listen(2, 1, func(data []byte) bool { // tls first byte can be 0x16 only when vhost https port is not same with bind port - return int(data[0]) == utilnet.FRPTLSHeadByte || int(data[0]) == 0x16 + return int(data[0]) == netpkg.FRPTLSHeadByte || int(data[0]) == 0x16 }) // Create nat hole controller. nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour) if err != nil { - err = fmt.Errorf("create nat hole controller error, %v", err) - return + return nil, fmt.Errorf("create nat hole controller error, %v", err) } svr.rc.NatHoleController = nc - - var statsEnable bool - // Create dashboard web server. - if cfg.WebServer.Port > 0 { - // Init dashboard assets - assets.Load(cfg.WebServer.AssetsDir) - - address := net.JoinHostPort(cfg.WebServer.Addr, strconv.Itoa(cfg.WebServer.Port)) - err = svr.RunDashboardServer(address) - if err != nil { - err = fmt.Errorf("create dashboard web server error, %v", err) - return - } - log.Info("Dashboard listen on %s", address) - statsEnable = true - } - if statsEnable { - modelmetrics.EnableMem() - if cfg.EnablePrometheus { - modelmetrics.EnablePrometheus() - } - } - return + return svr, nil } func (svr *Service) Run(ctx context.Context) { @@ -326,19 +332,36 @@ func (svr *Service) Run(ctx context.Context) { svr.ctx = ctx svr.cancel = cancel + // run dashboard web server. + if svr.webServer != nil { + go func() { + log.Info("dashboard listen on %s", svr.webServer.Address()) + if err := svr.webServer.Run(); err != nil { + log.Warn("dashboard server exit with error: %v", err) + } + }() + } + + go svr.HandleListener(svr.sshTunnelListener, true) + if svr.kcpListener != nil { - go svr.HandleListener(svr.kcpListener) + go svr.HandleListener(svr.kcpListener, false) } if svr.quicListener != nil { go svr.HandleQUICListener(svr.quicListener) } - go svr.HandleListener(svr.websocketListener) - go svr.HandleListener(svr.tlsListener) + go svr.HandleListener(svr.websocketListener, false) + go svr.HandleListener(svr.tlsListener, false) if svr.rc.NatHoleController != nil { go svr.rc.NatHoleController.CleanWorker(svr.ctx) } - svr.HandleListener(svr.listener) + + if svr.sshTunnelGateway != nil { + go svr.sshTunnelGateway.Run() + } + + 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 @@ -375,7 +398,7 @@ func (svr *Service) Close() error { return nil } -func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { +func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) { xl := xlog.FromContextSafe(ctx) var ( @@ -401,7 +424,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { retContent, err := svr.pluginManager.Login(content) if err == nil { m = &retContent.Login - err = svr.RegisterControl(conn, m) + err = svr.RegisterControl(conn, m, internal) } // If login failed, send error message there. @@ -438,7 +461,10 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { } } -func (svr *Service) HandleListener(l net.Listener) { +// HandleListener accepts connections from client and call handleConnection to handle them. +// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway. +// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters. +func (svr *Service) HandleListener(l net.Listener, internal bool) { // Listen for incoming connections from client. for { c, err := l.Accept() @@ -450,22 +476,25 @@ func (svr *Service) HandleListener(l net.Listener) { xl := xlog.New() ctx := context.Background() - c = utilnet.NewContextConn(xlog.NewContext(ctx, xl), c) + c = netpkg.NewContextConn(xlog.NewContext(ctx, xl), c) - log.Trace("start check TLS connection...") - originConn := c - var isTLS, custom bool - c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout) - if err != nil { - log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) - originConn.Close() - continue + if !internal { + log.Trace("start check TLS connection...") + originConn := c + forceTLS := svr.cfg.Transport.TLS.Force + var isTLS, custom bool + c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout) + if err != nil { + log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) + originConn.Close() + continue + } + log.Trace("check TLS connection success, isTLS: %v custom: %v internal: %v", isTLS, custom, internal) } - log.Trace("check TLS connection success, isTLS: %v custom: %v", isTLS, custom) // Start a new goroutine to handle connection. go func(ctx context.Context, frpConn net.Conn) { - if lo.FromPtr(svr.cfg.Transport.TCPMux) { + 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 @@ -484,10 +513,10 @@ func (svr *Service) HandleListener(l net.Listener) { session.Close() return } - go svr.handleConnection(ctx, stream) + go svr.handleConnection(ctx, stream, internal) } } else { - svr.handleConnection(ctx, frpConn) + svr.handleConnection(ctx, frpConn, internal) } }(ctx, c) } @@ -510,23 +539,24 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) { _ = frpConn.CloseWithError(0, "") return } - go svr.handleConnection(ctx, utilnet.QuicStreamToNetConn(stream, frpConn)) + go svr.handleConnection(ctx, netpkg.QuicStreamToNetConn(stream, frpConn), false) } }(context.Background(), c) } } -func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) { +func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, internal bool) error { // If client's RunID is empty, it's a new client, we just create a new controller. // Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one. + var err error if loginMsg.RunID == "" { loginMsg.RunID, err = util.RandID() if err != nil { - return + return err } } - ctx := utilnet.NewContextFromConn(ctlConn) + ctx := netpkg.NewContextFromConn(ctlConn) xl := xlog.FromContextSafe(ctx) xl.AppendPrefix(loginMsg.RunID) ctx = xlog.NewContext(ctx, xl) @@ -534,11 +564,21 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch) // Check auth. - if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil { - return + authVerifier := svr.authVerifier + if internal && loginMsg.ClientSpec.AlwaysAuthPass { + authVerifier = auth.AlwaysPassVerifier + } + if err := authVerifier.VerifyLogin(loginMsg); err != nil { + return err } - ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg) + // TODO(fatedier): use SessionContext + ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg) + if err != nil { + xl.Warn("create new controller error: %v", err) + // don't return detailed errors to client + return fmt.Errorf("unexpected error when creating new controller") + } if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil { oldCtl.WaitClosed() } @@ -553,12 +593,12 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err ctl.WaitClosed() svr.ctlManager.Del(loginMsg.RunID, ctl) }() - return + return nil } // RegisterWorkConn register a new work connection to control and proxies need it. func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error { - xl := utilnet.NewLogFromConn(workConn) + xl := netpkg.NewLogFromConn(workConn) ctl, exist := svr.ctlManager.GetByID(newMsg.RunID) if !exist { xl.Warn("No client control found for run id [%s]", newMsg.RunID) @@ -577,7 +617,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) if err == nil { newMsg = &retContent.NewWorkConn // Check auth. - err = svr.authVerifier.VerifyNewWorkConn(newMsg) + err = ctl.authVerifier.VerifyNewWorkConn(newMsg) } if err != nil { xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID) diff --git a/server/visitor/visitor.go b/server/visitor/visitor.go index c76bcee1d92..ed06dc4b4e9 100644 --- a/server/visitor/visitor.go +++ b/server/visitor/visitor.go @@ -23,12 +23,12 @@ import ( libio "github.com/fatedier/golib/io" "github.com/samber/lo" - utilnet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" ) type listenerBundle struct { - l *utilnet.InternalListener + l *netpkg.InternalListener sk string allowUsers []string } @@ -46,22 +46,21 @@ func NewManager() *Manager { } } -func (vm *Manager) Listen(name string, sk string, allowUsers []string) (l *utilnet.InternalListener, err error) { +func (vm *Manager) Listen(name string, sk string, allowUsers []string) (*netpkg.InternalListener, error) { vm.mu.Lock() defer vm.mu.Unlock() if _, ok := vm.listeners[name]; ok { - err = fmt.Errorf("custom listener for [%s] is repeated", name) - return + return nil, fmt.Errorf("custom listener for [%s] is repeated", name) } - l = utilnet.NewInternalListener() + l := netpkg.NewInternalListener() vm.listeners[name] = &listenerBundle{ l: l, sk: sk, allowUsers: allowUsers, } - return + return l, nil } func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string, @@ -91,7 +90,7 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey if useCompression { rwc = libio.WithCompression(rwc) } - err = l.l.PutConn(utilnet.WrapReadWriteCloserToConn(rwc, conn)) + err = l.l.PutConn(netpkg.WrapReadWriteCloserToConn(rwc, conn)) } else { err = fmt.Errorf("custom listener for [%s] doesn't exist", name) return diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 6a7a655f266..f8b8aa03389 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -29,8 +29,8 @@ type Framework struct { // ports used in this framework indexed by port name. usedPorts map[string]int - // record ports alloced by this framework and release them after each test - allocedPorts []int + // record ports allocated by this framework and release them after each test + allocatedPorts []int // portAllocator to alloc port for this test case. portAllocator *port.Allocator @@ -153,11 +153,11 @@ func (f *Framework) AfterEach() { } f.usedPorts = make(map[string]int) - // release alloced ports - for _, port := range f.allocedPorts { + // release allocated ports + for _, port := range f.allocatedPorts { f.portAllocator.Release(port) } - f.allocedPorts = make([]int, 0) + f.allocatedPorts = make([]int, 0) // clear os envs f.osEnvs = make([]string, 0) @@ -237,7 +237,7 @@ func (f *Framework) PortByName(name string) int { func (f *Framework) AllocPort() int { port := f.portAllocator.Get() ExpectTrue(port > 0, "alloc port failed") - f.allocedPorts = append(f.allocedPorts, port) + f.allocatedPorts = append(f.allocatedPorts, port) return port } diff --git a/test/e2e/legacy/basic/client.go b/test/e2e/legacy/basic/client.go index da23db9c1fe..d4862e529dc 100644 --- a/test/e2e/legacy/basic/client.go +++ b/test/e2e/legacy/basic/client.go @@ -69,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { err = client.UpdateConfig(newClientConf) framework.ExpectNoError(err) - err = client.Reload() + err = client.Reload(true) framework.ExpectNoError(err) time.Sleep(time.Second) diff --git a/test/e2e/legacy/basic/tcpmux.go b/test/e2e/legacy/basic/tcpmux.go index 5bb742bc8e3..15477837a32 100644 --- a/test/e2e/legacy/basic/tcpmux.go +++ b/test/e2e/legacy/basic/tcpmux.go @@ -8,7 +8,7 @@ import ( "github.com/onsi/ginkgo/v2" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/streamserver" @@ -176,7 +176,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { connectRequestHost = req.Host // return ok response - res := util.OkResponse() + res := httppkg.OkResponse() if res.Body != nil { defer res.Body.Close() } diff --git a/test/e2e/legacy/plugin/server.go b/test/e2e/legacy/plugin/server.go index 3f14a42dcf6..cf600be2ee0 100644 --- a/test/e2e/legacy/plugin/server.go +++ b/test/e2e/legacy/plugin/server.go @@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { framework.NewRequestExpect(f).Port(remotePort).Ensure() }) - ginkgo.It("Mofify RemotePort", func() { + ginkgo.It("Modify RemotePort", func() { localPort := f.AllocPort() remotePort := f.AllocPort() handler := func(req *plugin.Request) *plugin.Response { diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go index 50deb3bf31a..740bc4fbf2c 100644 --- a/test/e2e/pkg/request/request.go +++ b/test/e2e/pkg/request/request.go @@ -14,7 +14,7 @@ import ( libdial "github.com/fatedier/golib/net/dial" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/test/e2e/pkg/rpc" ) @@ -115,7 +115,7 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request { } func (r *Request) HTTPAuth(user, password string) *Request { - r.authValue = util.BasicAuth(user, password) + r.authValue = httppkg.BasicAuth(user, password) return r } diff --git a/test/e2e/pkg/ssh/client.go b/test/e2e/pkg/ssh/client.go new file mode 100644 index 00000000000..b45e39daa0d --- /dev/null +++ b/test/e2e/pkg/ssh/client.go @@ -0,0 +1,89 @@ +package ssh + +import ( + "net" + + libio "github.com/fatedier/golib/io" + "golang.org/x/crypto/ssh" +) + +type TunnelClient struct { + localAddr string + sshServer string + commands string + + sshConn *ssh.Client + ln net.Listener +} + +func NewTunnelClient(localAddr string, sshServer string, commands string) *TunnelClient { + return &TunnelClient{ + localAddr: localAddr, + sshServer: sshServer, + commands: commands, + } +} + +func (c *TunnelClient) Start() error { + config := &ssh.ClientConfig{ + User: "v0", + HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { return nil }, + } + + conn, err := ssh.Dial("tcp", c.sshServer, config) + if err != nil { + return err + } + c.sshConn = conn + + l, err := conn.Listen("tcp", "0.0.0.0:80") + if err != nil { + return err + } + c.ln = l + ch, req, err := conn.OpenChannel("session", []byte("")) + if err != nil { + return err + } + defer ch.Close() + go ssh.DiscardRequests(req) + + type command struct { + Cmd string + } + _, err = ch.SendRequest("exec", false, ssh.Marshal(command{Cmd: c.commands})) + if err != nil { + return err + } + + go c.serveListener() + return nil +} + +func (c *TunnelClient) Close() { + if c.sshConn != nil { + _ = c.sshConn.Close() + } + if c.ln != nil { + _ = c.ln.Close() + } +} + +func (c *TunnelClient) serveListener() { + for { + conn, err := c.ln.Accept() + if err != nil { + return + } + go c.hanldeConn(conn) + } +} + +func (c *TunnelClient) hanldeConn(conn net.Conn) { + defer conn.Close() + local, err := net.Dial("tcp", c.localAddr) + if err != nil { + return + } + _, _, _ = libio.Join(local, conn) +} diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go index 25b99424101..b0b258db308 100644 --- a/test/e2e/v1/basic/client.go +++ b/test/e2e/v1/basic/client.go @@ -72,7 +72,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { err = client.UpdateConfig(newClientConf) framework.ExpectNoError(err) - err = client.Reload() + err = client.Reload(true) framework.ExpectNoError(err) time.Sleep(time.Second) diff --git a/test/e2e/v1/basic/tcpmux.go b/test/e2e/v1/basic/tcpmux.go index 356a18be978..7ee58a79c01 100644 --- a/test/e2e/v1/basic/tcpmux.go +++ b/test/e2e/v1/basic/tcpmux.go @@ -8,7 +8,7 @@ import ( "github.com/onsi/ginkgo/v2" - "github.com/fatedier/frp/pkg/util/util" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/streamserver" @@ -180,7 +180,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { connectRequestHost = req.Host // return ok response - res := util.OkResponse() + res := httppkg.OkResponse() if res.Body != nil { defer res.Body.Close() } diff --git a/test/e2e/v1/features/ssh_tunnel.go b/test/e2e/v1/features/ssh_tunnel.go new file mode 100644 index 00000000000..f67d87aaf99 --- /dev/null +++ b/test/e2e/v1/features/ssh_tunnel.go @@ -0,0 +1,193 @@ +package features + +import ( + "crypto/tls" + "fmt" + "time" + + "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" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/ssh" +) + +var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("tcp", func() { + sshPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + sshTunnelGateway.bindPort = %d + `, sshPort) + + f.RunProcesses([]string{serverConf}, nil) + + localPort := f.PortByName(framework.TCPEchoServerPort) + remotePort := f.AllocPort() + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("tcp --remote_port %d", remotePort), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("http", func() { + sshPort := f.AllocPort() + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + sshTunnelGateway.bindPort = %d + `, vhostPort, sshPort) + + f.RunProcesses([]string{serverConf}, nil) + + localPort := f.PortByName(framework.HTTPSimpleServerPort) + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + "http --custom_domain test.example.com", + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + }) + + ginkgo.It("https", func() { + sshPort := f.AllocPort() + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPSPort = %d + sshTunnelGateway.bindPort = %d + `, vhostPort, sshPort) + + f.RunProcesses([]string{serverConf}, nil) + + localPort := f.AllocPort() + testDomain := "test.example.com" + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("https --custom_domain %s", testDomain), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + time.Sleep(time.Second) + framework.NewRequestExpect(f). + Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(testDomain).TLSConfig(&tls.Config{ + ServerName: testDomain, + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("tcpmux", func() { + sshPort := f.AllocPort() + tcpmuxPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + tcpmuxHTTPConnectPort = %d + sshTunnelGateway.bindPort = %d + `, tcpmuxPort, sshPort) + + f.RunProcesses([]string{serverConf}, nil) + + localPort := f.AllocPort() + testDomain := "test.example.com" + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("tcpmux --mux=httpconnect --custom_domain %s", testDomain), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + localServer := streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(localPort), + streamserver.WithRespContent([]byte("test")), + ) + f.RunServer("", localServer) + + time.Sleep(time.Second) + // Request without HTTP connect should get error + framework.NewRequestExpect(f). + Port(tcpmuxPort). + ExpectError(true). + Explain("request without HTTP connect expect error"). + Ensure() + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", tcpmuxPort) + // Request with incorrect connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr("invalid").Proxy(proxyURL) + }).ExpectError(true).Explain("request without HTTP connect expect error").Ensure() + + // Request with correct connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr(testDomain).Proxy(proxyURL) + }).ExpectResp([]byte("test")).Ensure() + }) + + ginkgo.It("stcp", func() { + sshPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + sshTunnelGateway.bindPort = %d + `, sshPort) + + bindPort := f.AllocPort() + visitorConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[visitors]] + name = "stcp-test-visitor" + type = "stcp" + serverName = "stcp-test" + secretKey = "abcdefg" + bindPort = %d + `, bindPort) + + f.RunProcesses([]string{serverConf}, []string{visitorConf}) + + localPort := f.PortByName(framework.TCPEchoServerPort) + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + "stcp -n stcp-test --sk=abcdefg --allow_users=\"*\"", + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + + framework.NewRequestExpect(f). + Port(bindPort). + Ensure() + }) +}) diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go index 66456f57f95..b043c57f13f 100644 --- a/test/e2e/v1/plugin/server.go +++ b/test/e2e/v1/plugin/server.go @@ -129,7 +129,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { framework.NewRequestExpect(f).Port(remotePort).Ensure() }) - ginkgo.It("Mofify RemotePort", func() { + ginkgo.It("Modify RemotePort", func() { localPort := f.AllocPort() remotePort := f.AllocPort() handler := func(req *plugin.Request) *plugin.Response {