Merge pull request #3845 from fatedier/dev

bump version to v0.53.0
This commit is contained in:
fatedier 2023-12-14 20:58:11 +08:00 committed by GitHub
commit 051299ec25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 3486 additions and 1561 deletions

View File

@ -22,7 +22,7 @@ jobs:
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # 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. # Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0 # args: --issues-exit-code=0

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ lastversion/
dist/ dist/
.idea/ .idea/
.vscode/ .vscode/
.autogen_ssh_key
# Cache # Cache
*.swp *.swp

View File

@ -1,5 +1,5 @@
service: 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: run:
concurrency: 4 concurrency: 4

View File

@ -26,10 +26,10 @@ vet:
go vet ./... go vet ./...
frps: 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: 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 test: gotest

View File

@ -11,6 +11,10 @@
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank"> <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png"> <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a> </a>
<a>&nbsp</a>
<a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
<img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
</a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@ -42,7 +46,7 @@ frp also offers a P2P connect mode.
* [Using Environment Variables](#using-environment-variables) * [Using Environment Variables](#using-environment-variables)
* [Split Configures Into Different Files](#split-configures-into-different-files) * [Split Configures Into Different Files](#split-configures-into-different-files)
* [Server Dashboard](#server-dashboard) * [Server Dashboard](#server-dashboard)
* [Admin UI](#admin-ui) * [Client Admin UI](#client-admin-ui)
* [Monitor](#monitor) * [Monitor](#monitor)
* [Prometheus](#prometheus) * [Prometheus](#prometheus)
* [Authenticating the Client](#authenticating-the-client) * [Authenticating the Client](#authenticating-the-client)
@ -71,9 +75,10 @@ frp also offers a P2P connect mode.
* [Custom Subdomain Names](#custom-subdomain-names) * [Custom Subdomain Names](#custom-subdomain-names)
* [URL Routing](#url-routing) * [URL Routing](#url-routing)
* [TCP Port Multiplexing](#tcp-port-multiplexing) * [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) * [Client Plugins](#client-plugins)
* [Server Manage Plugins](#server-manage-plugins) * [Server Manage Plugins](#server-manage-plugins)
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
* [Contributing](#contributing) * [Contributing](#contributing)
* [Donation](#donation) * [Donation](#donation)
* [GitHub Sponsors](#github-sponsors) * [GitHub Sponsors](#github-sponsors)
@ -505,6 +510,7 @@ includes = ["./confd/*.toml"]
```toml ```toml
# ./confd/test.toml # ./confd/test.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -616,6 +622,7 @@ The features are off by default. You can turn on encryption and/or compression:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -771,6 +778,7 @@ We would like to try to allow multiple proxies bind a same remote port with diff
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "ssh" name = "ssh"
type = "tcp" type = "tcp"
@ -876,6 +884,7 @@ This feature is only available for types `tcp`, `http`, `tcpmux` now.
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "test1" name = "test1"
type = "tcp" type = "tcp"
@ -911,6 +920,7 @@ With health check type **tcp**, the service port will be pinged (TCPing):
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "test1" name = "test1"
type = "tcp" type = "tcp"
@ -930,6 +940,7 @@ With health check type **http**, an HTTP request will be sent to the service and
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -954,6 +965,7 @@ However, speaking of web servers and HTTP requests, your web server might rely o
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -970,6 +982,7 @@ Similar to `Host`, You can override other HTTP request headers with proxy type `
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -997,6 +1010,7 @@ Here is an example for https service:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "https" type = "https"
@ -1019,6 +1033,7 @@ It can only be enabled when proxy type is http.
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -1043,6 +1058,7 @@ Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web" name = "web"
type = "http" type = "http"
@ -1062,6 +1078,7 @@ frp supports forwarding HTTP requests to different backend web services by url r
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "web01" name = "web01"
type = "http" type = "http"
@ -1147,6 +1164,7 @@ Using plugin **http_proxy**:
```toml ```toml
# frpc.toml # frpc.toml
[[proxies]] [[proxies]]
name = "http_proxy" name = "http_proxy"
type = "tcp" type = "tcp"
@ -1165,6 +1183,44 @@ Read the [document](/doc/server_plugin.md).
Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin). 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 ## Contributing
Interested in getting involved? We would like to help you! Interested in getting involved? We would like to help you!

View File

@ -13,6 +13,10 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank"> <a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png"> <img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a> </a>
<a>&nbsp</a>
<a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
<img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
</a>
</p> </p>
<!--gold sponsors end--> <!--gold sponsors end-->
@ -84,7 +88,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
### 知识星球 ### 知识星球
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: 如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg) ![zsxq](/doc/pic/zsxq.jpg)

View File

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

View File

@ -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
}

View File

@ -31,7 +31,9 @@ import (
"github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/config" "github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
) )
type GeneralResponse struct { type GeneralResponse struct {
@ -39,14 +41,42 @@ type GeneralResponse struct {
Msg string 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 // /healthz
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) { func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
} }
// GET /api/reload // 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} res := GeneralResponse{Code: 200}
strictConfigMode := false
strictStr := r.URL.Query().Get("strictConfig")
if strictStr != "" {
strictConfigMode, _ = strconv.ParseBool(strictStr)
}
log.Info("api request [/api/reload]") log.Info("api request [/api/reload]")
defer func() { 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 { if err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) log.Warn("reload frpc proxy config error: %s", res.Msg)
return return
} }
if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil { if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) log.Warn("reload frpc proxy config error: %s", res.Msg)
return return
} }
if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
res.Code = 500 res.Code = 500
res.Msg = err.Error() res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg) 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) _, _ = 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 { 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 { 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.Code = 400
res.Msg = "frpc has no config file path" res.Msg = "frpc has no config file path"
log.Warn("%s", res.Msg) log.Warn("%s", res.Msg)
return return
} }
content, err := os.ReadFile(svr.cfgFile) content, err := os.ReadFile(svr.configFilePath)
if err != nil { if err != nil {
res.Code = 400 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
@ -218,7 +255,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
return 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.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err) res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warn("%s", res.Msg) log.Warn("%s", res.Msg)

227
client/connector.go Normal file
View File

@ -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
}

View File

@ -16,13 +16,10 @@ package client
import ( import (
"context" "context"
"io"
"net" "net"
"runtime/debug" "sync/atomic"
"time" "time"
"github.com/fatedier/golib/control/shutdown"
"github.com/fatedier/golib/crypto"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/proxy"
@ -31,101 +28,99 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport" "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" "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 { type Control struct {
// service context // service context
ctx context.Context ctx context.Context
xl *xlog.Logger xl *xlog.Logger
// Unique ID obtained from frps. // session context
// It should be attached to the login message when reconnecting. sessionCtx *SessionContext
runID string
// manage all proxies // manage all proxies
pxyCfgs []v1.ProxyConfigurer
pm *proxy.Manager pm *proxy.Manager
// manage all visitors // manage all visitors
vm *visitor.Manager vm *visitor.Manager
// control connection doneCh chan struct{}
conn net.Conn
cm *ConnectionManager // of time.Time, last time got the Pong message
lastPong atomic.Value
// 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
// 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 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( func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
clientCfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
authSetter auth.Setter,
) *Control {
// new xlog instance // new xlog instance
ctl := &Control{ ctl := &Control{
ctx: ctx, ctx: ctx,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
runID: runID, sessionCtx: sessionCtx,
conn: conn, doneCh: make(chan struct{}),
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,
} }
ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) ctl.lastPong.Store(time.Now())
ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter) if sessionCtx.ConnEncrypted {
ctl.vm.Reload(visitorCfgs) cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
return ctl 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() go ctl.worker()
// start all proxies // start all proxies
ctl.pm.Reload(ctl.pxyCfgs) ctl.pm.UpdateAll(proxyCfgs)
// start all visitors // start all visitors
go ctl.vm.Run() ctl.vm.UpdateAll(visitorCfgs)
} }
func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) { func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
ctl.pm.SetInWorkConnCallback(cb)
}
func (ctl *Control) handleReqWorkConn(_ msg.Message) {
xl := ctl.xl xl := ctl.xl
workConn, err := ctl.connectServer() workConn, err := ctl.connectServer()
if err != nil { if err != nil {
@ -134,9 +129,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
} }
m := &msg.NewWorkConn{ 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) xl.Warn("error during NewWorkConn authentication: %v", err)
return return
} }
@ -162,8 +157,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
} }
func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { func (ctl *Control) handleNewProxyResp(m msg.Message) {
xl := ctl.xl xl := ctl.xl
inMsg := m.(*msg.NewProxyResp)
// Server will return NewProxyResp message to each NewProxy message. // Server will return NewProxyResp message to each NewProxy message.
// Start a new proxy handler if no error got // Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) 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 xl := ctl.xl
inMsg := m.(*msg.NatHoleResp)
// Dispatch the NatHoleResp message to the related proxy. // Dispatch the NatHoleResp message to the related proxy.
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID) 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 { func (ctl *Control) Close() error {
return ctl.GracefulClose(0) return ctl.GracefulClose(0)
} }
@ -194,170 +210,86 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
time.Sleep(d) time.Sleep(d)
ctl.conn.Close() ctl.closeSession()
ctl.cm.Close()
return nil return nil
} }
// ClosedDoneCh returns a channel that will be closed after all resources are released // Done returns a channel that will be closed after all resources are released
func (ctl *Control) ClosedDoneCh() <-chan struct{} { func (ctl *Control) Done() <-chan struct{} {
return ctl.closedDoneCh return ctl.doneCh
} }
// connectServer return a new connection to frps // connectServer return a new connection to frps
func (ctl *Control) connectServer() (conn net.Conn, err error) { func (ctl *Control) connectServer() (net.Conn, error) {
return ctl.cm.Connect() return ctl.sessionCtx.Connector.Connect()
} }
// reader read all messages from frps and send to readCh func (ctl *Control) registerMsgHandlers() {
func (ctl *Control) reader() { ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
xl := ctl.xl ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
defer func() { ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
if err := recover(); err != nil { ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
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
}
} }
// writer writes messages got from sendCh to frps // headerWorker sends heartbeat to server and check heartbeat timeout.
func (ctl *Control) writer() { func (ctl *Control) heartbeatWorker() {
xl := ctl.xl 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 { // TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
xl.Warn("write message to control connection error: %v", err) // Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
return if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
}
}
}
// 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
}
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
}
ctl.lastPong = time.Now()
for {
select {
case <-hbSendCh:
// send heartbeat to server // send heartbeat to server
sendHeartBeat := func() error {
xl.Debug("send heartbeat to server") xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{} pingMsg := &msg.Ping{}
if err := ctl.authSetter.SetPing(pingMsg); err != nil { if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
xl.Warn("error during ping authentication: %v", err) xl.Warn("error during ping authentication: %v, skip sending ping message", err)
return return err
} }
ctl.sendCh <- pingMsg _ = ctl.msgDispatcher.Send(pingMsg)
case <-hbCheckCh: return nil
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
// let reader() stop
ctl.conn.Close()
return
}
case rawMsg, ok := <-ctl.readCh:
if !ok {
return
} }
switch m := rawMsg.(type) { go wait.BackoffUntil(sendHeartBeat,
case *msg.ReqWorkConn: wait.NewFastBackoffManager(wait.FastBackoffOptions{
go ctl.HandleReqWorkConn(m) Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
case *msg.NewProxyResp: InitDurationIfFail: time.Second,
ctl.HandleNewProxyResp(m) Factor: 2.0,
case *msg.NatHoleResp: Jitter: 0.1,
ctl.HandleNatHoleResp(m) MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
case *msg.Pong: }),
if m.Error != "" { true, ctl.doneCh,
xl.Error("Pong contains error: %s", m.Error) )
ctl.conn.Close() }
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 &&
!lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) {
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")
ctl.closeSession()
return return
} }
ctl.lastPong = time.Now() }, time.Second, ctl.doneCh)
xl.Debug("receive heartbeat from server")
}
}
} }
} }
// If controler is notified by closedCh, reader and writer and handler will exit
func (ctl *Control) worker() { func (ctl *Control) worker() {
go ctl.msgHandler() go ctl.heartbeatWorker()
go ctl.reader() go ctl.msgDispatcher.Run()
go ctl.writer()
<-ctl.closedCh <-ctl.msgDispatcher.Done()
// close related channels and wait until other goroutines done ctl.closeSession()
close(ctl.readCh)
ctl.readerShutdown.WaitDone()
ctl.msgHandlerShutdown.WaitDone()
close(ctl.sendCh)
ctl.writerShutdown.WaitDone()
ctl.pm.Close() ctl.pm.Close()
ctl.vm.Close() ctl.vm.Close()
close(ctl.doneCh)
close(ctl.closedDoneCh)
ctl.cm.Close()
} }
func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
ctl.vm.Reload(visitorCfgs) ctl.vm.UpdateAll(visitorCfgs)
ctl.pm.Reload(pxyCfgs) ctl.pm.UpdateAll(proxyCfgs)
return nil return nil
} }

View File

@ -47,10 +47,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v
// Proxy defines how to handle work connections for different proxy type. // Proxy defines how to handle work connections for different proxy type.
type Proxy interface { type Proxy interface {
Run() error Run() error
// InWorkConn accept work connections registered to server. // InWorkConn accept work connections registered to server.
InWorkConn(net.Conn, *msg.StartWorkConn) InWorkConn(net.Conn, *msg.StartWorkConn)
SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
Close() Close()
} }
@ -90,6 +89,7 @@ type BaseProxy struct {
// proxyPlugin is used to handle connections instead of dialing to local service. // proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now. // 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 mu sync.RWMutex
xl *xlog.Logger 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) { 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)) 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) baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
if baseCfg.Transport.UseEncryption { if baseCfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, encKey) remote, err = libio.WithEncryption(remote, encKey)

View File

@ -33,6 +33,7 @@ import (
type Manager struct { type Manager struct {
proxies map[string]*Wrapper proxies map[string]*Wrapper
msgTransporter transport.MessageTransporter msgTransporter transport.MessageTransporter
inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
closed bool closed bool
mu sync.RWMutex mu sync.RWMutex
@ -71,6 +72,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri
return nil return nil
} }
func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
pm.inWorkConnCallback = cb
}
func (pm *Manager) Close() { func (pm *Manager) Close() {
pm.mu.Lock() pm.mu.Lock()
defer pm.mu.Unlock() defer pm.mu.Unlock()
@ -115,9 +120,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
return ps 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) 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 return c.GetBaseConfig().Name
}) })
pm.mu.Lock() pm.mu.Lock()
@ -126,7 +140,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
delPxyNames := make([]string, 0) delPxyNames := make([]string, 0)
for name, pxy := range pm.proxies { for name, pxy := range pm.proxies {
del := false del := false
cfg, ok := pxyCfgsMap[name] cfg, ok := proxyCfgsMap[name]
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) { if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
del = true del = true
} }
@ -142,10 +156,13 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
} }
addPxyNames := make([]string, 0) addPxyNames := make([]string, 0)
for _, cfg := range pxyCfgs { for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok { if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter) pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
if pm.inWorkConnCallback != nil {
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
}
pm.proxies[name] = pxy pm.proxies[name] = pxy
addPxyNames = append(addPxyNames, name) addPxyNames = append(addPxyNames, name)

View File

@ -121,6 +121,10 @@ func NewWrapper(
return pw 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 { func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
pw.mu.Lock() pw.mu.Lock()
defer pw.mu.Unlock() defer pw.mu.Unlock()

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -29,7 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit" "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() { func init() {
@ -99,7 +101,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if pxy.cfg.Transport.UseCompression { if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc) rwc = libio.WithCompression(rwc)
} }
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn workConn := conn
readCh := make(chan *msg.UDPPacket, 1024) readCh := make(chan *msg.UDPPacket, 1024)

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -28,7 +30,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit" "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() { func init() {
@ -89,7 +91,7 @@ func (pxy *UDPProxy) Close() {
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) 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() pxy.Close()
var rwc io.ReadWriteCloser = conn var rwc io.ReadWriteCloser = conn
@ -110,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if pxy.cfg.Transport.UseCompression { if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc) rwc = libio.WithCompression(rwc)
} }
conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock() pxy.mu.Lock()
pxy.workConn = conn pxy.workConn = conn

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package proxy package proxy
import ( import (
@ -27,7 +29,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -131,7 +133,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
} }
defer lConn.Close() defer lConn.Close()
remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String()) remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil { if err != nil {
xl.Warn("create kcp connection from udp connection error: %v", err) xl.Warn("create kcp connection from udp connection error: %v", err)
return return
@ -192,6 +194,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
_ = c.CloseWithError(0, "") _ = c.CloseWithError(0, "")
return 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))
} }
} }

View File

@ -16,32 +16,25 @@ package client
import ( import (
"context" "context"
"crypto/tls" "errors"
"fmt" "fmt"
"io"
"net" "net"
"runtime" "runtime"
"strconv"
"strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/fatedier/golib/crypto" "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/samber/lo"
"github.com/fatedier/frp/assets" "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "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" "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/util"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
@ -49,212 +42,197 @@ func init() {
crypto.DefaultSalt = "frp" crypto.DefaultSalt = "frp"
} }
// Service is a client service. type cancelErr struct {
type Service struct { Err error
// uniq id got from frps, attach it in loginMsg }
runID string
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 // manager control connection with server
ctl *Control ctl *Control
ctlMu sync.RWMutex // Uniq id got from frps, it will be attached to loginMsg.
runID string
// Sets authentication based on selected method // Sets authentication based on selected method
authSetter auth.Setter authSetter auth.Setter
cfg *v1.ClientCommonConfig // web server for admin UI and apis
pxyCfgs []v1.ProxyConfigurer webServer *httppkg.Server
visitorCfgs []v1.VisitorConfigurer
cfgMu sync.RWMutex 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 // The configuration file used to initialize this client, or an empty
// string if no configuration file was used. // string if no configuration file was used.
cfgFile string configFilePath string
exit uint32 // 0 means not exit
// service context // service context
ctx context.Context ctx context.Context
// call cancel to stop service // call cancel to stop service
cancel context.CancelFunc cancel context.CancelCauseFunc
gracefulShutdownDuration time.Duration
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
} }
func NewService( func NewService(options ServiceOptions) (*Service, error) {
cfg *v1.ClientCommonConfig, setServiceOptionsDefault(&options)
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer, var webServer *httppkg.Server
cfgFile string, if options.Common.WebServer.Port > 0 {
) (svr *Service, err error) { ws, err := httppkg.NewServer(options.Common.WebServer)
svr = &Service{ if err != nil {
authSetter: auth.NewAuthSetter(cfg.Auth), return nil, err
cfg: cfg,
cfgFile: cfgFile,
pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs,
ctx: context.Background(),
exit: 0,
} }
return webServer = ws
} }
s := &Service{
func (svr *Service) GetController() *Control { ctx: context.Background(),
svr.ctlMu.RLock() authSetter: auth.NewAuthSetter(options.Common.Auth),
defer svr.ctlMu.RUnlock() webServer: webServer,
return svr.ctl 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 { func (svr *Service) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancelCause(ctx)
svr.ctx = xlog.NewContext(ctx, xlog.New()) svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
svr.cancel = cancel svr.cancel = cancel
xl := xlog.FromContextSafe(svr.ctx)
// set custom DNSServer // set custom DNSServer
if svr.cfg.DNSServer != "" { if svr.common.DNSServer != "" {
dnsAddr := svr.cfg.DNSServer netpkg.SetDefaultDNSAddress(svr.common.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)
},
}
} }
// login to frps // first login to frps
for { svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
conn, cm, err := svr.login() if svr.ctl == nil {
if err != nil { cancelCause := cancelErr{}
xl.Warn("login to server failed: %v", err) _ = 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)
// 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
}
} }
go svr.keepControllerWorking() go svr.keepControllerWorking()
if svr.cfg.WebServer.Port != 0 { if svr.webServer != nil {
// Init admin server assets go func() {
assets.Load(svr.cfg.WebServer.AssetsDir) log.Info("admin server listen on %s", svr.webServer.Address())
if err := svr.webServer.Run(); err != nil {
address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port)) log.Warn("admin server exit with error: %v", err)
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) }()
} }
<-svr.ctx.Done() <-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources svr.stop()
if atomic.LoadUint32(&svr.exit) == 0 {
svr.Close()
}
return nil return nil
} }
func (svr *Service) keepControllerWorking() { func (svr *Service) keepControllerWorking() {
xl := xlog.FromContextSafe(svr.ctx) <-svr.ctl.Done()
maxDelayTime := 20 * time.Second
delayTime := time.Second
// if frpc reconnect frps, we need to limit retry times in 1min // There is a situation where the login is successful but due to certain reasons,
// current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ... // the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
// when exceed 1min, we will reset delay and counts // The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
cutoffTime := time.Now().Add(time.Minute) // The maximum interval is 20 seconds.
reconnectDelay := time.Second wait.BackoffUntil(func() error {
reconnectCounts := 1 // loopLoginUntilSuccess is another layer of loop that will continuously attempt to
// login to the server until successful.
for { svr.loopLoginUntilSuccess(20*time.Second, false)
<-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 { if svr.ctl != nil {
svr.ctl.Close() <-svr.ctl.Done()
} return errors.New("control is closed and try another loop")
svr.ctl = ctl
svr.ctlMu.Unlock()
break
}
} }
// 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 // login creates a connection to frps and registers it self as a client
// conn: control connection // conn: control connection
// session: if it's not nil, using tcp mux // 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) xl := xlog.FromContextSafe(svr.ctx)
cm = NewConnectionManager(svr.ctx, svr.cfg) connector = svr.connectorCreator(svr.ctx, svr.common)
if err = connector.Open(); err != nil {
if err = cm.OpenConnection(); err != nil {
return nil, nil, err return nil, nil, err
} }
defer func() { defer func() {
if err != nil { if err != nil {
cm.Close() connector.Close()
} }
}() }()
conn, err = cm.Connect() conn, err = connector.Connect()
if err != nil { if err != nil {
return return
} }
@ -262,12 +240,15 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
loginMsg := &msg.Login{ loginMsg := &msg.Login{
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Os: runtime.GOOS, Os: runtime.GOOS,
PoolCount: svr.cfg.Transport.PoolCount, PoolCount: svr.common.Transport.PoolCount,
User: svr.cfg.User, User: svr.common.User,
Version: version.Full(), Version: version.Full(),
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
RunID: svr.runID, RunID: svr.runID,
Metas: svr.cfg.Metadatas, Metas: svr.common.Metadatas,
}
if svr.clientSpec != nil {
loginMsg.ClientSpec = *svr.clientSpec
} }
// Add auth // Add auth
@ -293,16 +274,79 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
} }
svr.runID = loginRespMsg.RunID svr.runID = loginRespMsg.RunID
xl.ResetPrefixes() xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
xl.AppendPrefix(svr.runID)
xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID) xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
return 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.cfgMu.Lock()
svr.pxyCfgs = pxyCfgs svr.proxyCfgs = proxyCfgs
svr.visitorCfgs = visitorCfgs svr.visitorCfgs = visitorCfgs
svr.cfgMu.Unlock() svr.cfgMu.Unlock()
@ -311,7 +355,7 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi
svr.ctlMu.RUnlock() svr.ctlMu.RUnlock()
if ctl != nil { if ctl != nil {
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs) return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
} }
return nil return nil
} }
@ -321,191 +365,31 @@ func (svr *Service) Close() {
} }
func (svr *Service) GracefulClose(d time.Duration) { 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 { if svr.ctl != nil {
svr.ctl.GracefulClose(d) svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
svr.ctl = nil svr.ctl = nil
} }
}
// 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() svr.ctlMu.RUnlock()
if svr.cancel != nil { if ctl == nil {
svr.cancel() return nil, fmt.Errorf("control is not running")
} }
} ws, ok := ctl.pm.GetProxyStatus(name)
if !ok {
type ConnectionManager struct { return nil, fmt.Errorf("proxy [%s] is not found", name)
ctx context.Context }
cfg *v1.ClientCommonConfig return ws, nil
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
}
func (cm *ConnectionManager) Close() error {
if cm.quicConn != nil {
_ = cm.quicConn.CloseWithError(0, "")
}
if cm.muxSession != nil {
_ = cm.muxSession.Close()
}
return nil
} }

View File

@ -28,7 +28,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "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/util"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
) )
@ -242,7 +242,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
if sv.cfg.Transport.UseCompression { if sv.cfg.Transport.UseCompression {
remote = libio.WithCompression(remote) remote = libio.WithCompression(remote)
} }
return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
} }
func (sv *SUDPVisitor) Close() { func (sv *SUDPVisitor) Close() {

View File

@ -21,11 +21,11 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport" "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" "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 { type Helper interface {
// ConnectServer directly connects to the frp server. // ConnectServer directly connects to the frp server.
ConnectServer() (net.Conn, error) ConnectServer() (net.Conn, error)
@ -56,7 +56,7 @@ func NewVisitor(
clientCfg: clientCfg, clientCfg: clientCfg,
helper: helper, helper: helper,
ctx: xlog.NewContext(ctx, xl), ctx: xlog.NewContext(ctx, xl),
internalLn: utilnet.NewInternalListener(), internalLn: netpkg.NewInternalListener(),
} }
switch cfg := cfg.(type) { switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig: case *v1.STCPVisitorConfig:
@ -84,7 +84,7 @@ type BaseVisitor struct {
clientCfg *v1.ClientCommonConfig clientCfg *v1.ClientCommonConfig
helper Helper helper Helper
l net.Listener l net.Listener
internalLn *utilnet.InternalListener internalLn *netpkg.InternalListener
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context

View File

@ -36,6 +36,7 @@ type Manager struct {
helper Helper helper Helper
checkInterval time.Duration checkInterval time.Duration
keepVisitorsRunningOnce sync.Once
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@ -67,7 +68,9 @@ func NewManager(
return m 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) xl := xlog.FromContextSafe(vm.ctx)
ticker := time.NewTicker(vm.checkInterval) ticker := time.NewTicker(vm.checkInterval)
@ -76,7 +79,7 @@ func (vm *Manager) Run() {
for { for {
select { select {
case <-vm.stopCh: case <-vm.stopCh:
xl.Info("gracefully shutdown visitor manager") xl.Trace("gracefully shutdown visitor manager")
return return
case <-ticker.C: case <-ticker.C:
vm.mu.Lock() vm.mu.Lock()
@ -120,7 +123,14 @@ func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
return 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) xl := xlog.FromContextSafe(vm.ctx)
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string { cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
return c.GetBaseConfig().Name return c.GetBaseConfig().Name

View File

@ -33,7 +33,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport" "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/util"
"github.com/fatedier/frp/pkg/util/xlog" "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 { if err != nil {
return fmt.Errorf("dial udp error: %v", err) 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 { if err != nil {
return fmt.Errorf("create kcp connection from udp connection error: %v", err) 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 { if err != nil {
return nil, err return nil, err
} }
return utilnet.QuicStreamToNetConn(stream, session), nil return netpkg.QuicStreamToNetConn(stream, session), nil
} }
func (qs *QUICTunnelSession) Close() { func (qs *QUICTunnelSession) Close() {

View File

@ -52,7 +52,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
Use: name, Use: name,
Short: short, Short: short,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cfg, _, _, _, err := config.LoadClientConfig(cfgFile) cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -73,7 +73,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Reload(); err != nil { if err := client.Reload(strictConfigMode); err != nil {
return err return err
} }
fmt.Println("reload success") fmt.Println("reload success")

View File

@ -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")
}

View File

@ -48,9 +48,10 @@ var natholeDiscoveryCmd = &cobra.Command{
Short: "Discover nathole information from stun server", Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters // ignore error here, because we can use command line pameters
cfg, _, _, _, err := config.LoadClientConfig(cfgFile) cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
cfg = &v1.ClientCommonConfig{} cfg = &v1.ClientCommonConfig{}
cfg.Complete()
} }
if natHoleSTUNServer != "" { if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer cfg.NatHoleSTUNServer = natHoleSTUNServer

View File

@ -21,6 +21,7 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/config/v1/validation"
) )
@ -50,8 +51,8 @@ func init() {
} }
clientCfg := v1.ClientCommonConfig{} clientCfg := v1.ClientCommonConfig{}
cmd := NewProxyCommand(string(typ), c, &clientCfg) cmd := NewProxyCommand(string(typ), c, &clientCfg)
RegisterClientCommonConfigFlags(cmd, &clientCfg) config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
RegisterProxyFlags(cmd, c) config.RegisterProxyFlags(cmd, c)
// add sub command for visitor // add sub command for visitor
if lo.Contains(visitorTypes, v1.VisitorType(typ)) { if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
@ -60,7 +61,7 @@ func init() {
panic("visitor type: " + typ + " not support") panic("visitor type: " + typ + " not support")
} }
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg) visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
RegisterVisitorFlags(visitorCmd, vc) config.RegisterVisitorFlags(visitorCmd, vc)
cmd.AddCommand(visitorCmd) cmd.AddCommand(visitorCmd)
} }
rootCmd.AddCommand(cmd) rootCmd.AddCommand(cmd)

View File

@ -39,12 +39,14 @@ var (
cfgFile string cfgFile string
cfgDir string cfgDir string
showVersion bool showVersion bool
strictConfigMode bool
) )
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") 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().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(&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{ var rootCmd = &cobra.Command{
@ -108,7 +110,7 @@ func handleTermSignal(svr *client.Service) {
} }
func runClient(cfgFilePath string) error { 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 { if err != nil {
return err return err
} }
@ -117,19 +119,19 @@ func runClient(cfgFilePath string) error {
"please use yaml/json/toml format instead!\n") "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 { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }
if err != nil { if err != nil {
return err return err
} }
return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
} }
func startService( func startService(
cfg *v1.ClientCommonConfig, cfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer, proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer, visitorCfgs []v1.VisitorConfigurer,
cfgFile string, cfgFile string,
) error { ) error {
@ -139,7 +141,12 @@ func startService(
log.Info("start frpc service for config file [%s]", cfgFile) log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", 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 { if err != nil {
return err return err
} }
@ -149,7 +156,5 @@ func startService(
if shouldGracefulClose { if shouldGracefulClose {
go handleTermSignal(svr) go handleTermSignal(svr)
} }
return svr.Run(context.Background())
_ = svr.Run(context.Background())
return nil
} }

View File

@ -37,12 +37,12 @@ var verifyCmd = &cobra.Command{
return nil return nil
} }
cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile) cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs) warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
if warning != nil { if warning != nil {
fmt.Printf("WARNING: %v\n", warning) fmt.Printf("WARNING: %v\n", warning)
} }

View File

@ -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")
}

View File

@ -32,6 +32,7 @@ import (
var ( var (
cfgFile string cfgFile string
showVersion bool showVersion bool
strictConfigMode bool
serverCfg v1.ServerConfig serverCfg v1.ServerConfig
) )
@ -39,8 +40,9 @@ var (
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version 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{ var rootCmd = &cobra.Command{
@ -58,7 +60,7 @@ var rootCmd = &cobra.Command{
err error err error
) )
if cfgFile != "" { if cfgFile != "" {
svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile) svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{
fmt.Println("frps: the configuration file is not specified") fmt.Println("frps: the configuration file is not specified")
return nil return nil
} }
svrCfg, _, err := config.LoadServerConfig(cfgFile) svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

View File

@ -38,7 +38,7 @@ auth.token = "12345678"
# auth.oidc.clientSecret = "" # auth.oidc.clientSecret = ""
# oidc.audience specifies the audience of the token in OIDC authentication. # oidc.audience specifies the audience of the token in OIDC authentication.
# auth.oidc.audience = "" # 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 = "" # auth.oidc.scope = ""
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint. # oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token. # It will be used to get an OIDC token.

View File

@ -143,6 +143,14 @@ udpPacketSize = 1500
# Retention time for NAT hole punching strategy data. # Retention time for NAT hole punching strategy data.
natholeAnalysisDataReserveHours = 168 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]] [[httpPlugins]]
name = "user-manager" name = "user-manager"
addr = "127.0.0.1:9000" addr = "127.0.0.1:9000"

View File

@ -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 specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_audience = 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_scope =
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint. # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
doc/pic/sponsor_nango.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

160
doc/ssh_tunnel_gateway.md Normal file
View File

@ -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.

12
go.mod
View File

@ -21,9 +21,11 @@ require (
github.com/quic-go/quic-go v0.37.4 github.com/quic-go/quic-go v0.37.4
github.com/rodaine/table v1.1.0 github.com/rodaine/table v1.1.0
github.com/samber/lo v1.38.1 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 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/oauth2 v0.10.0
golang.org/x/sync v0.3.0 golang.org/x/sync v0.3.0
golang.org/x/time 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/prometheus/procfs v0.10.1 // indirect
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // 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/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // 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/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.10.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.10.0 // indirect golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.11.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.9.3 // indirect golang.org/x/tools v0.9.3 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect

23
go.sum
View File

@ -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/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 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 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/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 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 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.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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-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.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.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 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-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 h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-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.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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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.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 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= 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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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.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.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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

31
pkg/auth/pass.go Normal file
View File

@ -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 }

244
pkg/config/flags.go Normal file
View File

@ -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 := &registerFlagOptions{}
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 := &registerFlagOptions{}
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")
}

View File

@ -99,7 +99,7 @@ type ClientCommonConf struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` 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. // 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"` TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// User specifies a prefix for proxy names to distinguish them from other // User specifies a prefix for proxy names to distinguish them from other

View File

@ -23,7 +23,7 @@ import (
func ParseClientConfig(filePath string) ( func ParseClientConfig(filePath string) (
cfg ClientCommonConf, cfg ClientCommonConf,
pxyCfgs map[string]ProxyConf, proxyCfgs map[string]ProxyConf,
visitorCfgs map[string]VisitorConf, visitorCfgs map[string]VisitorConf,
err error, err error,
) { ) {
@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) (
configBuffer.Write(buf) configBuffer.Write(buf)
// Parse all proxy and visitor configs. // 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 { if err != nil {
return return
} }

View File

@ -139,7 +139,7 @@ type ServerCommonConf struct {
// from a client to share a single TCP connection. By default, this value // from a client to share a single TCP connection. By default, this value
// is true. // is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` 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. // 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"` 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. // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.

View File

@ -100,26 +100,42 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
return RenderWithTemplate(b, values) 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()) content, err := LoadFileContentWithTemplate(path, GetValues())
if err != nil { if err != nil {
return err return err
} }
return LoadConfigure(content, c) return LoadConfigure(content, c, strict)
} }
// LoadConfigure loads configuration from bytes and unmarshal into c. // LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format. // 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{} 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 { if err := toml.Unmarshal(b, &tomlObj); err == nil {
b, err = json.Marshal(&tomlObj) b, err = json.Marshal(&tomlObj)
if err != nil { if err != nil {
return err return err
} }
} }
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096) // 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) 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) { 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 return configurer, nil
} }
func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) {
var ( var (
svrCfg *v1.ServerConfig svrCfg *v1.ServerConfig
isLegacyFormat bool isLegacyFormat bool
@ -158,7 +174,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
isLegacyFormat = true isLegacyFormat = true
} else { } else {
svrCfg = &v1.ServerConfig{} svrCfg = &v1.ServerConfig{}
if err := LoadConfigureFromFile(path, svrCfg); err != nil { if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil {
return nil, false, err return nil, false, err
} }
} }
@ -168,7 +184,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
return svrCfg, isLegacyFormat, nil return svrCfg, isLegacyFormat, nil
} }
func LoadClientConfig(path string) ( func LoadClientConfig(path string, strict bool) (
*v1.ClientCommonConfig, *v1.ClientCommonConfig,
[]v1.ProxyConfigurer, []v1.ProxyConfigurer,
[]v1.VisitorConfigurer, []v1.VisitorConfigurer,
@ -176,19 +192,19 @@ func LoadClientConfig(path string) (
) { ) {
var ( var (
cliCfg *v1.ClientCommonConfig cliCfg *v1.ClientCommonConfig
pxyCfgs = make([]v1.ProxyConfigurer, 0) proxyCfgs = make([]v1.ProxyConfigurer, 0)
visitorCfgs = make([]v1.VisitorConfigurer, 0) visitorCfgs = make([]v1.VisitorConfigurer, 0)
isLegacyFormat bool isLegacyFormat bool
) )
if DetectLegacyINIFormatFromFile(path) { if DetectLegacyINIFormatFromFile(path) {
legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
if err != nil { if err != nil {
return nil, nil, nil, true, err return nil, nil, nil, true, err
} }
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
for _, c := range legacyPxyCfgs { for _, c := range legacyProxyCfgs {
pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
} }
for _, c := range legacyVisitorCfgs { for _, c := range legacyVisitorCfgs {
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c)) visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
@ -196,12 +212,12 @@ func LoadClientConfig(path string) (
isLegacyFormat = true isLegacyFormat = true
} else { } else {
allCfg := v1.ClientConfig{} allCfg := v1.ClientConfig{}
if err := LoadConfigureFromFile(path, &allCfg); err != nil { if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
return nil, nil, nil, false, err return nil, nil, nil, false, err
} }
cliCfg = &allCfg.ClientCommonConfig cliCfg = &allCfg.ClientCommonConfig
for _, c := range allCfg.Proxies { for _, c := range allCfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
} }
for _, c := range allCfg.Visitors { for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@ -209,20 +225,20 @@ func LoadClientConfig(path string) (
} }
// Load additional config from includes. // 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 { if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat) extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
if err != nil { if err != nil {
return nil, nil, nil, isLegacyFormat, err return nil, nil, nil, isLegacyFormat, err
} }
pxyCfgs = append(pxyCfgs, extPxyCfgs...) proxyCfgs = append(proxyCfgs, extProxyCfgs...)
visitorCfgs = append(visitorCfgs, extVisitorCfgs...) visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
} }
// Filter by start // Filter by start
if len(cliCfg.Start) > 0 { if len(cliCfg.Start) > 0 {
startSet := sets.New(cliCfg.Start...) 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) return startSet.Has(c.GetBaseConfig().Name)
}) })
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
@ -233,17 +249,17 @@ func LoadClientConfig(path string) (
if cliCfg != nil { if cliCfg != nil {
cliCfg.Complete() cliCfg.Complete()
} }
for _, c := range pxyCfgs { for _, c := range proxyCfgs {
c.Complete(cliCfg.User) c.Complete(cliCfg.User)
} }
for _, c := range visitorCfgs { for _, c := range visitorCfgs {
c.Complete(cliCfg) 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) { func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
pxyCfgs := make([]v1.ProxyConfigurer, 0) proxyCfgs := make([]v1.ProxyConfigurer, 0)
visitorCfgs := make([]v1.VisitorConfigurer, 0) visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, path := range paths { for _, path := range paths {
absDir, err := filepath.Abs(filepath.Dir(path)) 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 { if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
// support yaml/json/toml // support yaml/json/toml
cfg := v1.ClientConfig{} 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) return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
} }
for _, c := range cfg.Proxies { for _, c := range cfg.Proxies {
pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
} }
for _, c := range cfg.Visitors { for _, c := range cfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) 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
} }

View File

@ -15,6 +15,8 @@
package config package config
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -22,9 +24,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
) )
func TestLoadConfigure(t *testing.T) { const tomlServerContent = `
require := require.New(t)
content := `
bindAddr = "127.0.0.1" bindAddr = "127.0.0.1"
kcpBindPort = 7000 kcpBindPort = 7000
quicBindPort = 7001 quicBindPort = 7001
@ -33,8 +33,43 @@ custom404Page = "/abc.html"
transport.tcpKeepalive = 10 transport.tcpKeepalive = 10
` `
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{} svrCfg := v1.ServerConfig{}
err := LoadConfigure([]byte(content), &svrCfg) err := LoadConfigure([]byte(test.content), &svrCfg, true)
require.NoError(err) require.NoError(err)
require.EqualValues("127.0.0.1", svrCfg.BindAddr) require.EqualValues("127.0.0.1", svrCfg.BindAddr)
require.EqualValues(7000, svrCfg.KCPBindPort) require.EqualValues(7000, svrCfg.KCPBindPort)
@ -42,4 +77,90 @@ transport.tcpKeepalive = 10
require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
require.EqualValues("/abc.html", svrCfg.Custom404Page) require.EqualValues("/abc.html", svrCfg.Custom404Page)
require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) 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)
pluginStr += `unknown = "unknown"`
err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
require.Error(err)
} }

View File

@ -111,7 +111,7 @@ type ClientTransportConfig struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux *bool `json:"tcpMux,omitempty"` 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. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// QUIC protocol options. // QUIC protocol options.

View File

@ -15,9 +15,23 @@
package v1 package v1
import ( import (
"sync"
"github.com/fatedier/frp/pkg/util/util" "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 type AuthScope string
const ( const (
@ -83,7 +97,7 @@ type TLSConfig struct {
} }
type LogConfig 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, // If "console" is used, logs will be printed to stdout, otherwise,
// logs will be written to the specified file. // logs will be written to the specified file.
// By default, this value is "console". // By default, this value is "console".

View File

@ -15,6 +15,7 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
@ -49,7 +50,13 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type) return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
} }
options := reflect.New(v).Interface().(ClientPluginOptions) 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 return err
} }
c.ClientPluginOptions = options c.ClientPluginOptions = options
@ -77,17 +84,20 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{
} }
type HTTP2HTTPSPluginOptions struct { type HTTP2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
} }
type HTTPProxyPluginOptions struct { type HTTPProxyPluginOptions struct {
Type string `json:"type,omitempty"`
HTTPUser string `json:"httpUser,omitempty"` HTTPUser string `json:"httpUser,omitempty"`
HTTPPassword string `json:"httpPassword,omitempty"` HTTPPassword string `json:"httpPassword,omitempty"`
} }
type HTTPS2HTTPPluginOptions struct { type HTTPS2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
@ -96,6 +106,7 @@ type HTTPS2HTTPPluginOptions struct {
} }
type HTTPS2HTTPSPluginOptions struct { type HTTPS2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"` LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
@ -104,11 +115,13 @@ type HTTPS2HTTPSPluginOptions struct {
} }
type Socks5PluginOptions struct { type Socks5PluginOptions struct {
Type string `json:"type,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
} }
type StaticFilePluginOptions struct { type StaticFilePluginOptions struct {
Type string `json:"type,omitempty"`
LocalPath string `json:"localPath,omitempty"` LocalPath string `json:"localPath,omitempty"`
StripPrefix string `json:"stripPrefix,omitempty"` StripPrefix string `json:"stripPrefix,omitempty"`
HTTPUser string `json:"httpUser,omitempty"` HTTPUser string `json:"httpUser,omitempty"`
@ -116,5 +129,6 @@ type StaticFilePluginOptions struct {
} }
type UnixDomainSocketPluginOptions struct { type UnixDomainSocketPluginOptions struct {
Type string `json:"type,omitempty"`
UnixPath string `json:"unixPath,omitempty"` UnixPath string `json:"unixPath,omitempty"`
} }

View File

@ -15,6 +15,7 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -177,7 +178,11 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
if configurer == nil { if configurer == nil {
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type) 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 return err
} }
c.ProxyConfigurer = configurer c.ProxyConfigurer = configurer
@ -224,7 +229,9 @@ func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
if !ok { if !ok {
return nil return nil
} }
return reflect.New(v).Interface().(ProxyConfigurer) pc := reflect.New(v).Interface().(ProxyConfigurer)
pc.GetBaseConfig().Type = string(proxyType)
return pc
} }
var _ ProxyConfigurer = &TCPProxyConfig{} var _ ProxyConfigurer = &TCPProxyConfig{}

View File

@ -67,6 +67,8 @@ type ServerConfig struct {
// value is "", a default page will be displayed. // value is "", a default page will be displayed.
Custom404Page string `json:"custom404Page,omitempty"` Custom404Page string `json:"custom404Page,omitempty"`
SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"` WebServer WebServerConfig `json:"webServer,omitempty"`
// EnablePrometheus will export prometheus metrics on webserver address // EnablePrometheus will export prometheus metrics on webserver address
// in /metrics api. // in /metrics api.
@ -101,6 +103,7 @@ func (c *ServerConfig) Complete() {
c.Log.Complete() c.Log.Complete()
c.Transport.Complete() c.Transport.Complete()
c.WebServer.Complete() c.WebServer.Complete()
c.SSHTunnelGateway.Complete()
c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0") c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
c.BindPort = util.EmptyOr(c.BindPort, 7000) c.BindPort = util.EmptyOr(c.BindPort, 7000)
@ -152,7 +155,7 @@ type ServerTransportConfig struct {
// is true. // is true.
// $HideFromDoc // $HideFromDoc
TCPMux *bool `json:"tcpMux,omitempty"` 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. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. // 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 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")
}

View File

@ -80,7 +80,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
return warnings, errs 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 var warnings Warning
if c != nil { if c != nil {
warning, err := ValidateClientCommonConfig(c) 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 { if err := ValidateProxyConfigurerForClient(c); err != nil {
return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err) return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err)
} }

View File

@ -15,6 +15,7 @@
package v1 package v1
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -108,7 +109,11 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
if configurer == nil { if configurer == nil {
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type) 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 return err
} }
c.VisitorConfigurer = configurer c.VisitorConfigurer = configurer
@ -120,7 +125,9 @@ func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
if !ok { if !ok {
return nil return nil
} }
return reflect.New(v).Interface().(VisitorConfigurer) vc := reflect.New(v).Interface().(VisitorConfigurer)
vc.GetBaseConfig().Type = string(t)
return vc
} }
var _ VisitorConfigurer = &STCPVisitorConfig{} var _ VisitorConfigurer = &STCPVisitorConfig{}

View File

@ -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 package metrics
import ( import (

103
pkg/msg/handler.go Normal file
View File

@ -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
}

View File

@ -63,6 +63,15 @@ var msgTypeMap = map[byte]interface{}{
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name() 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. // When frpc start, client send this message to login to server.
type Login struct { type Login struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
@ -75,6 +84,9 @@ type Login struct {
RunID string `json:"run_id,omitempty"` RunID string `json:"run_id,omitempty"`
Metas map[string]string `json:"metas,omitempty"` Metas map[string]string `json:"metas,omitempty"`
// Currently only effective for VirtualClient.
ClientSpec ClientSpec `json:"client_spec,omitempty"`
// Some global configures. // Some global configures.
PoolCount int `json:"pool_count,omitempty"` PoolCount int `json:"pool_count,omitempty"`
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -22,7 +24,7 @@ import (
"net/http/httputil" "net/http/httputil"
v1 "github.com/fatedier/frp/pkg/config/v1" 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() { func init() {
@ -77,7 +79,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
} }
func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { 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) _ = p.l.PutConn(wrapConn)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -27,7 +29,7 @@ import (
libnet "github.com/fatedier/golib/net" libnet "github.com/fatedier/golib/net"
v1 "github.com/fatedier/frp/pkg/config/v1" 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" "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) { 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) sc, rd := libnet.NewSharedConn(wrapConn)
firstBytes := make([]byte, 7) firstBytes := make([]byte, 7)

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -24,7 +26,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -96,7 +98,7 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
} }
func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { 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) _ = p.l.PutConn(wrapConn)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -24,7 +26,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
utilnet "github.com/fatedier/frp/pkg/util/net" netpkg "github.com/fatedier/frp/pkg/util/net"
) )
func init() { func init() {
@ -102,7 +104,7 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
} }
func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { 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) _ = p.l.PutConn(wrapConn)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -22,7 +24,7 @@ import (
gosocks5 "github.com/armon/go-socks5" gosocks5 "github.com/armon/go-socks5"
v1 "github.com/fatedier/frp/pkg/config/v1" 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() { 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) { func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
defer conn.Close() defer conn.Close()
wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = sp.Server.ServeConn(wrapConn) _ = sp.Server.ServeConn(wrapConn)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (
@ -23,7 +25,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
v1 "github.com/fatedier/frp/pkg/config/v1" 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() { func init() {
@ -55,8 +57,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
} }
router := mux.NewRouter() router := mux.NewRouter()
router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware) router.Use(netpkg.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.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
sp.s = &http.Server{ sp.s = &http.Server{
Handler: router, Handler: router,
} }
@ -67,7 +69,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
} }
func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { 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) _ = sp.l.PutConn(wrapConn)
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !frps
package plugin package plugin
import ( import (

View File

@ -6,11 +6,12 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
"github.com/fatedier/frp/client" "github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/util/util" httppkg "github.com/fatedier/frp/pkg/util/http"
) )
type Client struct { type Client struct {
@ -69,8 +70,16 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) {
return allStatus, nil return allStatus, nil
} }
func (c *Client) Reload() error { func (c *Client) Reload(strictMode bool) error {
req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil) 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 { if err != nil {
return err return err
} }
@ -106,7 +115,7 @@ func (c *Client) UpdateConfig(content string) error {
func (c *Client) setAuthHeader(req *http.Request) { func (c *Client) setAuthHeader(req *http.Request) {
if c.authUser != "" || c.authPwd != "" { 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))
} }
} }

143
pkg/ssh/gateway.go Normal file
View File

@ -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
}

383
pkg/ssh/server.go Normal file
View File

@ -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")
}
}
}

31
pkg/ssh/terminal.go Normal file
View File

@ -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
}

View File

@ -29,7 +29,9 @@ type MessageTransporter interface {
// Recv(ctx context.Context, laneKey string, msgType string) (Message, error) // 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 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) 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 Dispatch(m msg.Message, laneKey string) bool
// Same with Dispatch but with specified message type.
DispatchWithType(m msg.Message, msgType, laneKey string) bool DispatchWithType(m msg.Message, msgType, laneKey string) bool
} }
@ -44,7 +46,7 @@ type transporterImpl struct {
sendCh chan msg.Message sendCh chan msg.Message
// First key is message type and second key is lane key. // 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. // and lane key.
registry map[string]map[string]chan msg.Message registry map[string]map[string]chan msg.Message
mu sync.RWMutex mu sync.RWMutex

View File

@ -128,3 +128,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
return base, nil 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
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package util package http
import ( import (
"encoding/base64" "encoding/base64"

126
pkg/util/http/server.go Normal file
View File

@ -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)
}

View File

@ -22,6 +22,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/fatedier/golib/crypto"
quic "github.com/quic-go/quic-go" quic "github.com/quic-go/quic-go"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
@ -216,3 +217,18 @@ func (conn *wrapQuicStream) Close() error {
conn.Stream.CancelRead(0) conn.Stream.CancelRead(0)
return conn.Stream.Close() 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
}

33
pkg/util/net/dns.go Normal file
View File

@ -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)
},
}
}

View File

@ -24,21 +24,21 @@ import (
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
) )
type HTTPAuthWraper struct { type HTTPAuthWrapper struct {
h http.Handler h http.Handler
user string user string
passwd string passwd string
} }
func NewHTTPBasicAuthWraper(h http.Handler, user, passwd string) http.Handler { func NewHTTPBasicAuthWrapper(h http.Handler, user, passwd string) http.Handler {
return &HTTPAuthWraper{ return &HTTPAuthWrapper{
h: h, h: h,
user: user, user: user,
passwd: passwd, 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() user, passwd, hasAuth := r.BasicAuth()
if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) { if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
aw.h.ServeHTTP(w, r) 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 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") { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gw.h.ServeHTTP(w, r) gw.h.ServeHTTP(w, r)
return return
@ -100,7 +100,7 @@ func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func MakeHTTPGzipHandler(h http.Handler) http.Handler { func MakeHTTPGzipHandler(h http.Handler) http.Handler {
return &HTTPGzipWraper{ return &HTTPGzipWrapper{
h: h, h: h,
} }
} }

View File

@ -52,7 +52,10 @@ func (l *InternalListener) PutConn(conn net.Conn) error {
conn.Close() conn.Close()
} }
}) })
return err if err != nil {
return fmt.Errorf("put conn error: listener is closed")
}
return nil
} }
func (l *InternalListener) Close() error { func (l *InternalListener) Close() error {

View File

@ -24,7 +24,7 @@ import (
libnet "github.com/fatedier/golib/net" 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" "github.com/fatedier/frp/pkg/util/vhost"
) )
@ -59,10 +59,10 @@ func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host, ht
return return
} }
host, _ = util.CanonicalHost(req.Host) host, _ = httppkg.CanonicalHost(req.Host)
proxyAuth := req.Header.Get("Proxy-Authorization") proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth != "" { if proxyAuth != "" {
httpUser, httpPwd, _ = util.ParseBasicAuth(proxyAuth) httpUser, httpPwd, _ = httppkg.ParseBasicAuth(proxyAuth)
} }
return return
} }
@ -71,7 +71,7 @@ func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, _ map[string]s
if muxer.passthrough { if muxer.passthrough {
return nil return nil
} }
res := util.OkResponse() res := httppkg.OkResponse()
if res.Body != nil { if res.Body != nil {
defer res.Body.Close() defer res.Body.Close()
} }
@ -85,7 +85,7 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re
return true, nil return true, nil
} }
resp := util.ProxyUnauthorizedResponse() resp := httppkg.ProxyUnauthorizedResponse()
if resp.Body != nil { if resp.Body != nil {
defer resp.Body.Close() defer resp.Body.Close()
} }

View File

@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
var version = "0.52.3" var version = "0.53.0"
func Full() string { func Full() string {
return version return version

View File

@ -47,7 +47,7 @@ func TestVersion(t *testing.T) {
proto := Proto(Full()) proto := Proto(Full())
major := Major(Full()) major := Major(Full())
minor := Minor(Full()) minor := Minor(Full())
parseVerion := fmt.Sprintf("%d.%d.%d", proto, major, minor) parseVersion := fmt.Sprintf("%d.%d.%d", proto, major, minor)
version := Full() version := Full()
assert.Equal(parseVerion, version) assert.Equal(parseVersion, version)
} }

View File

@ -31,8 +31,8 @@ import (
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
"github.com/fatedier/golib/pool" "github.com/fatedier/golib/pool"
frpLog "github.com/fatedier/frp/pkg/util/log" httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/util" logpkg "github.com/fatedier/frp/pkg/util/log"
) )
var ErrNoRouteFound = errors.New("no route found") var ErrNoRouteFound = errors.New("no route found")
@ -61,7 +61,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
Director: func(req *http.Request) { Director: func(req *http.Request) {
req.URL.Scheme = "http" req.URL.Scheme = "http"
reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo) reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
oldHost, _ := util.CanonicalHost(reqRouteInfo.Host) oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if rc != nil { if rc != nil {
@ -74,7 +74,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// ignore error here, it will use CreateConnFn instead later // ignore error here, it will use CreateConnFn instead later
endpoint, _ = rc.ChooseEndpointFn() endpoint, _ = rc.ChooseEndpointFn()
reqRouteInfo.Endpoint = endpoint 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) endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
} }
// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections. // 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(), BufferPool: newWrapPool(),
ErrorLog: log.New(newWrapLogger(), "", 0), ErrorLog: log.New(newWrapLogger(), "", 0),
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { 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.WriteHeader(http.StatusNotFound)
_, _ = rw.Write(getNotFoundPageContent()) _, _ = rw.Write(getNotFoundPageContent())
}, },
@ -143,7 +143,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig { func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser) vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok { 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 vr.payload.(*RouteConfig)
} }
return nil return nil
@ -159,7 +159,7 @@ func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string)
// CreateConnection create a new connection by route config // CreateConnection create a new connection by route config
func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) { 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) vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if ok { if ok {
if byEndpoint { if byEndpoint {
@ -188,7 +188,7 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, p
return true 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) { func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) { findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser) 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) { func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
domain, _ := util.CanonicalHost(req.Host) domain, _ := httppkg.CanonicalHost(req.Host)
location := req.URL.Path location := req.URL.Path
user, passwd, _ := req.BasicAuth() user, passwd, _ := req.BasicAuth()
if !rp.CheckAuth(domain, location, user, user, passwd) { if !rp.CheckAuth(domain, location, user, user, passwd) {
@ -333,6 +333,6 @@ type wrapLogger struct{}
func newWrapLogger() *wrapLogger { return &wrapLogger{} } func newWrapLogger() *wrapLogger { return &wrapLogger{} }
func (l *wrapLogger) Write(p []byte) (n int, err error) { 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 return len(p), nil
} }

View File

@ -20,7 +20,7 @@ import (
"net/http" "net/http"
"os" "os"
frpLog "github.com/fatedier/frp/pkg/util/log" logpkg "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
) )
@ -58,7 +58,7 @@ func getNotFoundPageContent() []byte {
if NotFoundPagePath != "" { if NotFoundPagePath != "" {
buf, err = os.ReadFile(NotFoundPagePath) buf, err = os.ReadFile(NotFoundPagePath)
if err != nil { if err != nil {
frpLog.Warn("read custom 404 page error: %v", err) logpkg.Warn("read custom 404 page error: %v", err)
buf = []byte(NotFound) buf = []byte(NotFound)
} }
} else { } else {

View File

@ -22,7 +22,7 @@ import (
"github.com/fatedier/golib/errors" "github.com/fatedier/golib/errors"
"github.com/fatedier/frp/pkg/util/log" "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" "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) xl.Debug("rewrite host to [%s] success", l.rewriteHost)
conn = sConn conn = sConn
} }
return utilnet.NewContextConn(l.ctx, conn), nil return netpkg.NewContextConn(l.ctx, conn), nil
} }
func (l *Listener) Close() error { func (l *Listener) Close() error {

197
pkg/util/wait/backoff.go Normal file
View File

@ -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
}

View File

@ -15,40 +15,81 @@
package xlog package xlog
import ( import (
"sort"
"github.com/fatedier/frp/pkg/util/log" "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 // Logger is not thread safety for operations on prefix
type Logger struct { type Logger struct {
prefixes []string prefixes []LogPrefix
prefixString string prefixString string
} }
func New() *Logger { func New() *Logger {
return &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 old = l.prefixes
l.prefixes = make([]string, 0) l.prefixes = make([]LogPrefix, 0)
l.prefixString = "" l.prefixString = ""
return return
} }
func (l *Logger) AppendPrefix(prefix string) *Logger { func (l *Logger) AppendPrefix(prefix string) *Logger {
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.prefixes = append(l.prefixes, prefix)
l.prefixString += "[" + prefix + "] " }
l.renderPrefixString()
return l return l
} }
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 {
l.prefixString += "[" + v.Value + "] "
}
}
func (l *Logger) Spawn() *Logger { func (l *Logger) Spawn() *Logger {
nl := New() nl := New()
for _, v := range l.prefixes { nl.prefixes = append(nl.prefixes, l.prefixes...)
nl.AppendPrefix(v) nl.renderPrefixString()
}
return nl return nl
} }

107
pkg/virtual/client.go Normal file
View File

@ -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
}

View File

@ -17,15 +17,12 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net" "net"
"runtime/debug" "runtime/debug"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/fatedier/golib/control/shutdown"
"github.com/fatedier/golib/crypto"
"github.com/fatedier/golib/errors"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
@ -35,8 +32,10 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server" plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/transport" "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/util"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/metrics" "github.com/fatedier/frp/server/metrics"
@ -111,18 +110,16 @@ type Control struct {
// other components can use this to communicate with client // other components can use this to communicate with client
msgTransporter transport.MessageTransporter 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 // login message
loginMsg *msg.Login loginMsg *msg.Login
// control connection // control connection
conn net.Conn 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 // work connections
workConnCh chan net.Conn workConnCh chan net.Conn
@ -136,20 +133,13 @@ type Control struct {
portsUsedNum int portsUsedNum int
// last time got the Ping message // last time got the Ping message
lastPing time.Time lastPing atomic.Value
// A new run id will be generated when a new client login. // 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 // 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. // replace old controller instantly.
runID string runID string
readerShutdown *shutdown.Shutdown
writerShutdown *shutdown.Shutdown
managerShutdown *shutdown.Shutdown
allShutdown *shutdown.Shutdown
started bool
mu sync.RWMutex mu sync.RWMutex
// Server configuration information // Server configuration information
@ -157,8 +147,10 @@ type Control struct {
xl *xlog.Logger xl *xlog.Logger
ctx context.Context ctx context.Context
doneCh chan struct{}
} }
// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext.
func NewControl( func NewControl(
ctx context.Context, ctx context.Context,
rc *controller.ResourceController, rc *controller.ResourceController,
@ -166,9 +158,10 @@ func NewControl(
pluginManager *plugin.Manager, pluginManager *plugin.Manager,
authVerifier auth.Verifier, authVerifier auth.Verifier,
ctlConn net.Conn, ctlConn net.Conn,
ctlConnEncrypted bool,
loginMsg *msg.Login, loginMsg *msg.Login,
serverCfg *v1.ServerConfig, serverCfg *v1.ServerConfig,
) *Control { ) (*Control, error) {
poolCount := loginMsg.PoolCount poolCount := loginMsg.PoolCount
if poolCount > int(serverCfg.Transport.MaxPoolCount) { if poolCount > int(serverCfg.Transport.MaxPoolCount) {
poolCount = int(serverCfg.Transport.MaxPoolCount) poolCount = int(serverCfg.Transport.MaxPoolCount)
@ -180,24 +173,30 @@ func NewControl(
authVerifier: authVerifier, authVerifier: authVerifier,
conn: ctlConn, conn: ctlConn,
loginMsg: loginMsg, loginMsg: loginMsg,
sendCh: make(chan msg.Message, 10),
readCh: make(chan msg.Message, 10),
workConnCh: make(chan net.Conn, poolCount+10), workConnCh: make(chan net.Conn, poolCount+10),
proxies: make(map[string]proxy.Proxy), proxies: make(map[string]proxy.Proxy),
poolCount: poolCount, poolCount: poolCount,
portsUsedNum: 0, portsUsedNum: 0,
lastPing: time.Now(),
runID: loginMsg.RunID, runID: loginMsg.RunID,
readerShutdown: shutdown.New(),
writerShutdown: shutdown.New(),
managerShutdown: shutdown.New(),
allShutdown: shutdown.New(),
serverCfg: serverCfg, serverCfg: serverCfg,
xl: xlog.FromContextSafe(ctx), xl: xlog.FromContextSafe(ctx),
ctx: ctx, ctx: ctx,
doneCh: make(chan struct{}),
} }
ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) ctl.lastPing.Store(time.Now())
return ctl
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.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
return ctl, nil
} }
// Start send a login success message to client and start working. // Start send a login success message to client and start working.
@ -208,27 +207,18 @@ func (ctl *Control) Start() {
Error: "", Error: "",
} }
_ = msg.WriteMsg(ctl.conn, loginRespMsg) _ = msg.WriteMsg(ctl.conn, loginRespMsg)
ctl.mu.Lock()
ctl.started = true
ctl.mu.Unlock()
go ctl.writer()
go func() { go func() {
for i := 0; i < ctl.poolCount; i++ { for i := 0; i < ctl.poolCount; i++ {
// ignore error here, that means that this control is closed // ignore error here, that means that this control is closed
_ = errors.PanicToError(func() { _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
ctl.sendCh <- &msg.ReqWorkConn{}
})
} }
}() }()
go ctl.worker()
go ctl.manager()
go ctl.reader()
go ctl.stoper()
} }
func (ctl *Control) Close() error { func (ctl *Control) Close() error {
ctl.allShutdown.Start() ctl.conn.Close()
return nil return nil
} }
@ -236,7 +226,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
xl := ctl.xl xl := ctl.xl
xl.Info("Replaced by client [%s]", newCtl.runID) xl.Info("Replaced by client [%s]", newCtl.runID)
ctl.runID = "" ctl.runID = ""
ctl.allShutdown.Start() ctl.conn.Close()
} }
func (ctl *Control) RegisterWorkConn(conn net.Conn) error { 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") xl.Debug("get work connection from pool")
default: default:
// no work connections available in the poll, send message to frpc to get more // no work connections available in the poll, send message to frpc to get more
if err = errors.PanicToError(func() { if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil {
ctl.sendCh <- &msg.ReqWorkConn{}
}); err != nil {
return nil, fmt.Errorf("control is already closed") 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. // When we get a work connection from pool, replace it with a new one.
_ = errors.PanicToError(func() { _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
ctl.sendCh <- &msg.ReqWorkConn{}
})
return return
} }
func (ctl *Control) writer() { func (ctl *Control) heartbeatWorker() {
xl := ctl.xl 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() // Don't need application heartbeat if TCPMux is enabled,
defer ctl.writerShutdown.Done() // 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.
encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
if err != nil { go wait.Until(func() {
xl.Error("crypto new writer error: %v", err) if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
ctl.allShutdown.Start() xl.Warn("heartbeat timeout")
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() {
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")
return
}
xl.Warn("read error: %v", err)
ctl.conn.Close() ctl.conn.Close()
return return
} }
}, time.Second, ctl.doneCh)
ctl.readCh <- m
} }
} }
func (ctl *Control) stoper() { // block until Control closed
func (ctl *Control) WaitClosed() {
<-ctl.doneCh
}
func (ctl *Control) worker() {
xl := ctl.xl 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.conn.Close()
ctl.readerShutdown.WaitDone()
close(ctl.readCh)
ctl.managerShutdown.WaitDone()
close(ctl.sendCh)
ctl.writerShutdown.WaitDone()
ctl.mu.Lock() ctl.mu.Lock()
defer ctl.mu.Unlock() defer ctl.mu.Unlock()
@ -419,136 +355,104 @@ func (ctl *Control) stoper() {
}() }()
} }
ctl.allShutdown.Done()
xl.Info("client exit success")
metrics.Server.CloseClient() metrics.Server.CloseClient()
xl.Info("client exit success")
close(ctl.doneCh)
} }
// block until Control closed func (ctl *Control) registerMsgHandlers() {
func (ctl *Control) WaitClosed() { ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy)
ctl.mu.RLock() ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing)
started := ctl.started ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor))
ctl.mu.RUnlock() ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient))
ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport))
if !started { ctl.msgDispatcher.RegisterHandler(&msg.CloseProxy{}, ctl.handleCloseProxy)
ctl.allShutdown.Done()
return
}
ctl.allShutdown.WaitDone()
} }
func (ctl *Control) manager() { func (ctl *Control) handleNewProxy(m msg.Message) {
xl := ctl.xl xl := ctl.xl
defer func() { inMsg := m.(*msg.NewProxy)
if err := recover(); err != nil {
xl.Error("panic error: %v", err)
xl.Error(string(debug.Stack()))
}
}()
defer ctl.allShutdown.Start()
defer ctl.managerShutdown.Done()
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
}
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
}
switch m := rawMsg.(type) {
case *msg.NewProxy:
content := &plugin.NewProxyContent{ content := &plugin.NewProxyContent{
User: plugin.UserInfo{ User: plugin.UserInfo{
User: ctl.loginMsg.User, User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas, Metas: ctl.loginMsg.Metas,
RunID: ctl.loginMsg.RunID, RunID: ctl.loginMsg.RunID,
}, },
NewProxy: *m, NewProxy: *inMsg,
} }
var remoteAddr string var remoteAddr string
retContent, err := ctl.pluginManager.NewProxy(content) retContent, err := ctl.pluginManager.NewProxy(content)
if err == nil { if err == nil {
m = &retContent.NewProxy inMsg = &retContent.NewProxy
remoteAddr, err = ctl.RegisterProxy(m) remoteAddr, err = ctl.RegisterProxy(inMsg)
} }
// register proxy in this control // register proxy in this control
resp := &msg.NewProxyResp{ resp := &msg.NewProxyResp{
ProxyName: m.ProxyName, ProxyName: inMsg.ProxyName,
} }
if err != nil { if err != nil {
xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err) xl.Warn("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)) err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
} else { } else {
resp.RemoteAddr = remoteAddr resp.RemoteAddr = remoteAddr
xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType) xl.Info("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
metrics.Server.NewProxy(m.ProxyName, m.ProxyType) metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType)
} }
ctl.sendCh <- resp _ = ctl.msgDispatcher.Send(resp)
case *msg.NatHoleVisitor: }
go ctl.HandleNatHoleVisitor(m)
case *msg.NatHoleClient: func (ctl *Control) handlePing(m msg.Message) {
go ctl.HandleNatHoleClient(m) xl := ctl.xl
case *msg.NatHoleReport: inMsg := m.(*msg.Ping)
go ctl.HandleNatHoleReport(m)
case *msg.CloseProxy:
_ = ctl.CloseProxy(m)
xl.Info("close proxy [%s] success", m.ProxyName)
case *msg.Ping:
content := &plugin.PingContent{ content := &plugin.PingContent{
User: plugin.UserInfo{ User: plugin.UserInfo{
User: ctl.loginMsg.User, User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas, Metas: ctl.loginMsg.Metas,
RunID: ctl.loginMsg.RunID, RunID: ctl.loginMsg.RunID,
}, },
Ping: *m, Ping: *inMsg,
} }
retContent, err := ctl.pluginManager.Ping(content) retContent, err := ctl.pluginManager.Ping(content)
if err == nil { if err == nil {
m = &retContent.Ping inMsg = &retContent.Ping
err = ctl.authVerifier.VerifyPing(m) err = ctl.authVerifier.VerifyPing(inMsg)
} }
if err != nil { if err != nil {
xl.Warn("received invalid ping: %v", err) xl.Warn("received invalid ping: %v", err)
ctl.sendCh <- &msg.Pong{ _ = ctl.msgDispatcher.Send(&msg.Pong{
Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
} })
return return
} }
ctl.lastPing = time.Now() ctl.lastPing.Store(time.Now())
xl.Debug("receive heartbeat") xl.Debug("receive heartbeat")
ctl.sendCh <- &msg.Pong{} _ = ctl.msgDispatcher.Send(&msg.Pong{})
}
}
}
} }
func (ctl *Control) HandleNatHoleVisitor(m *msg.NatHoleVisitor) { func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter, ctl.loginMsg.User) inMsg := m.(*msg.NatHoleVisitor)
ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User)
} }
func (ctl *Control) HandleNatHoleClient(m *msg.NatHoleClient) { func (ctl *Control) handleNatHoleClient(m msg.Message) {
ctl.rc.NatHoleController.HandleClient(m, ctl.msgTransporter) inMsg := m.(*msg.NatHoleClient)
ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
} }
func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) { func (ctl *Control) handleNatHoleReport(m msg.Message) {
ctl.rc.NatHoleController.HandleReport(m) 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) { 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() { go func() {
_ = ctl.pluginManager.CloseProxy(notifyContent) _ = ctl.pluginManager.CloseProxy(notifyContent)
}() }()
return return
} }

View File

@ -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
}

View File

@ -19,19 +19,52 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/fatedier/frp/pkg/config/types" "github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/metrics/mem" "github.com/fatedier/frp/pkg/metrics/mem"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
) )
// TODO(fatedier): add an API to clean status of all offline proxies.
type GeneralResponse struct { type GeneralResponse struct {
Code int Code int
Msg string 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 { type serverInfoResp struct {
Version string `json:"version"` Version string `json:"version"`
BindPort int `json:"bindPort"` BindPort int `json:"bindPort"`
@ -55,12 +88,12 @@ type serverInfoResp struct {
} }
// /healthz // /healthz
func (svr *Service) Healthz(w http.ResponseWriter, _ *http.Request) { func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
} }
// /api/serverinfo // /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} res := GeneralResponse{Code: 200}
defer func() { defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
@ -177,7 +210,7 @@ type GetProxyInfoResp struct {
} }
// /api/proxy/:type // /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} res := GeneralResponse{Code: 200}
params := mux.Vars(r) params := mux.Vars(r)
proxyType := params["type"] proxyType := params["type"]
@ -245,7 +278,7 @@ type GetProxyStatsResp struct {
} }
// /api/proxy/:type/:name // /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} res := GeneralResponse{Code: 200}
params := mux.Vars(r) params := mux.Vars(r)
proxyType := params["type"] proxyType := params["type"]
@ -314,7 +347,7 @@ type GetProxyTrafficResp struct {
TrafficOut []int64 `json:"trafficOut"` 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} res := GeneralResponse{Code: 200}
params := mux.Vars(r) params := mux.Vars(r)
name := params["name"] name := params["name"]

View File

@ -24,7 +24,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/limit" "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/util"
"github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/pkg/util/vhost"
"github.com/fatedier/frp/server/metrics" "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 = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
return return
} }

View File

@ -32,7 +32,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server" plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/util/limit" "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/pkg/util/xlog"
"github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/metrics" "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.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
xl.Spawn().AppendPrefix(pxy.GetName()) xl.Spawn().AppendPrefix(pxy.GetName())
workConn = utilnet.NewContextConn(pxy.ctx, workConn) workConn = netpkg.NewContextConn(pxy.ctx, workConn)
var ( var (
srcAddr string srcAddr string

View File

@ -30,7 +30,7 @@ import (
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit" "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" "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()) ctx, cancel := context.WithCancel(context.Background())
go workConnReaderFn(pxy.workConn) go workConnReaderFn(pxy.workConn)
go workConnSenderFn(pxy.workConn, ctx) go workConnSenderFn(pxy.workConn, ctx)

View File

@ -30,16 +30,17 @@ import (
quic "github.com/quic-go/quic-go" quic "github.com/quic-go/quic-go"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
modelmetrics "github.com/fatedier/frp/pkg/metrics" modelmetrics "github.com/fatedier/frp/pkg/metrics"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/nathole"
plugin "github.com/fatedier/frp/pkg/plugin/server" plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/ssh"
"github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/transport"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log" "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/tcpmux"
"github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/pkg/util/version"
@ -78,6 +79,9 @@ type Service struct {
// Accept frp tls connections // Accept frp tls connections
tlsListener net.Listener tlsListener net.Listener
// Accept pipe connections from ssh tunnel gateway
sshTunnelListener *netpkg.InternalListener
// Manage all controllers // Manage all controllers
ctlManager *ControlManager ctlManager *ControlManager
@ -93,6 +97,11 @@ type Service struct {
// All resource managers and controllers // All resource managers and controllers
rc *controller.ResourceController rc *controller.ResourceController
// web server for dashboard UI and apis
webServer *httppkg.Server
sshTunnelGateway *ssh.Gateway
// Verifies authentication based on selected method // Verifies authentication based on selected method
authVerifier auth.Verifier authVerifier auth.Verifier
@ -106,16 +115,30 @@ type Service struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { func NewService(cfg *v1.ServerConfig) (*Service, error) {
tlsConfig, err := transport.NewServerTLSConfig( tlsConfig, err := transport.NewServerTLSConfig(
cfg.Transport.TLS.CertFile, cfg.Transport.TLS.CertFile,
cfg.Transport.TLS.KeyFile, cfg.Transport.TLS.KeyFile,
cfg.Transport.TLS.TrustedCaFile) cfg.Transport.TLS.TrustedCaFile)
if err != nil { if err != nil {
return return nil, err
} }
svr = &Service{ 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{
ctlManager: NewControlManager(), ctlManager: NewControlManager(),
pxyManager: proxy.NewManager(), pxyManager: proxy.NewManager(),
pluginManager: plugin.NewManager(), pluginManager: plugin.NewManager(),
@ -124,12 +147,17 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts), TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts), UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
}, },
sshTunnelListener: netpkg.NewInternalListener(),
httpVhostRouter: vhost.NewRouters(), httpVhostRouter: vhost.NewRouters(),
authVerifier: auth.NewAuthVerifier(cfg.Auth), authVerifier: auth.NewAuthVerifier(cfg.Auth),
webServer: webServer,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
cfg: cfg, cfg: cfg,
ctx: context.Background(), ctx: context.Background(),
} }
if webServer != nil {
webServer.RouteRegister(svr.registerRouteHandlers)
}
// Create tcpmux httpconnect multiplexer. // Create tcpmux httpconnect multiplexer.
if cfg.TCPMuxHTTPConnectPort > 0 { if cfg.TCPMuxHTTPConnectPort > 0 {
@ -137,14 +165,12 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort)) address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort))
l, err = net.Listen("tcp", address) l, err = net.Listen("tcp", address)
if err != nil { if err != nil {
err = fmt.Errorf("create server listener error, %v", err) return nil, fmt.Errorf("create server listener error, %v", err)
return
} }
svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout) svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout)
if err != nil { if err != nil {
err = fmt.Errorf("create vhost tcpMuxer error, %v", err) return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err)
return
} }
log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough) 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)) address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort))
ln, err := net.Listen("tcp", address) ln, err := net.Listen("tcp", address)
if err != nil { if err != nil {
err = fmt.Errorf("create server listener error, %v", err) return nil, fmt.Errorf("create server listener error, %v", err)
return
} }
svr.muxer = mux.NewMux(ln) 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. // Listen for accepting connections from client using kcp protocol.
if cfg.KCPBindPort > 0 { if cfg.KCPBindPort > 0 {
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort)) address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
svr.kcpListener, err = utilnet.ListenKcp(address) svr.kcpListener, err = netpkg.ListenKcp(address)
if err != nil { if err != nil {
err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err) return nil, fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
return
} }
log.Info("frps kcp listen on udp %s", address) 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, KeepAlivePeriod: time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
}) })
if err != nil { if err != nil {
err = fmt.Errorf("listen on quic udp address %s error: %v", address, err) return nil, fmt.Errorf("listen on quic udp address %s error: %v", address, err)
return
} }
log.Info("frps quic listen on quic %s", address) 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)
}
svr.sshTunnelGateway = sshGateway
log.Info("frps sshTunnelGateway listen on port %d", cfg.SSHTunnelGateway.BindPort)
} }
// Listen for accepting connections from client using websocket protocol. // 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 { websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool {
return bytes.Equal(data, websocketPrefix) return bytes.Equal(data, websocketPrefix)
}) })
svr.websocketListener = utilnet.NewWebsocketListener(websocketLn) svr.websocketListener = netpkg.NewWebsocketListener(websocketLn)
// Create http vhost muxer. // Create http vhost muxer.
if cfg.VhostHTTPPort > 0 { if cfg.VhostHTTPPort > 0 {
@ -251,8 +283,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
} else { } else {
l, err = net.Listen("tcp", address) l, err = net.Listen("tcp", address)
if err != nil { if err != nil {
err = fmt.Errorf("create vhost http listener error, %v", err) return nil, fmt.Errorf("create vhost http listener error, %v", err)
return
} }
} }
go func() { go func() {
@ -270,55 +301,30 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort)) address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
l, err = net.Listen("tcp", address) l, err = net.Listen("tcp", address)
if err != nil { if err != nil {
err = fmt.Errorf("create server listener error, %v", err) return nil, fmt.Errorf("create server listener error, %v", err)
return
} }
log.Info("https service listen on %s", address) log.Info("https service listen on %s", address)
} }
svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout)
if err != nil { if err != nil {
err = fmt.Errorf("create vhost httpsMuxer error, %v", err) return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
return
} }
} }
// frp tls listener // frp tls listener
svr.tlsListener = svr.muxer.Listen(2, 1, func(data []byte) bool { 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 // 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. // Create nat hole controller.
nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour) nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour)
if err != nil { if err != nil {
err = fmt.Errorf("create nat hole controller error, %v", err) return nil, fmt.Errorf("create nat hole controller error, %v", err)
return
} }
svr.rc.NatHoleController = nc svr.rc.NatHoleController = nc
return svr, nil
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
} }
func (svr *Service) Run(ctx context.Context) { func (svr *Service) Run(ctx context.Context) {
@ -326,19 +332,36 @@ func (svr *Service) Run(ctx context.Context) {
svr.ctx = ctx svr.ctx = ctx
svr.cancel = cancel 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 { if svr.kcpListener != nil {
go svr.HandleListener(svr.kcpListener) go svr.HandleListener(svr.kcpListener, false)
} }
if svr.quicListener != nil { if svr.quicListener != nil {
go svr.HandleQUICListener(svr.quicListener) go svr.HandleQUICListener(svr.quicListener)
} }
go svr.HandleListener(svr.websocketListener) go svr.HandleListener(svr.websocketListener, false)
go svr.HandleListener(svr.tlsListener) go svr.HandleListener(svr.tlsListener, false)
if svr.rc.NatHoleController != nil { if svr.rc.NatHoleController != nil {
go svr.rc.NatHoleController.CleanWorker(svr.ctx) 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() <-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources // 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 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) xl := xlog.FromContextSafe(ctx)
var ( var (
@ -401,7 +424,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
retContent, err := svr.pluginManager.Login(content) retContent, err := svr.pluginManager.Login(content)
if err == nil { if err == nil {
m = &retContent.Login m = &retContent.Login
err = svr.RegisterControl(conn, m) err = svr.RegisterControl(conn, m, internal)
} }
// If login failed, send error message there. // 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. // Listen for incoming connections from client.
for { for {
c, err := l.Accept() c, err := l.Accept()
@ -450,22 +476,25 @@ func (svr *Service) HandleListener(l net.Listener) {
xl := xlog.New() xl := xlog.New()
ctx := context.Background() ctx := context.Background()
c = utilnet.NewContextConn(xlog.NewContext(ctx, xl), c) c = netpkg.NewContextConn(xlog.NewContext(ctx, xl), c)
if !internal {
log.Trace("start check TLS connection...") log.Trace("start check TLS connection...")
originConn := c originConn := c
forceTLS := svr.cfg.Transport.TLS.Force
var isTLS, custom bool var isTLS, custom bool
c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout) c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
if err != nil { if err != nil {
log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
originConn.Close() originConn.Close()
continue continue
} }
log.Trace("check TLS connection success, isTLS: %v custom: %v", isTLS, custom) log.Trace("check TLS connection success, isTLS: %v custom: %v internal: %v", isTLS, custom, internal)
}
// Start a new goroutine to handle connection. // Start a new goroutine to handle connection.
go func(ctx context.Context, frpConn net.Conn) { 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 := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
@ -484,10 +513,10 @@ func (svr *Service) HandleListener(l net.Listener) {
session.Close() session.Close()
return return
} }
go svr.handleConnection(ctx, stream) go svr.handleConnection(ctx, stream, internal)
} }
} else { } else {
svr.handleConnection(ctx, frpConn) svr.handleConnection(ctx, frpConn, internal)
} }
}(ctx, c) }(ctx, c)
} }
@ -510,23 +539,24 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
_ = frpConn.CloseWithError(0, "") _ = frpConn.CloseWithError(0, "")
return return
} }
go svr.handleConnection(ctx, utilnet.QuicStreamToNetConn(stream, frpConn)) go svr.handleConnection(ctx, netpkg.QuicStreamToNetConn(stream, frpConn), false)
} }
}(context.Background(), c) }(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. // 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. // 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 == "" { if loginMsg.RunID == "" {
loginMsg.RunID, err = util.RandID() loginMsg.RunID, err = util.RandID()
if err != nil { if err != nil {
return return err
} }
} }
ctx := utilnet.NewContextFromConn(ctlConn) ctx := netpkg.NewContextFromConn(ctlConn)
xl := xlog.FromContextSafe(ctx) xl := xlog.FromContextSafe(ctx)
xl.AppendPrefix(loginMsg.RunID) xl.AppendPrefix(loginMsg.RunID)
ctx = xlog.NewContext(ctx, xl) 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) ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
// Check auth. // Check auth.
if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil { authVerifier := svr.authVerifier
return 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 { if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
oldCtl.WaitClosed() oldCtl.WaitClosed()
} }
@ -553,12 +593,12 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
ctl.WaitClosed() ctl.WaitClosed()
svr.ctlManager.Del(loginMsg.RunID, ctl) svr.ctlManager.Del(loginMsg.RunID, ctl)
}() }()
return return nil
} }
// RegisterWorkConn register a new work connection to control and proxies need it. // RegisterWorkConn register a new work connection to control and proxies need it.
func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error { 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) ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
if !exist { if !exist {
xl.Warn("No client control found for run id [%s]", newMsg.RunID) 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 { if err == nil {
newMsg = &retContent.NewWorkConn newMsg = &retContent.NewWorkConn
// Check auth. // Check auth.
err = svr.authVerifier.VerifyNewWorkConn(newMsg) err = ctl.authVerifier.VerifyNewWorkConn(newMsg)
} }
if err != nil { if err != nil {
xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID) xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID)

View File

@ -23,12 +23,12 @@ import (
libio "github.com/fatedier/golib/io" libio "github.com/fatedier/golib/io"
"github.com/samber/lo" "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" "github.com/fatedier/frp/pkg/util/util"
) )
type listenerBundle struct { type listenerBundle struct {
l *utilnet.InternalListener l *netpkg.InternalListener
sk string sk string
allowUsers []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() vm.mu.Lock()
defer vm.mu.Unlock() defer vm.mu.Unlock()
if _, ok := vm.listeners[name]; ok { if _, ok := vm.listeners[name]; ok {
err = fmt.Errorf("custom listener for [%s] is repeated", name) return nil, fmt.Errorf("custom listener for [%s] is repeated", name)
return
} }
l = utilnet.NewInternalListener() l := netpkg.NewInternalListener()
vm.listeners[name] = &listenerBundle{ vm.listeners[name] = &listenerBundle{
l: l, l: l,
sk: sk, sk: sk,
allowUsers: allowUsers, allowUsers: allowUsers,
} }
return return l, nil
} }
func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string, 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 { if useCompression {
rwc = libio.WithCompression(rwc) rwc = libio.WithCompression(rwc)
} }
err = l.l.PutConn(utilnet.WrapReadWriteCloserToConn(rwc, conn)) err = l.l.PutConn(netpkg.WrapReadWriteCloserToConn(rwc, conn))
} else { } else {
err = fmt.Errorf("custom listener for [%s] doesn't exist", name) err = fmt.Errorf("custom listener for [%s] doesn't exist", name)
return return

View File

@ -29,8 +29,8 @@ type Framework struct {
// ports used in this framework indexed by port name. // ports used in this framework indexed by port name.
usedPorts map[string]int usedPorts map[string]int
// record ports alloced by this framework and release them after each test // record ports allocated by this framework and release them after each test
allocedPorts []int allocatedPorts []int
// portAllocator to alloc port for this test case. // portAllocator to alloc port for this test case.
portAllocator *port.Allocator portAllocator *port.Allocator
@ -153,11 +153,11 @@ func (f *Framework) AfterEach() {
} }
f.usedPorts = make(map[string]int) f.usedPorts = make(map[string]int)
// release alloced ports // release allocated ports
for _, port := range f.allocedPorts { for _, port := range f.allocatedPorts {
f.portAllocator.Release(port) f.portAllocator.Release(port)
} }
f.allocedPorts = make([]int, 0) f.allocatedPorts = make([]int, 0)
// clear os envs // clear os envs
f.osEnvs = make([]string, 0) f.osEnvs = make([]string, 0)
@ -237,7 +237,7 @@ func (f *Framework) PortByName(name string) int {
func (f *Framework) AllocPort() int { func (f *Framework) AllocPort() int {
port := f.portAllocator.Get() port := f.portAllocator.Get()
ExpectTrue(port > 0, "alloc port failed") ExpectTrue(port > 0, "alloc port failed")
f.allocedPorts = append(f.allocedPorts, port) f.allocatedPorts = append(f.allocatedPorts, port)
return port return port
} }

View File

@ -69,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
err = client.UpdateConfig(newClientConf) err = client.UpdateConfig(newClientConf)
framework.ExpectNoError(err) framework.ExpectNoError(err)
err = client.Reload() err = client.Reload(true)
framework.ExpectNoError(err) framework.ExpectNoError(err)
time.Sleep(time.Second) time.Sleep(time.Second)

View File

@ -8,7 +8,7 @@ import (
"github.com/onsi/ginkgo/v2" "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"
"github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/mock/server/streamserver" "github.com/fatedier/frp/test/e2e/mock/server/streamserver"
@ -176,7 +176,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
connectRequestHost = req.Host connectRequestHost = req.Host
// return ok response // return ok response
res := util.OkResponse() res := httppkg.OkResponse()
if res.Body != nil { if res.Body != nil {
defer res.Body.Close() defer res.Body.Close()
} }

View File

@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort).Ensure()
}) })
ginkgo.It("Mofify RemotePort", func() { ginkgo.It("Modify RemotePort", func() {
localPort := f.AllocPort() localPort := f.AllocPort()
remotePort := f.AllocPort() remotePort := f.AllocPort()
handler := func(req *plugin.Request) *plugin.Response { handler := func(req *plugin.Request) *plugin.Response {

View File

@ -14,7 +14,7 @@ import (
libdial "github.com/fatedier/golib/net/dial" 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" "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 { func (r *Request) HTTPAuth(user, password string) *Request {
r.authValue = util.BasicAuth(user, password) r.authValue = httppkg.BasicAuth(user, password)
return r return r
} }

View File

@ -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)
}

View File

@ -72,7 +72,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
err = client.UpdateConfig(newClientConf) err = client.UpdateConfig(newClientConf)
framework.ExpectNoError(err) framework.ExpectNoError(err)
err = client.Reload() err = client.Reload(true)
framework.ExpectNoError(err) framework.ExpectNoError(err)
time.Sleep(time.Second) time.Sleep(time.Second)

Some files were not shown because too many files have changed in this diff Show More