libplugin

This commit is contained in:
Boshi Lian 2022-07-02 11:37:38 +00:00
parent b49669bdae
commit dbef31dd3d
18 changed files with 5250 additions and 4 deletions

135
cmd/sshpiperd/daemon.go Normal file
View file

@ -0,0 +1,135 @@
package main
import (
"fmt"
"io/ioutil"
"net"
"path/filepath"
"time"
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/cmd/sshpiperd/internal/plugin"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh"
)
type daemon struct {
config *ssh.PiperConfig
lis net.Listener
loginGraceTime time.Duration
}
func newDaemon(ctx *cli.Context) (*daemon, error) {
config := &ssh.PiperConfig{}
config.SetDefaults()
privateKeys, err := filepath.Glob(ctx.String("server-key"))
if err != nil {
return nil, err
}
if len(privateKeys) == 0 {
return nil, fmt.Errorf("no server key found")
}
log.Infof("found host keys %v", privateKeys)
for _, privateKey := range privateKeys {
log.Infof("loading host key %v", privateKey)
privateBytes, err := ioutil.ReadFile(privateKey)
if err != nil {
return nil, err
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return nil, err
}
config.AddHostKey(private)
}
lis, err := net.Listen("tcp", net.JoinHostPort(ctx.String("address"), ctx.String("port")))
if err != nil {
return nil, fmt.Errorf("failed to listen for connection: %v", err)
}
return &daemon{
config: config,
lis: lis,
loginGraceTime: ctx.Duration("login-grace-time"),
}, nil
}
func (d *daemon) install(plugins ...*plugin.GrpcPlugin) error {
if len(plugins) == 0 {
return fmt.Errorf("no plugins found")
}
// if len(plugins) == 1 {
// return plugins[0].InstallPiperConfig(d.config)
// }
m := plugin.ChainPlugins{}
for _, p := range plugins {
if err := m.Append(p); err != nil {
return err
}
}
return m.InstallPiperConfig(d.config)
}
func (d *daemon) run() error {
defer d.lis.Close()
log.Infof("sshpiperd is listening on: %v", d.lis.Addr().String())
for {
conn, err := d.lis.Accept()
if err != nil {
log.Debugf("failed to accept connection: %v", err)
continue
}
log.Debugf("connection accepted: %v", conn.RemoteAddr())
go func(c net.Conn) {
defer c.Close()
pipec := make(chan *ssh.PiperConn)
errorc := make(chan error)
go func() {
p, err := ssh.NewSSHPiperConn(c, d.config)
if err != nil {
errorc <- err
return
}
pipec <- p
}()
var p *ssh.PiperConn
select {
case p = <-pipec:
case err := <-errorc:
log.Debugf("connection from %v establishing failed reason: %v", c.RemoteAddr(), err)
return
case <-time.After(d.loginGraceTime):
log.Debugf("pipe establishing timeout, disconnected connection from %v", c.RemoteAddr())
return
}
defer p.Close()
log.Infof("ssh connection pipe created %v -> %v", p.DownstreamConnMeta().RemoteAddr(), p.UpstreamConnMeta().RemoteAddr().String())
// TODO add screen recording
err = p.Wait()
log.Infof("connection from %v closed reason: %v", c.RemoteAddr(), err)
}(conn)
}
}

View file

@ -0,0 +1,144 @@
package plugin
import (
"fmt"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/libplugin"
"golang.org/x/crypto/ssh"
)
type ChainPlugins struct {
pluginsCallback []*ssh.PiperConfig
plugins []*GrpcPlugin
}
func (cp *ChainPlugins) Append(p *GrpcPlugin) error {
config, err := p.CreatePiperConfig()
if err != nil {
return err
}
p.OnNextPlugin = cp.onNextPlugin
cp.pluginsCallback = append(cp.pluginsCallback, config)
cp.plugins = append(cp.plugins, p)
return nil
}
func (cp *ChainPlugins) onNextPlugin(challengeCtx ssh.ChallengeContext, upstream *libplugin.UpstreamNextPluginAuth) error {
chain := challengeCtx.(*chainConnMeta)
if chain.current+1 >= len(cp.pluginsCallback) {
return fmt.Errorf("no more plugins")
}
chain.current++
return nil
}
type chainConnMeta struct {
connMeta
current int
}
func (cp *ChainPlugins) CreateChallengeContext(conn ssh.ConnMetadata) (ssh.ChallengeContext, error) {
uiq, err := uuid.NewRandom()
if err != nil {
return nil, err
}
meta := chainConnMeta{
connMeta: connMeta{
UserName: conn.User(),
FromAddr: conn.RemoteAddr().String(),
UniqId: uiq.String(),
},
}
for _, p := range cp.plugins {
if err := p.NewConnection(&meta.connMeta); err != nil {
return nil, err
}
}
return &meta, nil
}
func (cp *ChainPlugins) NextAuthMethods(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) ([]string, error) {
chain := challengeCtx.(*chainConnMeta)
config := cp.pluginsCallback[chain.current]
if config.NextAuthMethods != nil {
return config.NextAuthMethods(conn, challengeCtx)
}
var methods []string
if config.NoneAuthCallback != nil {
methods = append(methods, "none")
}
if config.PasswordCallback != nil {
methods = append(methods, "password")
}
if config.PublicKeyCallback != nil {
methods = append(methods, "publickey")
}
if config.KeyboardInteractiveCallback != nil {
methods = append(methods, "keyboard-interactive")
}
log.Debugf("next auth methods %v", methods)
return methods, nil
}
func (cp *ChainPlugins) InstallPiperConfig(config *ssh.PiperConfig) error {
config.CreateChallengeContext = func(conn ssh.ConnMetadata) (ssh.ChallengeContext, error) {
ctx, err := cp.CreateChallengeContext(conn)
if err != nil {
log.Errorf("cannot create challenge context %v", err)
}
return ctx, err
}
config.NextAuthMethods = cp.NextAuthMethods
config.NoneAuthCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
return cp.pluginsCallback[challengeCtx.(*chainConnMeta).current].NoneAuthCallback(conn, challengeCtx)
}
config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
return cp.pluginsCallback[challengeCtx.(*chainConnMeta).current].PasswordCallback(conn, password, challengeCtx)
}
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
return cp.pluginsCallback[challengeCtx.(*chainConnMeta).current].PublicKeyCallback(conn, key, challengeCtx)
}
config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
return cp.pluginsCallback[challengeCtx.(*chainConnMeta).current].KeyboardInteractiveCallback(conn, client, challengeCtx)
}
config.UpstreamAuthFailureCallback = func(conn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
cur := cp.pluginsCallback[challengeCtx.(*chainConnMeta).current]
if cur.UpstreamAuthFailureCallback != nil {
cur.UpstreamAuthFailureCallback(conn, method, err, challengeCtx)
}
}
config.BannerCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) string {
cur := cp.pluginsCallback[challengeCtx.(*chainConnMeta).current]
if cur.BannerCallback != nil {
return cur.BannerCallback(conn, challengeCtx)
}
return ""
}
return nil
}

View file

@ -0,0 +1,519 @@
package plugin
import (
"context"
"fmt"
"io"
"net"
"os/exec"
"strconv"
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/tg123/remotesigner"
"github.com/tg123/remotesigner/grpcsigner"
"github.com/tg123/sshpiper/libplugin"
"github.com/tg123/sshpiper/libplugin/ioconn"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
)
type GrpcPlugin struct {
OnNextPlugin func(conn ssh.ChallengeContext, upstream *libplugin.UpstreamNextPluginAuth) error
grpcconn *grpc.ClientConn
client libplugin.SshPiperPluginClient
remotesignerClient grpcsigner.SignerClient
hasNewConnectionCallback bool
allowedMethod map[string]bool
}
func DialGrpc(conn *grpc.ClientConn) (*GrpcPlugin, error) {
p := &GrpcPlugin{
grpcconn: conn,
client: libplugin.NewSshPiperPluginClient(conn),
remotesignerClient: grpcsigner.NewSignerClient(conn),
}
return p, nil
}
func (g *GrpcPlugin) InstallPiperConfig(config *ssh.PiperConfig) error {
cb, err := g.client.ListCallbacks(context.Background(), &libplugin.ListCallbackRequest{})
if err != nil {
return err
}
// config.NextAuthMethods = g.NextAuthMethodsLocal
// config.UpstreamAuthFailureCallback = g.UpstreamAuthFailureCallbackLocal
config.CreateChallengeContext = func(conn ssh.ConnMetadata) (ssh.ChallengeContext, error) {
ctx, err := g.CreateChallengeContext(conn)
if err != nil {
log.Errorf("cannot create challenge context %v", err)
}
return ctx, err
}
for _, c := range cb.Callbacks {
switch c {
case "NewConnection":
g.hasNewConnectionCallback = true
case "NextAuthMethods":
config.NextAuthMethods = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) ([]string, error) {
methods, err := g.NextAuthMethodsRemote(conn, challengeCtx)
if err != nil {
log.Errorf("cannot get next auth methods %v", err)
}
log.Debugf("next auth methods %v", methods)
return methods, err
}
case "NoneAuth":
config.NoneAuthCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
log.Debugf("downstream %v is sending none auth", conn.RemoteAddr().String())
u, err := g.NoneAuthCallback(conn, challengeCtx)
if err != nil {
log.Debugf("cannot create upstream for %v with none auth: %v", conn.RemoteAddr().String(), err)
}
return u, err
}
case "PasswordAuth":
config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
log.Debugf("downstream %v is sending password auth", conn.RemoteAddr().String())
u, err := g.PasswordCallback(conn, password, challengeCtx)
if err != nil {
log.Debugf("cannot create upstream for %v with password auth: %v", conn.RemoteAddr().String(), err)
}
return u, err
}
case "PublicKeyAuth":
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
log.Debugf("downstream %v is sending public key auth", conn.RemoteAddr().String())
u, err := g.PublicKeyCallback(conn, key, challengeCtx)
if err != nil {
log.Debugf("cannot create upstream for %v with public key auth: %v", conn.RemoteAddr().String(), err)
}
return u, err
}
case "KeyboardInteractiveAuth":
config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
log.Debugf("downstream %v is sending keyboard interactive auth", conn.RemoteAddr().String())
u, err := g.KeyboardInteractiveCallback(conn, challenge, challengeCtx)
if err != nil {
log.Debugf("cannot create upstream for %v with keyboard interactive auth: %v", conn.RemoteAddr().String(), err)
}
return u, err
}
case "UpstreamAuthFailure":
config.UpstreamAuthFailureCallback = func(conn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
log.Debugf("upstream rejected [%v] auth: %v", method, err)
g.UpstreamAuthFailureCallbackRemote(conn, method, err, challengeCtx)
}
case "Banner":
config.BannerCallback = g.BannerCallback
default:
return fmt.Errorf("unknown callback %s", c)
}
}
return nil
}
func (g *GrpcPlugin) CreatePiperConfig() (*ssh.PiperConfig, error) {
config := &ssh.PiperConfig{}
return config, g.InstallPiperConfig(config)
}
type connMeta libplugin.ConnMeta
// ChallengedUsername implements ssh.ChallengeContext
func (m *connMeta) ChallengedUsername() string {
return m.UserName
}
// Meta implements ssh.ChallengeContext
func (m *connMeta) Meta() interface{} {
return m
}
func (g *GrpcPlugin) CreateChallengeContext(conn ssh.ConnMetadata) (ssh.ChallengeContext, error) {
uiq, err := uuid.NewRandom()
if err != nil {
return nil, err
}
meta := connMeta{
UserName: conn.User(),
FromAddr: conn.RemoteAddr().String(),
UniqId: uiq.String(),
}
return &meta, g.NewConnection(&meta)
}
func (g *GrpcPlugin) NewConnection(meta *connMeta) error {
if g.hasNewConnectionCallback {
_, err := g.client.NewConnection(context.Background(), &libplugin.NewConnectionRequest{
Meta: &libplugin.ConnMeta{
UserName: meta.UserName,
FromAddr: meta.FromAddr,
UniqId: meta.UniqId,
},
})
return err
}
return nil
}
func (g *GrpcPlugin) NextAuthMethodsLocal(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) ([]string, error) {
var allow []string
for k, v := range g.allowedMethod {
if v {
allow = append(allow, k)
}
}
return allow, nil
}
func toMeta(challengeCtx ssh.ChallengeContext) *libplugin.ConnMeta {
switch meta := challengeCtx.(type) {
case *connMeta:
return (*libplugin.ConnMeta)(meta)
case *chainConnMeta:
return (*libplugin.ConnMeta)(&meta.connMeta)
}
panic("unknown challenge context")
}
func (g *GrpcPlugin) NextAuthMethodsRemote(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) ([]string, error) {
meta := toMeta(challengeCtx)
reply, err := g.client.NextAuthMethods(context.Background(), &libplugin.NextAuthMethodsRequest{
Meta: meta,
})
if err != nil {
return nil, err
}
var methods []string
for _, method := range reply.Methods {
m := libplugin.AuthMethodTypeToName(method)
if m == "" {
continue
}
methods = append(methods, m)
}
return methods, nil
}
func (g *GrpcPlugin) UpstreamAuthFailureCallbackLocal(onn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
noMoreMethodErr, ok := err.(ssh.NoMoreMethodsErr)
if ok {
for _, allowed := range noMoreMethodErr.Allowed {
g.allowedMethod[allowed] = true
}
return
}
g.allowedMethod[method] = false
}
func (g *GrpcPlugin) UpstreamAuthFailureCallbackRemote(onn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
noMoreMethodErr, ok := err.(ssh.NoMoreMethodsErr)
allowed := make([]libplugin.AuthMethod, len(noMoreMethodErr.Allowed))
if ok {
for _, method := range noMoreMethodErr.Allowed {
m := libplugin.AuthMethodFromName(method)
if m == -1 {
continue
}
allowed = append(allowed, m)
}
}
g.client.UpstreamAuthFailureNotice(context.Background(), &libplugin.UpstreamAuthFailureNoticeRequest{
Meta: toMeta(challengeCtx),
Method: method,
Error: err.Error(),
AllowedMethods: allowed,
})
}
func (g *GrpcPlugin) createUpstream(challengeCtx ssh.ChallengeContext, upstream *libplugin.Upstream) (*ssh.Upstream, error) {
if upstream.GetNextPlugin() != nil {
if g.OnNextPlugin == nil {
return nil, fmt.Errorf("next plugin is not supported")
}
return nil, g.OnNextPlugin(challengeCtx, upstream.GetNextPlugin())
}
meta := toMeta(challengeCtx)
port := upstream.Port
if port <= 0 {
port = 22
}
addr := net.JoinHostPort(upstream.Host, strconv.Itoa(int(port)))
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
config := ssh.ClientConfig{
HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
if upstream.IgnoreHostKey {
return nil
}
verify, err := g.client.VerifyHostKey(context.Background(), &libplugin.VerifyHostKeyRequest{
Meta: meta,
Key: key.Marshal(),
})
if err != nil {
return err
}
if !verify.Verified {
return fmt.Errorf("host key verification failed")
}
return nil
},
}
auth := make([]string, 0)
if upstream.GetNone() != nil {
config.Auth = append(config.Auth, ssh.NoneAuth())
auth = append(auth, "none")
}
if a := upstream.GetPassword(); a != nil {
config.Auth = append(config.Auth, ssh.Password(a.GetPassword()))
auth = append(auth, "password")
}
if a := upstream.GetPrivateKey(); a != nil {
private, err := ssh.ParsePrivateKey(a.GetPrivateKey())
if err != nil {
return nil, err
}
config.Auth = append(config.Auth, ssh.PublicKeys(private))
auth = append(auth, "privatekey")
}
if a := upstream.GetRemoteSigner(); a != nil {
rs := remotesigner.New(grpcsigner.New(g.remotesignerClient, a.Meta))
signer, err := ssh.NewSignerFromSigner(rs)
if err != nil {
return nil, err
}
config.Auth = append(config.Auth, ssh.PublicKeys(signer))
auth = append(auth, "remotesigner")
}
if len(config.Auth) == 0 {
log.Warnf("no auth method found for upstream %s, add none auth", addr)
auth = append(auth, "none")
config.Auth = append(config.Auth, ssh.NoneAuth())
}
log.Debugf("connecting to upstream %v with auth %v", c.RemoteAddr().String(), auth)
return &ssh.Upstream{
Conn: c,
Address: addr,
ClientConfig: config,
}, nil
}
func (g *GrpcPlugin) NoneAuthCallback(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
meta := toMeta(challengeCtx)
reply, err := g.client.NoneAuth(context.Background(), &libplugin.NoneAuthRequest{
Meta: meta,
})
if err != nil {
return nil, err
}
return g.createUpstream(challengeCtx, reply.Upstream)
}
func (g *GrpcPlugin) PasswordCallback(conn ssh.ConnMetadata, password []byte, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
meta := toMeta(challengeCtx)
reply, err := g.client.PasswordAuth(context.Background(), &libplugin.PasswordAuthRequest{
Meta: meta,
Password: password,
})
if err != nil {
return nil, err
}
return g.createUpstream(challengeCtx, reply.Upstream)
}
func (g *GrpcPlugin) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
meta := toMeta(challengeCtx)
reply, err := g.client.PublicKeyAuth(context.Background(), &libplugin.PublicKeyAuthRequest{
Meta: meta,
PublicKey: key.Marshal(),
})
if err != nil {
return nil, err
}
return g.createUpstream(challengeCtx, reply.Upstream)
}
func (g *GrpcPlugin) KeyboardInteractiveCallback(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
stream, err := g.client.KeyboardInteractiveAuth(context.Background())
if err != nil {
return nil, err
}
defer stream.CloseSend()
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil, nil
}
if err != nil {
return nil, err
}
if r := msg.GetPromptRequest(); r != nil {
var questions []string
var echo []bool
for _, q := range r.GetQuestions() {
questions = append(questions, q.GetText())
echo = append(echo, q.GetEcho())
}
ans, err := client(conn.User(), r.GetInstruction(), questions, echo)
if err != nil {
return nil, err
}
if len(questions) > 0 {
if err := stream.Send(&libplugin.KeyboardInteractiveAuthMessage{
Message: &libplugin.KeyboardInteractiveAuthMessage_UserResponse{
UserResponse: &libplugin.KeyboardInteractiveUserResponse{
Answers: ans,
},
},
}); err != nil {
return nil, err
}
}
} else if r := msg.GetMetaRequest(); r != nil {
meta := toMeta(challengeCtx)
if err := stream.Send(&libplugin.KeyboardInteractiveAuthMessage{
Message: &libplugin.KeyboardInteractiveAuthMessage_MetaResponse{
MetaResponse: &libplugin.KeyboardInteractiveMetaResponse{
Meta: meta,
},
},
}); err != nil {
return nil, err
}
} else if r := msg.GetFinishRequest(); r != nil {
if r.GetUpstream() != nil {
return g.createUpstream(challengeCtx, r.GetUpstream())
}
return nil, fmt.Errorf("auth failed: %s", r.GetErrorMessage())
}
}
}
func (g *GrpcPlugin) BannerCallback(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) string {
meta := toMeta(challengeCtx)
reply, err := g.client.Banner(context.Background(), &libplugin.BannerRequest{
Meta: meta,
})
if err != nil {
log.Debugf("failed to get banner: %v", err)
return ""
}
return reply.GetMessage()
}
func (g *GrpcPlugin) RecvLogs(writer io.Writer) error {
stream, err := g.client.Logs(context.Background(), &libplugin.StartLogRequest{})
if err != nil {
return err
}
for {
line, err := stream.Recv()
if err != nil {
log.Errorf("recv log error: %v", err)
return err
}
fmt.Fprintln(writer, line.GetMessage())
}
}
type CmdPlugin struct {
GrpcPlugin
}
func DialCmd(cmd *exec.Cmd) (*CmdPlugin, error) {
cmdconn, stderr, err := ioconn.DialCmd(cmd)
if err != nil {
return nil, err
}
go io.Copy(log.StandardLogger().Out, stderr)
go func() {
err := cmd.Wait()
if err != nil {
log.Errorf("cmd %v error: %v", cmd.Path, err)
}
}()
conn, err := grpc.Dial("", grpc.WithInsecure(), grpc.WithDialer(func(_ string, _ time.Duration) (net.Conn, error) {
return cmdconn, nil
}))
if err != nil {
return nil, err
}
g, err := DialGrpc(conn)
if err != nil {
return nil, err
}
return &CmdPlugin{*g}, nil
}

163
cmd/sshpiperd/main.go Normal file
View file

@ -0,0 +1,163 @@
package main
import (
"fmt"
"os"
"os/exec"
"runtime/debug"
"time"
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/cmd/sshpiperd/internal/plugin"
"github.com/urfave/cli/v2"
)
var mainver string = "(devel)"
func version() string {
var v = mainver
bi, ok := debug.ReadBuildInfo()
if !ok {
return v
}
for _, s := range bi.Settings {
switch s.Key {
case "vcs.revision":
v = fmt.Sprintf("%v, %v", v, s.Value[:9])
case "vcs.time":
v = fmt.Sprintf("%v, %v", v, s.Value)
}
}
v = fmt.Sprintf("%v, %v", v, bi.GoVersion)
return v
}
func splitByDash(args []string) ([]string, []string) {
for i, arg := range args {
if arg == "--" {
return args[:i], args[i+1:]
}
}
return args, nil
}
func main() {
app := &cli.App{
Name: "sshpiperd",
Usage: "the missing reverse proxy for ssh scp",
UsageText: "sshpiperd [options] <plugin1> [plugin options] [-- [plugin2] [plugin options] [-- ...]]",
Description: "sshpiperd works as a proxy-like ware, and route connections by username, src ip , etc.\nhttps://github.com/tg123/sshpiper",
Version: version(),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Aliases: []string{"l"},
Value: "0.0.0.0",
Usage: "listening address",
EnvVars: []string{"SSHPIPERD_ADDRESS"},
},
&cli.IntFlag{
Name: "port",
Aliases: []string{"p"},
Value: 2222,
Usage: "listening port",
EnvVars: []string{"SSHPIPERD_PORT"},
},
&cli.StringFlag{
Name: "server-key",
Aliases: []string{"i"},
Usage: "server key files, support wildcard",
Value: "/etc/ssh/ssh_host_rsa_key",
EnvVars: []string{"SSHPIPERD_SERVER_KEY"},
},
&cli.DurationFlag{
Name: "login-grace-time",
Value: 30 * time.Second,
Usage: "sshpiperd forcely close the connection after this time if the pipe has not successfully established",
EnvVars: []string{"SSHPIPERD_LOGIN_GRACE_TIME"},
},
&cli.StringFlag{
Name: "log-level",
Value: "info",
Usage: "log level, one of: trace, debug, info, warn, error, fatal, panic",
EnvVars: []string{"SSHPIPERD_LOG_LEVEL"},
},
},
// Commands: []*cli.Command{
// &cli.Command{
// Name: "plug",
// },
// // &cli.Command{
// // Name: "grpc",
// // Action: func(ctx *cli.Context) error {
// // return fmt.Errorf("not implemented")
// // },
// // },
// },
Action: func(ctx *cli.Context) error {
level, err := log.ParseLevel(ctx.String("log-level"))
if err != nil {
return err
}
log.SetLevel(level)
log.Info("starting sshpiperd version: ", version())
d, err := newDaemon(ctx)
if err != nil {
return err
}
var plugins []*plugin.GrpcPlugin
args := ctx.Args().Slice()
remain := args
for {
if len(remain) <= 0 {
break
}
args, remain = splitByDash(remain)
if len(args) <= 0 {
continue
}
exe := args[0]
cmd := exec.Command(exe)
cmd.Args = args
log.Info("starting plugin: ", cmd.Args)
p, err := plugin.DialCmd(cmd)
if err != nil {
return err
}
go p.RecvLogs(log.StandardLogger().Out)
plugins = append(plugins, &p.GrpcPlugin)
}
if err := d.install(plugins...); err != nil {
return err
}
return d.run()
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}