working dir to support v1

This commit is contained in:
Boshi Lian 2022-06-27 06:37:05 +00:00
parent 4c37ba232f
commit ee76ceab6c
7 changed files with 213 additions and 49 deletions

View file

@ -72,6 +72,12 @@ func installDrivers(piper *ssh.PiperConfig, config *piperdConfig, logger *log.Lo
return upstream.Get(n)
},
func(plugin registry.Plugin) error {
v1, ok := plugin.(upstream.V1Provider)
if ok {
return v1.InstallUpstream(piper)
}
handler := plugin.(upstream.Provider).GetHandler()
if handler == nil {

View file

@ -55,6 +55,12 @@ type Provider interface {
GetHandler() Handler
}
// temp
type V1Provider interface {
registry.Plugin
InstallUpstream(config *ssh.PiperConfig) error
}
var (
drivers = registry.NewRegistry()
)

View file

@ -2,10 +2,11 @@ package workingdir
var (
config = struct {
WorkingDir string `long:"upstream-workingdir" default:"/var/sshpiper" description:"Path to working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR" ini-name:"upstream-workingdir"`
AllowBadUsername bool `long:"upstream-workingdir-allowbadusername" description:"Disable username check while search the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_ALLOWBADUSERNAME" ini-name:"upstream-workingdir-allowbadusername"`
NoCheckPerm bool `long:"upstream-workingdir-nocheckperm" description:"Disable 0400 checking when using files in the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM" ini-name:"upstream-workingdir-nocheckperm"`
FallbackUsername string `long:"upstream-workingdir-fallbackusername" description:"Fallback to a user when user does not exists in directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_FALLBACKUSERNAME" ini-name:"upstream-workingdir-fallbackusername"`
StrictHostKey bool `long:"upstream-workingdir-stricthostkey" description:"Upstream host public key must be in known_hosts file, otherwise drop the connection" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_STRICTHOSTKEY" ini-name:"upstream-workingdir-stricthostkey"`
WorkingDir string `long:"upstream-workingdir" default:"/var/sshpiper" description:"Path to working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR" ini-name:"upstream-workingdir"`
AllowBadUsername bool `long:"upstream-workingdir-allowbadusername" description:"Disable username check while search the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_ALLOWBADUSERNAME" ini-name:"upstream-workingdir-allowbadusername"`
NoCheckPerm bool `long:"upstream-workingdir-nocheckperm" description:"Disable 0400 checking when using files in the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM" ini-name:"upstream-workingdir-nocheckperm"`
FallbackUsername string `long:"upstream-workingdir-fallbackusername" description:"Fallback to a user when user does not exists in directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_FALLBACKUSERNAME" ini-name:"upstream-workingdir-fallbackusername"`
StrictHostKey bool `long:"upstream-workingdir-stricthostkey" description:"Upstream host public key must be in known_hosts file, otherwise drop the connection" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_STRICTHOSTKEY" ini-name:"upstream-workingdir-stricthostkey"`
MatchPublicKeyInSubDir bool `long:"upstream-workingdir-matchpublickeyinsubdir" description:"Remap user in user's sub dir with publickey" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_MATCHPUBLICKeYINSUBDIR" ini-name:"upstream-workingdir-matchpublickeyinsubdir"`
}{}
)

View file

@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"github.com/tg123/sshpiper/sshpiperd/upstream"
)
@ -20,7 +21,8 @@ func (p *plugin) ListPipe() ([]upstream.Pipe, error) {
continue
}
data, err := userUpstreamFile.read(file.Name())
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, file.Name())}
data, err := userUpstreamFile.read()
if err != nil {
continue
}
@ -42,12 +44,14 @@ func (p *plugin) ListPipe() ([]upstream.Pipe, error) {
}
func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error {
err := os.MkdirAll(config.WorkingDir+"/"+opt.Username, 0775)
userdir := path.Join(config.WorkingDir, opt.Username)
err := os.MkdirAll(userdir, 0775)
if err != nil {
return err
}
path := userUpstreamFile.realPath(opt.Username)
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir}
path := userUpstreamFile.realPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
upuser := opt.UpstreamUsername
@ -66,7 +70,8 @@ func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error {
}
func (p *plugin) RemovePipe(name string) error {
path := userUpstreamFile.realPath(name)
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, name)}
path := userUpstreamFile.realPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
}

View file

@ -0,0 +1,133 @@
package workingdir
import (
"fmt"
"net"
"os"
"path"
"path/filepath"
"github.com/tg123/sshpiper/sshpiperd/v0bridge"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
func (p *plugin) InstallUpstream(piper *ssh.PiperConfig) error {
v0bridge.InstallUpstream(piper, p.GetHandler())
piper.PublicKeyCallback = p.matchPublicKeyInSubDir
return nil
}
func (p *plugin) matchPublicKeyDir(conn ssh.ConnMetadata, key ssh.PublicKey, user, userdir string) (*ssh.Upstream, error) {
if !checkUsername(user) {
return nil, fmt.Errorf("downstream is not using a valid username")
}
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir}
err := userUpstreamFile.checkPerm()
if os.IsNotExist(err) && len(config.FallbackUsername) > 0 {
user = config.FallbackUsername
} else if err != nil {
return nil, err
}
data, err := userUpstreamFile.read()
if err != nil {
return nil, err
}
host, port, mappedUser, err := parseUpstreamFile(string(data))
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%v:%v", host, port)
logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr)
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
hostKeyCallback := ssh.InsecureIgnoreHostKey()
if config.StrictHostKey {
userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir}
hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath())
if err != nil {
return nil, err
}
}
signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key)
if err != nil {
return nil, err
}
if signer == nil {
return nil, fmt.Errorf("cant find public key in user folder")
}
return &ssh.Upstream{
Conn: c,
ClientConfig: ssh.ClientConfig{
User: mappedUser,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: hostKeyCallback,
},
}, nil
}
func (p *plugin) matchPublicKeyInSubDir(conn ssh.ConnMetadata, key ssh.PublicKey, _ ssh.ChallengeContext) (*ssh.Upstream, error) {
{
userdir := path.Join(config.WorkingDir, conn.User())
u, err := p.matchPublicKeyDir(conn, key, conn.User(), userdir)
if err != nil && !config.MatchPublicKeyInSubDir {
logger.Errorf("cannot map private key in %v: %v", userdir, err)
return nil, err
}
if u != nil {
return u, nil
}
}
var upstream *ssh.Upstream
// search in working dir
filepath.Walk(config.WorkingDir, func(path string, info os.FileInfo, err error) error {
logger.Debug("search public key in path: %v", path)
if err != nil {
logger.Debug("error walking path: ", err)
return nil
}
if !info.IsDir() {
return nil
}
u, err := p.matchPublicKeyDir(conn, key, conn.User(), path)
if err != nil {
logger.Info("cannot map private key in %v: %v, search next", path, err)
}
if u != nil {
upstream = u
return fmt.Errorf("stop")
}
return nil
})
if upstream != nil {
return upstream, nil
}
return nil, fmt.Errorf("no matching public key found in %v", config.WorkingDir)
}

View file

@ -18,19 +18,24 @@ import (
"github.com/tg123/sshpiper/sshpiperd/upstream"
"github.com/tg123/sshpiper/sshpiperd/v0bridge"
"golang.org/x/crypto/ssh/knownhosts"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
type userFile string
type userFile struct {
filename string
userdir string
}
const (
userAuthorizedKeysFile = "authorized_keys"
userKeyFile = "id_rsa"
userUpstreamFile = "sshpiper_upstream"
userKnownHosts = "known_hosts"
)
var (
userAuthorizedKeysFile userFile = "authorized_keys"
userKeyFile userFile = "id_rsa"
userUpstreamFile userFile = "sshpiper_upstream"
userKnownHosts userFile = "known_hosts"
usernameRule *regexp.Regexp
)
@ -41,21 +46,25 @@ func init() {
usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$")
}
func userSpecFile(user, file string) string {
return path.Join(config.WorkingDir, user, file)
func (file userFile) userSpecFile(filename string) string {
p := file.userdir
if p == "" {
p = config.WorkingDir
}
return path.Join(p, filename)
}
func (file userFile) read(user string) ([]byte, error) {
return ioutil.ReadFile(userSpecFile(user, string(file)))
func (file userFile) read() ([]byte, error) {
return ioutil.ReadFile(file.userSpecFile(file.filename))
}
func (file userFile) realPath(user string) string {
return userSpecFile(user, string(file))
func (file userFile) realPath() string {
return file.userSpecFile(file.filename)
}
// return error if other and group have access right
func (file userFile) checkPerm(user string) error {
filename := userSpecFile(user, string(file))
func (file userFile) checkPerm() error {
filename := file.userSpecFile(file.filename)
f, err := os.Open(filename)
if err != nil {
return err
@ -114,14 +123,16 @@ func parseUpstreamFile(data string) (host string, port int, user string, err err
return
}
func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *v0bridge.AuthPipe, error) {
func findUpstreamFromUserfile(conn ssh.ConnMetadata, _ ssh.ChallengeContext) (net.Conn, *v0bridge.AuthPipe, error) {
user := conn.User()
userdir := path.Join(config.WorkingDir, conn.User())
if !checkUsername(user) {
return nil, nil, fmt.Errorf("downstream is not using a valid username")
}
err := userUpstreamFile.checkPerm(user)
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir}
err := userUpstreamFile.checkPerm()
if os.IsNotExist(err) && len(config.FallbackUsername) > 0 {
user = config.FallbackUsername
@ -129,7 +140,7 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe
return nil, nil, err
}
data, err := userUpstreamFile.read(user)
data, err := userUpstreamFile.read()
if err != nil {
return nil, nil, err
}
@ -150,7 +161,8 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe
hostKeyCallback := ssh.InsecureIgnoreHostKey()
if config.StrictHostKey {
hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath(user))
userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir}
hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath())
if err != nil {
return nil, nil, err
@ -161,7 +173,7 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe
User: mappedUser,
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (v0bridge.AuthPipeType, ssh.AuthMethod, error) {
signer, err := mapPublicKeyFromUserfile(conn, key)
signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key)
if err != nil || signer == nil {
// try one
@ -175,20 +187,15 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe
}, nil
}
func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer ssh.Signer, err error) {
user := conn.User()
if !checkUsername(user) {
return nil, fmt.Errorf("downstream is not using a valid username")
}
func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, user, userdir string, key ssh.PublicKey) (signer ssh.Signer, err error) {
defer func() { // print error when func exit
if err != nil {
logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr())
}
}()
err = userAuthorizedKeysFile.checkPerm(user)
userAuthorizedKeysFile := userFile{filename: userAuthorizedKeysFile, userdir: userdir}
err = userAuthorizedKeysFile.checkPerm()
if os.IsNotExist(err) && len(config.FallbackUsername) > 0 {
err = nil
@ -200,11 +207,12 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer
keydata := key.Marshal()
var rest []byte
rest, err = userAuthorizedKeysFile.read(user)
rest, err = userAuthorizedKeysFile.read()
if err != nil {
return nil, err
}
userKeyFile := userFile{filename: userKeyFile, userdir: userdir}
var authedPubkey ssh.PublicKey
for len(rest) > 0 {
@ -215,13 +223,13 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer
}
if bytes.Equal(authedPubkey.Marshal(), keydata) {
err = userKeyFile.checkPerm(user)
err = userKeyFile.checkPerm()
if err != nil {
return nil, err
}
var privateBytes []byte
privateBytes, err = userKeyFile.read(user)
privateBytes, err = userKeyFile.read()
if err != nil {
return nil, err
}
@ -233,7 +241,7 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer
}
// in log may see this twice, one is for query the other is real sign again
logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", userKeyFile.realPath(user), user, conn.RemoteAddr())
logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", userKeyFile.realPath(), user, conn.RemoteAddr())
return private, nil
}
}

View file

@ -73,7 +73,7 @@ func (p *proxy) CreateChallengeContext(conn ssh.ConnMetadata) (ssh.ChallengeCont
return p, nil
}
func (p *proxy) createUpstream(conn net.Conn, pipe *AuthPipe, authType AuthPipeType, oldMethod, mappedMethod ssh.AuthMethod) (*ssh.Upstream, error) {
func CreateUpstream(conn net.Conn, pipe *AuthPipe, authType AuthPipeType, oldMethod, mappedMethod ssh.AuthMethod) (*ssh.Upstream, error) {
clientConfig := ssh.ClientConfig{
User: pipe.User,
@ -130,7 +130,7 @@ func (p *proxy) NoneAuthCallback(conn ssh.ConnMetadata, challengeCtx ssh.Challen
}
if pipe.NoneAuthCallback == nil {
return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil)
return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil)
}
t, m, err := pipe.NoneAuthCallback(conn)
@ -138,7 +138,7 @@ func (p *proxy) NoneAuthCallback(conn ssh.ConnMetadata, challengeCtx ssh.Challen
return nil, err
}
return p.createUpstream(c, pipe, t, ssh.NoneAuth(), m)
return CreateUpstream(c, pipe, t, ssh.NoneAuth(), m)
}
func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
@ -148,7 +148,7 @@ func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challen
}
if pipe.PasswordCallback == nil {
return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.Password(string(password)), nil)
return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.Password(string(password)), nil)
}
t, m, err := pipe.PasswordCallback(conn, password)
@ -156,7 +156,7 @@ func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challen
return nil, err
}
return p.createUpstream(c, pipe, t, ssh.Password(string(password)), m)
return CreateUpstream(c, pipe, t, ssh.Password(string(password)), m)
}
func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) {
@ -166,7 +166,7 @@ func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, chal
}
if pipe.PublicKeyCallback == nil {
return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil)
return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil)
}
t, m, err := pipe.PublicKeyCallback(conn, key)
@ -174,10 +174,13 @@ func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, chal
return nil, err
}
return p.createUpstream(c, pipe, t, ssh.NoneAuth(), m) // cannt passthrough public key, use none instead
return CreateUpstream(c, pipe, t, ssh.NoneAuth(), m) // cannt passthrough public key, use none instead
}
func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *AuthPipe, error)) {
func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *AuthPipe, error)) error {
if handler == nil {
return fmt.Errorf("handler is nil")
}
p := &proxy{
handler: handler,
@ -192,4 +195,6 @@ func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata
config.NoneAuthCallback = p.NoneAuthCallback
config.PasswordCallback = p.PasswordCallback
config.PublicKeyCallback = p.PublicKeyCallback
return nil
}