Skip to content

Commit

Permalink
feat: frps support ssh
Browse files Browse the repository at this point in the history
  • Loading branch information
int7 committed Nov 8, 2023
1 parent 184223c commit fdd0699
Show file tree
Hide file tree
Showing 13 changed files with 834 additions and 2 deletions.
11 changes: 11 additions & 0 deletions client/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"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/util"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
)
Expand Down Expand Up @@ -104,6 +105,9 @@ func NewControl(

ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
ctl.registerMsgHandlers()

ctl.xl.Info("get pxy cfgs: %v", util.JSONDump(ctl.pxyCfgs))

ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())

ctl.pm = proxy.NewManager(ctl.ctx, clientCfg, ctl.msgTransporter)
Expand Down Expand Up @@ -133,10 +137,12 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
m := &msg.NewWorkConn{
RunID: ctl.runID,
}

if err = ctl.authSetter.SetNewWorkConn(m); err != nil {
xl.Warn("error during NewWorkConn authentication: %v", err)
return
}

if err = msg.WriteMsg(workConn, m); err != nil {
xl.Warn("work connection write to server error: %v", err)
workConn.Close()
Expand All @@ -149,6 +155,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
workConn.Close()
return
}

if startMsg.Error != "" {
xl.Error("StartWorkConn contains error: %s", startMsg.Error)
workConn.Close()
Expand All @@ -161,7 +168,11 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {

func (ctl *Control) handleNewProxyResp(m msg.Message) {
xl := ctl.xl

inMsg := m.(*msg.NewProxyResp)

xl.Info("proxy: %v, remote addr: %v, err: %v", inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)

// 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)
Expand Down
1 change: 1 addition & 0 deletions client/proxy/proxy_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func (pw *Wrapper) checkWorker() {
var newProxyMsg msg.NewProxy
pw.Cfg.MarshalToMsg(&newProxyMsg)
pw.lastSendStartMsg = now

_ = pw.handler(&event.StartProxyPayload{
NewProxyMsg: &newProxyMsg,
})
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.14.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
Expand Down Expand Up @@ -64,7 +65,6 @@ 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
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
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=
Expand Down
1 change: 1 addition & 0 deletions pkg/config/legacy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ServerCommonConf struct {
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `ini:"bind_port" json:"bind_port"`

// KCPBindPort specifies the KCP port that the server listens on. If this
// value is 0, the server will not listen for KCP connections. By default,
// this value is 0.
Expand Down
9 changes: 9 additions & 0 deletions pkg/config/v1/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"github.com/fatedier/frp/pkg/util/util"
)

type SSHGatewayConfig struct {
SSHBindPort int `json:"sshBindPort,omitempty" validate:"gte=0,lte=65535"`
SSHPrivateKeyFilePath string `json:"sshPrivateKeyFilePath,omitempty"`
SSHPublicKeyFilesPath string `json:"sshPublicKeyFilesPath,omitempty"`
}

type ServerConfig struct {
APIMetadata

Expand All @@ -31,6 +37,9 @@ type ServerConfig struct {
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `json:"bindPort,omitempty"`

SSHGatewayConfig SSHGatewayConfig `json:"sshGatewayConfig"`

// 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"`
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/v1/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package v1

const (
// custom define
SSHClientLoginUserPrefix = "_frpc_ssh_client_"
)
6 changes: 6 additions & 0 deletions pkg/util/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
mathrand "math/rand"
"net"
Expand Down Expand Up @@ -144,3 +145,8 @@ func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Durati
func ConstantTimeEqString(a, b string) bool {
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

func JSONDump(v interface{}) string {
prettyJSON, _ := json.MarshalIndent(v, "", "\t")
return string(prettyJSON)
}
9 changes: 8 additions & 1 deletion server/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions server/proxy/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) {
pxy.rc.TCPPortManager.Release(pxy.realBindPort)
}
}()

listener, errRet := net.Listen("tcp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort)))
if errRet != nil {
err = errRet
Expand Down
118 changes: 118 additions & 0 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import (
"io"
"net"
"net/http"
"os"
"strconv"
"time"

"github.com/fatedier/golib/net/mux"
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"
Expand Down Expand Up @@ -66,6 +68,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

Expand Down Expand Up @@ -199,6 +205,56 @@ func NewService(cfg *v1.ServerConfig) (svr *Service, err error) {
svr.listener = ln
log.Info("frps tcp listen on %s", address)

if cfg.SSHGatewayConfig.SSHBindPort > 0 {
svr.sshConfig = &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
file := fmt.Sprintf("%v/%v.pub", cfg.SSHGatewayConfig.SSHPublicKeyFilesPath, ssh.FingerprintSHA256(key))
authorizedKey, err := os.ReadFile(file)
if err != nil {
return nil, err
}

parsedAuthorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(authorizedKey)
if err != nil {
return nil, err
}

if key.Type() == parsedAuthorizedKey.Type() && bytes.Equal(key.Marshal(), parsedAuthorizedKey.Marshal()) {
log.Info("ssh file: %v fingerprint sha256: %v", file, ssh.FingerprintSHA256(key))

return &ssh.Permissions{
Extensions: map[string]string{
ssh.FingerprintSHA256(key): string(authorizedKey),
},
}, nil
}
return nil, fmt.Errorf("unknown public key for %q", conn.User())
},
}

privateBytes, err := os.ReadFile(cfg.SSHGatewayConfig.SSHPrivateKeyFilePath)
if err != nil {
log.Error("Failed to load private key")
return nil, err
}

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.SSHGatewayConfig.SSHBindPort))
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))
Expand Down Expand Up @@ -326,6 +382,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)
}
Expand All @@ -348,6 +408,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
Expand Down Expand Up @@ -493,6 +557,60 @@ 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 := 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()

vs, err := 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 {
Expand Down
Loading

0 comments on commit fdd0699

Please sign in to comment.