feat: ssh client implement (#3671)

* feat: frps support ssh

* fix: comments

* fix: update pkg

* fix: remove useless change

---------

Co-authored-by: int7 <int7@gmail.com>
This commit is contained in:
0x7fff
2023-11-14 15:16:24 +08:00
committed by fatedier
parent f5d5a00eef
commit 8b432e179d
8 changed files with 909 additions and 10 deletions

View File

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

View File

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