diff --git a/.gitignore b/.gitignore index 0b1f7724..9ca1672b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Folders _obj _test +.vscode/ # Architecture specific extensions/prefixes #*.[568vq] @@ -25,3 +26,5 @@ _testmain.go sshpiperd/example/sshpiperd_key* sshpiperd/snap sshpiperd/sshpiperd +sshpiperd/__debug_bin +sshpiperd/sshpiperd.ini diff --git a/sshpiperd/pipemgr.go b/sshpiperd/pipemgr.go index 8e57fce1..ff53b797 100644 --- a/sshpiperd/pipemgr.go +++ b/sshpiperd/pipemgr.go @@ -2,9 +2,10 @@ package main import ( "fmt" - "github.com/tg123/sshpiper/sshpiperd/upstream" "os" "text/template" + + "github.com/tg123/sshpiper/sshpiperd/upstream" ) func createPipeMgr(load func() (upstream.Provider, error)) interface{} { diff --git a/sshpiperd/upstream/database/handler.go b/sshpiperd/upstream/database/handler.go index 151b53b1..a0cde132 100644 --- a/sshpiperd/upstream/database/handler.go +++ b/sshpiperd/upstream/database/handler.go @@ -2,86 +2,280 @@ package database import ( "bytes" - "github.com/jinzhu/gorm" + "fmt" "net" + "github.com/jinzhu/gorm" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" upstreamprovider "github.com/tg123/sshpiper/sshpiperd/upstream" ) -func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) { +type pipeConfig struct { + Username string + UpstreamHost string + MappedUsername string + FromType authMapType + FromPassword string + FromPrivateKey downstreamPrivateKey + FromAuthorizedKeys []downstreamAuthorizedKey + FromAllowAnyPublicKey bool + ToType authMapType + ToPassword string + ToPrivateKey upstreamPrivateKey + ToAuthorizedKeys []upstreamAuthorizedKey + NoPassthrough bool + KnownHosts string + KnownHostsData string + IgnoreHostkey bool +} + +func (p *plugin) loadPipeFromDB(conn ssh.ConnMetadata) (pipeConfig, error) { user := conn.User() d, err := lookupDownstreamWithFallback(p.db, user) if err != nil { - return nil, nil, err + return pipeConfig{}, err } - addr := d.Upstream.Server.Address - upuser := d.Upstream.Username - - if upuser == "" { - upuser = d.Username + pipe := pipeConfig{ + Username: user, + UpstreamHost: d.Upstream.Server.Address, + MappedUsername: d.Upstream.Username, + FromType: d.AuthMapType, + FromPassword: d.Password, + FromAuthorizedKeys: d.AuthorizedKeys, + FromAllowAnyPublicKey: d.AllowAnyPublicKey, + ToType: d.Upstream.AuthMapType, + ToPassword: d.Upstream.Password, + ToPrivateKey: d.Upstream.PrivateKey, + ToAuthorizedKeys: d.Upstream.AuthorizedKeys, + NoPassthrough: d.NoPassthrough, + KnownHosts: d.Upstream.KnownHosts, + IgnoreHostkey: d.Upstream.Server.IgnoreHostKey, } - logger.Printf("mapping user [%v] to [%v@%v]", user, upuser, addr) + return pipe, nil +} - c, err := upstreamprovider.DialForSSH(addr) +func (p *plugin) createAuthPipe(pipe pipeConfig, conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (*ssh.AuthPipe, error) { + hostKeyCallback := ssh.InsecureIgnoreHostKey() + if !pipe.IgnoreHostkey { + + var err error + data := []byte(pipe.KnownHosts) + + if len(data) == 0 { + return nil, fmt.Errorf("no known hosts specified") + } + + hostKeyCallback, err = knownhosts.NewFromReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + } + + to := func(key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) { + var err error + + switch pipe.ToType { + case authMapTypeNone: + return ssh.AuthPipeTypeNone, nil, nil + + case authMapTypePassword: + return ssh.AuthPipeTypeMap, ssh.Password(pipe.ToPassword), nil + + case authMapTypePrivateKey: + + privateBytes := []byte(pipe.ToPrivateKey.Key.Data) + + // did not find to 1 private key try key map + if len(privateBytes) == 0 && key != nil { + for _, privkey := range pipe.ToAuthorizedKeys { + rest := []byte(privkey.Key.Data) + + var authedPubkey ssh.PublicKey + + for len(rest) > 0 { + authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + if err != nil { + return ssh.AuthPipeTypeDiscard, nil, err + } + + keydata := key.Marshal() + + if bytes.Equal(authedPubkey.Marshal(), keydata) { + privateBytes = []byte(pipe.ToPrivateKey.Key.Data) + + if len(privateBytes) > 0 { + // found mapped + break + } + } + } + + } + } + + if len(privateBytes) == 0 { + return ssh.AuthPipeTypeDiscard, nil, fmt.Errorf("no private key found") + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + return ssh.AuthPipeTypeDiscard, nil, err + } + + return ssh.AuthPipeTypeMap, ssh.PublicKeys(private), nil + + default: + logger.Printf("unsupport type [%v] fallback to passthrough", pipe.ToType) + } + + if pipe.NoPassthrough { + return ssh.AuthPipeTypeDiscard, nil, nil + } + + return ssh.AuthPipeTypePassThrough, nil, nil + } + + allowPasswords := make(map[string]bool) + var allowPubKeys []ssh.PublicKey + allowAnyPubKey := false + + a := &ssh.AuthPipe{ + User: pipe.MappedUsername, + + UpstreamHostKeyCallback: hostKeyCallback, + } + + switch pipe.FromType { + case authMapTypeNone: + + if a.NoneAuthCallback == nil { + a.NoneAuthCallback = func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) { + return to(nil) + } + } + + case authMapTypePassword: + + allowPasswords[pipe.FromPassword] = true + + if a.PasswordCallback == nil { + a.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) { + + _, ok := allowPasswords[string(password)] + + if ok { + return to(nil) + } + + if pipe.NoPassthrough { + return ssh.AuthPipeTypeDiscard, nil, nil + } + + return ssh.AuthPipeTypePassThrough, nil, nil + } + } + + case authMapTypePrivateKey: + var err error + + allowAnyPubKey = allowAnyPubKey || pipe.FromAllowAnyPublicKey + + if !allowAnyPubKey { + for _, privkey := range pipe.FromAuthorizedKeys { + rest := []byte(privkey.Key.Data) + + var authedPubkey ssh.PublicKey + + for len(rest) > 0 { + authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + if err != nil { + return nil, err + } + + allowPubKeys = append(allowPubKeys, authedPubkey) + } + } + } + + if a.PublicKeyCallback == nil { + a.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) { + + if allowAnyPubKey { + return to(key) + } + + keydata := key.Marshal() + + for _, authedPubkey := range allowPubKeys { + if bytes.Equal(authedPubkey.Marshal(), keydata) { + return to(key) + } + } + + if pipe.NoPassthrough { + return ssh.AuthPipeTypeDiscard, nil, nil + } + + // will fail but discard will lead a timeout + return ssh.AuthPipeTypePassThrough, nil, nil + } + } + + case authMapTypeAny: + a.NoneAuthCallback = func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) { + return to(nil) + } + + a.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) { + return to(nil) + } + + a.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) { + return to(key) + } + + return a, nil + + default: + return a, fmt.Errorf("unsupported auth type [%v],", pipe.FromType) + } + + return a, nil + +} + +func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) { + + pipe, err := p.loadPipeFromDB(conn) if err != nil { return nil, nil, err } - hostKeyCallback := ssh.InsecureIgnoreHostKey() + if pipe.Username != "" { - if !d.Upstream.Server.IgnoreHostKey { + logger.Printf("mapping [%v] to [%v@%v] from %v to %v", pipe.Username, pipe.MappedUsername, pipe.UpstreamHost, pipe.FromType, pipe.ToType) - key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(d.Upstream.Server.HostKey.Key.Data)) + c, err := upstreamprovider.DialForSSH(pipe.UpstreamHost) if err != nil { return nil, nil, err } - - hostKeyCallback = ssh.FixedHostKey(key) + a, err := p.createAuthPipe(pipe, conn, challengeContext) + if err != nil { + return nil, nil, err + } + return c, a, nil } - pipe := ssh.AuthPipe{ - User: upuser, + return nil, nil, fmt.Errorf("username should not be empty") - PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) { - - expectKey := key.Marshal() - for _, k := range d.AuthorizedKeys { - publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.Key.Data)) - - if err != nil { - logger.Printf("parse [keyid = %v] error :%v. skip to next key", k.Key.ID, err) - continue - } - - if bytes.Equal(publicKey.Marshal(), expectKey) { - - kinterf, err := ssh.ParseRawPrivateKey([]byte(d.Upstream.PrivateKey.Key.Data)) - if err != nil { - break - } - - signer, err := ssh.NewSignerFromKey(kinterf) - if err != nil || signer == nil { - break - } - - return ssh.AuthPipeTypeMap, ssh.PublicKeys(signer), nil - } - } - - return ssh.AuthPipeTypeNone, nil, nil - }, - - UpstreamHostKeyCallback: hostKeyCallback, - } - return c, &pipe, nil } func lookupDownstreamWithFallback(db *gorm.DB, user string) (*downstream, error) { diff --git a/sshpiperd/upstream/database/handler_test.go b/sshpiperd/upstream/database/handler_test.go index 72a2117d..dd1938f8 100644 --- a/sshpiperd/upstream/database/handler_test.go +++ b/sshpiperd/upstream/database/handler_test.go @@ -1,13 +1,14 @@ package database import ( - "github.com/gokyle/sshkey" - "golang.org/x/crypto/ssh" "log" "net" "os" "testing" + "github.com/gokyle/sshkey" + "golang.org/x/crypto/ssh" + "github.com/jinzhu/gorm" upstreamprovider "github.com/tg123/sshpiper/sshpiperd/upstream" ) @@ -80,7 +81,7 @@ func createEntry(t *testing.T, db *gorm.DB, downUser, upUser, serverAddr string, err = db.Create(&downstream{ Username: downUser, - AuthorizedKeys: []authorizedKey{ + AuthorizedKeys: []downstreamAuthorizedKey{ { Key: keydata{ Data: pub, @@ -91,7 +92,7 @@ func createEntry(t *testing.T, db *gorm.DB, downUser, upUser, serverAddr string, Upstream: upstream{ Username: upUser, AuthMapType: authMapTypePrivateKey, - PrivateKey: privateKey{ + PrivateKey: upstreamPrivateKey{ Key: keydata{ Data: priv, Type: "rsa", diff --git a/sshpiperd/upstream/database/model.go b/sshpiperd/upstream/database/model.go index e7bf63d6..33f98a31 100644 --- a/sshpiperd/upstream/database/model.go +++ b/sshpiperd/upstream/database/model.go @@ -10,6 +10,7 @@ const ( authMapTypeNone = iota authMapTypePassword authMapTypePrivateKey + authMapTypeAny ) const fallbackUserEntry = "FALLBACK_USER" @@ -22,13 +23,20 @@ type keydata struct { Type string `gorm:"type:varchar(45)"` } -type privateKey struct { +type upstreamPrivateKey struct { Key keydata KeyID int UpstreamID int } +type downstreamPrivateKey struct { + Key keydata + KeyID int + + DownstreamID int +} + type hostKey struct { Key keydata KeyID int @@ -57,11 +65,14 @@ type upstream struct { Username string `gorm:"type:varchar(45)"` Password string `gorm:"type:varchar(60)"` PrivateKeyID int - PrivateKey privateKey + PrivateKey upstreamPrivateKey AuthMapType authMapType + KnownHosts string `gorm:"type:varchar(100)"` + + AuthorizedKeys []upstreamAuthorizedKey } -type authorizedKey struct { +type upstreamAuthorizedKey struct { Key keydata KeyID int @@ -71,13 +82,26 @@ type authorizedKey struct { type downstream struct { gorm.Model - Name string `gorm:"type:varchar(45)"` - Username string `gorm:"type:varchar(45);unique_index"` + Name string `gorm:"type:varchar(45)"` + Username string `gorm:"type:varchar(45);unique_index"` + Password string `gorm:"type:varchar(60)"` + PrivateKeyID int + PrivateKey downstreamPrivateKey + AuthMapType authMapType + AllowAnyPublicKey bool + NoPassthrough bool UpstreamID int Upstream upstream - AuthorizedKeys []authorizedKey + AuthorizedKeys []downstreamAuthorizedKey +} + +type downstreamAuthorizedKey struct { + Key keydata + KeyID int + + UpstreamID int } type config struct { diff --git a/sshpiperd/upstream/database/plugin.go b/sshpiperd/upstream/database/plugin.go index b1055165..3487f91c 100644 --- a/sshpiperd/upstream/database/plugin.go +++ b/sshpiperd/upstream/database/plugin.go @@ -38,11 +38,13 @@ func (p *plugin) Init(glogger *log.Logger) error { err = db.AutoMigrate( new(keydata), - new(privateKey), + new(upstreamPrivateKey), + new(downstreamPrivateKey), new(hostKey), new(server), new(upstream), - new(authorizedKey), + new(upstreamAuthorizedKey), + new(downstreamAuthorizedKey), new(downstream), new(config), ).Error