From 46266e4d3063703d597526a673567df4db4f2324 Mon Sep 17 00:00:00 2001
From: 0x7fff <4812302+blizard863@users.noreply.github.com>
Date: Mon, 30 Oct 2023 20:24:57 +0800
Subject: [PATCH 01/21] fix: set ping (#3734)
Co-authored-by: int7
---
client/control.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/control.go b/client/control.go
index 63c6c331fc9..33fe2b509bc 100644
--- a/client/control.go
+++ b/client/control.go
@@ -298,8 +298,8 @@ func (ctl *Control) msgHandler() {
xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{}
if err := ctl.authSetter.SetPing(pingMsg); err != nil {
- xl.Warn("error during ping authentication: %v", err)
- return
+ xl.Warn("error during ping authentication: %v. skip sending ping message", err)
+ continue
}
ctl.sendCh <- pingMsg
case <-hbCheckCh:
From 5c4d820eb4eea4c5bc8e6e8c7d753d7a00ed2d65 Mon Sep 17 00:00:00 2001
From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
Date: Tue, 31 Oct 2023 12:40:48 +0100
Subject: [PATCH 02/21] chore: Update dependencies (#3738)
* chore: Update dependencies
* Removed all foolish updates
---
go.mod | 8 ++++----
go.sum | 16 ++++++++--------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index f7996399484..8d27e522871 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
- golang.org/x/net v0.12.0
+ golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
golang.org/x/time v0.3.0
@@ -64,11 +64,11 @@ require (
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
- golang.org/x/crypto v0.11.0 // indirect
+ golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.10.0 // indirect
- golang.org/x/sys v0.10.0 // indirect
- golang.org/x/text v0.11.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.9.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
diff --git a/go.sum b/go.sum
index 4cab567e29b..af509c3f22f 100644
--- a/go.sum
+++ b/go.sum
@@ -157,8 +157,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -183,8 +183,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
@@ -210,8 +210,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -222,8 +222,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
From 5760c1cf92b87a9d734e6c31c626f9a4d282fde0 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Wed, 1 Nov 2023 17:06:55 +0800
Subject: [PATCH 03/21] frpc: exit with code 1 if first login failed (#3740)
---
Release.md | 2 +-
client/service.go | 5 ++---
cmd/frpc/sub/root.go | 8 ++------
3 files changed, 5 insertions(+), 10 deletions(-)
diff --git a/Release.md b/Release.md
index 1660f1e19e2..16e8324b8dd 100644
--- a/Release.md
+++ b/Release.md
@@ -1,3 +1,3 @@
### Fixes
-* `admin_user` is not effective in the INI configuration.
+* frpc: Return code 1 when the first login attempt fails and exits.
diff --git a/client/service.go b/client/service.go
index 184a87a3305..4b394d8a163 100644
--- a/client/service.go
+++ b/client/service.go
@@ -83,8 +83,8 @@ func NewService(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
-) (svr *Service, err error) {
- svr = &Service{
+) *Service {
+ return &Service{
authSetter: auth.NewAuthSetter(cfg.Auth),
cfg: cfg,
cfgFile: cfgFile,
@@ -93,7 +93,6 @@ func NewService(
ctx: context.Background(),
exit: 0,
}
- return
}
func (svr *Service) GetController() *Control {
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index 915e6b35174..125c88c05c4 100644
--- a/cmd/frpc/sub/root.go
+++ b/cmd/frpc/sub/root.go
@@ -139,10 +139,7 @@ func startService(
log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
}
- svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
- if err != nil {
- return err
- }
+ svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
@@ -150,6 +147,5 @@ func startService(
go handleTermSignal(svr)
}
- _ = svr.Run(context.Background())
- return nil
+ return svr.Run(context.Background())
}
From 184223cb2f240b844f90b3390645672d2225da88 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 6 Nov 2023 10:51:48 +0800
Subject: [PATCH 04/21] Code refactoring related to message handling and retry
logic. (#3745)
---
Release.md | 1 +
client/admin_api.go | 9 +-
client/control.go | 275 +++++++++++-----------------
client/service.go | 199 ++++++++++----------
pkg/metrics/metrics.go | 14 ++
pkg/msg/handler.go | 103 +++++++++++
pkg/transport/message.go | 2 +
pkg/util/net/conn.go | 16 ++
pkg/util/wait/backoff.go | 197 ++++++++++++++++++++
server/control.go | 385 ++++++++++++++-------------------------
server/service.go | 18 +-
11 files changed, 690 insertions(+), 529 deletions(-)
create mode 100644 pkg/msg/handler.go
create mode 100644 pkg/util/wait/backoff.go
diff --git a/Release.md b/Release.md
index 16e8324b8dd..a834392cf7f 100644
--- a/Release.md
+++ b/Release.md
@@ -1,3 +1,4 @@
### Fixes
* frpc: Return code 1 when the first login attempt fails and exits.
+* When auth.method is `oidc` and auth.additionalScopes contains `HeartBeats`, if obtaining AccessToken fails, the application will be unresponsive.
diff --git a/client/admin_api.go b/client/admin_api.go
index a348e8dd332..e775f526ad3 100644
--- a/client/admin_api.go
+++ b/client/admin_api.go
@@ -144,7 +144,14 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(buf)
}()
- ps := svr.ctl.pm.GetAllProxyStatus()
+ svr.ctlMu.RLock()
+ ctl := svr.ctl
+ svr.ctlMu.RUnlock()
+ if ctl == nil {
+ return
+ }
+
+ ps := ctl.pm.GetAllProxyStatus()
for _, status := range ps {
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr))
}
diff --git a/client/control.go b/client/control.go
index 33fe2b509bc..c8d186ca14b 100644
--- a/client/control.go
+++ b/client/control.go
@@ -16,13 +16,10 @@ package client
import (
"context"
- "io"
"net"
- "runtime/debug"
+ "sync/atomic"
"time"
- "github.com/fatedier/golib/control/shutdown"
- "github.com/fatedier/golib/crypto"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy"
@@ -31,6 +28,8 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
+ utilnet "github.com/fatedier/frp/pkg/util/net"
+ "github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -39,6 +38,12 @@ type Control struct {
ctx context.Context
xl *xlog.Logger
+ // The client configuration
+ clientCfg *v1.ClientCommonConfig
+
+ // sets authentication based on selected method
+ authSetter auth.Setter
+
// Unique ID obtained from frps.
// It should be attached to the login message when reconnecting.
runID string
@@ -50,36 +55,25 @@ type Control struct {
// manage all visitors
vm *visitor.Manager
- // control connection
+ // control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
conn net.Conn
+ // use cm to create new connections, which could be real TCP connections or virtual streams.
cm *ConnectionManager
- // 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{}
+ doneCh chan struct{}
- // last time got the Pong message
- lastPong time.Time
-
- // The client configuration
- clientCfg *v1.ClientCommonConfig
-
- readerShutdown *shutdown.Shutdown
- writerShutdown *shutdown.Shutdown
- msgHandlerShutdown *shutdown.Shutdown
-
- // sets authentication based on selected method
- authSetter auth.Setter
+ // of time.Time, last time got the Pong message
+ lastPong atomic.Value
+ // The role of msgTransporter is similar to HTTP2.
+ // It allows multiple messages to be sent simultaneously on the same control connection.
+ // The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type.
msgTransporter transport.MessageTransporter
+
+ // msgDispatcher is a wrapper for control connection.
+ // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
+ msgDispatcher *msg.Dispatcher
}
func NewControl(
@@ -88,31 +82,34 @@ func NewControl(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
authSetter auth.Setter,
-) *Control {
+) (*Control, error) {
// new xlog instance
ctl := &Control{
- ctx: ctx,
- xl: xlog.FromContextSafe(ctx),
- runID: runID,
- conn: conn,
- cm: cm,
- pxyCfgs: pxyCfgs,
- sendCh: make(chan msg.Message, 100),
- readCh: make(chan msg.Message, 100),
- closedCh: make(chan struct{}),
- closedDoneCh: make(chan struct{}),
- clientCfg: clientCfg,
- readerShutdown: shutdown.New(),
- writerShutdown: shutdown.New(),
- msgHandlerShutdown: shutdown.New(),
- authSetter: authSetter,
+ ctx: ctx,
+ xl: xlog.FromContextSafe(ctx),
+ clientCfg: clientCfg,
+ authSetter: authSetter,
+ runID: runID,
+ pxyCfgs: pxyCfgs,
+ conn: conn,
+ cm: cm,
+ doneCh: make(chan struct{}),
+ }
+ ctl.lastPong.Store(time.Now())
+
+ cryptoRW, err := utilnet.NewCryptoReadWriter(conn, []byte(clientCfg.Auth.Token))
+ if err != nil {
+ return nil, err
}
- ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
- ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
+ ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
+ ctl.registerMsgHandlers()
+ ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
+
+ ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
ctl.vm.Reload(visitorCfgs)
- return ctl
+ return ctl, nil
}
func (ctl *Control) Run() {
@@ -125,7 +122,7 @@ func (ctl *Control) Run() {
go ctl.vm.Run()
}
-func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
+func (ctl *Control) handleReqWorkConn(_ msg.Message) {
xl := ctl.xl
workConn, err := ctl.connectServer()
if err != nil {
@@ -162,8 +159,9 @@ func (ctl *Control) HandleReqWorkConn(_ *msg.ReqWorkConn) {
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
}
-func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
+func (ctl *Control) handleNewProxyResp(m msg.Message) {
xl := ctl.xl
+ inMsg := m.(*msg.NewProxyResp)
// Server will return NewProxyResp message to each NewProxy message.
// Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
@@ -174,8 +172,9 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
}
}
-func (ctl *Control) HandleNatHoleResp(inMsg *msg.NatHoleResp) {
+func (ctl *Control) handleNatHoleResp(m msg.Message) {
xl := ctl.xl
+ inMsg := m.(*msg.NatHoleResp)
// Dispatch the NatHoleResp message to the related proxy.
ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID)
@@ -184,6 +183,19 @@ 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.conn.Close()
+ return
+ }
+ ctl.lastPong.Store(time.Now())
+ xl.Debug("receive heartbeat from server")
+}
+
func (ctl *Control) Close() error {
return ctl.GracefulClose(0)
}
@@ -199,9 +211,9 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
return nil
}
-// ClosedDoneCh returns a channel that will be closed after all resources are released
-func (ctl *Control) ClosedDoneCh() <-chan struct{} {
- return ctl.closedDoneCh
+// Done returns a channel that will be closed after all resources are released
+func (ctl *Control) Done() <-chan struct{} {
+ return ctl.doneCh
}
// connectServer return a new connection to frps
@@ -209,151 +221,70 @@ func (ctl *Control) connectServer() (conn net.Conn, err error) {
return ctl.cm.Connect()
}
-// reader read all messages from frps and send to readCh
-func (ctl *Control) reader() {
- xl := ctl.xl
- defer func() {
- if err := recover(); err != nil {
- xl.Error("panic error: %v", err)
- xl.Error(string(debug.Stack()))
- }
- }()
- defer ctl.readerShutdown.Done()
- defer close(ctl.closedCh)
-
- encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
- for {
- m, err := msg.ReadMsg(encReader)
- if err != nil {
- if err == io.EOF {
- xl.Debug("read from control connection EOF")
- return
- }
- xl.Warn("read error: %v", err)
- ctl.conn.Close()
- return
- }
- ctl.readCh <- m
- }
+func (ctl *Control) registerMsgHandlers() {
+ ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn))
+ ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp)
+ ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp)
+ ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong)
}
-// writer writes messages got from sendCh to frps
-func (ctl *Control) writer() {
+// headerWorker sends heartbeat to server and check heartbeat timeout.
+func (ctl *Control) heartbeatWorker() {
xl := ctl.xl
- defer ctl.writerShutdown.Done()
- encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token))
- if err != nil {
- xl.Error("crypto new writer error: %v", err)
- ctl.conn.Close()
- return
- }
- for {
- m, ok := <-ctl.sendCh
- if !ok {
- xl.Info("control writer is closing")
- return
- }
- if err := msg.WriteMsg(encWriter, m); err != nil {
- xl.Warn("write message to control connection error: %v", err)
- return
- }
- }
-}
-
-// 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()))
+ // TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
+ // Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
+ if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
+ // send heartbeat to server
+ sendHeartBeat := func() error {
+ xl.Debug("send heartbeat to server")
+ pingMsg := &msg.Ping{}
+ if err := ctl.authSetter.SetPing(pingMsg); err != nil {
+ xl.Warn("error during ping authentication: %v, skip sending ping message", err)
+ return err
+ }
+ _ = ctl.msgDispatcher.Send(pingMsg)
+ return nil
}
- }()
- defer ctl.msgHandlerShutdown.Done()
- var hbSendCh <-chan time.Time
- // TODO(fatedier): disable heartbeat if TCPMux is enabled.
- // Just keep it here to keep compatible with old version frps.
- if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
- hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second)
- defer hbSend.Stop()
- hbSendCh = hbSend.C
+ go wait.BackoffUntil(sendHeartBeat,
+ wait.NewFastBackoffManager(wait.FastBackoffOptions{
+ Duration: time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second,
+ InitDurationIfFail: time.Second,
+ Factor: 2.0,
+ Jitter: 0.1,
+ MaxDuration: time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second,
+ }),
+ true, ctl.doneCh,
+ )
}
- var hbCheckCh <-chan time.Time
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 &&
!lo.FromPtr(ctl.clientCfg.Transport.TCPMux) {
- hbCheck := time.NewTicker(time.Second)
- defer hbCheck.Stop()
- hbCheckCh = hbCheck.C
- }
- ctl.lastPong = time.Now()
- for {
- select {
- case <-hbSendCh:
- // send heartbeat to server
- xl.Debug("send heartbeat to server")
- pingMsg := &msg.Ping{}
- if err := ctl.authSetter.SetPing(pingMsg); err != nil {
- xl.Warn("error during ping authentication: %v. skip sending ping message", err)
- continue
- }
- ctl.sendCh <- pingMsg
- case <-hbCheckCh:
- if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second {
+ go wait.Until(func() {
+ if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.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) {
- case *msg.ReqWorkConn:
- go ctl.HandleReqWorkConn(m)
- case *msg.NewProxyResp:
- ctl.HandleNewProxyResp(m)
- case *msg.NatHoleResp:
- ctl.HandleNatHoleResp(m)
- case *msg.Pong:
- if m.Error != "" {
- xl.Error("Pong contains error: %s", m.Error)
- ctl.conn.Close()
- return
- }
- ctl.lastPong = time.Now()
- xl.Debug("receive heartbeat from server")
- }
- }
+ }, time.Second, ctl.doneCh)
}
}
-// If controler is notified by closedCh, reader and writer and handler will exit
func (ctl *Control) worker() {
- go ctl.msgHandler()
- go ctl.reader()
- go ctl.writer()
+ go ctl.heartbeatWorker()
+ go ctl.msgDispatcher.Run()
- <-ctl.closedCh
- // close related channels and wait until other goroutines done
- close(ctl.readCh)
- ctl.readerShutdown.WaitDone()
- ctl.msgHandlerShutdown.WaitDone()
-
- close(ctl.sendCh)
- ctl.writerShutdown.WaitDone()
+ <-ctl.msgDispatcher.Done()
+ ctl.conn.Close()
ctl.pm.Close()
ctl.vm.Close()
-
- close(ctl.closedDoneCh)
ctl.cm.Close()
+
+ close(ctl.doneCh)
}
func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
diff --git a/client/service.go b/client/service.go
index 4b394d8a163..66a642c1bf0 100644
--- a/client/service.go
+++ b/client/service.go
@@ -17,6 +17,7 @@ package client
import (
"context"
"crypto/tls"
+ "errors"
"fmt"
"io"
"net"
@@ -24,7 +25,6 @@ import (
"strconv"
"strings"
"sync"
- "sync/atomic"
"time"
"github.com/fatedier/golib/crypto"
@@ -40,8 +40,8 @@ import (
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
utilnet "github.com/fatedier/frp/pkg/util/net"
- "github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
+ "github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -70,12 +70,11 @@ type Service struct {
// string if no configuration file was used.
cfgFile string
- exit uint32 // 0 means not exit
-
// service context
ctx context.Context
// call cancel to stop service
- cancel context.CancelFunc
+ cancel context.CancelFunc
+ gracefulDuration time.Duration
}
func NewService(
@@ -91,7 +90,6 @@ func NewService(
pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs,
ctx: context.Background(),
- exit: 0,
}
}
@@ -106,8 +104,6 @@ func (svr *Service) Run(ctx context.Context) error {
svr.ctx = xlog.NewContext(ctx, xlog.New())
svr.cancel = cancel
- xl := xlog.FromContextSafe(svr.ctx)
-
// set custom DNSServer
if svr.cfg.DNSServer != "" {
dnsAddr := svr.cfg.DNSServer
@@ -124,26 +120,9 @@ func (svr *Service) Run(ctx context.Context) error {
}
// login to frps
- for {
- conn, cm, err := svr.login()
- if err != nil {
- xl.Warn("login to server failed: %v", err)
-
- // if login_fail_exit is true, just exit this program
- // otherwise sleep a while and try again to connect to server
- if lo.FromPtr(svr.cfg.LoginFailExit) {
- return err
- }
- util.RandomSleep(5*time.Second, 0.9, 1.1)
- } else {
- // login success
- ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
- ctl.Run()
- svr.ctlMu.Lock()
- svr.ctl = ctl
- svr.ctlMu.Unlock()
- break
- }
+ svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.cfg.LoginFailExit))
+ if svr.ctl == nil {
+ return fmt.Errorf("the process exited because the first login to the server failed, and the loginFailExit feature is enabled")
}
go svr.keepControllerWorking()
@@ -160,80 +139,35 @@ func (svr *Service) Run(ctx context.Context) error {
log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
}
<-svr.ctx.Done()
- // service context may not be canceled by svr.Close(), we should call it here to release resources
- if atomic.LoadUint32(&svr.exit) == 0 {
- svr.Close()
- }
+ svr.stop()
return nil
}
func (svr *Service) keepControllerWorking() {
- xl := xlog.FromContextSafe(svr.ctx)
- maxDelayTime := 20 * time.Second
- delayTime := time.Second
-
- // if frpc reconnect frps, we need to limit retry times in 1min
- // current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ...
- // when exceed 1min, we will reset delay and counts
- cutoffTime := time.Now().Add(time.Minute)
- reconnectDelay := time.Second
- reconnectCounts := 1
-
- for {
- <-svr.ctl.ClosedDoneCh()
- if atomic.LoadUint32(&svr.exit) != 0 {
- return
- }
-
- // the first three attempts with a low delay
- if reconnectCounts > 3 {
- util.RandomSleep(reconnectDelay, 0.9, 1.1)
- xl.Info("wait %v to reconnect", reconnectDelay)
- reconnectDelay *= 2
- } else {
- util.RandomSleep(time.Second, 0, 0.5)
- }
- reconnectCounts++
-
- now := time.Now()
- if now.After(cutoffTime) {
- // reset
- cutoffTime = now.Add(time.Minute)
- reconnectDelay = time.Second
- reconnectCounts = 1
- }
-
- for {
- if atomic.LoadUint32(&svr.exit) != 0 {
- return
- }
-
- xl.Info("try to reconnect to server...")
- conn, cm, err := svr.login()
- if err != nil {
- xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
- util.RandomSleep(delayTime, 0.9, 1.1)
-
- delayTime *= 2
- if delayTime > maxDelayTime {
- delayTime = maxDelayTime
- }
- continue
- }
- // reconnect success, init delayTime
- delayTime = time.Second
-
- ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
- ctl.Run()
- svr.ctlMu.Lock()
- if svr.ctl != nil {
- svr.ctl.Close()
- }
- svr.ctl = ctl
- svr.ctlMu.Unlock()
- break
- }
- }
+ <-svr.ctl.Done()
+
+ // There is a situation where the login is successful but due to certain reasons,
+ // the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
+ // The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
+ // The maximum interval is 20 seconds.
+ wait.BackoffUntil(func() error {
+ // loopLoginUntilSuccess is another layer of loop that will continuously attempt to
+ // login to the server until successful.
+ svr.loopLoginUntilSuccess(20*time.Second, false)
+ <-svr.ctl.Done()
+ return errors.New("control is closed and try another loop")
+ }, 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
@@ -299,6 +233,54 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
return
}
+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, cm, err := svr.login()
+ if err != nil {
+ xl.Warn("connect to server error: %v", err)
+ if firstLoginExit {
+ svr.cancel()
+ }
+ return err
+ }
+
+ ctl, err := NewControl(svr.ctx, svr.runID, conn, cm,
+ svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
+ if err != nil {
+ conn.Close()
+ xl.Error("NewControl error: %v", err)
+ return err
+ }
+
+ ctl.Run()
+ // 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) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
svr.cfgMu.Lock()
svr.pxyCfgs = pxyCfgs
@@ -320,20 +302,20 @@ func (svr *Service) Close() {
}
func (svr *Service) GracefulClose(d time.Duration) {
- atomic.StoreUint32(&svr.exit, 1)
+ svr.gracefulDuration = d
+ svr.cancel()
+}
- svr.ctlMu.RLock()
+func (svr *Service) stop() {
+ svr.ctlMu.Lock()
+ defer svr.ctlMu.Unlock()
if svr.ctl != nil {
- svr.ctl.GracefulClose(d)
+ svr.ctl.GracefulClose(svr.gracefulDuration)
svr.ctl = nil
}
- svr.ctlMu.RUnlock()
-
- if svr.cancel != nil {
- svr.cancel()
- }
}
+// ConnectionManager is a wrapper for establishing connections to the server.
type ConnectionManager struct {
ctx context.Context
cfg *v1.ClientCommonConfig
@@ -349,6 +331,10 @@ func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *Conn
}
}
+// OpenConnection 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 (cm *ConnectionManager) OpenConnection() error {
xl := xlog.FromContextSafe(cm.ctx)
@@ -411,6 +397,7 @@ func (cm *ConnectionManager) OpenConnection() error {
return nil
}
+// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
func (cm *ConnectionManager) Connect() (net.Conn, error) {
if cm.quicConn != nil {
stream, err := cm.quicConn.OpenStreamSync(context.Background())
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 696496a2357..12c388a5229 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -1,3 +1,17 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
package metrics
import (
diff --git a/pkg/msg/handler.go b/pkg/msg/handler.go
new file mode 100644
index 00000000000..cb1eb15a307
--- /dev/null
+++ b/pkg/msg/handler.go
@@ -0,0 +1,103 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package msg
+
+import (
+ "io"
+ "reflect"
+)
+
+func AsyncHandler(f func(Message)) func(Message) {
+ return func(m Message) {
+ go f(m)
+ }
+}
+
+// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn.
+type Dispatcher struct {
+ rw io.ReadWriter
+
+ sendCh chan Message
+ doneCh chan struct{}
+ msgHandlers map[reflect.Type]func(Message)
+ defaultHandler func(Message)
+}
+
+func NewDispatcher(rw io.ReadWriter) *Dispatcher {
+ return &Dispatcher{
+ rw: rw,
+ sendCh: make(chan Message, 100),
+ doneCh: make(chan struct{}),
+ msgHandlers: make(map[reflect.Type]func(Message)),
+ }
+}
+
+// Run will block until io.EOF or some error occurs.
+func (d *Dispatcher) Run() {
+ go d.sendLoop()
+ go d.readLoop()
+}
+
+func (d *Dispatcher) sendLoop() {
+ for {
+ select {
+ case <-d.doneCh:
+ return
+ case m := <-d.sendCh:
+ _ = WriteMsg(d.rw, m)
+ }
+ }
+}
+
+func (d *Dispatcher) readLoop() {
+ for {
+ m, err := ReadMsg(d.rw)
+ if err != nil {
+ close(d.doneCh)
+ return
+ }
+
+ if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok {
+ handler(m)
+ } else if d.defaultHandler != nil {
+ d.defaultHandler(m)
+ }
+ }
+}
+
+func (d *Dispatcher) Send(m Message) error {
+ select {
+ case <-d.doneCh:
+ return io.EOF
+ case d.sendCh <- m:
+ return nil
+ }
+}
+
+func (d *Dispatcher) SendChannel() chan Message {
+ return d.sendCh
+}
+
+func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
+ d.msgHandlers[reflect.TypeOf(msg)] = handler
+}
+
+func (d *Dispatcher) RegisterDefaultHandler(handler func(Message)) {
+ d.defaultHandler = handler
+}
+
+func (d *Dispatcher) Done() chan struct{} {
+ return d.doneCh
+}
diff --git a/pkg/transport/message.go b/pkg/transport/message.go
index 6bcd8ce86f7..7163a8adcb4 100644
--- a/pkg/transport/message.go
+++ b/pkg/transport/message.go
@@ -29,7 +29,9 @@ type MessageTransporter interface {
// Recv(ctx context.Context, laneKey string, msgType string) (Message, error)
// Do will first send msg, then recv msg with the same laneKey and specified msgType.
Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error)
+ // Dispatch will dispatch message to releated channel registered in Do function by its message type and laneKey.
Dispatch(m msg.Message, laneKey string) bool
+ // Same with Dispatch but with specified message type.
DispatchWithType(m msg.Message, msgType, laneKey string) bool
}
diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go
index fb2ff677730..a5bbe7371c5 100644
--- a/pkg/util/net/conn.go
+++ b/pkg/util/net/conn.go
@@ -22,6 +22,7 @@ import (
"sync/atomic"
"time"
+ "github.com/fatedier/golib/crypto"
quic "github.com/quic-go/quic-go"
"github.com/fatedier/frp/pkg/util/xlog"
@@ -216,3 +217,18 @@ func (conn *wrapQuicStream) Close() error {
conn.Stream.CancelRead(0)
return conn.Stream.Close()
}
+
+func NewCryptoReadWriter(rw io.ReadWriter, key []byte) (io.ReadWriter, error) {
+ encReader := crypto.NewReader(rw, key)
+ encWriter, err := crypto.NewWriter(rw, key)
+ if err != nil {
+ return nil, err
+ }
+ return struct {
+ io.Reader
+ io.Writer
+ }{
+ Reader: encReader,
+ Writer: encWriter,
+ }, nil
+}
diff --git a/pkg/util/wait/backoff.go b/pkg/util/wait/backoff.go
new file mode 100644
index 00000000000..45e0ab68a52
--- /dev/null
+++ b/pkg/util/wait/backoff.go
@@ -0,0 +1,197 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package wait
+
+import (
+ "math/rand"
+ "time"
+
+ "github.com/samber/lo"
+
+ "github.com/fatedier/frp/pkg/util/util"
+)
+
+type BackoffFunc func(previousDuration time.Duration, previousConditionError bool) time.Duration
+
+func (f BackoffFunc) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration {
+ return f(previousDuration, previousConditionError)
+}
+
+type BackoffManager interface {
+ Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration
+}
+
+type FastBackoffOptions struct {
+ Duration time.Duration
+ Factor float64
+ Jitter float64
+ MaxDuration time.Duration
+ InitDurationIfFail time.Duration
+
+ // If FastRetryCount > 0, then within the FastRetryWindow time window,
+ // the retry will be performed with a delay of FastRetryDelay for the first FastRetryCount calls.
+ FastRetryCount int
+ FastRetryDelay time.Duration
+ FastRetryJitter float64
+ FastRetryWindow time.Duration
+}
+
+type fastBackoffImpl struct {
+ options FastBackoffOptions
+
+ lastCalledTime time.Time
+ consecutiveErrCount int
+
+ fastRetryCutoffTime time.Time
+ countsInFastRetryWindow int
+}
+
+func NewFastBackoffManager(options FastBackoffOptions) BackoffManager {
+ return &fastBackoffImpl{
+ options: options,
+ countsInFastRetryWindow: 1,
+ }
+}
+
+func (f *fastBackoffImpl) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration {
+ if f.lastCalledTime.IsZero() {
+ f.lastCalledTime = time.Now()
+ return f.options.Duration
+ }
+ now := time.Now()
+ f.lastCalledTime = now
+
+ if previousConditionError {
+ f.consecutiveErrCount++
+ } else {
+ f.consecutiveErrCount = 0
+ }
+
+ if f.options.FastRetryCount > 0 && previousConditionError {
+ f.countsInFastRetryWindow++
+ if f.countsInFastRetryWindow <= f.options.FastRetryCount {
+ return Jitter(f.options.FastRetryDelay, f.options.FastRetryJitter)
+ }
+ if now.After(f.fastRetryCutoffTime) {
+ // reset
+ f.fastRetryCutoffTime = now.Add(f.options.FastRetryWindow)
+ f.countsInFastRetryWindow = 0
+ }
+ }
+
+ if previousConditionError {
+ var duration time.Duration
+ if f.consecutiveErrCount == 1 {
+ duration = util.EmptyOr(f.options.InitDurationIfFail, previousDuration)
+ } else {
+ duration = previousDuration
+ }
+
+ duration = util.EmptyOr(duration, time.Second)
+ if f.options.Factor != 0 {
+ duration = time.Duration(float64(duration) * f.options.Factor)
+ }
+ if f.options.Jitter > 0 {
+ duration = Jitter(duration, f.options.Jitter)
+ }
+ if f.options.MaxDuration > 0 && duration > f.options.MaxDuration {
+ duration = f.options.MaxDuration
+ }
+ return duration
+ }
+ return f.options.Duration
+}
+
+func BackoffUntil(f func() error, backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
+ var delay time.Duration
+ previousError := false
+
+ ticker := time.NewTicker(backoff.Backoff(delay, previousError))
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-stopCh:
+ return
+ default:
+ }
+
+ if !sliding {
+ delay = backoff.Backoff(delay, previousError)
+ }
+
+ if err := f(); err != nil {
+ previousError = true
+ } else {
+ previousError = false
+ }
+
+ if sliding {
+ delay = backoff.Backoff(delay, previousError)
+ }
+
+ ticker.Reset(delay)
+ select {
+ case <-stopCh:
+ return
+ default:
+ }
+
+ select {
+ case <-stopCh:
+ return
+ case <-ticker.C:
+ }
+ }
+}
+
+// Jitter returns a time.Duration between duration and duration + maxFactor *
+// duration.
+//
+// This allows clients to avoid converging on periodic behavior. If maxFactor
+// is 0.0, a suggested default value will be chosen.
+func Jitter(duration time.Duration, maxFactor float64) time.Duration {
+ if maxFactor <= 0.0 {
+ maxFactor = 1.0
+ }
+ wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
+ return wait
+}
+
+func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
+ ff := func() error {
+ f()
+ return nil
+ }
+ BackoffUntil(ff, BackoffFunc(func(time.Duration, bool) time.Duration {
+ return period
+ }), true, stopCh)
+}
+
+func MergeAndCloseOnAnyStopChannel[T any](upstreams ...<-chan T) <-chan T {
+ out := make(chan T)
+
+ for _, upstream := range upstreams {
+ ch := upstream
+ go lo.Try0(func() {
+ select {
+ case <-ch:
+ close(out)
+ case <-out:
+ }
+ })
+ }
+ return out
+}
diff --git a/server/control.go b/server/control.go
index f2eaaa56ad7..e651a97e846 100644
--- a/server/control.go
+++ b/server/control.go
@@ -17,15 +17,12 @@ package server
import (
"context"
"fmt"
- "io"
"net"
"runtime/debug"
"sync"
+ "sync/atomic"
"time"
- "github.com/fatedier/golib/control/shutdown"
- "github.com/fatedier/golib/crypto"
- "github.com/fatedier/golib/errors"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/auth"
@@ -35,8 +32,10 @@ import (
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/transport"
+ utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
+ "github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/metrics"
@@ -111,18 +110,16 @@ type Control struct {
// other components can use this to communicate with client
msgTransporter transport.MessageTransporter
+ // msgDispatcher is a wrapper for control connection.
+ // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types.
+ msgDispatcher *msg.Dispatcher
+
// login message
loginMsg *msg.Login
// control connection
conn net.Conn
- // put a message in this channel to send it over control connection to client
- sendCh chan (msg.Message)
-
- // read from this channel to get the next message sent by client
- readCh chan (msg.Message)
-
// work connections
workConnCh chan net.Conn
@@ -136,27 +133,21 @@ type Control struct {
portsUsedNum int
// last time got the Ping message
- lastPing time.Time
+ lastPing atomic.Value
// A new run id will be generated when a new client login.
// If run id got from login message has same run id, it means it's the same client, so we can
// replace old controller instantly.
runID string
- readerShutdown *shutdown.Shutdown
- writerShutdown *shutdown.Shutdown
- managerShutdown *shutdown.Shutdown
- allShutdown *shutdown.Shutdown
-
- started bool
-
mu sync.RWMutex
// Server configuration information
serverCfg *v1.ServerConfig
- xl *xlog.Logger
- ctx context.Context
+ xl *xlog.Logger
+ ctx context.Context
+ doneCh chan struct{}
}
func NewControl(
@@ -168,36 +159,38 @@ func NewControl(
ctlConn net.Conn,
loginMsg *msg.Login,
serverCfg *v1.ServerConfig,
-) *Control {
+) (*Control, error) {
poolCount := loginMsg.PoolCount
if poolCount > int(serverCfg.Transport.MaxPoolCount) {
poolCount = int(serverCfg.Transport.MaxPoolCount)
}
ctl := &Control{
- rc: rc,
- pxyManager: pxyManager,
- pluginManager: pluginManager,
- authVerifier: authVerifier,
- conn: ctlConn,
- loginMsg: loginMsg,
- sendCh: make(chan msg.Message, 10),
- readCh: make(chan msg.Message, 10),
- workConnCh: make(chan net.Conn, poolCount+10),
- proxies: make(map[string]proxy.Proxy),
- poolCount: poolCount,
- portsUsedNum: 0,
- lastPing: time.Now(),
- runID: loginMsg.RunID,
- readerShutdown: shutdown.New(),
- writerShutdown: shutdown.New(),
- managerShutdown: shutdown.New(),
- allShutdown: shutdown.New(),
- serverCfg: serverCfg,
- xl: xlog.FromContextSafe(ctx),
- ctx: ctx,
+ rc: rc,
+ pxyManager: pxyManager,
+ pluginManager: pluginManager,
+ authVerifier: authVerifier,
+ conn: ctlConn,
+ loginMsg: loginMsg,
+ workConnCh: make(chan net.Conn, poolCount+10),
+ proxies: make(map[string]proxy.Proxy),
+ poolCount: poolCount,
+ portsUsedNum: 0,
+ runID: loginMsg.RunID,
+ serverCfg: serverCfg,
+ xl: xlog.FromContextSafe(ctx),
+ ctx: ctx,
+ doneCh: make(chan struct{}),
+ }
+ ctl.lastPing.Store(time.Now())
+
+ cryptoRW, err := utilnet.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
+ if err != nil {
+ return nil, err
}
- ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh)
- return ctl
+ ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
+ ctl.registerMsgHandlers()
+ ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
+ return ctl, nil
}
// Start send a login success message to client and start working.
@@ -208,27 +201,18 @@ func (ctl *Control) Start() {
Error: "",
}
_ = msg.WriteMsg(ctl.conn, loginRespMsg)
- ctl.mu.Lock()
- ctl.started = true
- ctl.mu.Unlock()
- go ctl.writer()
go func() {
for i := 0; i < ctl.poolCount; i++ {
// ignore error here, that means that this control is closed
- _ = errors.PanicToError(func() {
- ctl.sendCh <- &msg.ReqWorkConn{}
- })
+ _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
}
}()
-
- go ctl.manager()
- go ctl.reader()
- go ctl.stoper()
+ go ctl.worker()
}
func (ctl *Control) Close() error {
- ctl.allShutdown.Start()
+ ctl.conn.Close()
return nil
}
@@ -236,7 +220,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
xl := ctl.xl
xl.Info("Replaced by client [%s]", newCtl.runID)
ctl.runID = ""
- ctl.allShutdown.Start()
+ ctl.conn.Close()
}
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
@@ -282,9 +266,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
xl.Debug("get work connection from pool")
default:
// no work connections available in the poll, send message to frpc to get more
- if err = errors.PanicToError(func() {
- ctl.sendCh <- &msg.ReqWorkConn{}
- }); err != nil {
+ if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil {
return nil, fmt.Errorf("control is already closed")
}
@@ -304,92 +286,39 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
}
// When we get a work connection from pool, replace it with a new one.
- _ = errors.PanicToError(func() {
- ctl.sendCh <- &msg.ReqWorkConn{}
- })
+ _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
return
}
-func (ctl *Control) writer() {
+func (ctl *Control) heartbeatWorker() {
xl := ctl.xl
- defer func() {
- if err := recover(); err != nil {
- xl.Error("panic error: %v", err)
- xl.Error(string(debug.Stack()))
- }
- }()
-
- defer ctl.allShutdown.Start()
- defer ctl.writerShutdown.Done()
-
- encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
- if err != nil {
- xl.Error("crypto new writer error: %v", err)
- ctl.allShutdown.Start()
- return
- }
- for {
- m, ok := <-ctl.sendCh
- if !ok {
- xl.Info("control writer is closing")
- return
- }
- if err := msg.WriteMsg(encWriter, m); err != nil {
- xl.Warn("write message to control connection error: %v", err)
- return
- }
- }
-}
-
-func (ctl *Control) reader() {
- xl := ctl.xl
- defer func() {
- if err := recover(); err != nil {
- xl.Error("panic error: %v", err)
- xl.Error(string(debug.Stack()))
- }
- }()
-
- defer ctl.allShutdown.Start()
- defer ctl.readerShutdown.Done()
-
- encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
- for {
- m, err := msg.ReadMsg(encReader)
- if err != nil {
- if err == io.EOF {
- xl.Debug("control connection closed")
+ // Don't need application heartbeat if TCPMux is enabled,
+ // yamux will do same thing.
+ // TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it.
+ if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
+ go wait.Until(func() {
+ if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
+ xl.Warn("heartbeat timeout")
return
}
- xl.Warn("read error: %v", err)
- ctl.conn.Close()
- return
- }
-
- ctl.readCh <- m
+ }, time.Second, ctl.doneCh)
}
}
-func (ctl *Control) stoper() {
+// block until Control closed
+func (ctl *Control) WaitClosed() {
+ <-ctl.doneCh
+}
+
+func (ctl *Control) worker() {
xl := ctl.xl
- defer func() {
- if err := recover(); err != nil {
- xl.Error("panic error: %v", err)
- xl.Error(string(debug.Stack()))
- }
- }()
- ctl.allShutdown.WaitStart()
+ go ctl.heartbeatWorker()
+ go ctl.msgDispatcher.Run()
+ <-ctl.msgDispatcher.Done()
ctl.conn.Close()
- ctl.readerShutdown.WaitDone()
-
- close(ctl.readCh)
- ctl.managerShutdown.WaitDone()
-
- close(ctl.sendCh)
- ctl.writerShutdown.WaitDone()
ctl.mu.Lock()
defer ctl.mu.Unlock()
@@ -419,136 +348,104 @@ func (ctl *Control) stoper() {
}()
}
- ctl.allShutdown.Done()
- xl.Info("client exit success")
metrics.Server.CloseClient()
+ xl.Info("client exit success")
+ close(ctl.doneCh)
}
-// block until Control closed
-func (ctl *Control) WaitClosed() {
- ctl.mu.RLock()
- started := ctl.started
- ctl.mu.RUnlock()
-
- if !started {
- ctl.allShutdown.Done()
- return
- }
- ctl.allShutdown.WaitDone()
+func (ctl *Control) registerMsgHandlers() {
+ ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy)
+ ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing)
+ ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor))
+ ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient))
+ ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport))
+ ctl.msgDispatcher.RegisterHandler(&msg.CloseProxy{}, ctl.handleCloseProxy)
}
-func (ctl *Control) manager() {
+func (ctl *Control) handleNewProxy(m msg.Message) {
xl := ctl.xl
- defer func() {
- if err := recover(); err != nil {
- xl.Error("panic error: %v", err)
- xl.Error(string(debug.Stack()))
- }
- }()
+ inMsg := m.(*msg.NewProxy)
- defer ctl.allShutdown.Start()
- defer ctl.managerShutdown.Done()
+ content := &plugin.NewProxyContent{
+ User: plugin.UserInfo{
+ User: ctl.loginMsg.User,
+ Metas: ctl.loginMsg.Metas,
+ RunID: ctl.loginMsg.RunID,
+ },
+ NewProxy: *inMsg,
+ }
+ var remoteAddr string
+ retContent, err := ctl.pluginManager.NewProxy(content)
+ if err == nil {
+ inMsg = &retContent.NewProxy
+ remoteAddr, err = ctl.RegisterProxy(inMsg)
+ }
- var heartbeatCh <-chan time.Time
- // Don't need application heartbeat if TCPMux is enabled,
- // yamux will do same thing.
- if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
- heartbeat := time.NewTicker(time.Second)
- defer heartbeat.Stop()
- heartbeatCh = heartbeat.C
+ // register proxy in this control
+ resp := &msg.NewProxyResp{
+ ProxyName: inMsg.ProxyName,
+ }
+ if err != nil {
+ xl.Warn("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err)
+ resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName),
+ err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
+ } else {
+ resp.RemoteAddr = remoteAddr
+ xl.Info("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
+ metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType)
}
+ _ = ctl.msgDispatcher.Send(resp)
+}
- for {
- select {
- case <-heartbeatCh:
- if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
- xl.Warn("heartbeat timeout")
- return
- }
- case rawMsg, ok := <-ctl.readCh:
- if !ok {
- return
- }
+func (ctl *Control) handlePing(m msg.Message) {
+ xl := ctl.xl
+ inMsg := m.(*msg.Ping)
- switch m := rawMsg.(type) {
- case *msg.NewProxy:
- content := &plugin.NewProxyContent{
- User: plugin.UserInfo{
- User: ctl.loginMsg.User,
- Metas: ctl.loginMsg.Metas,
- RunID: ctl.loginMsg.RunID,
- },
- NewProxy: *m,
- }
- var remoteAddr string
- retContent, err := ctl.pluginManager.NewProxy(content)
- if err == nil {
- m = &retContent.NewProxy
- remoteAddr, err = ctl.RegisterProxy(m)
- }
-
- // register proxy in this control
- resp := &msg.NewProxyResp{
- ProxyName: m.ProxyName,
- }
- if err != nil {
- xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
- resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName),
- err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient))
- } else {
- resp.RemoteAddr = remoteAddr
- xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
- metrics.Server.NewProxy(m.ProxyName, m.ProxyType)
- }
- ctl.sendCh <- resp
- case *msg.NatHoleVisitor:
- go ctl.HandleNatHoleVisitor(m)
- case *msg.NatHoleClient:
- go ctl.HandleNatHoleClient(m)
- case *msg.NatHoleReport:
- go ctl.HandleNatHoleReport(m)
- case *msg.CloseProxy:
- _ = ctl.CloseProxy(m)
- xl.Info("close proxy [%s] success", m.ProxyName)
- case *msg.Ping:
- content := &plugin.PingContent{
- User: plugin.UserInfo{
- User: ctl.loginMsg.User,
- Metas: ctl.loginMsg.Metas,
- RunID: ctl.loginMsg.RunID,
- },
- Ping: *m,
- }
- retContent, err := ctl.pluginManager.Ping(content)
- if err == nil {
- m = &retContent.Ping
- err = ctl.authVerifier.VerifyPing(m)
- }
- if err != nil {
- xl.Warn("received invalid ping: %v", err)
- ctl.sendCh <- &msg.Pong{
- Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
- }
- return
- }
- ctl.lastPing = time.Now()
- xl.Debug("receive heartbeat")
- ctl.sendCh <- &msg.Pong{}
- }
- }
+ content := &plugin.PingContent{
+ User: plugin.UserInfo{
+ User: ctl.loginMsg.User,
+ Metas: ctl.loginMsg.Metas,
+ RunID: ctl.loginMsg.RunID,
+ },
+ Ping: *inMsg,
+ }
+ retContent, err := ctl.pluginManager.Ping(content)
+ if err == nil {
+ inMsg = &retContent.Ping
+ err = ctl.authVerifier.VerifyPing(inMsg)
}
+ if err != nil {
+ xl.Warn("received invalid ping: %v", err)
+ _ = ctl.msgDispatcher.Send(&msg.Pong{
+ Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)),
+ })
+ return
+ }
+ ctl.lastPing.Store(time.Now())
+ xl.Debug("receive heartbeat")
+ _ = ctl.msgDispatcher.Send(&msg.Pong{})
}
-func (ctl *Control) HandleNatHoleVisitor(m *msg.NatHoleVisitor) {
- ctl.rc.NatHoleController.HandleVisitor(m, ctl.msgTransporter, ctl.loginMsg.User)
+func (ctl *Control) handleNatHoleVisitor(m msg.Message) {
+ inMsg := m.(*msg.NatHoleVisitor)
+ ctl.rc.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.loginMsg.User)
}
-func (ctl *Control) HandleNatHoleClient(m *msg.NatHoleClient) {
- ctl.rc.NatHoleController.HandleClient(m, ctl.msgTransporter)
+func (ctl *Control) handleNatHoleClient(m msg.Message) {
+ inMsg := m.(*msg.NatHoleClient)
+ ctl.rc.NatHoleController.HandleClient(inMsg, ctl.msgTransporter)
}
-func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) {
- ctl.rc.NatHoleController.HandleReport(m)
+func (ctl *Control) handleNatHoleReport(m msg.Message) {
+ inMsg := m.(*msg.NatHoleReport)
+ ctl.rc.NatHoleController.HandleReport(inMsg)
+}
+
+func (ctl *Control) handleCloseProxy(m msg.Message) {
+ xl := ctl.xl
+ inMsg := m.(*msg.CloseProxy)
+ _ = ctl.CloseProxy(inMsg)
+ xl.Info("close proxy [%s] success", inMsg.ProxyName)
}
func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
diff --git a/server/service.go b/server/service.go
index 9deffa020f0..2629b345011 100644
--- a/server/service.go
+++ b/server/service.go
@@ -516,13 +516,14 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
}
}
-func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) {
+func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error {
// If client's RunID is empty, it's a new client, we just create a new controller.
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
+ var err error
if loginMsg.RunID == "" {
loginMsg.RunID, err = util.RandID()
if err != nil {
- return
+ return err
}
}
@@ -534,11 +535,16 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
// Check auth.
- if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil {
- return
+ if err := svr.authVerifier.VerifyLogin(loginMsg); err != nil {
+ return err
}
- ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg)
+ ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg)
+ if err != nil {
+ xl.Warn("create new controller error: %v", err)
+ // don't return detailed errors to client
+ return fmt.Errorf("unexpect error when creating new controller")
+ }
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
oldCtl.WaitClosed()
}
@@ -553,7 +559,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
ctl.WaitClosed()
svr.ctlManager.Del(loginMsg.RunID, ctl)
}()
- return
+ return nil
}
// RegisterWorkConn register a new work connection to control and proxies need it.
From e8deb65c4b173407bad116ed349534ae159107fc Mon Sep 17 00:00:00 2001
From: Aarni Koskela
Date: Thu, 16 Nov 2023 09:42:49 +0200
Subject: [PATCH 05/21] Strict configuration parsing (#3773)
* Test configuration loading more precisely
* Add strict configuration parsing
---
client/admin_api.go | 2 +-
client/service.go | 17 ++++++----
cmd/frpc/sub/admin.go | 2 +-
cmd/frpc/sub/nathole.go | 2 +-
cmd/frpc/sub/proxy.go | 4 +--
cmd/frpc/sub/root.go | 18 +++++++----
cmd/frpc/sub/verify.go | 2 +-
cmd/frps/root.go | 8 +++--
cmd/frps/verify.go | 2 +-
pkg/config/load.go | 39 +++++++++++++++--------
pkg/config/load_test.go | 70 ++++++++++++++++++++++++++++++++++-------
11 files changed, 119 insertions(+), 47 deletions(-)
diff --git a/client/admin_api.go b/client/admin_api.go
index e775f526ad3..84db31c5fdd 100644
--- a/client/admin_api.go
+++ b/client/admin_api.go
@@ -57,7 +57,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
}
}()
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile)
+ cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile, svr.strictConfig)
if err != nil {
res.Code = 400
res.Msg = err.Error()
diff --git a/client/service.go b/client/service.go
index 66a642c1bf0..0a25ae08635 100644
--- a/client/service.go
+++ b/client/service.go
@@ -70,6 +70,9 @@ type Service struct {
// string if no configuration file was used.
cfgFile string
+ // Whether strict configuration parsing had been requested.
+ strictConfig bool
+
// service context
ctx context.Context
// call cancel to stop service
@@ -82,14 +85,16 @@ func NewService(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
+ strictConfig bool,
) *Service {
return &Service{
- authSetter: auth.NewAuthSetter(cfg.Auth),
- cfg: cfg,
- cfgFile: cfgFile,
- pxyCfgs: pxyCfgs,
- visitorCfgs: visitorCfgs,
- ctx: context.Background(),
+ authSetter: auth.NewAuthSetter(cfg.Auth),
+ cfg: cfg,
+ cfgFile: cfgFile,
+ strictConfig: strictConfig,
+ pxyCfgs: pxyCfgs,
+ visitorCfgs: visitorCfgs,
+ ctx: context.Background(),
}
}
diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go
index 2a5f2830a15..d98b4d3e0af 100644
--- a/cmd/frpc/sub/admin.go
+++ b/cmd/frpc/sub/admin.go
@@ -52,7 +52,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
Use: name,
Short: short,
Run: func(cmd *cobra.Command, args []string) {
- cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
+ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go
index 72b635f1bf1..eafea27e05f 100644
--- a/cmd/frpc/sub/nathole.go
+++ b/cmd/frpc/sub/nathole.go
@@ -48,7 +48,7 @@ var natholeDiscoveryCmd = &cobra.Command{
Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters
- cfg, _, _, _, err := config.LoadClientConfig(cfgFile)
+ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfig)
if err != nil {
cfg = &v1.ClientCommonConfig{}
}
diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go
index 7ae8d353b39..41c20bc21ab 100644
--- a/cmd/frpc/sub/proxy.go
+++ b/cmd/frpc/sub/proxy.go
@@ -84,7 +84,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
fmt.Println(err)
os.Exit(1)
}
- err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
+ err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "", strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -110,7 +110,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
fmt.Println(err)
os.Exit(1)
}
- err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
+ err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "", strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index 125c88c05c4..855c7abb81e 100644
--- a/cmd/frpc/sub/root.go
+++ b/cmd/frpc/sub/root.go
@@ -36,15 +36,17 @@ import (
)
var (
- cfgFile string
- cfgDir string
- showVersion bool
+ cfgFile string
+ cfgDir string
+ showVersion bool
+ strictConfig bool
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
+ rootCmd.PersistentFlags().BoolVarP(&strictConfig, "strict_config", "", false, "strict config parsing mode")
}
var rootCmd = &cobra.Command{
@@ -108,7 +110,7 @@ func handleTermSignal(svr *client.Service) {
}
func runClient(cfgFilePath string) error {
- cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath)
+ cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfig)
if err != nil {
return err
}
@@ -120,11 +122,14 @@ func runClient(cfgFilePath string) error {
warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
+ if strictConfig {
+ return fmt.Errorf("warning: %v", warning)
+ }
}
if err != nil {
return err
}
- return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
+ return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath, strictConfig)
}
func startService(
@@ -132,6 +137,7 @@ func startService(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
+ strictConfig bool,
) error {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
@@ -139,7 +145,7 @@ func startService(
log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
}
- svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
+ svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile, strictConfig)
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go
index a84f54f2a8f..0e6adca851f 100644
--- a/cmd/frpc/sub/verify.go
+++ b/cmd/frpc/sub/verify.go
@@ -37,7 +37,7 @@ var verifyCmd = &cobra.Command{
return nil
}
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile)
+ cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frps/root.go b/cmd/frps/root.go
index 4a6f0117620..adb8852e136 100644
--- a/cmd/frps/root.go
+++ b/cmd/frps/root.go
@@ -30,8 +30,9 @@ import (
)
var (
- cfgFile string
- showVersion bool
+ cfgFile string
+ showVersion bool
+ strictConfig bool
serverCfg v1.ServerConfig
)
@@ -39,6 +40,7 @@ var (
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
+ rootCmd.PersistentFlags().BoolVarP(&strictConfig, "strict_config", "", false, "strict config parsing mode")
RegisterServerConfigFlags(rootCmd, &serverCfg)
}
@@ -58,7 +60,7 @@ var rootCmd = &cobra.Command{
err error
)
if cfgFile != "" {
- svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile)
+ svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go
index 4f0cefb18fb..838ac7b65b3 100644
--- a/cmd/frps/verify.go
+++ b/cmd/frps/verify.go
@@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{
fmt.Println("frps: the configuration file is not specified")
return nil
}
- svrCfg, _, err := config.LoadServerConfig(cfgFile)
+ svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/pkg/config/load.go b/pkg/config/load.go
index af2c3e80704..a4013c3247a 100644
--- a/pkg/config/load.go
+++ b/pkg/config/load.go
@@ -27,7 +27,7 @@ import (
"github.com/samber/lo"
"gopkg.in/ini.v1"
"k8s.io/apimachinery/pkg/util/sets"
- "k8s.io/apimachinery/pkg/util/yaml"
+ yaml "k8s.io/apimachinery/pkg/util/yaml"
"github.com/fatedier/frp/pkg/config/legacy"
v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -100,26 +100,39 @@ func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) {
return RenderWithTemplate(b, values)
}
-func LoadConfigureFromFile(path string, c any) error {
+func LoadConfigureFromFile(path string, c any, strict bool) error {
content, err := LoadFileContentWithTemplate(path, GetValues())
if err != nil {
return err
}
- return LoadConfigure(content, c)
+ return LoadConfigure(content, c, strict)
}
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
-func LoadConfigure(b []byte, c any) error {
+func LoadConfigure(b []byte, c any, strict bool) error {
var tomlObj interface{}
+ // Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
+ // TODO: caller should probably be able to specify the format, so we don't need to swallow errors.
if err := toml.Unmarshal(b, &tomlObj); err == nil {
b, err = json.Marshal(&tomlObj)
if err != nil {
return err
}
}
- decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096)
- return decoder.Decode(c)
+ // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
+ if yaml.IsJSONBuffer(b) {
+ decoder := json.NewDecoder(bytes.NewBuffer(b))
+ if strict {
+ decoder.DisallowUnknownFields()
+ }
+ return decoder.Decode(c)
+ }
+ // It wasn't JSON. Unmarshal as YAML.
+ if strict {
+ return yaml.UnmarshalStrict(b, c)
+ }
+ return yaml.Unmarshal(b, c)
}
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
@@ -139,7 +152,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
return configurer, nil
}
-func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
+func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) {
var (
svrCfg *v1.ServerConfig
isLegacyFormat bool
@@ -158,7 +171,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
isLegacyFormat = true
} else {
svrCfg = &v1.ServerConfig{}
- if err := LoadConfigureFromFile(path, svrCfg); err != nil {
+ if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil {
return nil, false, err
}
}
@@ -168,7 +181,7 @@ func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) {
return svrCfg, isLegacyFormat, nil
}
-func LoadClientConfig(path string) (
+func LoadClientConfig(path string, strict bool) (
*v1.ClientCommonConfig,
[]v1.ProxyConfigurer,
[]v1.VisitorConfigurer,
@@ -196,7 +209,7 @@ func LoadClientConfig(path string) (
isLegacyFormat = true
} else {
allCfg := v1.ClientConfig{}
- if err := LoadConfigureFromFile(path, &allCfg); err != nil {
+ if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
return nil, nil, nil, false, err
}
cliCfg = &allCfg.ClientCommonConfig
@@ -211,7 +224,7 @@ func LoadClientConfig(path string) (
// Load additional config from includes.
// legacy ini format alredy handle this in ParseClientConfig.
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
- extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat)
+ extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
if err != nil {
return nil, nil, nil, isLegacyFormat, err
}
@@ -242,7 +255,7 @@ func LoadClientConfig(path string) (
return cliCfg, pxyCfgs, 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)
visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, path := range paths {
@@ -265,7 +278,7 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.Prox
if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
// support yaml/json/toml
cfg := v1.ClientConfig{}
- if err := LoadConfigureFromFile(absFile, &cfg); err != nil {
+ if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil {
return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
}
for _, c := range cfg.Proxies {
diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go
index eab4ba96b7d..876d53e4a3a 100644
--- a/pkg/config/load_test.go
+++ b/pkg/config/load_test.go
@@ -15,6 +15,7 @@
package config
import (
+ "strings"
"testing"
"github.com/stretchr/testify/require"
@@ -22,9 +23,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
)
-func TestLoadConfigure(t *testing.T) {
- require := require.New(t)
- content := `
+const tomlServerContent = `
bindAddr = "127.0.0.1"
kcpBindPort = 7000
quicBindPort = 7001
@@ -33,13 +32,60 @@ custom404Page = "/abc.html"
transport.tcpKeepalive = 10
`
- svrCfg := v1.ServerConfig{}
- err := LoadConfigure([]byte(content), &svrCfg)
- require.NoError(err)
- require.EqualValues("127.0.0.1", svrCfg.BindAddr)
- require.EqualValues(7000, svrCfg.KCPBindPort)
- require.EqualValues(7001, svrCfg.QUICBindPort)
- require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
- require.EqualValues("/abc.html", svrCfg.Custom404Page)
- require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+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) {
+ for _, content := range []string{tomlServerContent, yamlServerContent, jsonServerContent} {
+ svrCfg := v1.ServerConfig{}
+ err := LoadConfigure([]byte(content), &svrCfg, true)
+ require := require.New(t)
+ require.NoError(err)
+ require.EqualValues("127.0.0.1", svrCfg.BindAddr)
+ require.EqualValues(7000, svrCfg.KCPBindPort)
+ require.EqualValues(7001, svrCfg.QUICBindPort)
+ require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
+ require.EqualValues("/abc.html", svrCfg.Custom404Page)
+ require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+ }
+}
+
+// Test that loading in strict mode fails when the config is invalid.
+func TestLoadServerConfigErrorMode(t *testing.T) {
+ for strict := range []bool{false, true} {
+ for _, content := range []string{tomlServerContent, yamlServerContent, jsonServerContent} {
+ // Break the content with an innocent typo
+ brokenContent := strings.Replace(content, "bindAddr", "bindAdur", 1)
+ svrCfg := v1.ServerConfig{}
+ err := LoadConfigure([]byte(brokenContent), &svrCfg, strict == 1)
+ require := require.New(t)
+ if strict == 1 {
+ require.ErrorContains(err, "bindAdur")
+ } else {
+ require.NoError(err)
+ // BindAddr didn't get parsed because of the typo.
+ require.EqualValues("", svrCfg.BindAddr)
+ }
+ }
+ }
}
From 526e809bd50eed4ff5c2211adc91126abb864530 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Thu, 16 Nov 2023 21:03:36 +0800
Subject: [PATCH 06/21] update for strict config (#3779)
---
Release.md | 4 ++
client/admin_api.go | 9 +++-
client/service.go | 17 +++-----
cmd/frpc/sub/admin.go | 4 +-
cmd/frpc/sub/nathole.go | 2 +-
cmd/frpc/sub/proxy.go | 4 +-
cmd/frpc/sub/root.go | 21 ++++------
cmd/frpc/sub/verify.go | 2 +-
cmd/frps/root.go | 10 ++---
cmd/frps/verify.go | 2 +-
pkg/config/load.go | 3 +-
pkg/config/load_test.go | 74 +++++++++++++++++++++------------
pkg/sdk/client/client.go | 13 +++++-
test/e2e/legacy/basic/client.go | 2 +-
test/e2e/v1/basic/client.go | 2 +-
15 files changed, 99 insertions(+), 70 deletions(-)
diff --git a/Release.md b/Release.md
index a834392cf7f..b4245189c41 100644
--- a/Release.md
+++ b/Release.md
@@ -1,3 +1,7 @@
+### Features
+
+* New command line parameter `--strict_config` is added to enable strict configuration validation mode. It will throw an error for non-existent fields instead of ignoring them.
+
### Fixes
* frpc: Return code 1 when the first login attempt fails and exits.
diff --git a/client/admin_api.go b/client/admin_api.go
index 84db31c5fdd..3a56a99fbaa 100644
--- a/client/admin_api.go
+++ b/client/admin_api.go
@@ -45,8 +45,13 @@ func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
}
// GET /api/reload
-func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
+func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
+ strictConfigMode := false
+ strictStr := r.URL.Query().Get("strictConfig")
+ if strictStr != "" {
+ strictConfigMode, _ = strconv.ParseBool(strictStr)
+ }
log.Info("api request [/api/reload]")
defer func() {
@@ -57,7 +62,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) {
}
}()
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile, svr.strictConfig)
+ cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile, strictConfigMode)
if err != nil {
res.Code = 400
res.Msg = err.Error()
diff --git a/client/service.go b/client/service.go
index 0a25ae08635..66a642c1bf0 100644
--- a/client/service.go
+++ b/client/service.go
@@ -70,9 +70,6 @@ type Service struct {
// string if no configuration file was used.
cfgFile string
- // Whether strict configuration parsing had been requested.
- strictConfig bool
-
// service context
ctx context.Context
// call cancel to stop service
@@ -85,16 +82,14 @@ func NewService(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
- strictConfig bool,
) *Service {
return &Service{
- authSetter: auth.NewAuthSetter(cfg.Auth),
- cfg: cfg,
- cfgFile: cfgFile,
- strictConfig: strictConfig,
- pxyCfgs: pxyCfgs,
- visitorCfgs: visitorCfgs,
- ctx: context.Background(),
+ authSetter: auth.NewAuthSetter(cfg.Auth),
+ cfg: cfg,
+ cfgFile: cfgFile,
+ pxyCfgs: pxyCfgs,
+ visitorCfgs: visitorCfgs,
+ ctx: context.Background(),
}
}
diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go
index d98b4d3e0af..5d478d44a58 100644
--- a/cmd/frpc/sub/admin.go
+++ b/cmd/frpc/sub/admin.go
@@ -52,7 +52,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
Use: name,
Short: short,
Run: func(cmd *cobra.Command, args []string) {
- cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfig)
+ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -73,7 +73,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
- if err := client.Reload(); err != nil {
+ if err := client.Reload(strictConfigMode); err != nil {
return err
}
fmt.Println("reload success")
diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go
index eafea27e05f..56fcf67b72a 100644
--- a/cmd/frpc/sub/nathole.go
+++ b/cmd/frpc/sub/nathole.go
@@ -48,7 +48,7 @@ var natholeDiscoveryCmd = &cobra.Command{
Short: "Discover nathole information from stun server",
RunE: func(cmd *cobra.Command, args []string) error {
// ignore error here, because we can use command line pameters
- cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfig)
+ cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
cfg = &v1.ClientCommonConfig{}
}
diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go
index 41c20bc21ab..7ae8d353b39 100644
--- a/cmd/frpc/sub/proxy.go
+++ b/cmd/frpc/sub/proxy.go
@@ -84,7 +84,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
fmt.Println(err)
os.Exit(1)
}
- err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "", strictConfig)
+ err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -110,7 +110,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
fmt.Println(err)
os.Exit(1)
}
- err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "", strictConfig)
+ err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index 855c7abb81e..c4a5acb6f66 100644
--- a/cmd/frpc/sub/root.go
+++ b/cmd/frpc/sub/root.go
@@ -36,17 +36,17 @@ import (
)
var (
- cfgFile string
- cfgDir string
- showVersion bool
- strictConfig bool
+ cfgFile string
+ cfgDir string
+ showVersion bool
+ strictConfigMode bool
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
- rootCmd.PersistentFlags().BoolVarP(&strictConfig, "strict_config", "", false, "strict config parsing mode")
+ rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fields will cause an error")
}
var rootCmd = &cobra.Command{
@@ -110,7 +110,7 @@ func handleTermSignal(svr *client.Service) {
}
func runClient(cfgFilePath string) error {
- cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfig)
+ cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
if err != nil {
return err
}
@@ -122,14 +122,11 @@ func runClient(cfgFilePath string) error {
warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
- if strictConfig {
- return fmt.Errorf("warning: %v", warning)
- }
}
if err != nil {
return err
}
- return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath, strictConfig)
+ return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}
func startService(
@@ -137,7 +134,6 @@ func startService(
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
- strictConfig bool,
) error {
log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor)
@@ -145,13 +141,12 @@ func startService(
log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
}
- svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile, strictConfig)
+ svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
if shouldGracefulClose {
go handleTermSignal(svr)
}
-
return svr.Run(context.Background())
}
diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go
index 0e6adca851f..1b6ac5a7a6c 100644
--- a/cmd/frpc/sub/verify.go
+++ b/cmd/frpc/sub/verify.go
@@ -37,7 +37,7 @@ var verifyCmd = &cobra.Command{
return nil
}
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfig)
+ cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frps/root.go b/cmd/frps/root.go
index adb8852e136..1fa57d95d7c 100644
--- a/cmd/frps/root.go
+++ b/cmd/frps/root.go
@@ -30,9 +30,9 @@ import (
)
var (
- cfgFile string
- showVersion bool
- strictConfig bool
+ cfgFile string
+ showVersion bool
+ strictConfigMode bool
serverCfg v1.ServerConfig
)
@@ -40,7 +40,7 @@ var (
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
- rootCmd.PersistentFlags().BoolVarP(&strictConfig, "strict_config", "", false, "strict config parsing mode")
+ rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fileds will cause error")
RegisterServerConfigFlags(rootCmd, &serverCfg)
}
@@ -60,7 +60,7 @@ var rootCmd = &cobra.Command{
err error
)
if cfgFile != "" {
- svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfig)
+ svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go
index 838ac7b65b3..33ad3f63229 100644
--- a/cmd/frps/verify.go
+++ b/cmd/frps/verify.go
@@ -36,7 +36,7 @@ var verifyCmd = &cobra.Command{
fmt.Println("frps: the configuration file is not specified")
return nil
}
- svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfig)
+ svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
if err != nil {
fmt.Println(err)
os.Exit(1)
diff --git a/pkg/config/load.go b/pkg/config/load.go
index a4013c3247a..41d1a231dd4 100644
--- a/pkg/config/load.go
+++ b/pkg/config/load.go
@@ -27,7 +27,7 @@ import (
"github.com/samber/lo"
"gopkg.in/ini.v1"
"k8s.io/apimachinery/pkg/util/sets"
- yaml "k8s.io/apimachinery/pkg/util/yaml"
+ "k8s.io/apimachinery/pkg/util/yaml"
"github.com/fatedier/frp/pkg/config/legacy"
v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -113,7 +113,6 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
func LoadConfigure(b []byte, c any, strict bool) error {
var tomlObj interface{}
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
- // TODO: caller should probably be able to specify the format, so we don't need to swallow errors.
if err := toml.Unmarshal(b, &tomlObj); err == nil {
b, err = json.Marshal(&tomlObj)
if err != nil {
diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go
index 876d53e4a3a..9bf7dbbc5be 100644
--- a/pkg/config/load_test.go
+++ b/pkg/config/load_test.go
@@ -15,6 +15,7 @@
package config
import (
+ "fmt"
"strings"
"testing"
@@ -56,36 +57,57 @@ const jsonServerContent = `
`
func TestLoadServerConfig(t *testing.T) {
- for _, content := range []string{tomlServerContent, yamlServerContent, jsonServerContent} {
- svrCfg := v1.ServerConfig{}
- err := LoadConfigure([]byte(content), &svrCfg, true)
- require := require.New(t)
- require.NoError(err)
- require.EqualValues("127.0.0.1", svrCfg.BindAddr)
- require.EqualValues(7000, svrCfg.KCPBindPort)
- require.EqualValues(7001, svrCfg.QUICBindPort)
- require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
- require.EqualValues("/abc.html", svrCfg.Custom404Page)
- require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+ tests := []struct {
+ name string
+ content string
+ }{
+ {"toml", tomlServerContent},
+ {"yaml", yamlServerContent},
+ {"json", jsonServerContent},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require := require.New(t)
+ svrCfg := v1.ServerConfig{}
+ err := LoadConfigure([]byte(test.content), &svrCfg, true)
+ require.NoError(err)
+ require.EqualValues("127.0.0.1", svrCfg.BindAddr)
+ require.EqualValues(7000, svrCfg.KCPBindPort)
+ require.EqualValues(7001, svrCfg.QUICBindPort)
+ require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
+ require.EqualValues("/abc.html", svrCfg.Custom404Page)
+ require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
+ })
}
}
// Test that loading in strict mode fails when the config is invalid.
-func TestLoadServerConfigErrorMode(t *testing.T) {
- for strict := range []bool{false, true} {
- for _, content := range []string{tomlServerContent, yamlServerContent, jsonServerContent} {
- // Break the content with an innocent typo
- brokenContent := strings.Replace(content, "bindAddr", "bindAdur", 1)
- svrCfg := v1.ServerConfig{}
- err := LoadConfigure([]byte(brokenContent), &svrCfg, strict == 1)
- require := require.New(t)
- if strict == 1 {
- require.ErrorContains(err, "bindAdur")
- } else {
- require.NoError(err)
- // BindAddr didn't get parsed because of the typo.
- require.EqualValues("", svrCfg.BindAddr)
- }
+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)
+ }
+ })
}
}
}
diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go
index c9657905f24..395063e5029 100644
--- a/pkg/sdk/client/client.go
+++ b/pkg/sdk/client/client.go
@@ -6,6 +6,7 @@ import (
"io"
"net"
"net/http"
+ "net/url"
"strconv"
"strings"
@@ -69,8 +70,16 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) {
return allStatus, nil
}
-func (c *Client) Reload() error {
- req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil)
+func (c *Client) Reload(strictMode bool) error {
+ v := url.Values{}
+ if strictMode {
+ v.Set("strictConfig", "true")
+ }
+ queryStr := ""
+ if len(v) > 0 {
+ queryStr = "?" + v.Encode()
+ }
+ req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload"+queryStr, nil)
if err != nil {
return err
}
diff --git a/test/e2e/legacy/basic/client.go b/test/e2e/legacy/basic/client.go
index da23db9c1fe..d4862e529dc 100644
--- a/test/e2e/legacy/basic/client.go
+++ b/test/e2e/legacy/basic/client.go
@@ -69,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
err = client.UpdateConfig(newClientConf)
framework.ExpectNoError(err)
- err = client.Reload()
+ err = client.Reload(true)
framework.ExpectNoError(err)
time.Sleep(time.Second)
diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go
index 25b99424101..b0b258db308 100644
--- a/test/e2e/v1/basic/client.go
+++ b/test/e2e/v1/basic/client.go
@@ -72,7 +72,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
err = client.UpdateConfig(newClientConf)
framework.ExpectNoError(err)
- err = client.Reload()
+ err = client.Reload(true)
framework.ExpectNoError(err)
time.Sleep(time.Second)
From f5d5a00eefbfc060046a8ff04ad3a116334ef227 Mon Sep 17 00:00:00 2001
From: Aarni Koskela
Date: Wed, 22 Nov 2023 08:30:22 +0200
Subject: [PATCH 07/21] Fix various typos (#3783)
---
client/proxy/udp.go | 2 +-
client/visitor/visitor.go | 2 +-
cmd/frps/root.go | 2 +-
conf/frpc_full_example.toml | 2 +-
conf/legacy/frpc_legacy_full.ini | 2 +-
pkg/config/legacy/client.go | 2 +-
pkg/config/legacy/server.go | 2 +-
pkg/config/load.go | 2 +-
pkg/config/v1/client.go | 2 +-
pkg/config/v1/common.go | 2 +-
pkg/config/v1/server.go | 2 +-
pkg/transport/message.go | 4 ++--
pkg/util/net/http.go | 14 +++++++-------
pkg/util/version/version_test.go | 4 ++--
pkg/util/vhost/http.go | 2 +-
server/service.go | 2 +-
test/e2e/framework/framework.go | 12 ++++++------
test/e2e/legacy/plugin/server.go | 2 +-
test/e2e/v1/plugin/server.go | 2 +-
19 files changed, 32 insertions(+), 32 deletions(-)
diff --git a/client/proxy/udp.go b/client/proxy/udp.go
index 0a5cefcc291..d7a790c146d 100644
--- a/client/proxy/udp.go
+++ b/client/proxy/udp.go
@@ -89,7 +89,7 @@ func (pxy *UDPProxy) Close() {
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
xl := pxy.xl
xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
- // close resources releated with old workConn
+ // close resources related with old workConn
pxy.Close()
var rwc io.ReadWriteCloser = conn
diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go
index dcd1f7b3047..4cfd61062b0 100644
--- a/client/visitor/visitor.go
+++ b/client/visitor/visitor.go
@@ -25,7 +25,7 @@ import (
"github.com/fatedier/frp/pkg/util/xlog"
)
-// Helper wrapps some functions for visitor to use.
+// Helper wraps some functions for visitor to use.
type Helper interface {
// ConnectServer directly connects to the frp server.
ConnectServer() (net.Conn, error)
diff --git a/cmd/frps/root.go b/cmd/frps/root.go
index 1fa57d95d7c..5f32fe9c353 100644
--- a/cmd/frps/root.go
+++ b/cmd/frps/root.go
@@ -40,7 +40,7 @@ var (
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
- rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fileds will cause error")
+ rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", false, "strict config parsing mode, unknown fields will cause error")
RegisterServerConfigFlags(rootCmd, &serverCfg)
}
diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml
index bdfc5643031..247d0a6af4f 100644
--- a/conf/frpc_full_example.toml
+++ b/conf/frpc_full_example.toml
@@ -38,7 +38,7 @@ auth.token = "12345678"
# auth.oidc.clientSecret = ""
# oidc.audience specifies the audience of the token in OIDC authentication.
# auth.oidc.audience = ""
-# oidc.scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
+# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
# auth.oidc.scope = ""
# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token.
diff --git a/conf/legacy/frpc_legacy_full.ini b/conf/legacy/frpc_legacy_full.ini
index f8eca6b774c..51ac9c47725 100644
--- a/conf/legacy/frpc_legacy_full.ini
+++ b/conf/legacy/frpc_legacy_full.ini
@@ -56,7 +56,7 @@ oidc_client_secret =
# oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_audience =
-# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
+# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_scope =
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.
diff --git a/pkg/config/legacy/client.go b/pkg/config/legacy/client.go
index f7257cb55f2..50f62bef1bc 100644
--- a/pkg/config/legacy/client.go
+++ b/pkg/config/legacy/client.go
@@ -99,7 +99,7 @@ type ClientCommonConf struct {
// the server must have TCP multiplexing enabled as well. By default, this
// value is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
- // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+ // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// User specifies a prefix for proxy names to distinguish them from other
diff --git a/pkg/config/legacy/server.go b/pkg/config/legacy/server.go
index 797770a3a94..1279a499057 100644
--- a/pkg/config/legacy/server.go
+++ b/pkg/config/legacy/server.go
@@ -139,7 +139,7 @@ type ServerCommonConf struct {
// from a client to share a single TCP connection. By default, this value
// is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
- // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+ // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
diff --git a/pkg/config/load.go b/pkg/config/load.go
index 41d1a231dd4..3014eb35a46 100644
--- a/pkg/config/load.go
+++ b/pkg/config/load.go
@@ -221,7 +221,7 @@ func LoadClientConfig(path string, strict bool) (
}
// Load additional config from includes.
- // legacy ini format alredy handle this in ParseClientConfig.
+ // legacy ini format already handle this in ParseClientConfig.
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
if err != nil {
diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go
index 9029aa73f40..52b876905d9 100644
--- a/pkg/config/v1/client.go
+++ b/pkg/config/v1/client.go
@@ -111,7 +111,7 @@ type ClientTransportConfig struct {
// the server must have TCP multiplexing enabled as well. By default, this
// value is true.
TCPMux *bool `json:"tcpMux,omitempty"`
- // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+ // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// QUIC protocol options.
diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go
index 422a8082ede..72c9d0362c9 100644
--- a/pkg/config/v1/common.go
+++ b/pkg/config/v1/common.go
@@ -83,7 +83,7 @@ type TLSConfig struct {
}
type LogConfig struct {
- // This is destination where frp should wirte the logs.
+ // This is destination where frp should write the logs.
// If "console" is used, logs will be printed to stdout, otherwise,
// logs will be written to the specified file.
// By default, this value is "console".
diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go
index c42c3ecaaac..e49921e9ce7 100644
--- a/pkg/config/v1/server.go
+++ b/pkg/config/v1/server.go
@@ -152,7 +152,7 @@ type ServerTransportConfig struct {
// is true.
// $HideFromDoc
TCPMux *bool `json:"tcpMux,omitempty"`
- // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+ // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
diff --git a/pkg/transport/message.go b/pkg/transport/message.go
index 7163a8adcb4..dd43fbdc0ed 100644
--- a/pkg/transport/message.go
+++ b/pkg/transport/message.go
@@ -29,7 +29,7 @@ type MessageTransporter interface {
// Recv(ctx context.Context, laneKey string, msgType string) (Message, error)
// Do will first send msg, then recv msg with the same laneKey and specified msgType.
Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error)
- // Dispatch will dispatch message to releated channel registered in Do function by its message type and laneKey.
+ // Dispatch will dispatch message to related channel registered in Do function by its message type and laneKey.
Dispatch(m msg.Message, laneKey string) bool
// Same with Dispatch but with specified message type.
DispatchWithType(m msg.Message, msgType, laneKey string) bool
@@ -46,7 +46,7 @@ type transporterImpl struct {
sendCh chan msg.Message
// First key is message type and second key is lane key.
- // Dispatch will dispatch message to releated channel by its message type
+ // Dispatch will dispatch message to related channel by its message type
// and lane key.
registry map[string]map[string]chan msg.Message
mu sync.RWMutex
diff --git a/pkg/util/net/http.go b/pkg/util/net/http.go
index 1a7da23f72f..642d15901e3 100644
--- a/pkg/util/net/http.go
+++ b/pkg/util/net/http.go
@@ -24,21 +24,21 @@ import (
"github.com/fatedier/frp/pkg/util/util"
)
-type HTTPAuthWraper struct {
+type HTTPAuthWrapper struct {
h http.Handler
user string
passwd string
}
-func NewHTTPBasicAuthWraper(h http.Handler, user, passwd string) http.Handler {
- return &HTTPAuthWraper{
+func NewHTTPBasicAuthWrapper(h http.Handler, user, passwd string) http.Handler {
+ return &HTTPAuthWrapper{
h: h,
user: user,
passwd: passwd,
}
}
-func (aw *HTTPAuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (aw *HTTPAuthWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, passwd, hasAuth := r.BasicAuth()
if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
aw.h.ServeHTTP(w, r)
@@ -83,11 +83,11 @@ func (authMid *HTTPAuthMiddleware) Middleware(next http.Handler) http.Handler {
})
}
-type HTTPGzipWraper struct {
+type HTTPGzipWrapper struct {
h http.Handler
}
-func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (gw *HTTPGzipWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gw.h.ServeHTTP(w, r)
return
@@ -100,7 +100,7 @@ func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func MakeHTTPGzipHandler(h http.Handler) http.Handler {
- return &HTTPGzipWraper{
+ return &HTTPGzipWrapper{
h: h,
}
}
diff --git a/pkg/util/version/version_test.go b/pkg/util/version/version_test.go
index 73b96a85f79..2b4077cf33b 100644
--- a/pkg/util/version/version_test.go
+++ b/pkg/util/version/version_test.go
@@ -47,7 +47,7 @@ func TestVersion(t *testing.T) {
proto := Proto(Full())
major := Major(Full())
minor := Minor(Full())
- parseVerion := fmt.Sprintf("%d.%d.%d", proto, major, minor)
+ parseVersion := fmt.Sprintf("%d.%d.%d", proto, major, minor)
version := Full()
- assert.Equal(parseVerion, version)
+ assert.Equal(parseVersion, version)
}
diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go
index 7b914ce9b74..1a5bea0b4a2 100644
--- a/pkg/util/vhost/http.go
+++ b/pkg/util/vhost/http.go
@@ -188,7 +188,7 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, p
return true
}
-// getVhost trys to get vhost router by route policy.
+// getVhost tries to get vhost router by route policy.
func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser)
diff --git a/server/service.go b/server/service.go
index 2629b345011..7478b97bd8f 100644
--- a/server/service.go
+++ b/server/service.go
@@ -543,7 +543,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error
if err != nil {
xl.Warn("create new controller error: %v", err)
// don't return detailed errors to client
- return fmt.Errorf("unexpect error when creating new controller")
+ return fmt.Errorf("unexpected error when creating new controller")
}
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
oldCtl.WaitClosed()
diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go
index 6a7a655f266..f8b8aa03389 100644
--- a/test/e2e/framework/framework.go
+++ b/test/e2e/framework/framework.go
@@ -29,8 +29,8 @@ type Framework struct {
// ports used in this framework indexed by port name.
usedPorts map[string]int
- // record ports alloced by this framework and release them after each test
- allocedPorts []int
+ // record ports allocated by this framework and release them after each test
+ allocatedPorts []int
// portAllocator to alloc port for this test case.
portAllocator *port.Allocator
@@ -153,11 +153,11 @@ func (f *Framework) AfterEach() {
}
f.usedPorts = make(map[string]int)
- // release alloced ports
- for _, port := range f.allocedPorts {
+ // release allocated ports
+ for _, port := range f.allocatedPorts {
f.portAllocator.Release(port)
}
- f.allocedPorts = make([]int, 0)
+ f.allocatedPorts = make([]int, 0)
// clear os envs
f.osEnvs = make([]string, 0)
@@ -237,7 +237,7 @@ func (f *Framework) PortByName(name string) int {
func (f *Framework) AllocPort() int {
port := f.portAllocator.Get()
ExpectTrue(port > 0, "alloc port failed")
- f.allocedPorts = append(f.allocedPorts, port)
+ f.allocatedPorts = append(f.allocatedPorts, port)
return port
}
diff --git a/test/e2e/legacy/plugin/server.go b/test/e2e/legacy/plugin/server.go
index 3f14a42dcf6..cf600be2ee0 100644
--- a/test/e2e/legacy/plugin/server.go
+++ b/test/e2e/legacy/plugin/server.go
@@ -124,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
framework.NewRequestExpect(f).Port(remotePort).Ensure()
})
- ginkgo.It("Mofify RemotePort", func() {
+ ginkgo.It("Modify RemotePort", func() {
localPort := f.AllocPort()
remotePort := f.AllocPort()
handler := func(req *plugin.Request) *plugin.Response {
diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go
index 66456f57f95..b043c57f13f 100644
--- a/test/e2e/v1/plugin/server.go
+++ b/test/e2e/v1/plugin/server.go
@@ -129,7 +129,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() {
framework.NewRequestExpect(f).Port(remotePort).Ensure()
})
- ginkgo.It("Mofify RemotePort", func() {
+ ginkgo.It("Modify RemotePort", func() {
localPort := f.AllocPort()
remotePort := f.AllocPort()
handler := func(req *plugin.Request) *plugin.Response {
From 8b432e179d6789838c22da78014da08c7c81616a Mon Sep 17 00:00:00 2001
From: 0x7fff <4812302+blizard863@users.noreply.github.com>
Date: Tue, 14 Nov 2023 15:16:24 +0800
Subject: [PATCH 08/21] feat: ssh client implement (#3671)
* feat: frps support ssh
* fix: comments
* fix: update pkg
* fix: remove useless change
---------
Co-authored-by: int7
---
go.mod | 6 +-
go.sum | 13 +-
pkg/config/v1/server.go | 13 ++
pkg/config/v1/ssh.go | 72 ++++++
pkg/ssh/service.go | 497 ++++++++++++++++++++++++++++++++++++++++
pkg/ssh/vclient.go | 185 +++++++++++++++
server/proxy/proxy.go | 9 +-
server/service.go | 124 ++++++++++
8 files changed, 909 insertions(+), 10 deletions(-)
create mode 100644 pkg/config/v1/ssh.go
create mode 100644 pkg/ssh/service.go
create mode 100644 pkg/ssh/vclient.go
diff --git a/go.mod b/go.mod
index 8d27e522871..8d0055e6069 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
+ golang.org/x/crypto v0.15.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
@@ -64,11 +65,10 @@ require (
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
- golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.10.0 // indirect
- golang.org/x/sys v0.13.0 // indirect
- golang.org/x/text v0.13.0 // indirect
+ golang.org/x/sys v0.14.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.9.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
diff --git a/go.sum b/go.sum
index af509c3f22f..49cef0b2cc4 100644
--- a/go.sum
+++ b/go.sum
@@ -157,8 +157,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
+golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -210,20 +210,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
+golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go
index e49921e9ce7..f562be8e12f 100644
--- a/pkg/config/v1/server.go
+++ b/pkg/config/v1/server.go
@@ -16,11 +16,21 @@ package v1
import (
"github.com/samber/lo"
+ "golang.org/x/crypto/ssh"
"github.com/fatedier/frp/pkg/config/types"
"github.com/fatedier/frp/pkg/util/util"
)
+type SSHTunnelGateway struct {
+ BindPort int `json:"bindPort,omitempty" validate:"gte=0,lte=65535"`
+ PrivateKeyFilePath string `json:"privateKeyFilePath,omitempty"`
+ PublicKeyFilesPath string `json:"publicKeyFilesPath,omitempty"`
+
+ // store all public key file. load all when init
+ PublicKeyFilesMap map[string]ssh.PublicKey
+}
+
type ServerConfig struct {
APIMetadata
@@ -31,6 +41,9 @@ type ServerConfig struct {
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `json:"bindPort,omitempty"`
+
+ SSHTunnelGateway SSHTunnelGateway `json:"sshGatewayConfig,omitempty"`
+
// KCPBindPort specifies the KCP port that the server listens on. If this
// value is 0, the server will not listen for KCP connections.
KCPBindPort int `json:"kcpBindPort,omitempty"`
diff --git a/pkg/config/v1/ssh.go b/pkg/config/v1/ssh.go
new file mode 100644
index 00000000000..440305d4fe3
--- /dev/null
+++ b/pkg/config/v1/ssh.go
@@ -0,0 +1,72 @@
+package v1
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "os"
+ "path/filepath"
+
+ "golang.org/x/crypto/ssh"
+)
+
+const (
+ // custom define
+ SSHClientLoginUserPrefix = "_frpc_ssh_client_"
+)
+
+// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
+func GeneratePrivateKey() ([]byte, error) {
+ privateKey, err := generatePrivateKey()
+ if err != nil {
+ return nil, errors.New("gen private key error")
+ }
+
+ privBlock := pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Headers: nil,
+ Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
+ }
+
+ return pem.EncodeToMemory(&privBlock), nil
+}
+
+// generatePrivateKey creates a RSA Private Key of specified byte size
+func generatePrivateKey() (*rsa.PrivateKey, error) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
+ if err != nil {
+ return nil, err
+ }
+
+ err = privateKey.Validate()
+ if err != nil {
+ return nil, err
+ }
+ return privateKey, nil
+}
+
+func LoadSSHPublicKeyFilesInDir(dirPath string) (map[string]ssh.PublicKey, error) {
+ fileMap := make(map[string]ssh.PublicKey)
+ files, err := os.ReadDir(dirPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, file := range files {
+ filePath := filepath.Join(dirPath, file.Name())
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+
+ parsedAuthorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(content)
+ if err != nil {
+ continue
+ }
+ fileMap[ssh.FingerprintSHA256(parsedAuthorizedKey)] = parsedAuthorizedKey
+ }
+
+ return fileMap, nil
+}
diff --git a/pkg/ssh/service.go b/pkg/ssh/service.go
new file mode 100644
index 00000000000..ce0bc52c931
--- /dev/null
+++ b/pkg/ssh/service.go
@@ -0,0 +1,497 @@
+package ssh
+
+import (
+ "encoding/binary"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ gerror "github.com/fatedier/golib/errors"
+ "golang.org/x/crypto/ssh"
+
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/util/log"
+)
+
+const (
+ // ssh protocol define
+ // https://datatracker.ietf.org/doc/html/rfc4254#page-16
+ ChannelTypeServerOpenChannel = "forwarded-tcpip"
+ RequestTypeForward = "tcpip-forward"
+
+ // golang ssh package define.
+ // https://pkg.go.dev/golang.org/x/crypto/ssh
+ RequestTypeHeartbeat = "keepalive@openssh.com"
+)
+
+// 当 proxy 失败会返回该错误
+type VProxyError struct{}
+
+// ssh protocol define
+// https://datatracker.ietf.org/doc/html/rfc4254#page-16
+// parse ssh client cmds input
+type forwardedTCPPayload struct {
+ Addr string
+ Port uint32
+
+ // can be default empty value but do not delete it
+ // because ssh protocol shoule be reserved
+ OriginAddr string
+ OriginPort uint32
+}
+
+// custom define
+// parse ssh client cmds input
+type CmdPayload struct {
+ Address string
+ Port uint32
+}
+
+// custom define
+// with frp control cmds
+type ExtraPayload struct {
+ Type string
+
+ // TODO port can be set by extra message and priority to ssh raw cmd
+ Address string
+ Port uint32
+}
+
+type Service struct {
+ tcpConn net.Conn
+ cfg *ssh.ServerConfig
+
+ sshConn *ssh.ServerConn
+ gChannel <-chan ssh.NewChannel
+ gReq <-chan *ssh.Request
+
+ addrPayloadCh chan CmdPayload
+ extraPayloadCh chan ExtraPayload
+
+ proxyPayloadCh chan v1.ProxyConfigurer
+ replyCh chan interface{}
+
+ closeCh chan struct{}
+ exit int32
+}
+
+func NewSSHService(
+ tcpConn net.Conn,
+ cfg *ssh.ServerConfig,
+ proxyPayloadCh chan v1.ProxyConfigurer,
+ replyCh chan interface{},
+) (ss *Service, err error) {
+ ss = &Service{
+ tcpConn: tcpConn,
+ cfg: cfg,
+
+ addrPayloadCh: make(chan CmdPayload),
+ extraPayloadCh: make(chan ExtraPayload),
+
+ proxyPayloadCh: proxyPayloadCh,
+ replyCh: replyCh,
+
+ closeCh: make(chan struct{}),
+ exit: 0,
+ }
+
+ ss.sshConn, ss.gChannel, ss.gReq, err = ssh.NewServerConn(tcpConn, cfg)
+ if err != nil {
+ log.Error("ssh handshake error: %v", err)
+ return nil, err
+ }
+
+ log.Info("ssh connection success")
+
+ return ss, nil
+}
+
+func (ss *Service) Run() {
+ go ss.loopGenerateProxy()
+ go ss.loopParseCmdPayload()
+ go ss.loopParseExtraPayload()
+ go ss.loopReply()
+}
+
+func (ss *Service) Exit() <-chan struct{} {
+ return ss.closeCh
+}
+
+func (ss *Service) Close() {
+ if atomic.LoadInt32(&ss.exit) == 1 {
+ return
+ }
+
+ select {
+ case <-ss.closeCh:
+ return
+ default:
+ }
+
+ close(ss.closeCh)
+ close(ss.addrPayloadCh)
+ close(ss.extraPayloadCh)
+
+ _ = ss.sshConn.Wait()
+
+ ss.sshConn.Close()
+ ss.tcpConn.Close()
+
+ atomic.StoreInt32(&ss.exit, 1)
+
+ log.Info("ssh service close")
+}
+
+func (ss *Service) loopParseCmdPayload() {
+ for {
+ select {
+ case req, ok := <-ss.gReq:
+ if !ok {
+ log.Info("global request is close")
+ ss.Close()
+ return
+ }
+
+ switch req.Type {
+ case RequestTypeForward:
+ var addrPayload CmdPayload
+ if err := ssh.Unmarshal(req.Payload, &addrPayload); err != nil {
+ log.Error("ssh unmarshal error: %v", err)
+ return
+ }
+ _ = gerror.PanicToError(func() {
+ ss.addrPayloadCh <- addrPayload
+ })
+ default:
+ if req.Type == RequestTypeHeartbeat {
+ log.Debug("ssh heartbeat data")
+ } else {
+ log.Info("default req, data: %v", req)
+ }
+ }
+ if req.WantReply {
+ err := req.Reply(true, nil)
+ if err != nil {
+ log.Error("reply to ssh client error: %v", err)
+ }
+ }
+ case <-ss.closeCh:
+ log.Info("loop parse cmd payload close")
+ return
+ }
+ }
+}
+
+func (ss *Service) loopSendHeartbeat(ch ssh.Channel) {
+ tk := time.NewTicker(time.Second * 60)
+ defer tk.Stop()
+
+ for {
+ select {
+ case <-tk.C:
+ ok, err := ch.SendRequest("heartbeat", false, nil)
+ if err != nil {
+ log.Error("channel send req error: %v", err)
+ if err == io.EOF {
+ ss.Close()
+ return
+ }
+ continue
+ }
+ log.Debug("heartbeat send success, ok: %v", ok)
+ case <-ss.closeCh:
+ return
+ }
+ }
+}
+
+func (ss *Service) loopParseExtraPayload() {
+ log.Info("loop parse extra payload start")
+
+ for newChannel := range ss.gChannel {
+ ch, req, err := newChannel.Accept()
+ if err != nil {
+ log.Error("channel accept error: %v", err)
+ return
+ }
+
+ go ss.loopSendHeartbeat(ch)
+
+ go func(req <-chan *ssh.Request) {
+ for r := range req {
+ if len(r.Payload) <= 4 {
+ log.Info("r.payload is less than 4")
+ continue
+ }
+ if !strings.Contains(string(r.Payload), "tcp") && !strings.Contains(string(r.Payload), "http") {
+ log.Info("ssh protocol exchange data")
+ continue
+ }
+
+ // [4byte data_len|data]
+ end := 4 + binary.BigEndian.Uint32(r.Payload[:4])
+ if end > uint32(len(r.Payload)) {
+ end = uint32(len(r.Payload))
+ }
+ p := string(r.Payload[4:end])
+
+ msg, err := parseSSHExtraMessage(p)
+ if err != nil {
+ log.Error("parse ssh extra message error: %v, payload: %v", err, r.Payload)
+ continue
+ }
+ _ = gerror.PanicToError(func() {
+ ss.extraPayloadCh <- msg
+ })
+ return
+ }
+ }(req)
+ }
+}
+
+func (ss *Service) SSHConn() *ssh.ServerConn {
+ return ss.sshConn
+}
+
+func (ss *Service) TCPConn() net.Conn {
+ return ss.tcpConn
+}
+
+func (ss *Service) loopReply() {
+ for {
+ select {
+ case <-ss.closeCh:
+ log.Info("loop reply close")
+ return
+ case req := <-ss.replyCh:
+ switch req.(type) {
+ case *VProxyError:
+ log.Error("run frp proxy error, close ssh service")
+ ss.Close()
+ default:
+ // TODO
+ }
+ }
+ }
+}
+
+func (ss *Service) loopGenerateProxy() {
+ log.Info("loop generate proxy start")
+
+ for {
+ if atomic.LoadInt32(&ss.exit) == 1 {
+ return
+ }
+
+ wg := new(sync.WaitGroup)
+ wg.Add(2)
+
+ var p1 CmdPayload
+ var p2 ExtraPayload
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case <-ss.closeCh:
+ return
+ case p1 = <-ss.addrPayloadCh:
+ return
+ }
+ }
+ }()
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case <-ss.closeCh:
+ return
+ case p2 = <-ss.extraPayloadCh:
+ return
+ }
+ }
+ }()
+
+ wg.Wait()
+
+ if atomic.LoadInt32(&ss.exit) == 1 {
+ return
+ }
+
+ switch p2.Type {
+ case "http":
+ case "tcp":
+ ss.proxyPayloadCh <- &v1.TCPProxyConfig{
+ ProxyBaseConfig: v1.ProxyBaseConfig{
+ Name: fmt.Sprintf("ssh-proxy-%v-%v", ss.tcpConn.RemoteAddr().String(), time.Now().UnixNano()),
+ Type: p2.Type,
+
+ ProxyBackend: v1.ProxyBackend{
+ LocalIP: p1.Address,
+ },
+ },
+ RemotePort: int(p1.Port),
+ }
+ default:
+ log.Warn("invalid frp proxy type: %v", p2.Type)
+ }
+ }
+}
+
+func parseSSHExtraMessage(s string) (p ExtraPayload, err error) {
+ sn := len(s)
+
+ log.Info("parse ssh extra message: %v", s)
+
+ ss := strings.Fields(s)
+ if len(ss) == 0 {
+ if sn != 0 {
+ ss = append(ss, s)
+ } else {
+ return p, fmt.Errorf("invalid ssh input, args: %v", ss)
+ }
+ }
+
+ for i, v := range ss {
+ ss[i] = strings.TrimSpace(v)
+ }
+
+ if ss[0] != "tcp" && ss[0] != "http" {
+ return p, fmt.Errorf("only support tcp/http now")
+ }
+
+ switch ss[0] {
+ case "tcp":
+ tcpCmd, err := ParseTCPCommand(ss)
+ if err != nil {
+ return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
+ }
+
+ port, _ := strconv.Atoi(tcpCmd.Port)
+
+ p = ExtraPayload{
+ Type: "tcp",
+ Address: tcpCmd.Address,
+ Port: uint32(port),
+ }
+ case "http":
+ httpCmd, err := ParseHTTPCommand(ss)
+ if err != nil {
+ return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
+ }
+
+ _ = httpCmd
+
+ p = ExtraPayload{
+ Type: "http",
+ }
+ }
+
+ return p, nil
+}
+
+type HTTPCommand struct {
+ Domain string
+ BasicAuthUser string
+ BasicAuthPass string
+}
+
+func ParseHTTPCommand(params []string) (*HTTPCommand, error) {
+ if len(params) < 2 {
+ return nil, errors.New("invalid HTTP command")
+ }
+
+ var (
+ basicAuth string
+ domainURL string
+ basicAuthUser string
+ basicAuthPass string
+ )
+
+ fs := flag.NewFlagSet("http", flag.ContinueOnError)
+ fs.StringVar(&basicAuth, "basic-auth", "", "")
+ fs.StringVar(&domainURL, "domain", "", "")
+
+ fs.SetOutput(&nullWriter{}) // Disables usage output
+
+ err := fs.Parse(params[2:])
+ if err != nil {
+ if !errors.Is(err, flag.ErrHelp) {
+ return nil, err
+ }
+ }
+
+ if basicAuth != "" {
+ authParts := strings.SplitN(basicAuth, ":", 2)
+ basicAuthUser = authParts[0]
+ if len(authParts) > 1 {
+ basicAuthPass = authParts[1]
+ }
+ }
+
+ httpCmd := &HTTPCommand{
+ Domain: domainURL,
+ BasicAuthUser: basicAuthUser,
+ BasicAuthPass: basicAuthPass,
+ }
+ return httpCmd, nil
+}
+
+type TCPCommand struct {
+ Address string
+ Port string
+}
+
+func ParseTCPCommand(params []string) (*TCPCommand, error) {
+ if len(params) == 0 || params[0] != "tcp" {
+ return nil, errors.New("invalid TCP command")
+ }
+
+ if len(params) == 1 {
+ return &TCPCommand{}, nil
+ }
+
+ var (
+ address string
+ port string
+ )
+
+ fs := flag.NewFlagSet("tcp", flag.ContinueOnError)
+ fs.StringVar(&address, "address", "", "The IP address to listen on")
+ fs.StringVar(&port, "port", "", "The port to listen on")
+ fs.SetOutput(&nullWriter{}) // Disables usage output
+
+ args := params[1:]
+ err := fs.Parse(args)
+ if err != nil {
+ if !errors.Is(err, flag.ErrHelp) {
+ return nil, err
+ }
+ }
+
+ parsedAddr, err := net.ResolveIPAddr("ip", address)
+ if err != nil {
+ return nil, err
+ }
+ if _, err := net.LookupPort("tcp", port); err != nil {
+ return nil, err
+ }
+
+ tcpCmd := &TCPCommand{
+ Address: parsedAddr.String(),
+ Port: port,
+ }
+ return tcpCmd, nil
+}
+
+type nullWriter struct{}
+
+func (w *nullWriter) Write(p []byte) (n int, err error) { return len(p), nil }
diff --git a/pkg/ssh/vclient.go b/pkg/ssh/vclient.go
new file mode 100644
index 00000000000..e78c82847d9
--- /dev/null
+++ b/pkg/ssh/vclient.go
@@ -0,0 +1,185 @@
+package ssh
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "sync/atomic"
+ "time"
+
+ "golang.org/x/crypto/ssh"
+
+ "github.com/fatedier/frp/pkg/config"
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/msg"
+ plugin "github.com/fatedier/frp/pkg/plugin/server"
+ "github.com/fatedier/frp/pkg/util/log"
+ frp_net "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/server/controller"
+ "github.com/fatedier/frp/server/proxy"
+)
+
+// VirtualService is a client VirtualService run in frps
+type VirtualService struct {
+ clientCfg v1.ClientCommonConfig
+ pxyCfg v1.ProxyConfigurer
+ serverCfg v1.ServerConfig
+
+ sshSvc *Service
+
+ // uniq id got from frps, attach it in loginMsg
+ runID string
+ loginMsg *msg.Login
+
+ // All resource managers and controllers
+ rc *controller.ResourceController
+
+ exit uint32 // 0 means not exit
+ // SSHService context
+ ctx context.Context
+ // call cancel to stop SSHService
+ cancel context.CancelFunc
+
+ replyCh chan interface{}
+ pxy proxy.Proxy
+}
+
+func NewVirtualService(
+ ctx context.Context,
+ clientCfg v1.ClientCommonConfig,
+ serverCfg v1.ServerConfig,
+ logMsg msg.Login,
+ rc *controller.ResourceController,
+ pxyCfg v1.ProxyConfigurer,
+ sshSvc *Service,
+ replyCh chan interface{},
+) (svr *VirtualService, err error) {
+ svr = &VirtualService{
+ clientCfg: clientCfg,
+ serverCfg: serverCfg,
+ rc: rc,
+
+ loginMsg: &logMsg,
+
+ sshSvc: sshSvc,
+ pxyCfg: pxyCfg,
+
+ ctx: ctx,
+ exit: 0,
+
+ replyCh: replyCh,
+ }
+
+ svr.runID, err = util.RandID()
+ if err != nil {
+ return nil, err
+ }
+
+ go svr.loopCheck()
+
+ return
+}
+
+func (svr *VirtualService) Run(ctx context.Context) (err error) {
+ ctx, cancel := context.WithCancel(ctx)
+ svr.ctx = xlog.NewContext(ctx, xlog.New())
+ svr.cancel = cancel
+
+ remoteAddr, err := svr.RegisterProxy(&msg.NewProxy{
+ ProxyName: svr.pxyCfg.(*v1.TCPProxyConfig).Name,
+ ProxyType: svr.pxyCfg.(*v1.TCPProxyConfig).Type,
+ RemotePort: svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort,
+ })
+ if err != nil {
+ return err
+ }
+
+ log.Info("run a reverse proxy on port: %v", remoteAddr)
+
+ return nil
+}
+
+func (svr *VirtualService) Close() {
+ svr.GracefulClose(time.Duration(0))
+}
+
+func (svr *VirtualService) GracefulClose(d time.Duration) {
+ atomic.StoreUint32(&svr.exit, 1)
+ svr.pxy.Close()
+
+ if svr.cancel != nil {
+ svr.cancel()
+ }
+
+ svr.replyCh <- &VProxyError{}
+}
+
+func (svr *VirtualService) loopCheck() {
+ <-svr.sshSvc.Exit()
+ svr.pxy.Close()
+ log.Info("virtual client service close")
+}
+
+func (svr *VirtualService) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
+ var pxyConf v1.ProxyConfigurer
+ pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, &svr.serverCfg)
+ if err != nil {
+ return
+ }
+
+ // User info
+ userInfo := plugin.UserInfo{
+ User: svr.loginMsg.User,
+ Metas: svr.loginMsg.Metas,
+ RunID: svr.runID,
+ }
+
+ svr.pxy, err = proxy.NewProxy(svr.ctx, &proxy.Options{
+ LoginMsg: svr.loginMsg,
+ UserInfo: userInfo,
+ Configurer: pxyConf,
+ ResourceController: svr.rc,
+
+ GetWorkConnFn: svr.GetWorkConn,
+ PoolCount: 10,
+
+ ServerCfg: &svr.serverCfg,
+ })
+ if err != nil {
+ return remoteAddr, err
+ }
+
+ remoteAddr, err = svr.pxy.Run()
+ if err != nil {
+ log.Warn("proxy run error: %v", err)
+ return
+ }
+
+ defer func() {
+ if err != nil {
+ log.Warn("proxy close")
+ svr.pxy.Close()
+ }
+ }()
+
+ return
+}
+
+func (svr *VirtualService) GetWorkConn() (workConn net.Conn, err error) {
+ // tell ssh client open a new stream for work
+ payload := forwardedTCPPayload{
+ Addr: svr.serverCfg.BindAddr, // TODO refine
+ Port: uint32(svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort),
+ }
+
+ channel, reqs, err := svr.sshSvc.SSHConn().OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(payload))
+ if err != nil {
+ return nil, fmt.Errorf("open ssh channel error: %v", err)
+ }
+ go ssh.DiscardRequests(reqs)
+
+ workConn = frp_net.WrapReadWriteCloserToConn(channel, svr.sshSvc.tcpConn)
+ return workConn, nil
+}
diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go
index fe6f781b728..5ea99f1ea6c 100644
--- a/server/proxy/proxy.go
+++ b/server/proxy/proxy.go
@@ -21,6 +21,7 @@ import (
"net"
"reflect"
"strconv"
+ "strings"
"sync"
"time"
@@ -229,8 +230,14 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
return
}
+ var workConn net.Conn
+
// try all connections from the pool
- workConn, err := pxy.GetWorkConnFromPool(userConn.RemoteAddr(), userConn.LocalAddr())
+ if strings.HasPrefix(pxy.GetLoginMsg().User, v1.SSHClientLoginUserPrefix) {
+ workConn, err = pxy.getWorkConnFn()
+ } else {
+ workConn, err = pxy.GetWorkConnFromPool(userConn.RemoteAddr(), userConn.LocalAddr())
+ }
if err != nil {
return
}
diff --git a/server/service.go b/server/service.go
index 7478b97bd8f..2ca501be8e3 100644
--- a/server/service.go
+++ b/server/service.go
@@ -18,10 +18,13 @@ import (
"bytes"
"context"
"crypto/tls"
+ "errors"
"fmt"
"io"
"net"
"net/http"
+ "os"
+ "reflect"
"strconv"
"time"
@@ -29,6 +32,7 @@ import (
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
+ "golang.org/x/crypto/ssh"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth"
@@ -37,6 +41,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
plugin "github.com/fatedier/frp/pkg/plugin/server"
+ frpssh "github.com/fatedier/frp/pkg/ssh"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
utilnet "github.com/fatedier/frp/pkg/util/net"
@@ -66,6 +71,10 @@ type Service struct {
// Accept connections from client
listener net.Listener
+ // Accept connections using ssh
+ sshListener net.Listener
+ sshConfig *ssh.ServerConfig
+
// Accept connections using kcp
kcpListener net.Listener
@@ -199,6 +208,67 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
svr.listener = ln
log.Info("frps tcp listen on %s", address)
+ if cfg.SSHTunnelGateway.BindPort > 0 {
+
+ if cfg.SSHTunnelGateway.PublicKeyFilesPath != "" {
+ cfg.SSHTunnelGateway.PublicKeyFilesMap, err = v1.LoadSSHPublicKeyFilesInDir(cfg.SSHTunnelGateway.PublicKeyFilesPath)
+ if err != nil {
+ return nil, fmt.Errorf("load ssh all public key files error: %v", err)
+ }
+ log.Info("load %v public key files success", cfg.SSHTunnelGateway.PublicKeyFilesPath)
+ }
+
+ svr.sshConfig = &ssh.ServerConfig{
+ NoClientAuth: lo.If(cfg.SSHTunnelGateway.PublicKeyFilesPath == "", true).Else(false),
+
+ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+ parsedAuthorizedKey, ok := cfg.SSHTunnelGateway.PublicKeyFilesMap[ssh.FingerprintSHA256(key)]
+ if !ok {
+ return nil, errors.New("cannot find public key file")
+ }
+
+ if key.Type() == parsedAuthorizedKey.Type() && reflect.DeepEqual(parsedAuthorizedKey, key) {
+ return &ssh.Permissions{
+ Extensions: map[string]string{},
+ }, nil
+ }
+ return nil, fmt.Errorf("unknown public key for %q", conn.User())
+ },
+ }
+
+ var privateBytes []byte
+ if cfg.SSHTunnelGateway.PrivateKeyFilePath != "" {
+ privateBytes, err = os.ReadFile(cfg.SSHTunnelGateway.PrivateKeyFilePath)
+ if err != nil {
+ log.Error("Failed to load private key")
+ return nil, err
+ }
+ log.Info("load %v private key file success", cfg.SSHTunnelGateway.PrivateKeyFilePath)
+ } else {
+ privateBytes, err = v1.GeneratePrivateKey()
+ if err != nil {
+ log.Error("Failed to load private key")
+ return nil, err
+ }
+ log.Info("auto gen private key file success")
+ }
+ private, err := ssh.ParsePrivateKey(privateBytes)
+ if err != nil {
+ log.Error("Failed to parse private key, error: %v", err)
+ return nil, err
+ }
+
+ svr.sshConfig.AddHostKey(private)
+
+ sshAddr := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.SSHTunnelGateway.BindPort))
+ svr.sshListener, err = net.Listen("tcp", sshAddr)
+ if err != nil {
+ log.Error("Failed to listen on %v, error: %v", sshAddr, err)
+ return nil, err
+ }
+ log.Info("ssh server listening on %v", sshAddr)
+ }
+
// Listen for accepting connections from client using kcp protocol.
if cfg.KCPBindPort > 0 {
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
@@ -326,6 +396,10 @@ func (svr *Service) Run(ctx context.Context) {
svr.ctx = ctx
svr.cancel = cancel
+ if svr.sshListener != nil {
+ go svr.HandleSSHListener(svr.sshListener)
+ }
+
if svr.kcpListener != nil {
go svr.HandleListener(svr.kcpListener)
}
@@ -348,6 +422,10 @@ func (svr *Service) Run(ctx context.Context) {
}
func (svr *Service) Close() error {
+ if svr.sshListener != nil {
+ svr.sshListener.Close()
+ svr.sshListener = nil
+ }
if svr.kcpListener != nil {
svr.kcpListener.Close()
svr.kcpListener = nil
@@ -493,6 +571,52 @@ func (svr *Service) HandleListener(l net.Listener) {
}
}
+func (svr *Service) HandleSSHListener(listener net.Listener) {
+ for {
+ tcpConn, err := listener.Accept()
+ if err != nil {
+ log.Error("failed to accept incoming ssh connection (%s)", err)
+ return
+ }
+ log.Info("new tcp conn connected: %v", tcpConn.RemoteAddr().String())
+
+ pxyPayloadCh := make(chan v1.ProxyConfigurer)
+ replyCh := make(chan interface{})
+
+ ss, err := frpssh.NewSSHService(tcpConn, svr.sshConfig, pxyPayloadCh, replyCh)
+ if err != nil {
+ log.Error("new ssh service error: %v", err)
+ continue
+ }
+ ss.Run()
+
+ go func() {
+ for {
+ pxyCfg := <-pxyPayloadCh
+
+ ctx := context.Background()
+
+ // TODO fill client common config and login msg
+ vs, err := frpssh.NewVirtualService(ctx, v1.ClientCommonConfig{}, *svr.cfg,
+ msg.Login{User: v1.SSHClientLoginUserPrefix + tcpConn.RemoteAddr().String()},
+ svr.rc, pxyCfg, ss, replyCh)
+ if err != nil {
+ log.Error("new virtual service error: %v", err)
+ ss.Close()
+ return
+ }
+
+ err = vs.Run(ctx)
+ if err != nil {
+ log.Error("proxy run error: %v", err)
+ vs.Close()
+ return
+ }
+ }
+ }()
+ }
+}
+
func (svr *Service) HandleQUICListener(l *quic.Listener) {
// Listen for incoming connections from client.
for {
From d5b41f1e1485f7205d96ea4522ed7655d145d47e Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 21 Nov 2023 11:19:35 +0800
Subject: [PATCH 09/21] sshTunnelGateway refactor (#3784)
---
Makefile | 4 +-
client/connector.go | 223 +++++++++++
client/control.go | 18 +-
client/proxy/proxy.go | 17 +-
client/proxy/proxy_manager.go | 12 +-
client/proxy/proxy_wrapper.go | 4 +
client/proxy/sudp.go | 2 +
client/proxy/udp.go | 2 +
client/proxy/xtcp.go | 2 +
client/service.go | 234 ++---------
cmd/frpc/sub/proxy.go | 7 +-
cmd/frps/flags.go | 110 ------
cmd/frps/root.go | 2 +-
go.mod | 2 +-
go.sum | 6 +-
{cmd/frpc/sub => pkg/config}/flags.go | 89 ++++-
pkg/config/v1/server.go | 27 +-
pkg/config/v1/ssh.go | 72 ----
pkg/plugin/client/http2https.go | 2 +
pkg/plugin/client/http_proxy.go | 2 +
pkg/plugin/client/https2http.go | 2 +
pkg/plugin/client/https2https.go | 2 +
pkg/plugin/client/socks5.go | 2 +
pkg/plugin/client/static_file.go | 2 +
pkg/plugin/client/unix_domain_socket.go | 2 +
pkg/ssh/gateway.go | 149 +++++++
pkg/ssh/server.go | 279 +++++++++++++
pkg/ssh/service.go | 497 ------------------------
pkg/ssh/vclient.go | 185 ---------
pkg/transport/tls.go | 12 +
pkg/util/xlog/xlog.go | 59 ++-
pkg/virtual/client.go | 92 +++++
server/proxy/proxy.go | 9 +-
server/service.go | 162 ++------
34 files changed, 1036 insertions(+), 1255 deletions(-)
create mode 100644 client/connector.go
delete mode 100644 cmd/frps/flags.go
rename {cmd/frpc/sub => pkg/config}/flags.go (61%)
delete mode 100644 pkg/config/v1/ssh.go
create mode 100644 pkg/ssh/gateway.go
create mode 100644 pkg/ssh/server.go
delete mode 100644 pkg/ssh/service.go
delete mode 100644 pkg/ssh/vclient.go
create mode 100644 pkg/virtual/client.go
diff --git a/Makefile b/Makefile
index d94e7c36543..f8326891de6 100644
--- a/Makefile
+++ b/Makefile
@@ -26,10 +26,10 @@ vet:
go vet ./...
frps:
- env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps
+ env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
frpc:
- env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc
+ env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
test: gotest
diff --git a/client/connector.go b/client/connector.go
new file mode 100644
index 00000000000..2ff9b491a9a
--- /dev/null
+++ b/client/connector.go
@@ -0,0 +1,223 @@
+// 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"
+ "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"
+ utilnet "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
+}
+
+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 utilnet.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: utilnet.DialHookWebsocket(protocol, "")}))
+ dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+ Hook: utilnet.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: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
+ default:
+ dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+ Hook: utilnet.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 {
+ if c.quicConn != nil {
+ _ = c.quicConn.CloseWithError(0, "")
+ }
+ if c.muxSession != nil {
+ _ = c.muxSession.Close()
+ }
+ return nil
+}
diff --git a/client/control.go b/client/control.go
index c8d186ca14b..be028ec43f3 100644
--- a/client/control.go
+++ b/client/control.go
@@ -58,8 +58,8 @@ type Control struct {
// control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
conn net.Conn
- // use cm to create new connections, which could be real TCP connections or virtual streams.
- cm *ConnectionManager
+ // use connector to create new connections, which could be real TCP connections or virtual streams.
+ connector Connector
doneCh chan struct{}
@@ -77,7 +77,7 @@ type Control struct {
}
func NewControl(
- ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
+ ctx context.Context, runID string, conn net.Conn, connector Connector,
clientCfg *v1.ClientCommonConfig,
pxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
@@ -92,7 +92,7 @@ func NewControl(
runID: runID,
pxyCfgs: pxyCfgs,
conn: conn,
- cm: cm,
+ connector: connector,
doneCh: make(chan struct{}),
}
ctl.lastPong.Store(time.Now())
@@ -122,6 +122,10 @@ func (ctl *Control) Run() {
go ctl.vm.Run()
}
+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
workConn, err := ctl.connectServer()
@@ -207,7 +211,7 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
time.Sleep(d)
ctl.conn.Close()
- ctl.cm.Close()
+ ctl.connector.Close()
return nil
}
@@ -218,7 +222,7 @@ func (ctl *Control) Done() <-chan struct{} {
// connectServer return a new connection to frps
func (ctl *Control) connectServer() (conn net.Conn, err error) {
- return ctl.cm.Connect()
+ return ctl.connector.Connect()
}
func (ctl *Control) registerMsgHandlers() {
@@ -282,7 +286,7 @@ func (ctl *Control) worker() {
ctl.pm.Close()
ctl.vm.Close()
- ctl.cm.Close()
+ ctl.connector.Close()
close(ctl.doneCh)
}
diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index 5ba63f94cce..396539c0837 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -47,10 +47,9 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v
// Proxy defines how to handle work connections for different proxy type.
type Proxy interface {
Run() error
-
// InWorkConn accept work connections registered to server.
InWorkConn(net.Conn, *msg.StartWorkConn)
-
+ SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool)
Close()
}
@@ -89,7 +88,8 @@ type BaseProxy struct {
limiter *rate.Limiter
// proxyPlugin is used to handle connections instead of dialing to local service.
// It's only validate for TCP protocol now.
- proxyPlugin plugin.Plugin
+ proxyPlugin plugin.Plugin
+ inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool
mu sync.RWMutex
xl *xlog.Logger
@@ -113,7 +113,16 @@ func (pxy *BaseProxy) Close() {
}
}
+func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+ pxy.inWorkConnCallback = cb
+}
+
func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
+ if pxy.inWorkConnCallback != nil {
+ if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) {
+ return
+ }
+ }
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
}
@@ -132,7 +141,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
})
}
- xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t",
+ xl.Trace("handle tcp work connection, useEncryption: %t, useCompression: %t",
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
if baseCfg.Transport.UseEncryption {
remote, err = libio.WithEncryption(remote, encKey)
diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go
index db66cb26397..dadf648150a 100644
--- a/client/proxy/proxy_manager.go
+++ b/client/proxy/proxy_manager.go
@@ -31,8 +31,9 @@ import (
)
type Manager struct {
- proxies map[string]*Wrapper
- msgTransporter transport.MessageTransporter
+ proxies map[string]*Wrapper
+ msgTransporter transport.MessageTransporter
+ inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
closed bool
mu sync.RWMutex
@@ -71,6 +72,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri
return nil
}
+func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+ pm.inWorkConnCallback = cb
+}
+
func (pm *Manager) Close() {
pm.mu.Lock()
defer pm.mu.Unlock()
@@ -146,6 +151,9 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
+ if pm.inWorkConnCallback != nil {
+ pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
+ }
pm.proxies[name] = pxy
addPxyNames = append(addPxyNames, name)
diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go
index 346c6d076a8..84f24abb668 100644
--- a/client/proxy/proxy_wrapper.go
+++ b/client/proxy/proxy_wrapper.go
@@ -121,6 +121,10 @@ func NewWrapper(
return pw
}
+func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+ pw.pxy.SetInWorkConnCallback(cb)
+}
+
func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
pw.mu.Lock()
defer pw.mu.Unlock()
diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go
index e67a33974f0..f9fe53bccc0 100644
--- a/client/proxy/sudp.go
+++ b/client/proxy/sudp.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package proxy
import (
diff --git a/client/proxy/udp.go b/client/proxy/udp.go
index d7a790c146d..d8590f68df4 100644
--- a/client/proxy/udp.go
+++ b/client/proxy/udp.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package proxy
import (
diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go
index 8271099bb50..b286a9318b9 100644
--- a/client/proxy/xtcp.go
+++ b/client/proxy/xtcp.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package proxy
import (
diff --git a/client/service.go b/client/service.go
index 66a642c1bf0..7c3cd03926d 100644
--- a/client/service.go
+++ b/client/service.go
@@ -16,30 +16,22 @@ package client
import (
"context"
- "crypto/tls"
"errors"
"fmt"
- "io"
"net"
"runtime"
"strconv"
- "strings"
"sync"
"time"
"github.com/fatedier/golib/crypto"
- libdial "github.com/fatedier/golib/net/dial"
- fmux "github.com/hashicorp/yamux"
- quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
- "github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
- utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
@@ -75,6 +67,9 @@ type Service struct {
// call cancel to stop service
cancel context.CancelFunc
gracefulDuration time.Duration
+
+ connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
+ inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
}
func NewService(
@@ -84,15 +79,24 @@ func NewService(
cfgFile string,
) *Service {
return &Service{
- authSetter: auth.NewAuthSetter(cfg.Auth),
- cfg: cfg,
- cfgFile: cfgFile,
- pxyCfgs: pxyCfgs,
- visitorCfgs: visitorCfgs,
- ctx: context.Background(),
+ authSetter: auth.NewAuthSetter(cfg.Auth),
+ cfg: cfg,
+ cfgFile: cfgFile,
+ pxyCfgs: pxyCfgs,
+ visitorCfgs: visitorCfgs,
+ ctx: context.Background(),
+ connectorCreator: NewConnector,
}
}
+func (svr *Service) SetConnectorCreator(h func(context.Context, *v1.ClientCommonConfig) Connector) {
+ svr.connectorCreator = h
+}
+
+func (svr *Service) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+ svr.inWorkConnCallback = cb
+}
+
func (svr *Service) GetController() *Control {
svr.ctlMu.RLock()
defer svr.ctlMu.RUnlock()
@@ -101,7 +105,7 @@ func (svr *Service) GetController() *Control {
func (svr *Service) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
- svr.ctx = xlog.NewContext(ctx, xlog.New())
+ svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
svr.cancel = cancel
// set custom DNSServer
@@ -173,21 +177,20 @@ func (svr *Service) keepControllerWorking() {
// login creates a connection to frps and registers it self as a client
// conn: control connection
// session: if it's not nil, using tcp mux
-func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
+func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
xl := xlog.FromContextSafe(svr.ctx)
- cm = NewConnectionManager(svr.ctx, svr.cfg)
-
- if err = cm.OpenConnection(); err != nil {
+ connector = svr.connectorCreator(svr.ctx, svr.cfg)
+ if err = connector.Open(); err != nil {
return nil, nil, err
}
defer func() {
if err != nil {
- cm.Close()
+ connector.Close()
}
}()
- conn, err = cm.Connect()
+ conn, err = connector.Connect()
if err != nil {
return
}
@@ -226,8 +229,7 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
}
svr.runID = loginRespMsg.RunID
- xl.ResetPrefixes()
- xl.AppendPrefix(svr.runID)
+ xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
xl.Info("login to server success, get run id [%s]", loginRespMsg.RunID)
return
@@ -239,7 +241,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
loginFunc := func() error {
xl.Info("try to connect to server...")
- conn, cm, err := svr.login()
+ conn, connector, err := svr.login()
if err != nil {
xl.Warn("connect to server error: %v", err)
if firstLoginExit {
@@ -248,13 +250,14 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
return err
}
- ctl, err := NewControl(svr.ctx, svr.runID, conn, cm,
+ ctl, err := NewControl(svr.ctx, svr.runID, conn, connector,
svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
if err != nil {
conn.Close()
xl.Error("NewControl error: %v", err)
return err
}
+ ctl.SetInWorkConnCallback(svr.inWorkConnCallback)
ctl.Run()
// close and replace previous control
@@ -314,184 +317,3 @@ func (svr *Service) stop() {
svr.ctl = nil
}
}
-
-// ConnectionManager is a wrapper for establishing connections to the server.
-type ConnectionManager struct {
- ctx context.Context
- cfg *v1.ClientCommonConfig
-
- muxSession *fmux.Session
- quicConn quic.Connection
-}
-
-func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager {
- return &ConnectionManager{
- ctx: ctx,
- cfg: cfg,
- }
-}
-
-// OpenConnection 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 (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
-}
-
-// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled.
-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
-}
diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go
index 7ae8d353b39..960509433ca 100644
--- a/cmd/frpc/sub/proxy.go
+++ b/cmd/frpc/sub/proxy.go
@@ -21,6 +21,7 @@ import (
"github.com/samber/lo"
"github.com/spf13/cobra"
+ "github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
)
@@ -50,8 +51,8 @@ func init() {
}
clientCfg := v1.ClientCommonConfig{}
cmd := NewProxyCommand(string(typ), c, &clientCfg)
- RegisterClientCommonConfigFlags(cmd, &clientCfg)
- RegisterProxyFlags(cmd, c)
+ config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
+ config.RegisterProxyFlags(cmd, c)
// add sub command for visitor
if lo.Contains(visitorTypes, v1.VisitorType(typ)) {
@@ -60,7 +61,7 @@ func init() {
panic("visitor type: " + typ + " not support")
}
visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg)
- RegisterVisitorFlags(visitorCmd, vc)
+ config.RegisterVisitorFlags(visitorCmd, vc)
cmd.AddCommand(visitorCmd)
}
rootCmd.AddCommand(cmd)
diff --git a/cmd/frps/flags.go b/cmd/frps/flags.go
deleted file mode 100644
index 50170684734..00000000000
--- a/cmd/frps/flags.go
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright 2023 The frp Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
- "strconv"
-
- "github.com/spf13/cobra"
-
- "github.com/fatedier/frp/pkg/config/types"
- v1 "github.com/fatedier/frp/pkg/config/v1"
-)
-
-type PortsRangeSliceFlag struct {
- V *[]types.PortsRange
-}
-
-func (f *PortsRangeSliceFlag) String() string {
- if f.V == nil {
- return ""
- }
- return types.PortsRangeSlice(*f.V).String()
-}
-
-func (f *PortsRangeSliceFlag) Set(s string) error {
- slice, err := types.NewPortsRangeSliceFromString(s)
- if err != nil {
- return err
- }
- *f.V = slice
- return nil
-}
-
-func (f *PortsRangeSliceFlag) Type() string {
- return "string"
-}
-
-type BoolFuncFlag struct {
- TrueFunc func()
- FalseFunc func()
-
- v bool
-}
-
-func (f *BoolFuncFlag) String() string {
- return strconv.FormatBool(f.v)
-}
-
-func (f *BoolFuncFlag) Set(s string) error {
- f.v = strconv.FormatBool(f.v) == "true"
-
- if !f.v {
- if f.FalseFunc != nil {
- f.FalseFunc()
- }
- return nil
- }
-
- if f.TrueFunc != nil {
- f.TrueFunc()
- }
- return nil
-}
-
-func (f *BoolFuncFlag) Type() string {
- return "bool"
-}
-
-func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
- cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
- cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
- cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
- cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
- cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
- cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
- cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
- cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
- cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
- cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
- cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
- cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
- cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
- cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
- cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
- cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
- cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
- cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
- cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
- cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
- cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
-
- webServerTLS := v1.TLSConfig{}
- cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
- cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
- cmd.PersistentFlags().VarP(&BoolFuncFlag{
- TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
- }, "dashboard_tls_mode", "", "if enable dashboard tls mode")
-}
diff --git a/cmd/frps/root.go b/cmd/frps/root.go
index 5f32fe9c353..0cf8e4e79d7 100644
--- a/cmd/frps/root.go
+++ b/cmd/frps/root.go
@@ -42,7 +42,7 @@ func init() {
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{
diff --git a/go.mod b/go.mod
index 8d0055e6069..d11e1ef4996 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/quic-go/quic-go v0.37.4
github.com/rodaine/table v1.1.0
github.com/samber/lo v1.38.1
- github.com/spf13/cobra v1.7.0
+ github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.15.0
golang.org/x/net v0.17.0
diff --git a/go.sum b/go.sum
index 49cef0b2cc4..56966be2f0d 100644
--- a/go.sum
+++ b/go.sum
@@ -16,7 +16,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -128,8 +128,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/cmd/frpc/sub/flags.go b/pkg/config/flags.go
similarity index 61%
rename from cmd/frpc/sub/flags.go
rename to pkg/config/flags.go
index eb3cc010629..0c37e608372 100644
--- a/cmd/frpc/sub/flags.go
+++ b/pkg/config/flags.go
@@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package sub
+package config
import (
"fmt"
+ "strconv"
"github.com/spf13/cobra"
@@ -123,3 +124,89 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
}
+
+type PortsRangeSliceFlag struct {
+ V *[]types.PortsRange
+}
+
+func (f *PortsRangeSliceFlag) String() string {
+ if f.V == nil {
+ return ""
+ }
+ return types.PortsRangeSlice(*f.V).String()
+}
+
+func (f *PortsRangeSliceFlag) Set(s string) error {
+ slice, err := types.NewPortsRangeSliceFromString(s)
+ if err != nil {
+ return err
+ }
+ *f.V = slice
+ return nil
+}
+
+func (f *PortsRangeSliceFlag) Type() string {
+ return "string"
+}
+
+type BoolFuncFlag struct {
+ TrueFunc func()
+ FalseFunc func()
+
+ v bool
+}
+
+func (f *BoolFuncFlag) String() string {
+ return strconv.FormatBool(f.v)
+}
+
+func (f *BoolFuncFlag) Set(s string) error {
+ f.v = strconv.FormatBool(f.v) == "true"
+
+ if !f.v {
+ if f.FalseFunc != nil {
+ f.FalseFunc()
+ }
+ return nil
+ }
+
+ if f.TrueFunc != nil {
+ f.TrueFunc()
+ }
+ return nil
+}
+
+func (f *BoolFuncFlag) Type() string {
+ return "bool"
+}
+
+func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
+ cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address")
+ cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port")
+ cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port")
+ cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address")
+ cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port")
+ cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port")
+ cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout")
+ cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address")
+ cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port")
+ cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user")
+ cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password")
+ cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
+ cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file")
+ cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
+ cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days")
+ cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
+ cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
+ cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
+ cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
+ cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
+ cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
+
+ webServerTLS := v1.TLSConfig{}
+ cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
+ cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file")
+ cmd.PersistentFlags().VarP(&BoolFuncFlag{
+ TrueFunc: func() { c.WebServer.TLS = &webServerTLS },
+ }, "dashboard_tls_mode", "", "if enable dashboard tls mode")
+}
diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go
index f562be8e12f..03b05d9d043 100644
--- a/pkg/config/v1/server.go
+++ b/pkg/config/v1/server.go
@@ -16,21 +16,11 @@ package v1
import (
"github.com/samber/lo"
- "golang.org/x/crypto/ssh"
"github.com/fatedier/frp/pkg/config/types"
"github.com/fatedier/frp/pkg/util/util"
)
-type SSHTunnelGateway struct {
- BindPort int `json:"bindPort,omitempty" validate:"gte=0,lte=65535"`
- PrivateKeyFilePath string `json:"privateKeyFilePath,omitempty"`
- PublicKeyFilesPath string `json:"publicKeyFilesPath,omitempty"`
-
- // store all public key file. load all when init
- PublicKeyFilesMap map[string]ssh.PublicKey
-}
-
type ServerConfig struct {
APIMetadata
@@ -41,9 +31,6 @@ type ServerConfig struct {
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `json:"bindPort,omitempty"`
-
- SSHTunnelGateway SSHTunnelGateway `json:"sshGatewayConfig,omitempty"`
-
// KCPBindPort specifies the KCP port that the server listens on. If this
// value is 0, the server will not listen for KCP connections.
KCPBindPort int `json:"kcpBindPort,omitempty"`
@@ -80,6 +67,8 @@ type ServerConfig struct {
// value is "", a default page will be displayed.
Custom404Page string `json:"custom404Page,omitempty"`
+ SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
+
WebServer WebServerConfig `json:"webServer,omitempty"`
// EnablePrometheus will export prometheus metrics on webserver address
// in /metrics api.
@@ -114,6 +103,7 @@ func (c *ServerConfig) Complete() {
c.Log.Complete()
c.Transport.Complete()
c.WebServer.Complete()
+ c.SSHTunnelGateway.Complete()
c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
c.BindPort = util.EmptyOr(c.BindPort, 7000)
@@ -202,3 +192,14 @@ type TLSServerConfig struct {
TLSConfig
}
+
+type SSHTunnelGateway struct {
+ BindPort int `json:"bindPort,omitempty"`
+ PrivateKeyFile string `json:"privateKeyFile,omitempty"`
+ AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"`
+ AuthorizedKeysFile string `json:"authorizedKeysFile,omitempty"`
+}
+
+func (c *SSHTunnelGateway) Complete() {
+ c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
+}
diff --git a/pkg/config/v1/ssh.go b/pkg/config/v1/ssh.go
deleted file mode 100644
index 440305d4fe3..00000000000
--- a/pkg/config/v1/ssh.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package v1
-
-import (
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "os"
- "path/filepath"
-
- "golang.org/x/crypto/ssh"
-)
-
-const (
- // custom define
- SSHClientLoginUserPrefix = "_frpc_ssh_client_"
-)
-
-// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
-func GeneratePrivateKey() ([]byte, error) {
- privateKey, err := generatePrivateKey()
- if err != nil {
- return nil, errors.New("gen private key error")
- }
-
- privBlock := pem.Block{
- Type: "RSA PRIVATE KEY",
- Headers: nil,
- Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
- }
-
- return pem.EncodeToMemory(&privBlock), nil
-}
-
-// generatePrivateKey creates a RSA Private Key of specified byte size
-func generatePrivateKey() (*rsa.PrivateKey, error) {
- privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
- if err != nil {
- return nil, err
- }
-
- err = privateKey.Validate()
- if err != nil {
- return nil, err
- }
- return privateKey, nil
-}
-
-func LoadSSHPublicKeyFilesInDir(dirPath string) (map[string]ssh.PublicKey, error) {
- fileMap := make(map[string]ssh.PublicKey)
- files, err := os.ReadDir(dirPath)
- if err != nil {
- return nil, err
- }
-
- for _, file := range files {
- filePath := filepath.Join(dirPath, file.Name())
- content, err := os.ReadFile(filePath)
- if err != nil {
- return nil, err
- }
-
- parsedAuthorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(content)
- if err != nil {
- continue
- }
- fileMap[ssh.FingerprintSHA256(parsedAuthorizedKey)] = parsedAuthorizedKey
- }
-
- return fileMap, nil
-}
diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go
index 7f093af1b0f..fd3e44b4637 100644
--- a/pkg/plugin/client/http2https.go
+++ b/pkg/plugin/client/http2https.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go
index 06c6296a11d..65abf19d68d 100644
--- a/pkg/plugin/client/http_proxy.go
+++ b/pkg/plugin/client/http_proxy.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index aa498f3f1a9..4a1c85b99e5 100644
--- a/pkg/plugin/client/https2http.go
+++ b/pkg/plugin/client/https2http.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index fc38f62b364..81386ac6c79 100644
--- a/pkg/plugin/client/https2https.go
+++ b/pkg/plugin/client/https2https.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go
index c2e253d241f..33e87b537a3 100644
--- a/pkg/plugin/client/socks5.go
+++ b/pkg/plugin/client/socks5.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go
index 20b79a099da..faf03f7d7d7 100644
--- a/pkg/plugin/client/static_file.go
+++ b/pkg/plugin/client/static_file.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go
index f186ec925ea..df68ffb469d 100644
--- a/pkg/plugin/client/unix_domain_socket.go
+++ b/pkg/plugin/client/unix_domain_socket.go
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !frps
+
package plugin
import (
diff --git a/pkg/ssh/gateway.go b/pkg/ssh/gateway.go
new file mode 100644
index 00000000000..8f87e9986c4
--- /dev/null
+++ b/pkg/ssh/gateway.go
@@ -0,0 +1,149 @@
+// 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"
+ utilnet "github.com/fatedier/frp/pkg/util/net"
+)
+
+type Gateway struct {
+ bindPort int
+ ln net.Listener
+
+ serverPeerListener *utilnet.InternalListener
+
+ sshConfig *ssh.ServerConfig
+}
+
+func NewGateway(
+ cfg v1.SSHTunnelGateway, bindAddr string,
+ serverPeerListener *utilnet.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.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+ if cfg.AuthorizedKeysFile == "" {
+ return &ssh.Permissions{
+ Extensions: map[string]string{
+ "user": "",
+ },
+ }, nil
+ }
+
+ authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile)
+ if err != nil {
+ 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,
+ serverPeerListener: serverPeerListener,
+ 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.serverPeerListener)
+ if err != nil {
+ return
+ }
+ if err := ts.Run(); err != nil {
+ log.Error("ssh tunnel server run error: %v", err)
+ }
+}
+
+func loadAuthorizedKeysFromFile(path string) (map[string]string, error) {
+ authorizedKeysMap := make(map[string]string) // value is username
+ authorizedKeysBytes, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ for len(authorizedKeysBytes) > 0 {
+ pubKey, comment, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ authorizedKeysMap[string(pubKey.Marshal())] = strings.TrimSpace(comment)
+ authorizedKeysBytes = rest
+ }
+ return authorizedKeysMap, nil
+}
diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go
new file mode 100644
index 00000000000..13c87b689da
--- /dev/null
+++ b/pkg/ssh/server.go
@@ -0,0 +1,279 @@
+// 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"
+ "fmt"
+ "net"
+ "strings"
+ "time"
+
+ libio "github.com/fatedier/golib/io"
+ "github.com/samber/lo"
+ "github.com/spf13/cobra"
+ "golang.org/x/crypto/ssh"
+
+ "github.com/fatedier/frp/pkg/config"
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+ "github.com/fatedier/frp/pkg/msg"
+ utilnet "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
+
+ // can be default empty value but do not delete it
+ // because ssh protocol shoule be reserved
+ OriginAddr string
+ OriginPort uint32
+}
+
+type TunnelServer struct {
+ underlyingConn net.Conn
+ sshConn *ssh.ServerConn
+ sc *ssh.ServerConfig
+
+ vc *virtual.Client
+ serverPeerListener *utilnet.InternalListener
+ doneCh chan struct{}
+}
+
+func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, serverPeerListener *utilnet.InternalListener) (*TunnelServer, error) {
+ s := &TunnelServer{
+ underlyingConn: conn,
+ sc: sc,
+ serverPeerListener: serverPeerListener,
+ 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, err := s.parseClientAndProxyConfigurer(addr, extraPayload)
+ if err != nil {
+ return err
+ }
+ clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
+ pc.Complete(clientCfg.User)
+
+ s.vc = virtual.NewClient(clientCfg)
+ // join workConn and ssh channel
+ s.vc.SetInWorkConnCallback(func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool {
+ c, err := s.openConn(addr)
+ if err != nil {
+ return false
+ }
+ libio.Join(c, workConn)
+ return false
+ })
+ // 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.serverPeerListener.PutConn(conn)
+ }
+ }()
+ xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100})
+ ctx := xlog.NewContext(context.Background(), xl)
+ go func() {
+ _ = s.vc.Run(ctx)
+ }()
+
+ s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc})
+
+ _ = sshConn.Wait()
+ _ = sshConn.Close()
+ s.vc.Close()
+ close(s.doneCh)
+ return nil
+}
+
+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 {
+ switch req.Type {
+ case RequestTypeForward:
+ if !addrGot {
+ payload := tcpipForward{}
+ if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
+ return
+ }
+ addrGot = true
+ addrCh <- &payload
+ }
+ default:
+ 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, error) {
+ cmd := &cobra.Command{}
+ args := strings.Split(extraPayload, " ")
+ if len(args) < 1 {
+ return nil, nil, 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, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes)
+ }
+ pc := v1.NewProxyConfigurerByType(v1.ProxyType(proxyType))
+ if pc == nil {
+ return nil, nil, fmt.Errorf("new proxy configurer error")
+ }
+ config.RegisterProxyFlags(cmd, pc)
+
+ clientCfg := v1.ClientCommonConfig{}
+ config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
+
+ if err := cmd.ParseFlags(args); err != nil {
+ return nil, nil, fmt.Errorf("parse flags from ssh client error: %v", err)
+ }
+ return &clientCfg, pc, nil
+}
+
+func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) {
+ ch, reqs, err := channel.Accept()
+ if err != nil {
+ return
+ }
+ go s.keepAlive(ch)
+
+ for req := range reqs {
+ if req.Type != "exec" {
+ continue
+ }
+ if 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,
+ }
+ 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 := utilnet.WrapReadWriteCloserToConn(channel, s.underlyingConn)
+ return conn, nil
+}
diff --git a/pkg/ssh/service.go b/pkg/ssh/service.go
deleted file mode 100644
index ce0bc52c931..00000000000
--- a/pkg/ssh/service.go
+++ /dev/null
@@ -1,497 +0,0 @@
-package ssh
-
-import (
- "encoding/binary"
- "errors"
- "flag"
- "fmt"
- "io"
- "net"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "time"
-
- gerror "github.com/fatedier/golib/errors"
- "golang.org/x/crypto/ssh"
-
- v1 "github.com/fatedier/frp/pkg/config/v1"
- "github.com/fatedier/frp/pkg/util/log"
-)
-
-const (
- // ssh protocol define
- // https://datatracker.ietf.org/doc/html/rfc4254#page-16
- ChannelTypeServerOpenChannel = "forwarded-tcpip"
- RequestTypeForward = "tcpip-forward"
-
- // golang ssh package define.
- // https://pkg.go.dev/golang.org/x/crypto/ssh
- RequestTypeHeartbeat = "keepalive@openssh.com"
-)
-
-// 当 proxy 失败会返回该错误
-type VProxyError struct{}
-
-// ssh protocol define
-// https://datatracker.ietf.org/doc/html/rfc4254#page-16
-// parse ssh client cmds input
-type forwardedTCPPayload struct {
- Addr string
- Port uint32
-
- // can be default empty value but do not delete it
- // because ssh protocol shoule be reserved
- OriginAddr string
- OriginPort uint32
-}
-
-// custom define
-// parse ssh client cmds input
-type CmdPayload struct {
- Address string
- Port uint32
-}
-
-// custom define
-// with frp control cmds
-type ExtraPayload struct {
- Type string
-
- // TODO port can be set by extra message and priority to ssh raw cmd
- Address string
- Port uint32
-}
-
-type Service struct {
- tcpConn net.Conn
- cfg *ssh.ServerConfig
-
- sshConn *ssh.ServerConn
- gChannel <-chan ssh.NewChannel
- gReq <-chan *ssh.Request
-
- addrPayloadCh chan CmdPayload
- extraPayloadCh chan ExtraPayload
-
- proxyPayloadCh chan v1.ProxyConfigurer
- replyCh chan interface{}
-
- closeCh chan struct{}
- exit int32
-}
-
-func NewSSHService(
- tcpConn net.Conn,
- cfg *ssh.ServerConfig,
- proxyPayloadCh chan v1.ProxyConfigurer,
- replyCh chan interface{},
-) (ss *Service, err error) {
- ss = &Service{
- tcpConn: tcpConn,
- cfg: cfg,
-
- addrPayloadCh: make(chan CmdPayload),
- extraPayloadCh: make(chan ExtraPayload),
-
- proxyPayloadCh: proxyPayloadCh,
- replyCh: replyCh,
-
- closeCh: make(chan struct{}),
- exit: 0,
- }
-
- ss.sshConn, ss.gChannel, ss.gReq, err = ssh.NewServerConn(tcpConn, cfg)
- if err != nil {
- log.Error("ssh handshake error: %v", err)
- return nil, err
- }
-
- log.Info("ssh connection success")
-
- return ss, nil
-}
-
-func (ss *Service) Run() {
- go ss.loopGenerateProxy()
- go ss.loopParseCmdPayload()
- go ss.loopParseExtraPayload()
- go ss.loopReply()
-}
-
-func (ss *Service) Exit() <-chan struct{} {
- return ss.closeCh
-}
-
-func (ss *Service) Close() {
- if atomic.LoadInt32(&ss.exit) == 1 {
- return
- }
-
- select {
- case <-ss.closeCh:
- return
- default:
- }
-
- close(ss.closeCh)
- close(ss.addrPayloadCh)
- close(ss.extraPayloadCh)
-
- _ = ss.sshConn.Wait()
-
- ss.sshConn.Close()
- ss.tcpConn.Close()
-
- atomic.StoreInt32(&ss.exit, 1)
-
- log.Info("ssh service close")
-}
-
-func (ss *Service) loopParseCmdPayload() {
- for {
- select {
- case req, ok := <-ss.gReq:
- if !ok {
- log.Info("global request is close")
- ss.Close()
- return
- }
-
- switch req.Type {
- case RequestTypeForward:
- var addrPayload CmdPayload
- if err := ssh.Unmarshal(req.Payload, &addrPayload); err != nil {
- log.Error("ssh unmarshal error: %v", err)
- return
- }
- _ = gerror.PanicToError(func() {
- ss.addrPayloadCh <- addrPayload
- })
- default:
- if req.Type == RequestTypeHeartbeat {
- log.Debug("ssh heartbeat data")
- } else {
- log.Info("default req, data: %v", req)
- }
- }
- if req.WantReply {
- err := req.Reply(true, nil)
- if err != nil {
- log.Error("reply to ssh client error: %v", err)
- }
- }
- case <-ss.closeCh:
- log.Info("loop parse cmd payload close")
- return
- }
- }
-}
-
-func (ss *Service) loopSendHeartbeat(ch ssh.Channel) {
- tk := time.NewTicker(time.Second * 60)
- defer tk.Stop()
-
- for {
- select {
- case <-tk.C:
- ok, err := ch.SendRequest("heartbeat", false, nil)
- if err != nil {
- log.Error("channel send req error: %v", err)
- if err == io.EOF {
- ss.Close()
- return
- }
- continue
- }
- log.Debug("heartbeat send success, ok: %v", ok)
- case <-ss.closeCh:
- return
- }
- }
-}
-
-func (ss *Service) loopParseExtraPayload() {
- log.Info("loop parse extra payload start")
-
- for newChannel := range ss.gChannel {
- ch, req, err := newChannel.Accept()
- if err != nil {
- log.Error("channel accept error: %v", err)
- return
- }
-
- go ss.loopSendHeartbeat(ch)
-
- go func(req <-chan *ssh.Request) {
- for r := range req {
- if len(r.Payload) <= 4 {
- log.Info("r.payload is less than 4")
- continue
- }
- if !strings.Contains(string(r.Payload), "tcp") && !strings.Contains(string(r.Payload), "http") {
- log.Info("ssh protocol exchange data")
- continue
- }
-
- // [4byte data_len|data]
- end := 4 + binary.BigEndian.Uint32(r.Payload[:4])
- if end > uint32(len(r.Payload)) {
- end = uint32(len(r.Payload))
- }
- p := string(r.Payload[4:end])
-
- msg, err := parseSSHExtraMessage(p)
- if err != nil {
- log.Error("parse ssh extra message error: %v, payload: %v", err, r.Payload)
- continue
- }
- _ = gerror.PanicToError(func() {
- ss.extraPayloadCh <- msg
- })
- return
- }
- }(req)
- }
-}
-
-func (ss *Service) SSHConn() *ssh.ServerConn {
- return ss.sshConn
-}
-
-func (ss *Service) TCPConn() net.Conn {
- return ss.tcpConn
-}
-
-func (ss *Service) loopReply() {
- for {
- select {
- case <-ss.closeCh:
- log.Info("loop reply close")
- return
- case req := <-ss.replyCh:
- switch req.(type) {
- case *VProxyError:
- log.Error("run frp proxy error, close ssh service")
- ss.Close()
- default:
- // TODO
- }
- }
- }
-}
-
-func (ss *Service) loopGenerateProxy() {
- log.Info("loop generate proxy start")
-
- for {
- if atomic.LoadInt32(&ss.exit) == 1 {
- return
- }
-
- wg := new(sync.WaitGroup)
- wg.Add(2)
-
- var p1 CmdPayload
- var p2 ExtraPayload
-
- go func() {
- defer wg.Done()
- for {
- select {
- case <-ss.closeCh:
- return
- case p1 = <-ss.addrPayloadCh:
- return
- }
- }
- }()
-
- go func() {
- defer wg.Done()
- for {
- select {
- case <-ss.closeCh:
- return
- case p2 = <-ss.extraPayloadCh:
- return
- }
- }
- }()
-
- wg.Wait()
-
- if atomic.LoadInt32(&ss.exit) == 1 {
- return
- }
-
- switch p2.Type {
- case "http":
- case "tcp":
- ss.proxyPayloadCh <- &v1.TCPProxyConfig{
- ProxyBaseConfig: v1.ProxyBaseConfig{
- Name: fmt.Sprintf("ssh-proxy-%v-%v", ss.tcpConn.RemoteAddr().String(), time.Now().UnixNano()),
- Type: p2.Type,
-
- ProxyBackend: v1.ProxyBackend{
- LocalIP: p1.Address,
- },
- },
- RemotePort: int(p1.Port),
- }
- default:
- log.Warn("invalid frp proxy type: %v", p2.Type)
- }
- }
-}
-
-func parseSSHExtraMessage(s string) (p ExtraPayload, err error) {
- sn := len(s)
-
- log.Info("parse ssh extra message: %v", s)
-
- ss := strings.Fields(s)
- if len(ss) == 0 {
- if sn != 0 {
- ss = append(ss, s)
- } else {
- return p, fmt.Errorf("invalid ssh input, args: %v", ss)
- }
- }
-
- for i, v := range ss {
- ss[i] = strings.TrimSpace(v)
- }
-
- if ss[0] != "tcp" && ss[0] != "http" {
- return p, fmt.Errorf("only support tcp/http now")
- }
-
- switch ss[0] {
- case "tcp":
- tcpCmd, err := ParseTCPCommand(ss)
- if err != nil {
- return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
- }
-
- port, _ := strconv.Atoi(tcpCmd.Port)
-
- p = ExtraPayload{
- Type: "tcp",
- Address: tcpCmd.Address,
- Port: uint32(port),
- }
- case "http":
- httpCmd, err := ParseHTTPCommand(ss)
- if err != nil {
- return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
- }
-
- _ = httpCmd
-
- p = ExtraPayload{
- Type: "http",
- }
- }
-
- return p, nil
-}
-
-type HTTPCommand struct {
- Domain string
- BasicAuthUser string
- BasicAuthPass string
-}
-
-func ParseHTTPCommand(params []string) (*HTTPCommand, error) {
- if len(params) < 2 {
- return nil, errors.New("invalid HTTP command")
- }
-
- var (
- basicAuth string
- domainURL string
- basicAuthUser string
- basicAuthPass string
- )
-
- fs := flag.NewFlagSet("http", flag.ContinueOnError)
- fs.StringVar(&basicAuth, "basic-auth", "", "")
- fs.StringVar(&domainURL, "domain", "", "")
-
- fs.SetOutput(&nullWriter{}) // Disables usage output
-
- err := fs.Parse(params[2:])
- if err != nil {
- if !errors.Is(err, flag.ErrHelp) {
- return nil, err
- }
- }
-
- if basicAuth != "" {
- authParts := strings.SplitN(basicAuth, ":", 2)
- basicAuthUser = authParts[0]
- if len(authParts) > 1 {
- basicAuthPass = authParts[1]
- }
- }
-
- httpCmd := &HTTPCommand{
- Domain: domainURL,
- BasicAuthUser: basicAuthUser,
- BasicAuthPass: basicAuthPass,
- }
- return httpCmd, nil
-}
-
-type TCPCommand struct {
- Address string
- Port string
-}
-
-func ParseTCPCommand(params []string) (*TCPCommand, error) {
- if len(params) == 0 || params[0] != "tcp" {
- return nil, errors.New("invalid TCP command")
- }
-
- if len(params) == 1 {
- return &TCPCommand{}, nil
- }
-
- var (
- address string
- port string
- )
-
- fs := flag.NewFlagSet("tcp", flag.ContinueOnError)
- fs.StringVar(&address, "address", "", "The IP address to listen on")
- fs.StringVar(&port, "port", "", "The port to listen on")
- fs.SetOutput(&nullWriter{}) // Disables usage output
-
- args := params[1:]
- err := fs.Parse(args)
- if err != nil {
- if !errors.Is(err, flag.ErrHelp) {
- return nil, err
- }
- }
-
- parsedAddr, err := net.ResolveIPAddr("ip", address)
- if err != nil {
- return nil, err
- }
- if _, err := net.LookupPort("tcp", port); err != nil {
- return nil, err
- }
-
- tcpCmd := &TCPCommand{
- Address: parsedAddr.String(),
- Port: port,
- }
- return tcpCmd, nil
-}
-
-type nullWriter struct{}
-
-func (w *nullWriter) Write(p []byte) (n int, err error) { return len(p), nil }
diff --git a/pkg/ssh/vclient.go b/pkg/ssh/vclient.go
deleted file mode 100644
index e78c82847d9..00000000000
--- a/pkg/ssh/vclient.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package ssh
-
-import (
- "context"
- "fmt"
- "net"
- "sync/atomic"
- "time"
-
- "golang.org/x/crypto/ssh"
-
- "github.com/fatedier/frp/pkg/config"
- v1 "github.com/fatedier/frp/pkg/config/v1"
- "github.com/fatedier/frp/pkg/msg"
- plugin "github.com/fatedier/frp/pkg/plugin/server"
- "github.com/fatedier/frp/pkg/util/log"
- frp_net "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/server/controller"
- "github.com/fatedier/frp/server/proxy"
-)
-
-// VirtualService is a client VirtualService run in frps
-type VirtualService struct {
- clientCfg v1.ClientCommonConfig
- pxyCfg v1.ProxyConfigurer
- serverCfg v1.ServerConfig
-
- sshSvc *Service
-
- // uniq id got from frps, attach it in loginMsg
- runID string
- loginMsg *msg.Login
-
- // All resource managers and controllers
- rc *controller.ResourceController
-
- exit uint32 // 0 means not exit
- // SSHService context
- ctx context.Context
- // call cancel to stop SSHService
- cancel context.CancelFunc
-
- replyCh chan interface{}
- pxy proxy.Proxy
-}
-
-func NewVirtualService(
- ctx context.Context,
- clientCfg v1.ClientCommonConfig,
- serverCfg v1.ServerConfig,
- logMsg msg.Login,
- rc *controller.ResourceController,
- pxyCfg v1.ProxyConfigurer,
- sshSvc *Service,
- replyCh chan interface{},
-) (svr *VirtualService, err error) {
- svr = &VirtualService{
- clientCfg: clientCfg,
- serverCfg: serverCfg,
- rc: rc,
-
- loginMsg: &logMsg,
-
- sshSvc: sshSvc,
- pxyCfg: pxyCfg,
-
- ctx: ctx,
- exit: 0,
-
- replyCh: replyCh,
- }
-
- svr.runID, err = util.RandID()
- if err != nil {
- return nil, err
- }
-
- go svr.loopCheck()
-
- return
-}
-
-func (svr *VirtualService) Run(ctx context.Context) (err error) {
- ctx, cancel := context.WithCancel(ctx)
- svr.ctx = xlog.NewContext(ctx, xlog.New())
- svr.cancel = cancel
-
- remoteAddr, err := svr.RegisterProxy(&msg.NewProxy{
- ProxyName: svr.pxyCfg.(*v1.TCPProxyConfig).Name,
- ProxyType: svr.pxyCfg.(*v1.TCPProxyConfig).Type,
- RemotePort: svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort,
- })
- if err != nil {
- return err
- }
-
- log.Info("run a reverse proxy on port: %v", remoteAddr)
-
- return nil
-}
-
-func (svr *VirtualService) Close() {
- svr.GracefulClose(time.Duration(0))
-}
-
-func (svr *VirtualService) GracefulClose(d time.Duration) {
- atomic.StoreUint32(&svr.exit, 1)
- svr.pxy.Close()
-
- if svr.cancel != nil {
- svr.cancel()
- }
-
- svr.replyCh <- &VProxyError{}
-}
-
-func (svr *VirtualService) loopCheck() {
- <-svr.sshSvc.Exit()
- svr.pxy.Close()
- log.Info("virtual client service close")
-}
-
-func (svr *VirtualService) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
- var pxyConf v1.ProxyConfigurer
- pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, &svr.serverCfg)
- if err != nil {
- return
- }
-
- // User info
- userInfo := plugin.UserInfo{
- User: svr.loginMsg.User,
- Metas: svr.loginMsg.Metas,
- RunID: svr.runID,
- }
-
- svr.pxy, err = proxy.NewProxy(svr.ctx, &proxy.Options{
- LoginMsg: svr.loginMsg,
- UserInfo: userInfo,
- Configurer: pxyConf,
- ResourceController: svr.rc,
-
- GetWorkConnFn: svr.GetWorkConn,
- PoolCount: 10,
-
- ServerCfg: &svr.serverCfg,
- })
- if err != nil {
- return remoteAddr, err
- }
-
- remoteAddr, err = svr.pxy.Run()
- if err != nil {
- log.Warn("proxy run error: %v", err)
- return
- }
-
- defer func() {
- if err != nil {
- log.Warn("proxy close")
- svr.pxy.Close()
- }
- }()
-
- return
-}
-
-func (svr *VirtualService) GetWorkConn() (workConn net.Conn, err error) {
- // tell ssh client open a new stream for work
- payload := forwardedTCPPayload{
- Addr: svr.serverCfg.BindAddr, // TODO refine
- Port: uint32(svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort),
- }
-
- channel, reqs, err := svr.sshSvc.SSHConn().OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(payload))
- if err != nil {
- return nil, fmt.Errorf("open ssh channel error: %v", err)
- }
- go ssh.DiscardRequests(reqs)
-
- workConn = frp_net.WrapReadWriteCloserToConn(channel, svr.sshSvc.tcpConn)
- return workConn, nil
-}
diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go
index d92b1a8205c..5bc75921cbd 100644
--- a/pkg/transport/tls.go
+++ b/pkg/transport/tls.go
@@ -128,3 +128,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
return base, nil
}
+
+func NewRandomPrivateKey() ([]byte, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+ keyPEM := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(key),
+ })
+ return keyPEM, nil
+}
diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go
index b5746f9dfb8..7b69dcafe0a 100644
--- a/pkg/util/xlog/xlog.go
+++ b/pkg/util/xlog/xlog.go
@@ -15,40 +15,81 @@
package xlog
import (
+ "sort"
+
"github.com/fatedier/frp/pkg/util/log"
)
+type LogPrefix struct {
+ // Name is the name of the prefix, it won't be displayed in log but used to identify the prefix.
+ Name string
+ // Value is the value of the prefix, it will be displayed in log.
+ Value string
+ // The prefix with higher priority will be displayed first, default is 10.
+ Priority int
+}
+
// Logger is not thread safety for operations on prefix
type Logger struct {
- prefixes []string
+ prefixes []LogPrefix
prefixString string
}
func New() *Logger {
return &Logger{
- prefixes: make([]string, 0),
+ prefixes: make([]LogPrefix, 0),
}
}
-func (l *Logger) ResetPrefixes() (old []string) {
+func (l *Logger) ResetPrefixes() (old []LogPrefix) {
old = l.prefixes
- l.prefixes = make([]string, 0)
+ l.prefixes = make([]LogPrefix, 0)
l.prefixString = ""
return
}
func (l *Logger) AppendPrefix(prefix string) *Logger {
- l.prefixes = append(l.prefixes, prefix)
- l.prefixString += "[" + prefix + "] "
+ return l.AddPrefix(LogPrefix{
+ Name: prefix,
+ Value: prefix,
+ Priority: 10,
+ })
+}
+
+func (l *Logger) AddPrefix(prefix LogPrefix) *Logger {
+ found := false
+ if prefix.Priority <= 0 {
+ prefix.Priority = 10
+ }
+ for _, p := range l.prefixes {
+ if p.Name == prefix.Name {
+ found = true
+ p.Value = prefix.Value
+ p.Priority = prefix.Priority
+ }
+ }
+ if !found {
+ l.prefixes = append(l.prefixes, prefix)
+ }
+ l.renderPrefixString()
return l
}
-func (l *Logger) Spawn() *Logger {
- nl := New()
+func (l *Logger) renderPrefixString() {
+ sort.SliceStable(l.prefixes, func(i, j int) bool {
+ return l.prefixes[i].Priority < l.prefixes[j].Priority
+ })
+ l.prefixString = ""
for _, v := range l.prefixes {
- nl.AppendPrefix(v)
+ l.prefixString += "[" + v.Value + "] "
}
+}
+
+func (l *Logger) Spawn() *Logger {
+ nl := New()
+ nl.prefixes = append(nl.prefixes, l.prefixes...)
+ nl.renderPrefixString()
return nl
}
diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go
new file mode 100644
index 00000000000..d0369a1af8a
--- /dev/null
+++ b/pkg/virtual/client.go
@@ -0,0 +1,92 @@
+// 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"
+ utilnet "github.com/fatedier/frp/pkg/util/net"
+)
+
+type Client struct {
+ l *utilnet.InternalListener
+ svr *client.Service
+}
+
+func NewClient(cfg *v1.ClientCommonConfig) *Client {
+ cfg.Complete()
+
+ ln := utilnet.NewInternalListener()
+
+ svr := client.NewService(cfg, nil, nil, "")
+ svr.SetConnectorCreator(func(context.Context, *v1.ClientCommonConfig) client.Connector {
+ return &pipeConnector{
+ peerListener: ln,
+ }
+ })
+
+ return &Client{
+ l: ln,
+ svr: svr,
+ }
+}
+
+func (c *Client) PeerListener() net.Listener {
+ return c.l
+}
+
+func (c *Client) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
+ c.svr.SetInWorkConnCallback(cb)
+}
+
+func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) {
+ _ = c.svr.ReloadConf(proxyCfgs, nil)
+}
+
+func (c *Client) Run(ctx context.Context) error {
+ return c.svr.Run(ctx)
+}
+
+func (c *Client) Close() {
+ c.l.Close()
+ c.svr.Close()
+}
+
+type pipeConnector struct {
+ peerListener *utilnet.InternalListener
+}
+
+func (pc *pipeConnector) Open() error {
+ return nil
+}
+
+func (pc *pipeConnector) Connect() (net.Conn, error) {
+ c1, c2 := net.Pipe()
+ if err := pc.peerListener.PutConn(c1); err != nil {
+ c1.Close()
+ c2.Close()
+ return nil, err
+ }
+ return c2, nil
+}
+
+func (pc *pipeConnector) Close() error {
+ pc.peerListener.Close()
+ return nil
+}
diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go
index 5ea99f1ea6c..fe6f781b728 100644
--- a/server/proxy/proxy.go
+++ b/server/proxy/proxy.go
@@ -21,7 +21,6 @@ import (
"net"
"reflect"
"strconv"
- "strings"
"sync"
"time"
@@ -230,14 +229,8 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
return
}
- var workConn net.Conn
-
// try all connections from the pool
- if strings.HasPrefix(pxy.GetLoginMsg().User, v1.SSHClientLoginUserPrefix) {
- workConn, err = pxy.getWorkConnFn()
- } else {
- workConn, err = pxy.GetWorkConnFromPool(userConn.RemoteAddr(), userConn.LocalAddr())
- }
+ workConn, err := pxy.GetWorkConnFromPool(userConn.RemoteAddr(), userConn.LocalAddr())
if err != nil {
return
}
diff --git a/server/service.go b/server/service.go
index 2ca501be8e3..02efec91a28 100644
--- a/server/service.go
+++ b/server/service.go
@@ -18,13 +18,10 @@ import (
"bytes"
"context"
"crypto/tls"
- "errors"
"fmt"
"io"
"net"
"net/http"
- "os"
- "reflect"
"strconv"
"time"
@@ -32,7 +29,6 @@ import (
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
- "golang.org/x/crypto/ssh"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth"
@@ -41,7 +37,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
plugin "github.com/fatedier/frp/pkg/plugin/server"
- frpssh "github.com/fatedier/frp/pkg/ssh"
+ "github.com/fatedier/frp/pkg/ssh"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
utilnet "github.com/fatedier/frp/pkg/util/net"
@@ -71,10 +67,6 @@ type Service struct {
// Accept connections from client
listener net.Listener
- // Accept connections using ssh
- sshListener net.Listener
- sshConfig *ssh.ServerConfig
-
// Accept connections using kcp
kcpListener net.Listener
@@ -87,6 +79,8 @@ type Service struct {
// Accept frp tls connections
tlsListener net.Listener
+ virtualListener *utilnet.InternalListener
+
// Manage all controllers
ctlManager *ControlManager
@@ -102,6 +96,8 @@ type Service struct {
// All resource managers and controllers
rc *controller.ResourceController
+ sshTunnelGateway *ssh.Gateway
+
// Verifies authentication based on selected method
authVerifier auth.Verifier
@@ -133,6 +129,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
},
+ virtualListener: utilnet.NewInternalListener(),
httpVhostRouter: vhost.NewRouters(),
authVerifier: auth.NewAuthVerifier(cfg.Auth),
tlsConfig: tlsConfig,
@@ -208,67 +205,6 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
svr.listener = ln
log.Info("frps tcp listen on %s", address)
- if cfg.SSHTunnelGateway.BindPort > 0 {
-
- if cfg.SSHTunnelGateway.PublicKeyFilesPath != "" {
- cfg.SSHTunnelGateway.PublicKeyFilesMap, err = v1.LoadSSHPublicKeyFilesInDir(cfg.SSHTunnelGateway.PublicKeyFilesPath)
- if err != nil {
- return nil, fmt.Errorf("load ssh all public key files error: %v", err)
- }
- log.Info("load %v public key files success", cfg.SSHTunnelGateway.PublicKeyFilesPath)
- }
-
- svr.sshConfig = &ssh.ServerConfig{
- NoClientAuth: lo.If(cfg.SSHTunnelGateway.PublicKeyFilesPath == "", true).Else(false),
-
- PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
- parsedAuthorizedKey, ok := cfg.SSHTunnelGateway.PublicKeyFilesMap[ssh.FingerprintSHA256(key)]
- if !ok {
- return nil, errors.New("cannot find public key file")
- }
-
- if key.Type() == parsedAuthorizedKey.Type() && reflect.DeepEqual(parsedAuthorizedKey, key) {
- return &ssh.Permissions{
- Extensions: map[string]string{},
- }, nil
- }
- return nil, fmt.Errorf("unknown public key for %q", conn.User())
- },
- }
-
- var privateBytes []byte
- if cfg.SSHTunnelGateway.PrivateKeyFilePath != "" {
- privateBytes, err = os.ReadFile(cfg.SSHTunnelGateway.PrivateKeyFilePath)
- if err != nil {
- log.Error("Failed to load private key")
- return nil, err
- }
- log.Info("load %v private key file success", cfg.SSHTunnelGateway.PrivateKeyFilePath)
- } else {
- privateBytes, err = v1.GeneratePrivateKey()
- if err != nil {
- log.Error("Failed to load private key")
- return nil, err
- }
- log.Info("auto gen private key file success")
- }
- private, err := ssh.ParsePrivateKey(privateBytes)
- if err != nil {
- log.Error("Failed to parse private key, error: %v", err)
- return nil, err
- }
-
- svr.sshConfig.AddHostKey(private)
-
- sshAddr := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.SSHTunnelGateway.BindPort))
- svr.sshListener, err = net.Listen("tcp", sshAddr)
- if err != nil {
- log.Error("Failed to listen on %v, error: %v", sshAddr, err)
- return nil, err
- }
- log.Info("ssh server listening on %v", sshAddr)
- }
-
// Listen for accepting connections from client using kcp protocol.
if cfg.KCPBindPort > 0 {
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
@@ -293,7 +229,17 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
err = 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.virtualListener)
+ if err != nil {
+ err = fmt.Errorf("create ssh gateway error: %v", err)
+ return nil, err
+ }
+ svr.sshTunnelGateway = sshGateway
+ log.Info("frps sshTunnelGateway listen on port %d", cfg.SSHTunnelGateway.BindPort)
}
// Listen for accepting connections from client using websocket protocol.
@@ -396,23 +342,26 @@ func (svr *Service) Run(ctx context.Context) {
svr.ctx = ctx
svr.cancel = cancel
- if svr.sshListener != nil {
- go svr.HandleSSHListener(svr.sshListener)
- }
+ go svr.HandleListener(svr.virtualListener, true)
if svr.kcpListener != nil {
- go svr.HandleListener(svr.kcpListener)
+ go svr.HandleListener(svr.kcpListener, false)
}
if svr.quicListener != nil {
go svr.HandleQUICListener(svr.quicListener)
}
- go svr.HandleListener(svr.websocketListener)
- go svr.HandleListener(svr.tlsListener)
+ go svr.HandleListener(svr.websocketListener, false)
+ go svr.HandleListener(svr.tlsListener, false)
if svr.rc.NatHoleController != nil {
go svr.rc.NatHoleController.CleanWorker(svr.ctx)
}
- svr.HandleListener(svr.listener)
+
+ if svr.sshTunnelGateway != nil {
+ go svr.sshTunnelGateway.Run()
+ }
+
+ svr.HandleListener(svr.listener, false)
<-svr.ctx.Done()
// service context may not be canceled by svr.Close(), we should call it here to release resources
@@ -422,10 +371,6 @@ func (svr *Service) Run(ctx context.Context) {
}
func (svr *Service) Close() error {
- if svr.sshListener != nil {
- svr.sshListener.Close()
- svr.sshListener = nil
- }
if svr.kcpListener != nil {
svr.kcpListener.Close()
svr.kcpListener = nil
@@ -516,7 +461,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
}
}
-func (svr *Service) HandleListener(l net.Listener) {
+func (svr *Service) HandleListener(l net.Listener, internal bool) {
// Listen for incoming connections from client.
for {
c, err := l.Accept()
@@ -532,8 +477,9 @@ func (svr *Service) HandleListener(l net.Listener) {
log.Trace("start check TLS connection...")
originConn := c
+ forceTLS := svr.cfg.Transport.TLS.Force && !internal
var isTLS, custom bool
- c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout)
+ c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
if err != nil {
log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
originConn.Close()
@@ -543,7 +489,7 @@ func (svr *Service) HandleListener(l net.Listener) {
// Start a new goroutine to handle connection.
go func(ctx context.Context, frpConn net.Conn) {
- if lo.FromPtr(svr.cfg.Transport.TCPMux) {
+ if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal {
fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard
@@ -571,52 +517,6 @@ func (svr *Service) HandleListener(l net.Listener) {
}
}
-func (svr *Service) HandleSSHListener(listener net.Listener) {
- for {
- tcpConn, err := listener.Accept()
- if err != nil {
- log.Error("failed to accept incoming ssh connection (%s)", err)
- return
- }
- log.Info("new tcp conn connected: %v", tcpConn.RemoteAddr().String())
-
- pxyPayloadCh := make(chan v1.ProxyConfigurer)
- replyCh := make(chan interface{})
-
- ss, err := frpssh.NewSSHService(tcpConn, svr.sshConfig, pxyPayloadCh, replyCh)
- if err != nil {
- log.Error("new ssh service error: %v", err)
- continue
- }
- ss.Run()
-
- go func() {
- for {
- pxyCfg := <-pxyPayloadCh
-
- ctx := context.Background()
-
- // TODO fill client common config and login msg
- vs, err := frpssh.NewVirtualService(ctx, v1.ClientCommonConfig{}, *svr.cfg,
- msg.Login{User: v1.SSHClientLoginUserPrefix + tcpConn.RemoteAddr().String()},
- svr.rc, pxyCfg, ss, replyCh)
- if err != nil {
- log.Error("new virtual service error: %v", err)
- ss.Close()
- return
- }
-
- err = vs.Run(ctx)
- if err != nil {
- log.Error("proxy run error: %v", err)
- vs.Close()
- return
- }
- }
- }()
- }
-}
-
func (svr *Service) HandleQUICListener(l *quic.Listener) {
// Listen for incoming connections from client.
for {
From 69ae2b0b690b659d023d1ec2d5b4b33e89def555 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 27 Nov 2023 15:47:49 +0800
Subject: [PATCH 10/21] optimize some code (#3801)
---
Release.md | 5 +-
client/admin.go | 85 -----------
client/admin_api.go | 39 ++++-
client/connector.go | 28 ++--
client/control.go | 123 ++++++++-------
client/proxy/proxy_manager.go | 17 ++-
client/proxy/sudp.go | 4 +-
client/proxy/udp.go | 4 +-
client/proxy/xtcp.go | 6 +-
client/service.go | 230 +++++++++++++++++++----------
client/visitor/sudp.go | 4 +-
client/visitor/visitor.go | 6 +-
client/visitor/visitor_manager.go | 18 ++-
client/visitor/xtcp.go | 6 +-
cmd/frpc/sub/root.go | 18 ++-
cmd/frpc/sub/verify.go | 4 +-
pkg/auth/pass.go | 31 ++++
pkg/config/flags.go | 5 +
pkg/config/legacy/parse.go | 4 +-
pkg/config/load.go | 27 ++--
pkg/config/v1/proxy.go | 4 +-
pkg/config/v1/validation/client.go | 4 +-
pkg/msg/msg.go | 12 ++
pkg/plugin/client/http2https.go | 4 +-
pkg/plugin/client/http_proxy.go | 4 +-
pkg/plugin/client/https2http.go | 4 +-
pkg/plugin/client/https2https.go | 4 +-
pkg/plugin/client/socks5.go | 4 +-
pkg/plugin/client/static_file.go | 8 +-
pkg/sdk/client/client.go | 4 +-
pkg/ssh/gateway.go | 19 +--
pkg/ssh/server.go | 107 +++++++++++---
pkg/util/{util => http}/http.go | 2 +-
pkg/util/http/server.go | 128 ++++++++++++++++
pkg/util/net/dns.go | 33 +++++
pkg/util/net/listener.go | 5 +-
pkg/util/tcpmux/httpconnect.go | 10 +-
pkg/util/vhost/http.go | 18 +--
pkg/util/vhost/resource.go | 4 +-
pkg/util/vhost/vhost.go | 4 +-
pkg/virtual/client.go | 57 ++++---
server/control.go | 18 ++-
server/dashboard.go | 99 -------------
server/dashboard_api.go | 43 +++++-
server/proxy/http.go | 6 +-
server/proxy/proxy.go | 4 +-
server/proxy/udp.go | 4 +-
server/service.go | 176 +++++++++++-----------
server/visitor/visitor.go | 15 +-
test/e2e/legacy/basic/tcpmux.go | 4 +-
test/e2e/pkg/request/request.go | 4 +-
test/e2e/v1/basic/tcpmux.go | 4 +-
52 files changed, 880 insertions(+), 600 deletions(-)
delete mode 100644 client/admin.go
create mode 100644 pkg/auth/pass.go
rename pkg/util/{util => http}/http.go (99%)
create mode 100644 pkg/util/http/server.go
create mode 100644 pkg/util/net/dns.go
delete mode 100644 server/dashboard.go
diff --git a/Release.md b/Release.md
index b4245189c41..ca8f3a72057 100644
--- a/Release.md
+++ b/Release.md
@@ -1,6 +1,9 @@
### Features
-* New command line parameter `--strict_config` is added to enable strict configuration validation mode. It will throw an error for non-existent fields instead of ignoring them.
+* New command line parameter `--strict_config` is added to enable strict configuration validation mode. It will throw an error for non-existent fields instead of ignoring them. In future versions, we may set the default value of this parameter to true.
+* 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
diff --git a/client/admin.go b/client/admin.go
deleted file mode 100644
index da8bab1bd5c..00000000000
--- a/client/admin.go
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright 2017 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
- "net"
- "net/http"
- "net/http/pprof"
- "time"
-
- "github.com/gorilla/mux"
-
- "github.com/fatedier/frp/assets"
- utilnet "github.com/fatedier/frp/pkg/util/net"
-)
-
-var (
- httpServerReadTimeout = 60 * time.Second
- httpServerWriteTimeout = 60 * time.Second
-)
-
-func (svr *Service) RunAdminServer(address string) (err error) {
- // url router
- router := mux.NewRouter()
-
- router.HandleFunc("/healthz", svr.healthz)
-
- // debug
- if svr.cfg.WebServer.PprofEnable {
- router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
- router.HandleFunc("/debug/pprof/profile", pprof.Profile)
- router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
- router.HandleFunc("/debug/pprof/trace", pprof.Trace)
- router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
- }
-
- subRouter := router.NewRoute().Subrouter()
- user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
- subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-
- // api, see admin_api.go
- subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
- subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
- subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
- subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
- subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
-
- // view
- subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
- subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
- subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
- })
-
- server := &http.Server{
- Addr: address,
- Handler: router,
- ReadTimeout: httpServerReadTimeout,
- WriteTimeout: httpServerWriteTimeout,
- }
- if address == "" {
- address = ":http"
- }
- ln, err := net.Listen("tcp", address)
- if err != nil {
- return err
- }
-
- go func() {
- _ = server.Serve(ln)
- }()
- return
-}
diff --git a/client/admin_api.go b/client/admin_api.go
index 3a56a99fbaa..5e4d67c60fa 100644
--- a/client/admin_api.go
+++ b/client/admin_api.go
@@ -31,7 +31,9 @@ import (
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
type GeneralResponse struct {
@@ -39,6 +41,29 @@ type GeneralResponse struct {
Msg string
}
+func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
+ helper.Router.HandleFunc("/healthz", svr.healthz)
+ subRouter := helper.Router.NewRoute().Subrouter()
+
+ subRouter.Use(helper.AuthMiddleware.Middleware)
+
+ // api, see admin_api.go
+ subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
+ subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
+ subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
+ subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
+ subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
+
+ // view
+ subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
+ subRouter.PathPrefix("/static/").Handler(
+ netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
+ ).Methods("GET")
+ subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
+ })
+}
+
// /healthz
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
@@ -62,21 +87,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
}
}()
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile, strictConfigMode)
+ cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
return
}
- if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil {
+ if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
return
}
- if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
+ if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
res.Code = 500
res.Msg = err.Error()
log.Warn("reload frpc proxy config error: %s", res.Msg)
@@ -158,7 +183,7 @@ func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
ps := ctl.pm.GetAllProxyStatus()
for _, status := range ps {
- res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.cfg.ServerAddr))
+ res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
}
for _, arrs := range res {
@@ -184,14 +209,14 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
}
}()
- if svr.cfgFile == "" {
+ if svr.configFilePath == "" {
res.Code = 400
res.Msg = "frpc has no config file path"
log.Warn("%s", res.Msg)
return
}
- content, err := os.ReadFile(svr.cfgFile)
+ content, err := os.ReadFile(svr.configFilePath)
if err != nil {
res.Code = 400
res.Msg = err.Error()
@@ -230,7 +255,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
return
}
- if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil {
+ if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil {
res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warn("%s", res.Msg)
diff --git a/client/connector.go b/client/connector.go
index 2ff9b491a9a..ba1441468ee 100644
--- a/client/connector.go
+++ b/client/connector.go
@@ -21,6 +21,7 @@ import (
"net"
"strconv"
"strings"
+ "sync"
"time"
libdial "github.com/fatedier/golib/net/dial"
@@ -30,7 +31,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -48,6 +49,7 @@ type defaultConnectorImpl struct {
muxSession *fmux.Session
quicConn quic.Connection
+ closeOnce sync.Once
}
func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
@@ -130,7 +132,7 @@ func (c *defaultConnectorImpl) Connect() (net.Conn, error) {
if err != nil {
return nil, err
}
- return utilnet.QuicStreamToNetConn(stream, c.quicConn), nil
+ return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil
} else if c.muxSession != nil {
stream, err := c.muxSession.OpenStream()
if err != nil {
@@ -177,19 +179,19 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
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: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
- Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
+ 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: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
+ dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
- Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
+ Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
}
@@ -213,11 +215,13 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
}
func (c *defaultConnectorImpl) Close() error {
- if c.quicConn != nil {
- _ = c.quicConn.CloseWithError(0, "")
- }
- if c.muxSession != nil {
- _ = c.muxSession.Close()
- }
+ c.closeOnce.Do(func() {
+ if c.quicConn != nil {
+ _ = c.quicConn.CloseWithError(0, "")
+ }
+ if c.muxSession != nil {
+ _ = c.muxSession.Close()
+ }
+ })
return nil
}
diff --git a/client/control.go b/client/control.go
index be028ec43f3..e4b01ae8c21 100644
--- a/client/control.go
+++ b/client/control.go
@@ -28,39 +28,42 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"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/wait"
"github.com/fatedier/frp/pkg/util/xlog"
)
+type SessionContext struct {
+ // The client common configuration.
+ Common *v1.ClientCommonConfig
+
+ // Unique ID obtained from frps.
+ // It should be attached to the login message when reconnecting.
+ RunID string
+ // Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
+ Conn net.Conn
+ // Indicates whether the connection is encrypted.
+ ConnEncrypted bool
+ // Sets authentication based on selected method
+ AuthSetter auth.Setter
+ // Connector is used to create new connections, which could be real TCP connections or virtual streams.
+ Connector Connector
+}
+
type Control struct {
// service context
ctx context.Context
xl *xlog.Logger
- // The client configuration
- clientCfg *v1.ClientCommonConfig
-
- // sets authentication based on selected method
- authSetter auth.Setter
-
- // Unique ID obtained from frps.
- // It should be attached to the login message when reconnecting.
- runID string
+ // session context
+ sessionCtx *SessionContext
// manage all proxies
- pxyCfgs []v1.ProxyConfigurer
- pm *proxy.Manager
+ pm *proxy.Manager
// manage all visitors
vm *visitor.Manager
- // control connection. Once conn is closed, the msgDispatcher and the entire Control will exit.
- conn net.Conn
-
- // use connector to create new connections, which could be real TCP connections or virtual streams.
- connector Connector
-
doneCh chan struct{}
// of time.Time, last time got the Pong message
@@ -76,50 +79,41 @@ type Control struct {
msgDispatcher *msg.Dispatcher
}
-func NewControl(
- ctx context.Context, runID string, conn net.Conn, connector Connector,
- clientCfg *v1.ClientCommonConfig,
- pxyCfgs []v1.ProxyConfigurer,
- visitorCfgs []v1.VisitorConfigurer,
- authSetter auth.Setter,
-) (*Control, error) {
+func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) {
// new xlog instance
ctl := &Control{
ctx: ctx,
xl: xlog.FromContextSafe(ctx),
- clientCfg: clientCfg,
- authSetter: authSetter,
- runID: runID,
- pxyCfgs: pxyCfgs,
- conn: conn,
- connector: connector,
+ sessionCtx: sessionCtx,
doneCh: make(chan struct{}),
}
ctl.lastPong.Store(time.Now())
- cryptoRW, err := utilnet.NewCryptoReadWriter(conn, []byte(clientCfg.Auth.Token))
- if err != nil {
- return nil, err
+ if sessionCtx.ConnEncrypted {
+ cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
+ if err != nil {
+ return nil, err
+ }
+ ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
+ } else {
+ ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
}
-
- ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
- ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
- ctl.vm = visitor.NewManager(ctl.ctx, ctl.runID, ctl.clientCfg, ctl.connectServer, ctl.msgTransporter)
- ctl.vm.Reload(visitorCfgs)
+ ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter)
+ ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common, ctl.connectServer, ctl.msgTransporter)
return ctl, nil
}
-func (ctl *Control) Run() {
+func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) {
go ctl.worker()
// start all proxies
- ctl.pm.Reload(ctl.pxyCfgs)
+ ctl.pm.UpdateAll(proxyCfgs)
// start all visitors
- go ctl.vm.Run()
+ ctl.vm.UpdateAll(visitorCfgs)
}
func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
@@ -135,9 +129,9 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
}
m := &msg.NewWorkConn{
- RunID: ctl.runID,
+ RunID: ctl.sessionCtx.RunID,
}
- if err = ctl.authSetter.SetNewWorkConn(m); err != nil {
+ if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
xl.Warn("error during NewWorkConn authentication: %v", err)
return
}
@@ -193,13 +187,19 @@ func (ctl *Control) handlePong(m msg.Message) {
if inMsg.Error != "" {
xl.Error("Pong message contains error: %s", inMsg.Error)
- ctl.conn.Close()
+ ctl.closeSession()
return
}
ctl.lastPong.Store(time.Now())
xl.Debug("receive heartbeat from server")
}
+// closeSession closes the control connection.
+func (ctl *Control) closeSession() {
+ ctl.sessionCtx.Conn.Close()
+ ctl.sessionCtx.Connector.Close()
+}
+
func (ctl *Control) Close() error {
return ctl.GracefulClose(0)
}
@@ -210,8 +210,7 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
time.Sleep(d)
- ctl.conn.Close()
- ctl.connector.Close()
+ ctl.closeSession()
return nil
}
@@ -221,8 +220,8 @@ func (ctl *Control) Done() <-chan struct{} {
}
// connectServer return a new connection to frps
-func (ctl *Control) connectServer() (conn net.Conn, err error) {
- return ctl.connector.Connect()
+func (ctl *Control) connectServer() (net.Conn, error) {
+ return ctl.sessionCtx.Connector.Connect()
}
func (ctl *Control) registerMsgHandlers() {
@@ -238,12 +237,12 @@ func (ctl *Control) heartbeatWorker() {
// TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
- if ctl.clientCfg.Transport.HeartbeatInterval > 0 {
+ if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
// send heartbeat to server
sendHeartBeat := func() error {
xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{}
- if err := ctl.authSetter.SetPing(pingMsg); err != nil {
+ if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
xl.Warn("error during ping authentication: %v, skip sending ping message", err)
return err
}
@@ -253,24 +252,24 @@ func (ctl *Control) heartbeatWorker() {
go wait.BackoffUntil(sendHeartBeat,
wait.NewFastBackoffManager(wait.FastBackoffOptions{
- Duration: time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second,
+ Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
InitDurationIfFail: time.Second,
Factor: 2.0,
Jitter: 0.1,
- MaxDuration: time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second,
+ MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second,
}),
true, ctl.doneCh,
)
}
// 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) {
+ 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.clientCfg.Transport.HeartbeatTimeout)*time.Second {
+ if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
- ctl.conn.Close()
+ ctl.closeSession()
return
}
}, time.Second, ctl.doneCh)
@@ -282,17 +281,15 @@ func (ctl *Control) worker() {
go ctl.msgDispatcher.Run()
<-ctl.msgDispatcher.Done()
- ctl.conn.Close()
+ ctl.closeSession()
ctl.pm.Close()
ctl.vm.Close()
- ctl.connector.Close()
-
close(ctl.doneCh)
}
-func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
- ctl.vm.Reload(visitorCfgs)
- ctl.pm.Reload(pxyCfgs)
+func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
+ ctl.vm.UpdateAll(visitorCfgs)
+ ctl.pm.UpdateAll(proxyCfgs)
return nil
}
diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go
index dadf648150a..12e2f6cfee8 100644
--- a/client/proxy/proxy_manager.go
+++ b/client/proxy/proxy_manager.go
@@ -120,9 +120,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
return ps
}
-func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
+func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) {
+ pm.mu.RLock()
+ defer pm.mu.RUnlock()
+ if pxy, ok := pm.proxies[name]; ok {
+ return pxy.GetStatus(), true
+ }
+ return nil, false
+}
+
+func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
xl := xlog.FromContextSafe(pm.ctx)
- pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string {
+ proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string {
return c.GetBaseConfig().Name
})
pm.mu.Lock()
@@ -131,7 +140,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
delPxyNames := make([]string, 0)
for name, pxy := range pm.proxies {
del := false
- cfg, ok := pxyCfgsMap[name]
+ cfg, ok := proxyCfgsMap[name]
if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) {
del = true
}
@@ -147,7 +156,7 @@ func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) {
}
addPxyNames := make([]string, 0)
- for _, cfg := range pxyCfgs {
+ for _, cfg := range proxyCfgs {
name := cfg.GetBaseConfig().Name
if _, ok := pm.proxies[name]; !ok {
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter)
diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go
index f9fe53bccc0..4d06170dc90 100644
--- a/client/proxy/sudp.go
+++ b/client/proxy/sudp.go
@@ -31,7 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -101,7 +101,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc)
}
- conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
+ conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
workConn := conn
readCh := make(chan *msg.UDPPacket, 1024)
diff --git a/client/proxy/udp.go b/client/proxy/udp.go
index d8590f68df4..38d14ff598b 100644
--- a/client/proxy/udp.go
+++ b/client/proxy/udp.go
@@ -30,7 +30,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -112,7 +112,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
if pxy.cfg.Transport.UseCompression {
rwc = libio.WithCompression(rwc)
}
- conn = utilnet.WrapReadWriteCloserToConn(rwc, conn)
+ conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
pxy.mu.Lock()
pxy.workConn = conn
diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go
index b286a9318b9..e5e5d47e280 100644
--- a/client/proxy/xtcp.go
+++ b/client/proxy/xtcp.go
@@ -29,7 +29,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -133,7 +133,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
}
defer lConn.Close()
- remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
+ remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil {
xl.Warn("create kcp connection from udp connection error: %v", err)
return
@@ -194,6 +194,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star
_ = c.CloseWithError(0, "")
return
}
- go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
+ go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey))
}
}
diff --git a/client/service.go b/client/service.go
index 7c3cd03926d..5db1bd283d5 100644
--- a/client/service.go
+++ b/client/service.go
@@ -20,18 +20,19 @@ import (
"fmt"
"net"
"runtime"
- "strconv"
"sync"
"time"
"github.com/fatedier/golib/crypto"
"github.com/samber/lo"
- "github.com/fatedier/frp/assets"
+ "github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
@@ -41,66 +42,106 @@ func init() {
crypto.DefaultSalt = "frp"
}
-// Service is a client service.
-type Service struct {
- // uniq id got from frps, attach it in loginMsg
- runID string
+// 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
+}
- // manager control connection with server
- ctl *Control
+// setServiceOptionsDefault sets the default values for ServiceOptions.
+func setServiceOptionsDefault(options *ServiceOptions) {
+ if options.Common != nil {
+ options.Common.Complete()
+ }
+ if options.ConnectorCreator == nil {
+ options.ConnectorCreator = NewConnector
+ }
+}
+
+// Service is the client service that connects to frps and provides proxy services.
+type Service struct {
ctlMu sync.RWMutex
+ // manager control connection with server
+ ctl *Control
+ // Uniq id got from frps, it will be attached to loginMsg.
+ runID string
// Sets authentication based on selected method
authSetter auth.Setter
- cfg *v1.ClientCommonConfig
- pxyCfgs []v1.ProxyConfigurer
- visitorCfgs []v1.VisitorConfigurer
+ // web server for admin UI and apis
+ webServer *httppkg.Server
+
cfgMu sync.RWMutex
+ common *v1.ClientCommonConfig
+ proxyCfgs []v1.ProxyConfigurer
+ visitorCfgs []v1.VisitorConfigurer
+ clientSpec *msg.ClientSpec
// The configuration file used to initialize this client, or an empty
// string if no configuration file was used.
- cfgFile string
+ configFilePath string
// service context
ctx context.Context
// call cancel to stop service
- cancel context.CancelFunc
- gracefulDuration time.Duration
+ cancel context.CancelFunc
+ gracefulShutdownDuration time.Duration
- connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
- inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
+ connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
+ handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
}
-func NewService(
- cfg *v1.ClientCommonConfig,
- pxyCfgs []v1.ProxyConfigurer,
- visitorCfgs []v1.VisitorConfigurer,
- cfgFile string,
-) *Service {
- return &Service{
- authSetter: auth.NewAuthSetter(cfg.Auth),
- cfg: cfg,
- cfgFile: cfgFile,
- pxyCfgs: pxyCfgs,
- visitorCfgs: visitorCfgs,
+func NewService(options ServiceOptions) (*Service, error) {
+ setServiceOptionsDefault(&options)
+
+ var webServer *httppkg.Server
+ if options.Common.WebServer.Port > 0 {
+ ws, err := httppkg.NewServer(options.Common.WebServer)
+ if err != nil {
+ return nil, err
+ }
+ webServer = ws
+ }
+ s := &Service{
ctx: context.Background(),
- connectorCreator: NewConnector,
+ authSetter: auth.NewAuthSetter(options.Common.Auth),
+ webServer: webServer,
+ common: options.Common,
+ configFilePath: options.ConfigFilePath,
+ proxyCfgs: options.ProxyCfgs,
+ visitorCfgs: options.VisitorCfgs,
+ clientSpec: options.ClientSpec,
+ connectorCreator: options.ConnectorCreator,
+ handleWorkConnCb: options.HandleWorkConnCb,
}
-}
-
-func (svr *Service) SetConnectorCreator(h func(context.Context, *v1.ClientCommonConfig) Connector) {
- svr.connectorCreator = h
-}
-
-func (svr *Service) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
- svr.inWorkConnCallback = cb
-}
-
-func (svr *Service) GetController() *Control {
- svr.ctlMu.RLock()
- defer svr.ctlMu.RUnlock()
- return svr.ctl
+ if webServer != nil {
+ webServer.RouteRegister(s.registerRouteHandlers)
+ }
+ return s, nil
}
func (svr *Service) Run(ctx context.Context) error {
@@ -109,38 +150,25 @@ func (svr *Service) Run(ctx context.Context) error {
svr.cancel = cancel
// set custom DNSServer
- if svr.cfg.DNSServer != "" {
- dnsAddr := svr.cfg.DNSServer
- if _, _, err := net.SplitHostPort(dnsAddr); err != nil {
- dnsAddr = net.JoinHostPort(dnsAddr, "53")
- }
- // Change default dns server for frpc
- net.DefaultResolver = &net.Resolver{
- PreferGo: true,
- Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
- return net.Dial("udp", dnsAddr)
- },
- }
+ if svr.common.DNSServer != "" {
+ netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
}
- // login to frps
- svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.cfg.LoginFailExit))
+ // first login to frps
+ svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
if svr.ctl == nil {
return fmt.Errorf("the process exited because the first login to the server failed, and the loginFailExit feature is enabled")
}
go svr.keepControllerWorking()
- if svr.cfg.WebServer.Port != 0 {
- // Init admin server assets
- assets.Load(svr.cfg.WebServer.AssetsDir)
-
- address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port))
- err := svr.RunAdminServer(address)
- if err != nil {
- log.Warn("run admin server error: %v", err)
- }
- log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port)
+ if svr.webServer != nil {
+ go func() {
+ log.Info("admin server listen on %s", svr.webServer.Address())
+ if err := svr.webServer.Run(); err != nil {
+ log.Warn("admin server exit with error: %v", err)
+ }
+ }()
}
<-svr.ctx.Done()
svr.stop()
@@ -158,8 +186,12 @@ func (svr *Service) keepControllerWorking() {
// loopLoginUntilSuccess is another layer of loop that will continuously attempt to
// login to the server until successful.
svr.loopLoginUntilSuccess(20*time.Second, false)
- <-svr.ctl.Done()
- return errors.New("control is closed and try another loop")
+ if svr.ctl != nil {
+ <-svr.ctl.Done()
+ return errors.New("control is closed and try another loop")
+ }
+ // If the control is nil, it means that the login failed and the service is also closed.
+ return nil
}, wait.NewFastBackoffManager(
wait.FastBackoffOptions{
Duration: time.Second,
@@ -179,7 +211,7 @@ func (svr *Service) keepControllerWorking() {
// session: if it's not nil, using tcp mux
func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
xl := xlog.FromContextSafe(svr.ctx)
- connector = svr.connectorCreator(svr.ctx, svr.cfg)
+ connector = svr.connectorCreator(svr.ctx, svr.common)
if err = connector.Open(); err != nil {
return nil, nil, err
}
@@ -198,12 +230,15 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
loginMsg := &msg.Login{
Arch: runtime.GOARCH,
Os: runtime.GOOS,
- PoolCount: svr.cfg.Transport.PoolCount,
- User: svr.cfg.User,
+ PoolCount: svr.common.Transport.PoolCount,
+ User: svr.common.User,
Version: version.Full(),
Timestamp: time.Now().Unix(),
RunID: svr.runID,
- Metas: svr.cfg.Metadatas,
+ Metas: svr.common.Metadatas,
+ }
+ if svr.clientSpec != nil {
+ loginMsg.ClientSpec = *svr.clientSpec
}
// Add auth
@@ -250,16 +285,31 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
return err
}
- ctl, err := NewControl(svr.ctx, svr.runID, conn, connector,
- svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
+ 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.inWorkConnCallback)
+ ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
- ctl.Run()
+ ctl.Run(proxyCfgs, visitorCfgs)
// close and replace previous control
svr.ctlMu.Lock()
if svr.ctl != nil {
@@ -284,9 +334,9 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
wait.MergeAndCloseOnAnyStopChannel(svr.ctx.Done(), successCh))
}
-func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
+func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
svr.cfgMu.Lock()
- svr.pxyCfgs = pxyCfgs
+ svr.proxyCfgs = proxyCfgs
svr.visitorCfgs = visitorCfgs
svr.cfgMu.Unlock()
@@ -295,7 +345,7 @@ func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.Vi
svr.ctlMu.RUnlock()
if ctl != nil {
- return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
+ return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
}
return nil
}
@@ -305,7 +355,7 @@ func (svr *Service) Close() {
}
func (svr *Service) GracefulClose(d time.Duration) {
- svr.gracefulDuration = d
+ svr.gracefulShutdownDuration = d
svr.cancel()
}
@@ -313,7 +363,23 @@ func (svr *Service) stop() {
svr.ctlMu.Lock()
defer svr.ctlMu.Unlock()
if svr.ctl != nil {
- svr.ctl.GracefulClose(svr.gracefulDuration)
+ svr.ctl.GracefulClose(svr.gracefulShutdownDuration)
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()
+
+ if ctl == nil {
+ return nil, fmt.Errorf("control is not running")
+ }
+ ws, ok := ctl.pm.GetProxyStatus(name)
+ if !ok {
+ return nil, fmt.Errorf("proxy [%s] is not found", name)
+ }
+ return ws, nil
+}
diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go
index 159f46ee074..1d489bec42b 100644
--- a/client/visitor/sudp.go
+++ b/client/visitor/sudp.go
@@ -28,7 +28,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -242,7 +242,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
if sv.cfg.Transport.UseCompression {
remote = libio.WithCompression(remote)
}
- return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil
+ return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
}
func (sv *SUDPVisitor) Close() {
diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go
index 4cfd61062b0..d520f735ddc 100644
--- a/client/visitor/visitor.go
+++ b/client/visitor/visitor.go
@@ -21,7 +21,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -56,7 +56,7 @@ func NewVisitor(
clientCfg: clientCfg,
helper: helper,
ctx: xlog.NewContext(ctx, xl),
- internalLn: utilnet.NewInternalListener(),
+ internalLn: netpkg.NewInternalListener(),
}
switch cfg := cfg.(type) {
case *v1.STCPVisitorConfig:
@@ -84,7 +84,7 @@ type BaseVisitor struct {
clientCfg *v1.ClientCommonConfig
helper Helper
l net.Listener
- internalLn *utilnet.InternalListener
+ internalLn *netpkg.InternalListener
mu sync.RWMutex
ctx context.Context
diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go
index 4b235cdb113..4f31f2706ed 100644
--- a/client/visitor/visitor_manager.go
+++ b/client/visitor/visitor_manager.go
@@ -35,7 +35,8 @@ type Manager struct {
visitors map[string]Visitor
helper Helper
- checkInterval time.Duration
+ checkInterval time.Duration
+ keepVisitorsRunningOnce sync.Once
mu sync.RWMutex
ctx context.Context
@@ -67,7 +68,9 @@ func NewManager(
return m
}
-func (vm *Manager) Run() {
+// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it.
+// It will only start after Reload is called and a new visitor is added.
+func (vm *Manager) keepVisitorsRunning() {
xl := xlog.FromContextSafe(vm.ctx)
ticker := time.NewTicker(vm.checkInterval)
@@ -76,7 +79,7 @@ func (vm *Manager) Run() {
for {
select {
case <-vm.stopCh:
- xl.Info("gracefully shutdown visitor manager")
+ xl.Trace("gracefully shutdown visitor manager")
return
case <-ticker.C:
vm.mu.Lock()
@@ -120,7 +123,14 @@ func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) {
return
}
-func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) {
+func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) {
+ if len(cfgs) > 0 {
+ // Only start keepVisitorsRunning goroutine once and only when there is at least one visitor.
+ vm.keepVisitorsRunningOnce.Do(func() {
+ go vm.keepVisitorsRunning()
+ })
+ }
+
xl := xlog.FromContextSafe(vm.ctx)
cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string {
return c.GetBaseConfig().Name
diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go
index c180621c29e..ad773503e03 100644
--- a/client/visitor/xtcp.go
+++ b/client/visitor/xtcp.go
@@ -33,7 +33,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -349,7 +349,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
if err != nil {
return fmt.Errorf("dial udp error: %v", err)
}
- remote, err := utilnet.NewKCPConnFromUDP(lConn, true, raddr.String())
+ remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil {
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
}
@@ -440,7 +440,7 @@ func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
if err != nil {
return nil, err
}
- return utilnet.QuicStreamToNetConn(stream, session), nil
+ return netpkg.QuicStreamToNetConn(stream, session), nil
}
func (qs *QUICTunnelSession) Close() {
diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go
index c4a5acb6f66..fffc49850de 100644
--- a/cmd/frpc/sub/root.go
+++ b/cmd/frpc/sub/root.go
@@ -110,7 +110,7 @@ func handleTermSignal(svr *client.Service) {
}
func runClient(cfgFilePath string) error {
- cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
+ cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
if err != nil {
return err
}
@@ -119,19 +119,19 @@ func runClient(cfgFilePath string) error {
"please use yaml/json/toml format instead!\n")
}
- warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs)
+ warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
return err
}
- return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
+ return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
}
func startService(
cfg *v1.ClientCommonConfig,
- pxyCfgs []v1.ProxyConfigurer,
+ proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
cfgFile string,
) error {
@@ -141,7 +141,15 @@ func startService(
log.Info("start frpc service for config file [%s]", cfgFile)
defer log.Info("frpc service for config file [%s] stopped", cfgFile)
}
- svr := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
+ svr, err := client.NewService(client.ServiceOptions{
+ Common: cfg,
+ ProxyCfgs: proxyCfgs,
+ VisitorCfgs: visitorCfgs,
+ ConfigFilePath: cfgFile,
+ })
+ if err != nil {
+ return err
+ }
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go
index 1b6ac5a7a6c..4b971f531c7 100644
--- a/cmd/frpc/sub/verify.go
+++ b/cmd/frpc/sub/verify.go
@@ -37,12 +37,12 @@ var verifyCmd = &cobra.Command{
return nil
}
- cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
+ cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
- warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs)
+ warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
diff --git a/pkg/auth/pass.go b/pkg/auth/pass.go
new file mode 100644
index 00000000000..2eaf3f0bd70
--- /dev/null
+++ b/pkg/auth/pass.go
@@ -0,0 +1,31 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth
+
+import (
+ "github.com/fatedier/frp/pkg/msg"
+)
+
+var AlwaysPassVerifier = &alwaysPass{}
+
+var _ Verifier = &alwaysPass{}
+
+type alwaysPass struct{}
+
+func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil }
+
+func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil }
+
+func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil }
diff --git a/pkg/config/flags.go b/pkg/config/flags.go
index 0c37e608372..c0e871645df 100644
--- a/pkg/config/flags.go
+++ b/pkg/config/flags.go
@@ -59,12 +59,17 @@ func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
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")
}
}
diff --git a/pkg/config/legacy/parse.go b/pkg/config/legacy/parse.go
index 637783bc73b..80850f6f957 100644
--- a/pkg/config/legacy/parse.go
+++ b/pkg/config/legacy/parse.go
@@ -23,7 +23,7 @@ import (
func ParseClientConfig(filePath string) (
cfg ClientCommonConf,
- pxyCfgs map[string]ProxyConf,
+ proxyCfgs map[string]ProxyConf,
visitorCfgs map[string]VisitorConf,
err error,
) {
@@ -56,7 +56,7 @@ func ParseClientConfig(filePath string) (
configBuffer.Write(buf)
// Parse all proxy and visitor configs.
- pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
+ proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
if err != nil {
return
}
diff --git a/pkg/config/load.go b/pkg/config/load.go
index 3014eb35a46..b5539745931 100644
--- a/pkg/config/load.go
+++ b/pkg/config/load.go
@@ -110,6 +110,7 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
+// TODO(fatedier): strict is not valide for ProxyConfigurer/VisitorConfigurer/ClientPluginOptions.
func LoadConfigure(b []byte, c any, strict bool) error {
var tomlObj interface{}
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
@@ -188,19 +189,19 @@ func LoadClientConfig(path string, strict bool) (
) {
var (
cliCfg *v1.ClientCommonConfig
- pxyCfgs = make([]v1.ProxyConfigurer, 0)
+ proxyCfgs = make([]v1.ProxyConfigurer, 0)
visitorCfgs = make([]v1.VisitorConfigurer, 0)
isLegacyFormat bool
)
if DetectLegacyINIFormatFromFile(path) {
- legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
+ legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
if err != nil {
return nil, nil, nil, true, err
}
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
- for _, c := range legacyPxyCfgs {
- pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
+ for _, c := range legacyProxyCfgs {
+ proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
}
for _, c := range legacyVisitorCfgs {
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
@@ -213,7 +214,7 @@ func LoadClientConfig(path string, strict bool) (
}
cliCfg = &allCfg.ClientCommonConfig
for _, c := range allCfg.Proxies {
- pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+ proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
}
for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@@ -223,18 +224,18 @@ func LoadClientConfig(path string, strict bool) (
// Load additional config from includes.
// legacy ini format already handle this in ParseClientConfig.
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
- extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
+ extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
if err != nil {
return nil, nil, nil, isLegacyFormat, err
}
- pxyCfgs = append(pxyCfgs, extPxyCfgs...)
+ proxyCfgs = append(proxyCfgs, extProxyCfgs...)
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
}
// Filter by start
if len(cliCfg.Start) > 0 {
startSet := sets.New(cliCfg.Start...)
- pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
+ proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
return startSet.Has(c.GetBaseConfig().Name)
})
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
@@ -245,17 +246,17 @@ func LoadClientConfig(path string, strict bool) (
if cliCfg != nil {
cliCfg.Complete()
}
- for _, c := range pxyCfgs {
+ for _, c := range proxyCfgs {
c.Complete(cliCfg.User)
}
for _, c := range visitorCfgs {
c.Complete(cliCfg)
}
- return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil
+ return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
}
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
- pxyCfgs := make([]v1.ProxyConfigurer, 0)
+ proxyCfgs := make([]v1.ProxyConfigurer, 0)
visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, path := range paths {
absDir, err := filepath.Abs(filepath.Dir(path))
@@ -281,7 +282,7 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict boo
return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err)
}
for _, c := range cfg.Proxies {
- pxyCfgs = append(pxyCfgs, c.ProxyConfigurer)
+ proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
}
for _, c := range cfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
@@ -289,5 +290,5 @@ func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict boo
}
}
}
- return pxyCfgs, visitorCfgs, nil
+ return proxyCfgs, visitorCfgs, nil
}
diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go
index 41bb13414b7..0752479f0c5 100644
--- a/pkg/config/v1/proxy.go
+++ b/pkg/config/v1/proxy.go
@@ -224,7 +224,9 @@ func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
if !ok {
return nil
}
- return reflect.New(v).Interface().(ProxyConfigurer)
+ pc := reflect.New(v).Interface().(ProxyConfigurer)
+ pc.GetBaseConfig().Type = string(proxyType)
+ return pc
}
var _ ProxyConfigurer = &TCPProxyConfig{}
diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go
index 38123946a59..16fc4ccb48e 100644
--- a/pkg/config/v1/validation/client.go
+++ b/pkg/config/v1/validation/client.go
@@ -80,7 +80,7 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
return warnings, errs
}
-func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
+func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
var warnings Warning
if c != nil {
warning, err := ValidateClientCommonConfig(c)
@@ -90,7 +90,7 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigu
}
}
- for _, c := range pxyCfgs {
+ for _, c := range proxyCfgs {
if err := ValidateProxyConfigurerForClient(c); err != nil {
return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err)
}
diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go
index 7a865785243..a85fcd5fe5b 100644
--- a/pkg/msg/msg.go
+++ b/pkg/msg/msg.go
@@ -63,6 +63,15 @@ var msgTypeMap = map[byte]interface{}{
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
+type ClientSpec struct {
+ // Due to the support of VirtualClient, frps needs to know the client type in order to
+ // differentiate the processing logic.
+ // Optional values: ssh-tunnel
+ Type string `json:"type,omitempty"`
+ // If the value is true, the client will not require authentication.
+ AlwaysAuthPass bool `json:"always_auth_pass,omitempty"`
+}
+
// When frpc start, client send this message to login to server.
type Login struct {
Version string `json:"version,omitempty"`
@@ -75,6 +84,9 @@ type Login struct {
RunID string `json:"run_id,omitempty"`
Metas map[string]string `json:"metas,omitempty"`
+ // Currently only effective for VirtualClient.
+ ClientSpec ClientSpec `json:"client_spec,omitempty"`
+
// Some global configures.
PoolCount int `json:"pool_count,omitempty"`
}
diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go
index fd3e44b4637..ac54551afbe 100644
--- a/pkg/plugin/client/http2https.go
+++ b/pkg/plugin/client/http2https.go
@@ -24,7 +24,7 @@ import (
"net/http/httputil"
v1 "github.com/fatedier/frp/pkg/config/v1"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -79,7 +79,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
}
func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go
index 65abf19d68d..90a99b09434 100644
--- a/pkg/plugin/client/http_proxy.go
+++ b/pkg/plugin/client/http_proxy.go
@@ -29,7 +29,7 @@ import (
libnet "github.com/fatedier/golib/net"
v1 "github.com/fatedier/frp/pkg/config/v1"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
)
@@ -68,7 +68,7 @@ func (hp *HTTPProxy) Name() string {
}
func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
sc, rd := libnet.NewSharedConn(wrapConn)
firstBytes := make([]byte, 7)
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index 4a1c85b99e5..ba66bfaeb93 100644
--- a/pkg/plugin/client/https2http.go
+++ b/pkg/plugin/client/https2http.go
@@ -26,7 +26,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -98,7 +98,7 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
}
func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index 81386ac6c79..a79ea3b106c 100644
--- a/pkg/plugin/client/https2https.go
+++ b/pkg/plugin/client/https2https.go
@@ -26,7 +26,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -104,7 +104,7 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
}
func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = p.l.PutConn(wrapConn)
}
diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go
index 33e87b537a3..a230bf55bdb 100644
--- a/pkg/plugin/client/socks5.go
+++ b/pkg/plugin/client/socks5.go
@@ -24,7 +24,7 @@ import (
gosocks5 "github.com/armon/go-socks5"
v1 "github.com/fatedier/frp/pkg/config/v1"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -52,7 +52,7 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
defer conn.Close()
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = sp.Server.ServeConn(wrapConn)
}
diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go
index faf03f7d7d7..a7db2657e7e 100644
--- a/pkg/plugin/client/static_file.go
+++ b/pkg/plugin/client/static_file.go
@@ -25,7 +25,7 @@ import (
"github.com/gorilla/mux"
v1 "github.com/fatedier/frp/pkg/config/v1"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
@@ -57,8 +57,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
}
router := mux.NewRouter()
- router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
- router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
+ router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
+ router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
sp.s = &http.Server{
Handler: router,
}
@@ -69,7 +69,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
}
func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
- wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn)
+ wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = sp.l.PutConn(wrapConn)
}
diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go
index 395063e5029..57bf77469f0 100644
--- a/pkg/sdk/client/client.go
+++ b/pkg/sdk/client/client.go
@@ -11,7 +11,7 @@ import (
"strings"
"github.com/fatedier/frp/client"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
)
type Client struct {
@@ -115,7 +115,7 @@ func (c *Client) UpdateConfig(content string) error {
func (c *Client) setAuthHeader(req *http.Request) {
if c.authUser != "" || c.authPwd != "" {
- req.Header.Set("Authorization", util.BasicAuth(c.authUser, c.authPwd))
+ req.Header.Set("Authorization", httppkg.BasicAuth(c.authUser, c.authPwd))
}
}
diff --git a/pkg/ssh/gateway.go b/pkg/ssh/gateway.go
index 8f87e9986c4..07ae9808860 100644
--- a/pkg/ssh/gateway.go
+++ b/pkg/ssh/gateway.go
@@ -26,21 +26,21 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
)
type Gateway struct {
bindPort int
ln net.Listener
- serverPeerListener *utilnet.InternalListener
+ peerServerListener *netpkg.InternalListener
sshConfig *ssh.ServerConfig
}
func NewGateway(
cfg v1.SSHTunnelGateway, bindAddr string,
- serverPeerListener *utilnet.InternalListener,
+ peerServerListener *netpkg.InternalListener,
) (*Gateway, error) {
sshConfig := &ssh.ServerConfig{}
@@ -71,15 +71,8 @@ func NewGateway(
}
sshConfig.AddHostKey(privateKey)
+ sshConfig.NoClientAuth = cfg.AuthorizedKeysFile == ""
sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
- if cfg.AuthorizedKeysFile == "" {
- return &ssh.Permissions{
- Extensions: map[string]string{
- "user": "",
- },
- }, nil
- }
-
authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile)
if err != nil {
return nil, fmt.Errorf("internal error")
@@ -103,7 +96,7 @@ func NewGateway(
return &Gateway{
bindPort: cfg.BindPort,
ln: ln,
- serverPeerListener: serverPeerListener,
+ peerServerListener: peerServerListener,
sshConfig: sshConfig,
}, nil
}
@@ -121,7 +114,7 @@ func (g *Gateway) Run() {
func (g *Gateway) handleConn(conn net.Conn) {
defer conn.Close()
- ts, err := NewTunnelServer(conn, g.sshConfig, g.serverPeerListener)
+ ts, err := NewTunnelServer(conn, g.sshConfig, g.peerServerListener)
if err != nil {
return
}
diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go
index 13c87b689da..042f6766f71 100644
--- a/pkg/ssh/server.go
+++ b/pkg/ssh/server.go
@@ -17,9 +17,11 @@ package ssh
import (
"context"
"encoding/binary"
+ "errors"
"fmt"
"net"
"strings"
+ "sync"
"time"
libio "github.com/fatedier/golib/io"
@@ -27,10 +29,12 @@ import (
"github.com/spf13/cobra"
"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"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ "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"
@@ -64,15 +68,16 @@ type TunnelServer struct {
sc *ssh.ServerConfig
vc *virtual.Client
- serverPeerListener *utilnet.InternalListener
+ peerServerListener *netpkg.InternalListener
doneCh chan struct{}
+ closeDoneChOnce sync.Once
}
-func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, serverPeerListener *utilnet.InternalListener) (*TunnelServer, error) {
+func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, peerServerListener *netpkg.InternalListener) (*TunnelServer, error) {
s := &TunnelServer{
underlyingConn: conn,
sc: sc,
- serverPeerListener: serverPeerListener,
+ peerServerListener: peerServerListener,
doneCh: make(chan struct{}),
}
return s, nil
@@ -94,19 +99,35 @@ func (s *TunnelServer) Run() error {
if err != nil {
return err
}
- clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
+ clientCfg.Complete()
+ if sshConn.Permissions != nil {
+ clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
+ }
pc.Complete(clientCfg.User)
- s.vc = virtual.NewClient(clientCfg)
- // join workConn and ssh channel
- s.vc.SetInWorkConnCallback(func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool {
- c, err := s.openConn(addr)
- if err != nil {
+ 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 {
+ return false
+ }
+ libio.Join(c, workConn)
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()
@@ -115,21 +136,35 @@ func (s *TunnelServer) Run() error {
if err != nil {
return
}
- _ = s.serverPeerListener.PutConn(conn)
+ _ = s.peerServerListener.PutConn(conn)
}
}()
xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100})
ctx := xlog.NewContext(context.Background(), xl)
go func() {
_ = s.vc.Run(ctx)
+ // 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})
- _ = sshConn.Wait()
- _ = sshConn.Close()
+ if err := s.waitProxyStatusReady(pc.GetBaseConfig().Name, time.Second); err != nil {
+ log.Warn("wait proxy status ready error: %v", err)
+ } else {
+ _ = sshConn.Wait()
+ }
+
s.vc.Close()
- close(s.doneCh)
+ log.Trace("ssh tunnel connection from %v closed", sshConn.RemoteAddr())
+ s.closeDoneChOnce.Do(func() {
+ _ = sshConn.Close()
+ close(s.doneCh)
+ })
return nil
}
@@ -217,6 +252,14 @@ func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPaylo
if err := cmd.ParseFlags(args); err != nil {
return nil, nil, fmt.Errorf("parse flags from ssh client error: %v", 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, fmt.Errorf("generate random id error: %v", err)
+ }
+ pc.GetBaseConfig().Name = fmt.Sprintf("sshtunnel-%s-%s", proxyType, id)
+ }
return &clientCfg, pc, nil
}
@@ -274,6 +317,34 @@ func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) {
}
go ssh.DiscardRequests(reqs)
- conn := utilnet.WrapReadWriteCloserToConn(channel, s.underlyingConn)
+ conn := netpkg.WrapReadWriteCloserToConn(channel, s.underlyingConn)
return conn, nil
}
+
+func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) 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 nil
+ case proxy.ProxyPhaseStartErr, proxy.ProxyPhaseClosed:
+ return errors.New(ps.Err)
+ }
+ case <-timer.C:
+ return fmt.Errorf("wait proxy status ready timeout")
+ case <-s.doneCh:
+ return fmt.Errorf("ssh tunnel server closed")
+ }
+ }
+}
diff --git a/pkg/util/util/http.go b/pkg/util/http/http.go
similarity index 99%
rename from pkg/util/util/http.go
rename to pkg/util/http/http.go
index a6a25a4cbe1..b85a46a328d 100644
--- a/pkg/util/util/http.go
+++ b/pkg/util/http/http.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package util
+package http
import (
"encoding/base64"
diff --git a/pkg/util/http/server.go b/pkg/util/http/server.go
new file mode 100644
index 00000000000..e49cefe49d4
--- /dev/null
+++ b/pkg/util/http/server.go
@@ -0,0 +1,128 @@
+// 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) {
+ if cfg.AssetsDir != "" {
+ assets.Load(cfg.AssetsDir)
+ }
+
+ addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port))
+ if addr == ":" {
+ addr = ":http"
+ }
+
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ router := mux.NewRouter()
+ hs := &http.Server{
+ Addr: addr,
+ Handler: router,
+ ReadTimeout: defaultReadTimeout,
+ WriteTimeout: defaultWriteTimeout,
+ }
+ s := &Server{
+ addr: addr,
+ ln: ln,
+ hs: hs,
+ router: router,
+ }
+ if cfg.PprofEnable {
+ s.registerPprofHandlers()
+ }
+ if cfg.TLS != nil {
+ cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
+ if err != nil {
+ return nil, err
+ }
+ s.tlsCfg = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ }
+ }
+ s.authMiddleware = netpkg.NewHTTPAuthMiddleware(cfg.User, cfg.Password).SetAuthFailDelay(200 * time.Millisecond).Middleware
+ return s, nil
+}
+
+func (s *Server) Address() string {
+ return s.addr
+}
+
+func (s *Server) Run() error {
+ ln := s.ln
+ if s.tlsCfg != nil {
+ ln = tls.NewListener(ln, s.tlsCfg)
+ }
+ return s.hs.Serve(ln)
+}
+
+func (s *Server) Close() error {
+ return s.hs.Close()
+}
+
+type RouterRegisterHelper struct {
+ Router *mux.Router
+ AssetsFS http.FileSystem
+ AuthMiddleware mux.MiddlewareFunc
+}
+
+func (s *Server) RouteRegister(register func(helper *RouterRegisterHelper)) {
+ register(&RouterRegisterHelper{
+ Router: s.router,
+ AssetsFS: assets.FileSystem,
+ AuthMiddleware: s.authMiddleware,
+ })
+}
+
+func (s *Server) registerPprofHandlers() {
+ s.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
+ s.router.HandleFunc("/debug/pprof/profile", pprof.Profile)
+ s.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
+ s.router.HandleFunc("/debug/pprof/trace", pprof.Trace)
+ s.router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
+}
diff --git a/pkg/util/net/dns.go b/pkg/util/net/dns.go
new file mode 100644
index 00000000000..5e1d5ccbfc1
--- /dev/null
+++ b/pkg/util/net/dns.go
@@ -0,0 +1,33 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+ "context"
+ "net"
+)
+
+func SetDefaultDNSAddress(dnsAddress string) {
+ if _, _, err := net.SplitHostPort(dnsAddress); err != nil {
+ dnsAddress = net.JoinHostPort(dnsAddress, "53")
+ }
+ // Change default dns server
+ net.DefaultResolver = &net.Resolver{
+ PreferGo: true,
+ Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
+ return net.Dial("udp", dnsAddress)
+ },
+ }
+}
diff --git a/pkg/util/net/listener.go b/pkg/util/net/listener.go
index 6f2d8a56646..c3aebcd6f6b 100644
--- a/pkg/util/net/listener.go
+++ b/pkg/util/net/listener.go
@@ -52,7 +52,10 @@ func (l *InternalListener) PutConn(conn net.Conn) error {
conn.Close()
}
})
- return err
+ if err != nil {
+ return fmt.Errorf("put conn error: listener is closed")
+ }
+ return nil
}
func (l *InternalListener) Close() error {
diff --git a/pkg/util/tcpmux/httpconnect.go b/pkg/util/tcpmux/httpconnect.go
index 17989adcad5..6be29a4a6a9 100644
--- a/pkg/util/tcpmux/httpconnect.go
+++ b/pkg/util/tcpmux/httpconnect.go
@@ -24,7 +24,7 @@ import (
libnet "github.com/fatedier/golib/net"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/vhost"
)
@@ -59,10 +59,10 @@ func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host, ht
return
}
- host, _ = util.CanonicalHost(req.Host)
+ host, _ = httppkg.CanonicalHost(req.Host)
proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth != "" {
- httpUser, httpPwd, _ = util.ParseBasicAuth(proxyAuth)
+ httpUser, httpPwd, _ = httppkg.ParseBasicAuth(proxyAuth)
}
return
}
@@ -71,7 +71,7 @@ func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, _ map[string]s
if muxer.passthrough {
return nil
}
- res := util.OkResponse()
+ res := httppkg.OkResponse()
if res.Body != nil {
defer res.Body.Close()
}
@@ -85,7 +85,7 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re
return true, nil
}
- resp := util.ProxyUnauthorizedResponse()
+ resp := httppkg.ProxyUnauthorizedResponse()
if resp.Body != nil {
defer resp.Body.Close()
}
diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go
index 1a5bea0b4a2..72ab4775cbd 100644
--- a/pkg/util/vhost/http.go
+++ b/pkg/util/vhost/http.go
@@ -31,8 +31,8 @@ import (
libio "github.com/fatedier/golib/io"
"github.com/fatedier/golib/pool"
- frpLog "github.com/fatedier/frp/pkg/util/log"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
+ logpkg "github.com/fatedier/frp/pkg/util/log"
)
var ErrNoRouteFound = errors.New("no route found")
@@ -61,7 +61,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
Director: func(req *http.Request) {
req.URL.Scheme = "http"
reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
- oldHost, _ := util.CanonicalHost(reqRouteInfo.Host)
+ oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if rc != nil {
@@ -74,7 +74,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// ignore error here, it will use CreateConnFn instead later
endpoint, _ = rc.ChooseEndpointFn()
reqRouteInfo.Endpoint = endpoint
- frpLog.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
+ logpkg.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
}
// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
@@ -116,7 +116,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
BufferPool: newWrapPool(),
ErrorLog: log.New(newWrapLogger(), "", 0),
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
- frpLog.Warn("do http proxy request [host: %s] error: %v", req.Host, err)
+ logpkg.Warn("do http proxy request [host: %s] error: %v", req.Host, err)
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write(getNotFoundPageContent())
},
@@ -143,7 +143,7 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
- frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
+ logpkg.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
return vr.payload.(*RouteConfig)
}
return nil
@@ -159,7 +159,7 @@ func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string)
// CreateConnection create a new connection by route config
func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
- host, _ := util.CanonicalHost(reqRouteInfo.Host)
+ host, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if ok {
if byEndpoint {
@@ -303,7 +303,7 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
}
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
- domain, _ := util.CanonicalHost(req.Host)
+ domain, _ := httppkg.CanonicalHost(req.Host)
location := req.URL.Path
user, passwd, _ := req.BasicAuth()
if !rp.CheckAuth(domain, location, user, user, passwd) {
@@ -333,6 +333,6 @@ type wrapLogger struct{}
func newWrapLogger() *wrapLogger { return &wrapLogger{} }
func (l *wrapLogger) Write(p []byte) (n int, err error) {
- frpLog.Warn("%s", string(bytes.TrimRight(p, "\n")))
+ logpkg.Warn("%s", string(bytes.TrimRight(p, "\n")))
return len(p), nil
}
diff --git a/pkg/util/vhost/resource.go b/pkg/util/vhost/resource.go
index d78082b24d0..bf91e13358f 100644
--- a/pkg/util/vhost/resource.go
+++ b/pkg/util/vhost/resource.go
@@ -20,7 +20,7 @@ import (
"net/http"
"os"
- frpLog "github.com/fatedier/frp/pkg/util/log"
+ logpkg "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
@@ -58,7 +58,7 @@ func getNotFoundPageContent() []byte {
if NotFoundPagePath != "" {
buf, err = os.ReadFile(NotFoundPagePath)
if err != nil {
- frpLog.Warn("read custom 404 page error: %v", err)
+ logpkg.Warn("read custom 404 page error: %v", err)
buf = []byte(NotFound)
}
} else {
diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go
index 29123b695d9..d529e4249e3 100644
--- a/pkg/util/vhost/vhost.go
+++ b/pkg/util/vhost/vhost.go
@@ -22,7 +22,7 @@ import (
"github.com/fatedier/golib/errors"
"github.com/fatedier/frp/pkg/util/log"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
@@ -284,7 +284,7 @@ func (l *Listener) Accept() (net.Conn, error) {
xl.Debug("rewrite host to [%s] success", l.rewriteHost)
conn = sConn
}
- return utilnet.NewContextConn(l.ctx, conn), nil
+ return netpkg.NewContextConn(l.ctx, conn), nil
}
func (l *Listener) Close() error {
diff --git a/pkg/virtual/client.go b/pkg/virtual/client.go
index d0369a1af8a..96835a48c7f 100644
--- a/pkg/virtual/client.go
+++ b/pkg/virtual/client.go
@@ -21,55 +21,70 @@ import (
"github.com/fatedier/frp/client"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ 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 *utilnet.InternalListener
+ l *netpkg.InternalListener
svr *client.Service
}
-func NewClient(cfg *v1.ClientCommonConfig) *Client {
- cfg.Complete()
-
- ln := utilnet.NewInternalListener()
-
- svr := client.NewService(cfg, nil, nil, "")
- svr.SetConnectorCreator(func(context.Context, *v1.ClientCommonConfig) client.Connector {
- return &pipeConnector{
- peerListener: ln,
- }
- })
+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) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
- c.svr.SetInWorkConnCallback(cb)
-}
-
func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) {
- _ = c.svr.ReloadConf(proxyCfgs, nil)
+ _ = 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.l.Close()
c.svr.Close()
+ c.l.Close()
}
type pipeConnector struct {
- peerListener *utilnet.InternalListener
+ peerListener *netpkg.InternalListener
}
func (pc *pipeConnector) Open() error {
diff --git a/server/control.go b/server/control.go
index e651a97e846..dbb1af0a038 100644
--- a/server/control.go
+++ b/server/control.go
@@ -32,7 +32,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/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/version"
"github.com/fatedier/frp/pkg/util/wait"
@@ -150,6 +150,7 @@ type Control struct {
doneCh chan struct{}
}
+// TODO(fatedier): Referencing the implementation of frpc, encapsulate the input parameters as SessionContext.
func NewControl(
ctx context.Context,
rc *controller.ResourceController,
@@ -157,6 +158,7 @@ func NewControl(
pluginManager *plugin.Manager,
authVerifier auth.Verifier,
ctlConn net.Conn,
+ ctlConnEncrypted bool,
loginMsg *msg.Login,
serverCfg *v1.ServerConfig,
) (*Control, error) {
@@ -183,11 +185,15 @@ func NewControl(
}
ctl.lastPing.Store(time.Now())
- cryptoRW, err := utilnet.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
- if err != nil {
- return nil, err
+ 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.msgDispatcher = msg.NewDispatcher(cryptoRW)
ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
return ctl, nil
@@ -300,6 +306,7 @@ func (ctl *Control) heartbeatWorker() {
go wait.Until(func() {
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout")
+ ctl.conn.Close()
return
}
}, time.Second, ctl.doneCh)
@@ -555,6 +562,5 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
go func() {
_ = ctl.pluginManager.CloseProxy(notifyContent)
}()
-
return
}
diff --git a/server/dashboard.go b/server/dashboard.go
deleted file mode 100644
index 1f290cf9a5a..00000000000
--- a/server/dashboard.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2017 fatedier, fatedier@gmail.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package server
-
-import (
- "crypto/tls"
- "net"
- "net/http"
- "net/http/pprof"
- "time"
-
- "github.com/gorilla/mux"
- "github.com/prometheus/client_golang/prometheus/promhttp"
-
- "github.com/fatedier/frp/assets"
- utilnet "github.com/fatedier/frp/pkg/util/net"
-)
-
-var (
- httpServerReadTimeout = 60 * time.Second
- httpServerWriteTimeout = 60 * time.Second
-)
-
-func (svr *Service) RunDashboardServer(address string) (err error) {
- // url router
- router := mux.NewRouter()
- router.HandleFunc("/healthz", svr.Healthz)
-
- // debug
- if svr.cfg.WebServer.PprofEnable {
- router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
- router.HandleFunc("/debug/pprof/profile", pprof.Profile)
- router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
- router.HandleFunc("/debug/pprof/trace", pprof.Trace)
- router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
- }
-
- subRouter := router.NewRoute().Subrouter()
-
- user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password
- subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware)
-
- // metrics
- if svr.cfg.EnablePrometheus {
- subRouter.Handle("/metrics", promhttp.Handler())
- }
-
- // api, see dashboard_api.go
- subRouter.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET")
- subRouter.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET")
- subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET")
- subRouter.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET")
-
- // view
- subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
- subRouter.PathPrefix("/static/").Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
-
- subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
- })
-
- server := &http.Server{
- Addr: address,
- Handler: router,
- ReadTimeout: httpServerReadTimeout,
- WriteTimeout: httpServerWriteTimeout,
- }
- ln, err := net.Listen("tcp", address)
- if err != nil {
- return err
- }
-
- if svr.cfg.WebServer.TLS != nil {
- cert, err := tls.LoadX509KeyPair(svr.cfg.WebServer.TLS.CertFile, svr.cfg.WebServer.TLS.KeyFile)
- if err != nil {
- return err
- }
- tlsCfg := &tls.Config{
- Certificates: []tls.Certificate{cert},
- }
- ln = tls.NewListener(ln, tlsCfg)
- }
- go func() {
- _ = server.Serve(ln)
- }()
- return
-}
diff --git a/server/dashboard_api.go b/server/dashboard_api.go
index b5a923f9456..27944b9a803 100644
--- a/server/dashboard_api.go
+++ b/server/dashboard_api.go
@@ -19,19 +19,52 @@ import (
"net/http"
"github.com/gorilla/mux"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/metrics/mem"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/version"
)
+// TODO(fatedier): add an API to clean status of all offline proxies.
+
type GeneralResponse struct {
Code int
Msg string
}
+func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
+ helper.Router.HandleFunc("/healthz", svr.healthz)
+ subRouter := helper.Router.NewRoute().Subrouter()
+
+ subRouter.Use(helper.AuthMiddleware.Middleware)
+
+ // metrics
+ if svr.cfg.EnablePrometheus {
+ subRouter.Handle("/metrics", promhttp.Handler())
+ }
+
+ // apis
+ subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
+ subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
+ subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
+ subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
+
+ // view
+ subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
+ subRouter.PathPrefix("/static/").Handler(
+ netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
+ ).Methods("GET")
+
+ subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
+ })
+}
+
type serverInfoResp struct {
Version string `json:"version"`
BindPort int `json:"bindPort"`
@@ -55,12 +88,12 @@ type serverInfoResp struct {
}
// /healthz
-func (svr *Service) Healthz(w http.ResponseWriter, _ *http.Request) {
+func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
}
// /api/serverinfo
-func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
@@ -177,7 +210,7 @@ type GetProxyInfoResp struct {
}
// /api/proxy/:type
-func (svr *Service) APIProxyByType(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
proxyType := params["type"]
@@ -245,7 +278,7 @@ type GetProxyStatsResp struct {
}
// /api/proxy/:type/:name
-func (svr *Service) APIProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
proxyType := params["type"]
@@ -314,7 +347,7 @@ type GetProxyTrafficResp struct {
TrafficOut []int64 `json:"trafficOut"`
}
-func (svr *Service) APIProxyTraffic(w http.ResponseWriter, r *http.Request) {
+func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
name := params["name"]
diff --git a/server/proxy/http.go b/server/proxy/http.go
index cafaf8f3d9a..44a462b7ecc 100644
--- a/server/proxy/http.go
+++ b/server/proxy/http.go
@@ -24,7 +24,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/limit"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/vhost"
"github.com/fatedier/frp/server/metrics"
@@ -180,8 +180,8 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
})
}
- workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn)
- workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
+ workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
+ workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
return
}
diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go
index fe6f781b728..f5c850e98af 100644
--- a/server/proxy/proxy.go
+++ b/server/proxy/proxy.go
@@ -32,7 +32,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/util/limit"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/metrics"
@@ -130,7 +130,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
}
xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
xl.Spawn().AppendPrefix(pxy.GetName())
- workConn = utilnet.NewContextConn(pxy.ctx, workConn)
+ workConn = netpkg.NewContextConn(pxy.ctx, workConn)
var (
srcAddr string
diff --git a/server/proxy/udp.go b/server/proxy/udp.go
index 772c3f0d1b9..ea970818d8e 100644
--- a/server/proxy/udp.go
+++ b/server/proxy/udp.go
@@ -30,7 +30,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/server/metrics"
)
@@ -222,7 +222,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
})
}
- pxy.workConn = utilnet.WrapReadWriteCloserToConn(rwc, workConn)
+ pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn)
ctx, cancel := context.WithCancel(context.Background())
go workConnReaderFn(pxy.workConn)
go workConnSenderFn(pxy.workConn, ctx)
diff --git a/server/service.go b/server/service.go
index 02efec91a28..c2410b06376 100644
--- a/server/service.go
+++ b/server/service.go
@@ -30,7 +30,6 @@ import (
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
- "github.com/fatedier/frp/assets"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
modelmetrics "github.com/fatedier/frp/pkg/metrics"
@@ -39,8 +38,9 @@ import (
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/ssh"
"github.com/fatedier/frp/pkg/transport"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/tcpmux"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
@@ -79,7 +79,8 @@ type Service struct {
// Accept frp tls connections
tlsListener net.Listener
- virtualListener *utilnet.InternalListener
+ // Accept pipe connections from ssh tunnel gateway
+ sshTunnelListener *netpkg.InternalListener
// Manage all controllers
ctlManager *ControlManager
@@ -96,6 +97,9 @@ type Service struct {
// All resource managers and controllers
rc *controller.ResourceController
+ // web server for dashboard UI and apis
+ webServer *httppkg.Server
+
sshTunnelGateway *ssh.Gateway
// Verifies authentication based on selected method
@@ -111,16 +115,30 @@ type Service struct {
cancel context.CancelFunc
}
-func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
+func NewService(cfg *v1.ServerConfig) (*Service, error) {
tlsConfig, err := transport.NewServerTLSConfig(
cfg.Transport.TLS.CertFile,
cfg.Transport.TLS.KeyFile,
cfg.Transport.TLS.TrustedCaFile)
if err != nil {
- return
+ return nil, err
+ }
+
+ var webServer *httppkg.Server
+ if cfg.WebServer.Port > 0 {
+ ws, err := httppkg.NewServer(cfg.WebServer)
+ if err != nil {
+ return nil, err
+ }
+ webServer = ws
+
+ modelmetrics.EnableMem()
+ if cfg.EnablePrometheus {
+ modelmetrics.EnablePrometheus()
+ }
}
- svr = &Service{
+ svr := &Service{
ctlManager: NewControlManager(),
pxyManager: proxy.NewManager(),
pluginManager: plugin.NewManager(),
@@ -129,12 +147,16 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
},
- virtualListener: utilnet.NewInternalListener(),
- httpVhostRouter: vhost.NewRouters(),
- authVerifier: auth.NewAuthVerifier(cfg.Auth),
- tlsConfig: tlsConfig,
- cfg: cfg,
- ctx: context.Background(),
+ sshTunnelListener: netpkg.NewInternalListener(),
+ httpVhostRouter: vhost.NewRouters(),
+ authVerifier: auth.NewAuthVerifier(cfg.Auth),
+ webServer: webServer,
+ tlsConfig: tlsConfig,
+ cfg: cfg,
+ ctx: context.Background(),
+ }
+ if webServer != nil {
+ webServer.RouteRegister(svr.registerRouteHandlers)
}
// Create tcpmux httpconnect multiplexer.
@@ -143,14 +165,12 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort))
l, err = net.Listen("tcp", address)
if err != nil {
- err = fmt.Errorf("create server listener error, %v", err)
- return
+ return nil, fmt.Errorf("create server listener error, %v", err)
}
svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout)
if err != nil {
- err = fmt.Errorf("create vhost tcpMuxer error, %v", err)
- return
+ return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err)
}
log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough)
}
@@ -191,8 +211,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort))
ln, err := net.Listen("tcp", address)
if err != nil {
- err = fmt.Errorf("create server listener error, %v", err)
- return
+ return nil, fmt.Errorf("create server listener error, %v", err)
}
svr.muxer = mux.NewMux(ln)
@@ -208,10 +227,9 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
// Listen for accepting connections from client using kcp protocol.
if cfg.KCPBindPort > 0 {
address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
- svr.kcpListener, err = utilnet.ListenKcp(address)
+ svr.kcpListener, err = netpkg.ListenKcp(address)
if err != nil {
- err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
- return
+ return nil, fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
}
log.Info("frps kcp listen on udp %s", address)
}
@@ -226,28 +244,26 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
KeepAlivePeriod: time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second,
})
if err != nil {
- err = fmt.Errorf("listen on quic udp address %s error: %v", address, err)
- return
+ return nil, fmt.Errorf("listen on quic udp address %s error: %v", address, err)
}
log.Info("frps quic listen on %s", address)
}
if cfg.SSHTunnelGateway.BindPort > 0 {
- sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.virtualListener)
+ sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.ProxyBindAddr, svr.sshTunnelListener)
if err != nil {
- err = fmt.Errorf("create ssh gateway error: %v", err)
- return nil, err
+ 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.
- websocketPrefix := []byte("GET " + utilnet.FrpWebsocketPath)
+ websocketPrefix := []byte("GET " + netpkg.FrpWebsocketPath)
websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool {
return bytes.Equal(data, websocketPrefix)
})
- svr.websocketListener = utilnet.NewWebsocketListener(websocketLn)
+ svr.websocketListener = netpkg.NewWebsocketListener(websocketLn)
// Create http vhost muxer.
if cfg.VhostHTTPPort > 0 {
@@ -267,8 +283,7 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
} else {
l, err = net.Listen("tcp", address)
if err != nil {
- err = fmt.Errorf("create vhost http listener error, %v", err)
- return
+ return nil, fmt.Errorf("create vhost http listener error, %v", err)
}
}
go func() {
@@ -286,55 +301,30 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
l, err = net.Listen("tcp", address)
if err != nil {
- err = fmt.Errorf("create server listener error, %v", err)
- return
+ return nil, fmt.Errorf("create server listener error, %v", err)
}
log.Info("https service listen on %s", address)
}
svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout)
if err != nil {
- err = fmt.Errorf("create vhost httpsMuxer error, %v", err)
- return
+ return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
}
}
// frp tls listener
svr.tlsListener = svr.muxer.Listen(2, 1, func(data []byte) bool {
// tls first byte can be 0x16 only when vhost https port is not same with bind port
- return int(data[0]) == utilnet.FRPTLSHeadByte || int(data[0]) == 0x16
+ return int(data[0]) == netpkg.FRPTLSHeadByte || int(data[0]) == 0x16
})
// Create nat hole controller.
nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour)
if err != nil {
- err = fmt.Errorf("create nat hole controller error, %v", err)
- return
+ return nil, fmt.Errorf("create nat hole controller error, %v", err)
}
svr.rc.NatHoleController = nc
-
- var statsEnable bool
- // Create dashboard web server.
- if cfg.WebServer.Port > 0 {
- // Init dashboard assets
- assets.Load(cfg.WebServer.AssetsDir)
-
- address := net.JoinHostPort(cfg.WebServer.Addr, strconv.Itoa(cfg.WebServer.Port))
- err = svr.RunDashboardServer(address)
- if err != nil {
- err = fmt.Errorf("create dashboard web server error, %v", err)
- return
- }
- log.Info("Dashboard listen on %s", address)
- statsEnable = true
- }
- if statsEnable {
- modelmetrics.EnableMem()
- if cfg.EnablePrometheus {
- modelmetrics.EnablePrometheus()
- }
- }
- return
+ return svr, nil
}
func (svr *Service) Run(ctx context.Context) {
@@ -342,7 +332,17 @@ func (svr *Service) Run(ctx context.Context) {
svr.ctx = ctx
svr.cancel = cancel
- go svr.HandleListener(svr.virtualListener, true)
+ // run dashboard web server.
+ if svr.webServer != nil {
+ go func() {
+ log.Info("dashboard listen on %s", svr.webServer.Address())
+ if err := svr.webServer.Run(); err != nil {
+ log.Warn("dashboard server exit with error: %v", err)
+ }
+ }()
+ }
+
+ go svr.HandleListener(svr.sshTunnelListener, true)
if svr.kcpListener != nil {
go svr.HandleListener(svr.kcpListener, false)
@@ -398,7 +398,7 @@ func (svr *Service) Close() error {
return nil
}
-func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
+func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) {
xl := xlog.FromContextSafe(ctx)
var (
@@ -424,7 +424,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
retContent, err := svr.pluginManager.Login(content)
if err == nil {
m = &retContent.Login
- err = svr.RegisterControl(conn, m)
+ err = svr.RegisterControl(conn, m, internal)
}
// If login failed, send error message there.
@@ -461,6 +461,9 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
}
}
+// HandleListener accepts connections from client and call handleConnection to handle them.
+// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway.
+// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters.
func (svr *Service) HandleListener(l net.Listener, internal bool) {
// Listen for incoming connections from client.
for {
@@ -473,19 +476,21 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
xl := xlog.New()
ctx := context.Background()
- c = utilnet.NewContextConn(xlog.NewContext(ctx, xl), c)
+ c = netpkg.NewContextConn(xlog.NewContext(ctx, xl), c)
- log.Trace("start check TLS connection...")
- originConn := c
- forceTLS := svr.cfg.Transport.TLS.Force && !internal
- var isTLS, custom bool
- c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
- if err != nil {
- log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
- originConn.Close()
- continue
+ if !internal {
+ log.Trace("start check TLS connection...")
+ originConn := c
+ forceTLS := svr.cfg.Transport.TLS.Force
+ var isTLS, custom bool
+ c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout)
+ if err != nil {
+ log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err)
+ originConn.Close()
+ continue
+ }
+ log.Trace("check TLS connection success, isTLS: %v custom: %v internal: %v", isTLS, custom, internal)
}
- log.Trace("check TLS connection success, isTLS: %v custom: %v", isTLS, custom)
// Start a new goroutine to handle connection.
go func(ctx context.Context, frpConn net.Conn) {
@@ -508,10 +513,10 @@ func (svr *Service) HandleListener(l net.Listener, internal bool) {
session.Close()
return
}
- go svr.handleConnection(ctx, stream)
+ go svr.handleConnection(ctx, stream, internal)
}
} else {
- svr.handleConnection(ctx, frpConn)
+ svr.handleConnection(ctx, frpConn, internal)
}
}(ctx, c)
}
@@ -534,13 +539,13 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
_ = frpConn.CloseWithError(0, "")
return
}
- go svr.handleConnection(ctx, utilnet.QuicStreamToNetConn(stream, frpConn))
+ go svr.handleConnection(ctx, netpkg.QuicStreamToNetConn(stream, frpConn), false)
}
}(context.Background(), c)
}
}
-func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error {
+func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, internal bool) error {
// If client's RunID is empty, it's a new client, we just create a new controller.
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
var err error
@@ -551,7 +556,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error
}
}
- ctx := utilnet.NewContextFromConn(ctlConn)
+ ctx := netpkg.NewContextFromConn(ctlConn)
xl := xlog.FromContextSafe(ctx)
xl.AppendPrefix(loginMsg.RunID)
ctx = xlog.NewContext(ctx, xl)
@@ -559,11 +564,16 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
// Check auth.
- if err := svr.authVerifier.VerifyLogin(loginMsg); err != nil {
+ authVerifier := svr.authVerifier
+ if internal && loginMsg.ClientSpec.AlwaysAuthPass {
+ authVerifier = auth.AlwaysPassVerifier
+ }
+ if err := authVerifier.VerifyLogin(loginMsg); err != nil {
return err
}
- ctl, err := 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
@@ -588,7 +598,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) error
// RegisterWorkConn register a new work connection to control and proxies need it.
func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error {
- xl := utilnet.NewLogFromConn(workConn)
+ xl := netpkg.NewLogFromConn(workConn)
ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
if !exist {
xl.Warn("No client control found for run id [%s]", newMsg.RunID)
@@ -607,7 +617,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
if err == nil {
newMsg = &retContent.NewWorkConn
// Check auth.
- err = svr.authVerifier.VerifyNewWorkConn(newMsg)
+ err = ctl.authVerifier.VerifyNewWorkConn(newMsg)
}
if err != nil {
xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID)
diff --git a/server/visitor/visitor.go b/server/visitor/visitor.go
index c76bcee1d92..ed06dc4b4e9 100644
--- a/server/visitor/visitor.go
+++ b/server/visitor/visitor.go
@@ -23,12 +23,12 @@ import (
libio "github.com/fatedier/golib/io"
"github.com/samber/lo"
- utilnet "github.com/fatedier/frp/pkg/util/net"
+ netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
)
type listenerBundle struct {
- l *utilnet.InternalListener
+ l *netpkg.InternalListener
sk string
allowUsers []string
}
@@ -46,22 +46,21 @@ func NewManager() *Manager {
}
}
-func (vm *Manager) Listen(name string, sk string, allowUsers []string) (l *utilnet.InternalListener, err error) {
+func (vm *Manager) Listen(name string, sk string, allowUsers []string) (*netpkg.InternalListener, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
if _, ok := vm.listeners[name]; ok {
- err = fmt.Errorf("custom listener for [%s] is repeated", name)
- return
+ return nil, fmt.Errorf("custom listener for [%s] is repeated", name)
}
- l = utilnet.NewInternalListener()
+ l := netpkg.NewInternalListener()
vm.listeners[name] = &listenerBundle{
l: l,
sk: sk,
allowUsers: allowUsers,
}
- return
+ return l, nil
}
func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string,
@@ -91,7 +90,7 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey
if useCompression {
rwc = libio.WithCompression(rwc)
}
- err = l.l.PutConn(utilnet.WrapReadWriteCloserToConn(rwc, conn))
+ err = l.l.PutConn(netpkg.WrapReadWriteCloserToConn(rwc, conn))
} else {
err = fmt.Errorf("custom listener for [%s] doesn't exist", name)
return
diff --git a/test/e2e/legacy/basic/tcpmux.go b/test/e2e/legacy/basic/tcpmux.go
index 5bb742bc8e3..15477837a32 100644
--- a/test/e2e/legacy/basic/tcpmux.go
+++ b/test/e2e/legacy/basic/tcpmux.go
@@ -8,7 +8,7 @@ import (
"github.com/onsi/ginkgo/v2"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/test/e2e/framework"
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
@@ -176,7 +176,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
connectRequestHost = req.Host
// return ok response
- res := util.OkResponse()
+ res := httppkg.OkResponse()
if res.Body != nil {
defer res.Body.Close()
}
diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go
index 50deb3bf31a..740bc4fbf2c 100644
--- a/test/e2e/pkg/request/request.go
+++ b/test/e2e/pkg/request/request.go
@@ -14,7 +14,7 @@ import (
libdial "github.com/fatedier/golib/net/dial"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/test/e2e/pkg/rpc"
)
@@ -115,7 +115,7 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request {
}
func (r *Request) HTTPAuth(user, password string) *Request {
- r.authValue = util.BasicAuth(user, password)
+ r.authValue = httppkg.BasicAuth(user, password)
return r
}
diff --git a/test/e2e/v1/basic/tcpmux.go b/test/e2e/v1/basic/tcpmux.go
index 356a18be978..7ee58a79c01 100644
--- a/test/e2e/v1/basic/tcpmux.go
+++ b/test/e2e/v1/basic/tcpmux.go
@@ -8,7 +8,7 @@ import (
"github.com/onsi/ginkgo/v2"
- "github.com/fatedier/frp/pkg/util/util"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/test/e2e/framework"
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/mock/server/streamserver"
@@ -180,7 +180,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() {
connectRequestHost = req.Host
// return ok response
- res := util.OkResponse()
+ res := httppkg.OkResponse()
if res.Body != nil {
defer res.Body.Close()
}
From 7c799ee9216521fddebab57aa160153f2aa09216 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 28 Nov 2023 13:48:32 +0800
Subject: [PATCH 11/21] add e2e tests for ssh tunnel (#3805)
---
.gitignore | 1 +
pkg/ssh/server.go | 35 +++---
test/e2e/pkg/ssh/client.go | 89 +++++++++++++
test/e2e/v1/features/ssh_tunnel.go | 193 +++++++++++++++++++++++++++++
4 files changed, 300 insertions(+), 18 deletions(-)
create mode 100644 test/e2e/pkg/ssh/client.go
create mode 100644 test/e2e/v1/features/ssh_tunnel.go
diff --git a/.gitignore b/.gitignore
index f6df315b1e4..c9480d52d3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ lastversion/
dist/
.idea/
.vscode/
+.autogen_ssh_key
# Cache
*.swp
diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go
index 042f6766f71..30e79c64318 100644
--- a/pkg/ssh/server.go
+++ b/pkg/ssh/server.go
@@ -56,8 +56,6 @@ type forwardedTCPPayload struct {
Addr string
Port uint32
- // can be default empty value but do not delete it
- // because ssh protocol shoule be reserved
OriginAddr string
OriginPort uint32
}
@@ -117,6 +115,8 @@ func (s *TunnelServer) Run() error {
// 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)
@@ -180,20 +180,16 @@ func (s *TunnelServer) waitForwardAddrAndExtraPayload(
go func() {
addrGot := false
for req := range requests {
- switch req.Type {
- case RequestTypeForward:
- if !addrGot {
- payload := tcpipForward{}
- if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
- return
- }
- addrGot = true
- addrCh <- &payload
- }
- default:
- if req.WantReply {
- _ = req.Reply(true, nil)
+ 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)
}
}
}()
@@ -271,10 +267,10 @@ func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh c
go s.keepAlive(ch)
for req := range reqs {
- if req.Type != "exec" {
- continue
+ if req.WantReply {
+ _ = req.Reply(true, nil)
}
- if len(req.Payload) <= 4 {
+ if req.Type != "exec" || len(req.Payload) <= 4 {
continue
}
end := 4 + binary.BigEndian.Uint32(req.Payload[:4])
@@ -310,6 +306,9 @@ 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 {
diff --git a/test/e2e/pkg/ssh/client.go b/test/e2e/pkg/ssh/client.go
new file mode 100644
index 00000000000..1a923e9c420
--- /dev/null
+++ b/test/e2e/pkg/ssh/client.go
@@ -0,0 +1,89 @@
+package ssh
+
+import (
+ "net"
+
+ libio "github.com/fatedier/golib/io"
+ "golang.org/x/crypto/ssh"
+)
+
+type TunnelClient struct {
+ localAddr string
+ sshServer string
+ commands string
+
+ sshConn *ssh.Client
+ ln net.Listener
+}
+
+func NewTunnelClient(localAddr string, sshServer string, commands string) *TunnelClient {
+ return &TunnelClient{
+ localAddr: localAddr,
+ sshServer: sshServer,
+ commands: commands,
+ }
+}
+
+func (c *TunnelClient) Start() error {
+ config := &ssh.ClientConfig{
+ User: "v0",
+ HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { return nil },
+ }
+
+ conn, err := ssh.Dial("tcp", c.sshServer, config)
+ if err != nil {
+ return err
+ }
+ c.sshConn = conn
+
+ l, err := conn.Listen("tcp", "0.0.0.0:80")
+ if err != nil {
+ return err
+ }
+ c.ln = l
+ ch, req, err := conn.OpenChannel("direct", []byte(""))
+ if err != nil {
+ return err
+ }
+ defer ch.Close()
+ go ssh.DiscardRequests(req)
+
+ type command struct {
+ Cmd string
+ }
+ _, err = ch.SendRequest("exec", false, ssh.Marshal(command{Cmd: c.commands}))
+ if err != nil {
+ return err
+ }
+
+ go c.serveListener()
+ return nil
+}
+
+func (c *TunnelClient) Close() {
+ if c.sshConn != nil {
+ _ = c.sshConn.Close()
+ }
+ if c.ln != nil {
+ _ = c.ln.Close()
+ }
+}
+
+func (c *TunnelClient) serveListener() {
+ for {
+ conn, err := c.ln.Accept()
+ if err != nil {
+ return
+ }
+ go c.hanldeConn(conn)
+ }
+}
+
+func (c *TunnelClient) hanldeConn(conn net.Conn) {
+ defer conn.Close()
+ local, err := net.Dial("tcp", c.localAddr)
+ if err != nil {
+ return
+ }
+ _, _, _ = libio.Join(local, conn)
+}
diff --git a/test/e2e/v1/features/ssh_tunnel.go b/test/e2e/v1/features/ssh_tunnel.go
new file mode 100644
index 00000000000..f67d87aaf99
--- /dev/null
+++ b/test/e2e/v1/features/ssh_tunnel.go
@@ -0,0 +1,193 @@
+package features
+
+import (
+ "crypto/tls"
+ "fmt"
+ "time"
+
+ "github.com/onsi/ginkgo/v2"
+
+ "github.com/fatedier/frp/pkg/transport"
+ "github.com/fatedier/frp/test/e2e/framework"
+ "github.com/fatedier/frp/test/e2e/framework/consts"
+ "github.com/fatedier/frp/test/e2e/mock/server/httpserver"
+ "github.com/fatedier/frp/test/e2e/mock/server/streamserver"
+ "github.com/fatedier/frp/test/e2e/pkg/request"
+ "github.com/fatedier/frp/test/e2e/pkg/ssh"
+)
+
+var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
+ f := framework.NewDefaultFramework()
+
+ ginkgo.It("tcp", func() {
+ sshPort := f.AllocPort()
+ serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+ sshTunnelGateway.bindPort = %d
+ `, sshPort)
+
+ f.RunProcesses([]string{serverConf}, nil)
+
+ localPort := f.PortByName(framework.TCPEchoServerPort)
+ remotePort := f.AllocPort()
+ tc := ssh.NewTunnelClient(
+ fmt.Sprintf("127.0.0.1:%d", localPort),
+ fmt.Sprintf("127.0.0.1:%d", sshPort),
+ fmt.Sprintf("tcp --remote_port %d", remotePort),
+ )
+ framework.ExpectNoError(tc.Start())
+ defer tc.Close()
+
+ time.Sleep(time.Second)
+ framework.NewRequestExpect(f).Port(remotePort).Ensure()
+ })
+
+ ginkgo.It("http", func() {
+ sshPort := f.AllocPort()
+ vhostPort := f.AllocPort()
+ serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+ vhostHTTPPort = %d
+ sshTunnelGateway.bindPort = %d
+ `, vhostPort, sshPort)
+
+ f.RunProcesses([]string{serverConf}, nil)
+
+ localPort := f.PortByName(framework.HTTPSimpleServerPort)
+ tc := ssh.NewTunnelClient(
+ fmt.Sprintf("127.0.0.1:%d", localPort),
+ fmt.Sprintf("127.0.0.1:%d", sshPort),
+ "http --custom_domain test.example.com",
+ )
+ framework.ExpectNoError(tc.Start())
+ defer tc.Close()
+
+ time.Sleep(time.Second)
+ framework.NewRequestExpect(f).Port(vhostPort).
+ RequestModify(func(r *request.Request) {
+ r.HTTP().HTTPHost("test.example.com")
+ }).
+ Ensure()
+ })
+
+ ginkgo.It("https", func() {
+ sshPort := f.AllocPort()
+ vhostPort := f.AllocPort()
+ serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+ vhostHTTPSPort = %d
+ sshTunnelGateway.bindPort = %d
+ `, vhostPort, sshPort)
+
+ f.RunProcesses([]string{serverConf}, nil)
+
+ localPort := f.AllocPort()
+ testDomain := "test.example.com"
+ tc := ssh.NewTunnelClient(
+ fmt.Sprintf("127.0.0.1:%d", localPort),
+ fmt.Sprintf("127.0.0.1:%d", sshPort),
+ fmt.Sprintf("https --custom_domain %s", testDomain),
+ )
+ framework.ExpectNoError(tc.Start())
+ defer tc.Close()
+
+ tlsConfig, err := transport.NewServerTLSConfig("", "", "")
+ framework.ExpectNoError(err)
+ localServer := httpserver.New(
+ httpserver.WithBindPort(localPort),
+ httpserver.WithTLSConfig(tlsConfig),
+ httpserver.WithResponse([]byte("test")),
+ )
+ f.RunServer("", localServer)
+
+ time.Sleep(time.Second)
+ framework.NewRequestExpect(f).
+ Port(vhostPort).
+ RequestModify(func(r *request.Request) {
+ r.HTTPS().HTTPHost(testDomain).TLSConfig(&tls.Config{
+ ServerName: testDomain,
+ InsecureSkipVerify: true,
+ })
+ }).
+ ExpectResp([]byte("test")).
+ Ensure()
+ })
+
+ ginkgo.It("tcpmux", func() {
+ sshPort := f.AllocPort()
+ tcpmuxPort := f.AllocPort()
+ serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+ tcpmuxHTTPConnectPort = %d
+ sshTunnelGateway.bindPort = %d
+ `, tcpmuxPort, sshPort)
+
+ f.RunProcesses([]string{serverConf}, nil)
+
+ localPort := f.AllocPort()
+ testDomain := "test.example.com"
+ tc := ssh.NewTunnelClient(
+ fmt.Sprintf("127.0.0.1:%d", localPort),
+ fmt.Sprintf("127.0.0.1:%d", sshPort),
+ fmt.Sprintf("tcpmux --mux=httpconnect --custom_domain %s", testDomain),
+ )
+ framework.ExpectNoError(tc.Start())
+ defer tc.Close()
+
+ localServer := streamserver.New(
+ streamserver.TCP,
+ streamserver.WithBindPort(localPort),
+ streamserver.WithRespContent([]byte("test")),
+ )
+ f.RunServer("", localServer)
+
+ time.Sleep(time.Second)
+ // Request without HTTP connect should get error
+ framework.NewRequestExpect(f).
+ Port(tcpmuxPort).
+ ExpectError(true).
+ Explain("request without HTTP connect expect error").
+ Ensure()
+
+ proxyURL := fmt.Sprintf("http://127.0.0.1:%d", tcpmuxPort)
+ // Request with incorrect connect hostname
+ framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+ r.Addr("invalid").Proxy(proxyURL)
+ }).ExpectError(true).Explain("request without HTTP connect expect error").Ensure()
+
+ // Request with correct connect hostname
+ framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+ r.Addr(testDomain).Proxy(proxyURL)
+ }).ExpectResp([]byte("test")).Ensure()
+ })
+
+ ginkgo.It("stcp", func() {
+ sshPort := f.AllocPort()
+ serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
+ sshTunnelGateway.bindPort = %d
+ `, sshPort)
+
+ bindPort := f.AllocPort()
+ visitorConf := consts.DefaultClientConfig + fmt.Sprintf(`
+ [[visitors]]
+ name = "stcp-test-visitor"
+ type = "stcp"
+ serverName = "stcp-test"
+ secretKey = "abcdefg"
+ bindPort = %d
+ `, bindPort)
+
+ f.RunProcesses([]string{serverConf}, []string{visitorConf})
+
+ localPort := f.PortByName(framework.TCPEchoServerPort)
+ tc := ssh.NewTunnelClient(
+ fmt.Sprintf("127.0.0.1:%d", localPort),
+ fmt.Sprintf("127.0.0.1:%d", sshPort),
+ "stcp -n stcp-test --sk=abcdefg --allow_users=\"*\"",
+ )
+ framework.ExpectNoError(tc.Start())
+ defer tc.Close()
+
+ time.Sleep(time.Second)
+
+ framework.NewRequestExpect(f).
+ Port(bindPort).
+ Ensure()
+ })
+})
From 38f297a395cab2cb44507e96ed10fb0ee80ecee7 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 28 Nov 2023 18:43:33 +0800
Subject: [PATCH 12/21] Improve the strict configuration validation (#3809)
---
Release.md | 2 +-
pkg/config/load.go | 5 +++-
pkg/config/load_test.go | 53 ++++++++++++++++++++++++++++++++++++++++
pkg/config/v1/common.go | 14 +++++++++++
pkg/config/v1/plugin.go | 16 +++++++++++-
pkg/config/v1/proxy.go | 7 +++++-
pkg/config/v1/visitor.go | 11 +++++++--
7 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/Release.md b/Release.md
index ca8f3a72057..8e1ea863ea7 100644
--- a/Release.md
+++ b/Release.md
@@ -1,6 +1,6 @@
### Features
-* New command line parameter `--strict_config` is added to enable strict configuration validation mode. It will throw an error for non-existent fields instead of ignoring them. In future versions, we may set the default value of this parameter to true.
+* 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`.
diff --git a/pkg/config/load.go b/pkg/config/load.go
index b5539745931..cdbb8e916f4 100644
--- a/pkg/config/load.go
+++ b/pkg/config/load.go
@@ -110,8 +110,11 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
// LoadConfigure loads configuration from bytes and unmarshal into c.
// Now it supports json, yaml and toml format.
-// TODO(fatedier): strict is not valide for ProxyConfigurer/VisitorConfigurer/ClientPluginOptions.
func LoadConfigure(b []byte, c any, strict bool) error {
+ v1.DisallowUnknownFieldsMu.Lock()
+ defer v1.DisallowUnknownFieldsMu.Unlock()
+ v1.DisallowUnknownFields = strict
+
var tomlObj interface{}
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
if err := toml.Unmarshal(b, &tomlObj); err == nil {
diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go
index 9bf7dbbc5be..b3f77800449 100644
--- a/pkg/config/load_test.go
+++ b/pkg/config/load_test.go
@@ -111,3 +111,56 @@ func TestLoadServerConfigStrictMode(t *testing.T) {
}
}
}
+
+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)
+}
diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go
index 72c9d0362c9..24ec9b0d825 100644
--- a/pkg/config/v1/common.go
+++ b/pkg/config/v1/common.go
@@ -15,9 +15,23 @@
package v1
import (
+ "sync"
+
"github.com/fatedier/frp/pkg/util/util"
)
+// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method
+// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder.
+// Here, a global variable is temporarily used to control whether unknown fields are allowed.
+// Once the v2 version is implemented by the community, we can switch to a standardized approach.
+//
+// https://github.com/golang/go/issues/41144
+// https://github.com/golang/go/discussions/63397
+var (
+ DisallowUnknownFields = false
+ DisallowUnknownFieldsMu sync.Mutex
+)
+
type AuthScope string
const (
diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go
index bd5ff384a56..db9d0d1a0b2 100644
--- a/pkg/config/v1/plugin.go
+++ b/pkg/config/v1/plugin.go
@@ -15,6 +15,7 @@
package v1
import (
+ "bytes"
"encoding/json"
"fmt"
"reflect"
@@ -49,7 +50,13 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
}
options := reflect.New(v).Interface().(ClientPluginOptions)
- if err := json.Unmarshal(b, options); err != nil {
+
+ decoder := json.NewDecoder(bytes.NewBuffer(b))
+ if DisallowUnknownFields {
+ decoder.DisallowUnknownFields()
+ }
+
+ if err := decoder.Decode(options); err != nil {
return err
}
c.ClientPluginOptions = options
@@ -77,17 +84,20 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{
}
type HTTP2HTTPSPluginOptions struct {
+ Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
}
type HTTPProxyPluginOptions struct {
+ Type string `json:"type,omitempty"`
HTTPUser string `json:"httpUser,omitempty"`
HTTPPassword string `json:"httpPassword,omitempty"`
}
type HTTPS2HTTPPluginOptions struct {
+ Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
@@ -96,6 +106,7 @@ type HTTPS2HTTPPluginOptions struct {
}
type HTTPS2HTTPSPluginOptions struct {
+ Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
@@ -104,11 +115,13 @@ type HTTPS2HTTPSPluginOptions struct {
}
type Socks5PluginOptions struct {
+ Type string `json:"type,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
type StaticFilePluginOptions struct {
+ Type string `json:"type,omitempty"`
LocalPath string `json:"localPath,omitempty"`
StripPrefix string `json:"stripPrefix,omitempty"`
HTTPUser string `json:"httpUser,omitempty"`
@@ -116,5 +129,6 @@ type StaticFilePluginOptions struct {
}
type UnixDomainSocketPluginOptions struct {
+ Type string `json:"type,omitempty"`
UnixPath string `json:"unixPath,omitempty"`
}
diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go
index 0752479f0c5..8e19d00481c 100644
--- a/pkg/config/v1/proxy.go
+++ b/pkg/config/v1/proxy.go
@@ -15,6 +15,7 @@
package v1
import (
+ "bytes"
"encoding/json"
"errors"
"fmt"
@@ -177,7 +178,11 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
if configurer == nil {
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
}
- if err := json.Unmarshal(b, configurer); err != nil {
+ decoder := json.NewDecoder(bytes.NewBuffer(b))
+ if DisallowUnknownFields {
+ decoder.DisallowUnknownFields()
+ }
+ if err := decoder.Decode(configurer); err != nil {
return err
}
c.ProxyConfigurer = configurer
diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go
index 90ecd86d191..a9b2411ab3d 100644
--- a/pkg/config/v1/visitor.go
+++ b/pkg/config/v1/visitor.go
@@ -15,6 +15,7 @@
package v1
import (
+ "bytes"
"encoding/json"
"errors"
"fmt"
@@ -108,7 +109,11 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
if configurer == nil {
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
}
- if err := json.Unmarshal(b, configurer); err != nil {
+ decoder := json.NewDecoder(bytes.NewBuffer(b))
+ if DisallowUnknownFields {
+ decoder.DisallowUnknownFields()
+ }
+ if err := decoder.Decode(configurer); err != nil {
return err
}
c.VisitorConfigurer = configurer
@@ -120,7 +125,9 @@ func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
if !ok {
return nil
}
- return reflect.New(v).Interface().(VisitorConfigurer)
+ vc := reflect.New(v).Interface().(VisitorConfigurer)
+ vc.GetBaseConfig().Type = string(t)
+ return vc
}
var _ VisitorConfigurer = &STCPVisitorConfig{}
From 97d3cf1a3bbb4314545ae31f17da0f150926fbd3 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 28 Nov 2023 19:02:51 +0800
Subject: [PATCH 13/21] call config complete in nathole discover (#3813)
---
cmd/frpc/sub/nathole.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go
index 56fcf67b72a..fb5b08078c2 100644
--- a/cmd/frpc/sub/nathole.go
+++ b/cmd/frpc/sub/nathole.go
@@ -51,6 +51,7 @@ var natholeDiscoveryCmd = &cobra.Command{
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
if err != nil {
cfg = &v1.ClientCommonConfig{}
+ cfg.Complete()
}
if natHoleSTUNServer != "" {
cfg.NatHoleSTUNServer = natHoleSTUNServer
From 6d9e0c20f6031402f19b13e08ce44a5265c925fe Mon Sep 17 00:00:00 2001
From: im_zhou <32025208+im-zhou@users.noreply.github.com>
Date: Thu, 30 Nov 2023 10:59:08 +0800
Subject: [PATCH 14/21] fix static assets (#3816)
---
pkg/util/http/server.go | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/pkg/util/http/server.go b/pkg/util/http/server.go
index e49cefe49d4..99bed3640d3 100644
--- a/pkg/util/http/server.go
+++ b/pkg/util/http/server.go
@@ -46,9 +46,7 @@ type Server struct {
}
func NewServer(cfg v1.WebServerConfig) (*Server, error) {
- if cfg.AssetsDir != "" {
- assets.Load(cfg.AssetsDir)
- }
+ assets.Load(cfg.AssetsDir)
addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port))
if addr == ":" {
From 95cf4189636327ef405568cf1a7a82a57e5c5bc6 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Fri, 1 Dec 2023 20:18:13 +0800
Subject: [PATCH 15/21] ssh: return informations to client (#3821)
---
client/service.go | 20 +++++++---
pkg/config/flags.go | 79 +++++++++++++++++++++++++-------------
pkg/ssh/server.go | 72 +++++++++++++++++++++++++---------
pkg/ssh/terminal.go | 31 +++++++++++++++
test/e2e/pkg/ssh/client.go | 2 +-
5 files changed, 153 insertions(+), 51 deletions(-)
create mode 100644 pkg/ssh/terminal.go
diff --git a/client/service.go b/client/service.go
index 5db1bd283d5..c43f8f60497 100644
--- a/client/service.go
+++ b/client/service.go
@@ -42,6 +42,14 @@ func init() {
crypto.DefaultSalt = "frp"
}
+type cancelErr struct {
+ Err error
+}
+
+func (e cancelErr) Error() string {
+ return e.Err.Error()
+}
+
// ServiceOptions contains options for creating a new client service.
type ServiceOptions struct {
Common *v1.ClientCommonConfig
@@ -108,7 +116,7 @@ type Service struct {
// service context
ctx context.Context
// call cancel to stop service
- cancel context.CancelFunc
+ cancel context.CancelCauseFunc
gracefulShutdownDuration time.Duration
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
@@ -145,7 +153,7 @@ func NewService(options ServiceOptions) (*Service, 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.FromContextSafe(ctx))
svr.cancel = cancel
@@ -157,7 +165,9 @@ func (svr *Service) Run(ctx context.Context) error {
// first login to frps
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
if svr.ctl == nil {
- return fmt.Errorf("the process exited because the first login to the server failed, and the loginFailExit feature is enabled")
+ cancelCause := cancelErr{}
+ _ = errors.As(context.Cause(svr.ctx), &cancelCause)
+ return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
}
go svr.keepControllerWorking()
@@ -280,7 +290,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
if err != nil {
xl.Warn("connect to server error: %v", err)
if firstLoginExit {
- svr.cancel()
+ svr.cancel(cancelErr{Err: err})
}
return err
}
@@ -356,7 +366,7 @@ func (svr *Service) Close() {
func (svr *Service) GracefulClose(d time.Duration) {
svr.gracefulShutdownDuration = d
- svr.cancel()
+ svr.cancel(nil)
}
func (svr *Service) stop() {
diff --git a/pkg/config/flags.go b/pkg/config/flags.go
index c0e871645df..712e3d3fba3 100644
--- a/pkg/config/flags.go
+++ b/pkg/config/flags.go
@@ -25,6 +25,18 @@ import (
"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
}
@@ -41,8 +53,9 @@ func (f *BandwidthQuantityFlag) Type() string {
return "string"
}
-func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
- registerProxyBaseConfigFlags(cmd, c.GetBaseConfig())
+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")
@@ -73,17 +86,25 @@ func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) {
}
}
-func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) {
+func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) {
if c == nil {
return
}
+ options := ®isterFlagOptions{}
+ for _, opt := range opts {
+ opt(options)
+ }
+
cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name")
- 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)")
+
+ 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) {
@@ -94,13 +115,13 @@ func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) {
cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain")
}
-func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) {
- registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig())
+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) {
+func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) {
if c == nil {
return
}
@@ -113,21 +134,27 @@ func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig)
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")
+func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig, opts ...RegisterFlagOption) {
+ options := ®isterFlagOptions{}
+ for _, opt := range opts {
+ opt(options)
+ }
+
+ if !options.sshMode {
+ cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
+ cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
+ cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
+ fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
+ cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
+ cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path")
+ cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days")
+ cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
+ cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
+ cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
+ c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
+ }
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
- cmd.PersistentFlags().StringVarP(&c.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")
}
type PortsRangeSliceFlag struct {
@@ -185,7 +212,7 @@ func (f *BoolFuncFlag) Type() string {
return "bool"
}
-func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) {
+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")
diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go
index 30e79c64318..264669a3ec3 100644
--- a/pkg/ssh/server.go
+++ b/pkg/ssh/server.go
@@ -27,6 +27,7 @@ import (
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"
@@ -64,6 +65,7 @@ type TunnelServer struct {
underlyingConn net.Conn
sshConn *ssh.ServerConn
sc *ssh.ServerConfig
+ firstChannel ssh.Channel
vc *virtual.Client
peerServerListener *netpkg.InternalListener
@@ -86,6 +88,7 @@ func (s *TunnelServer) Run() error {
if err != nil {
return err
}
+
s.sshConn = sshConn
addr, extraPayload, err := s.waitForwardAddrAndExtraPayload(channels, requests, 3*time.Second)
@@ -93,9 +96,14 @@ func (s *TunnelServer) Run() error {
return err
}
- clientCfg, pc, err := s.parseClientAndProxyConfigurer(addr, extraPayload)
+ clientCfg, pc, helpMessage, err := s.parseClientAndProxyConfigurer(addr, extraPayload)
if err != nil {
- return err
+ 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 {
@@ -142,7 +150,11 @@ func (s *TunnelServer) Run() error {
xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100})
ctx := xlog.NewContext(context.Background(), xl)
go func() {
- _ = s.vc.Run(ctx)
+ 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() {
@@ -153,9 +165,12 @@ func (s *TunnelServer) Run() error {
s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc})
- if err := s.waitProxyStatusReady(pc.GetBaseConfig().Name, time.Second); err != nil {
+ 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()
}
@@ -168,6 +183,13 @@ func (s *TunnelServer) Run() error {
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,
@@ -225,38 +247,47 @@ func (s *TunnelServer) waitForwardAddrAndExtraPayload(
return addr, extraPayload, nil
}
-func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPayload string) (*v1.ClientCommonConfig, v1.ProxyConfigurer, error) {
- cmd := &cobra.Command{}
+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, fmt.Errorf("invalid extra payload")
+ 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, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes)
+ 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, fmt.Errorf("new proxy configurer error")
+ return nil, nil, helpMessage, fmt.Errorf("new proxy configurer error")
}
- config.RegisterProxyFlags(cmd, pc)
+ config.RegisterProxyFlags(cmd, pc, config.WithSSHMode())
clientCfg := v1.ClientCommonConfig{}
- config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
+ config.RegisterClientCommonConfigFlags(cmd, &clientCfg, config.WithSSHMode())
+ cmd.InitDefaultHelpCmd()
if err := cmd.ParseFlags(args); err != nil {
- return nil, nil, fmt.Errorf("parse flags from ssh client error: %v", err)
+ 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, fmt.Errorf("generate random id error: %v", err)
+ 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, nil
+ return &clientCfg, pc, helpMessage, nil
}
func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) {
@@ -264,6 +295,9 @@ func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh c
if err != nil {
return
}
+ if s.firstChannel == nil {
+ s.firstChannel = ch
+ }
go s.keepAlive(ch)
for req := range reqs {
@@ -320,7 +354,7 @@ func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) {
return conn, nil
}
-func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) error {
+func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) (*proxy.WorkingStatus, error) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
@@ -336,14 +370,14 @@ func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration)
}
switch ps.Phase {
case proxy.ProxyPhaseRunning:
- return nil
+ return ps, nil
case proxy.ProxyPhaseStartErr, proxy.ProxyPhaseClosed:
- return errors.New(ps.Err)
+ return ps, errors.New(ps.Err)
}
case <-timer.C:
- return fmt.Errorf("wait proxy status ready timeout")
+ return nil, fmt.Errorf("wait proxy status ready timeout")
case <-s.doneCh:
- return fmt.Errorf("ssh tunnel server closed")
+ return nil, fmt.Errorf("ssh tunnel server closed")
}
}
}
diff --git a/pkg/ssh/terminal.go b/pkg/ssh/terminal.go
new file mode 100644
index 00000000000..a2e9a6ff362
--- /dev/null
+++ b/pkg/ssh/terminal.go
@@ -0,0 +1,31 @@
+// Copyright 2023 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssh
+
+import (
+ "github.com/fatedier/frp/client/proxy"
+ v1 "github.com/fatedier/frp/pkg/config/v1"
+)
+
+func createSuccessInfo(user string, pc v1.ProxyConfigurer, ps *proxy.WorkingStatus) string {
+ base := pc.GetBaseConfig()
+ out := "\n"
+ out += "frp (via SSH) (Ctrl+C to quit)\n\n"
+ out += "User: " + user + "\n"
+ out += "ProxyName: " + base.Name + "\n"
+ out += "Type: " + base.Type + "\n"
+ out += "RemoteAddress: " + ps.RemoteAddr + "\n"
+ return out
+}
diff --git a/test/e2e/pkg/ssh/client.go b/test/e2e/pkg/ssh/client.go
index 1a923e9c420..b45e39daa0d 100644
--- a/test/e2e/pkg/ssh/client.go
+++ b/test/e2e/pkg/ssh/client.go
@@ -41,7 +41,7 @@ func (c *TunnelClient) Start() error {
return err
}
c.ln = l
- ch, req, err := conn.OpenChannel("direct", []byte(""))
+ ch, req, err := conn.OpenChannel("session", []byte(""))
if err != nil {
return err
}
From 9ecafeab40c7d3fb06cfbef5a0e65e01610af3a8 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Fri, 1 Dec 2023 20:44:50 +0800
Subject: [PATCH 16/21] bump version to v0.53.0 (#3822)
---
pkg/util/version/version.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index 2dc60eee6fc..ab79a55b709 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -19,7 +19,7 @@ import (
"strings"
)
-var version = "0.52.3"
+var version = "0.53.0"
func Full() string {
return version
From 7ad62818bd8c31c7b6235acc8f8d98a7c224d3f3 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 2 Dec 2023 16:12:37 +0800
Subject: [PATCH 17/21] update sponsor doc (#3823)
---
README.md | 4 ++++
README_zh.md | 6 +++++-
doc/pic/donate-alipay.png | Bin 37153 -> 0 bytes
doc/pic/sponsor_asocks.jpg | Bin 29877 -> 0 bytes
doc/pic/sponsor_nango.png | Bin 0 -> 14710 bytes
5 files changed, 9 insertions(+), 1 deletion(-)
delete mode 100644 doc/pic/donate-alipay.png
delete mode 100644 doc/pic/sponsor_asocks.jpg
create mode 100644 doc/pic/sponsor_nango.png
diff --git a/README.md b/README.md
index fb1592879e6..ac04279ec87 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@
+
+
+
+
diff --git a/README_zh.md b/README_zh.md
index 77cf797467e..ac4eeec17c1 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -13,6 +13,10 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
+
+
+
+
@@ -84,7 +88,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
### 知识星球
-如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
+如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg)
diff --git a/doc/pic/donate-alipay.png b/doc/pic/donate-alipay.png
deleted file mode 100644
index f717145ca67ec9c686b9b9e0de4e8281c22608b8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 37153
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 u-}^>I)(Nl4
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
D3XZ)?
diff --git a/doc/pic/sponsor_asocks.jpg b/doc/pic/sponsor_asocks.jpg
deleted file mode 100644
index c970decf817bc1f0e1a678fb84f1dcec2f0108a0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 29877
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