finish moving working dir
This commit is contained in:
parent
fd0a0c628e
commit
19f851fc74
10 changed files with 270 additions and 970 deletions
153
plugin/internal/workingdir/workingdir.go
Normal file
153
plugin/internal/workingdir/workingdir.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package workingdir
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Workingdir struct {
|
||||
Path string
|
||||
NoCheckPerm bool
|
||||
Strict bool
|
||||
}
|
||||
|
||||
var (
|
||||
usernameRule *regexp.Regexp
|
||||
)
|
||||
|
||||
const (
|
||||
userAuthorizedKeysFile = "authorized_keys"
|
||||
userKeyFile = "id_rsa"
|
||||
userUpstreamFile = "sshpiper_upstream"
|
||||
userKnownHosts = "known_hosts"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Base username validation on Debians default: https://sources.debian.net/src/adduser/3.113%2Bnmu3/adduser.conf/#L85
|
||||
// -> NAME_REGEX="^[a-z][-a-z0-9_]*\$"
|
||||
// The length is limited to 32 characters. See man 8 useradd: https://linux.die.net/man/8/useradd
|
||||
usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$")
|
||||
}
|
||||
|
||||
func IsUsernameSecure(user string) bool {
|
||||
return usernameRule.MatchString(user)
|
||||
}
|
||||
|
||||
func (w *Workingdir) Mapkey(pub []byte) ([]byte, error) {
|
||||
|
||||
var rest []byte
|
||||
rest, err := w.readfile(userAuthorizedKeysFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var authedPubkey ssh.PublicKey
|
||||
|
||||
for len(rest) > 0 {
|
||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(authedPubkey.Marshal(), pub) {
|
||||
log.Infof("found mapping key %v", w.fullpath(userKeyFile))
|
||||
return w.readfile(userKeyFile)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no matching key found")
|
||||
}
|
||||
|
||||
func (w *Workingdir) CreateUpstream() (*libplugin.Upstream, error) {
|
||||
|
||||
data, err := w.readfile(userUpstreamFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, port, user, err := parseUpstreamFile(string(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &libplugin.Upstream{
|
||||
Host: host,
|
||||
Port: int32(port),
|
||||
UserName: user,
|
||||
IgnoreHostKey: !w.Strict,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Workingdir) checkPerm(file string) error {
|
||||
filename := path.Join(w.Path, file)
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.NoCheckPerm {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Mode().Perm()&0077 != 0 {
|
||||
return fmt.Errorf("%v's perm is too open", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Workingdir) fullpath(file string) string {
|
||||
return path.Join(w.Path, file)
|
||||
}
|
||||
|
||||
func (w *Workingdir) readfile(file string) ([]byte, error) {
|
||||
if err := w.checkPerm(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioutil.ReadFile(w.fullpath(file))
|
||||
}
|
||||
|
||||
func parseUpstreamFile(data string) (host string, port int, user string, err error) {
|
||||
r := bufio.NewReader(strings.NewReader(data))
|
||||
for {
|
||||
host, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
if host != "" && host[0] != '#' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t := strings.SplitN(host, "@", 2)
|
||||
|
||||
if len(t) > 1 {
|
||||
user = t[0]
|
||||
host = t[1]
|
||||
}
|
||||
|
||||
host, port, err = libplugin.SplitHostPortForSSH(host)
|
||||
return
|
||||
}
|
||||
|
|
@ -6,21 +6,23 @@ import (
|
|||
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tg123/sshpiper/plugin/internal/workingdir"
|
||||
)
|
||||
|
||||
func createWorkingdir(c *cli.Context, user string) (*workingdir, error) {
|
||||
func createWorkingdir(c *cli.Context, user string) (*workingdir.Workingdir, error) {
|
||||
if !c.Bool("allow-baduser-name") {
|
||||
if !isUsernameSecure(user) {
|
||||
if !workingdir.IsUsernameSecure(user) {
|
||||
return nil, fmt.Errorf("bad username: %s", user)
|
||||
}
|
||||
}
|
||||
|
||||
root := c.String("root")
|
||||
|
||||
return &workingdir{
|
||||
path: path.Join(root, user),
|
||||
noCheckPerm: c.Bool("no-check-perm"),
|
||||
strict: c.Bool("strict-hostkey"),
|
||||
return &workingdir.Workingdir{
|
||||
Path: path.Join(root, user),
|
||||
NoCheckPerm: c.Bool("no-check-perm"),
|
||||
Strict: c.Bool("strict-hostkey"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,7 @@ func main() {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
u, err := w.createUpstream()
|
||||
u, err := w.CreateUpstream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -81,12 +83,16 @@ func main() {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
u, err := w.createUpstream()
|
||||
u, err := w.CreateUpstream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k, err := w.Mapkey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k, err := w.mapkey(key)
|
||||
u.Auth = libplugin.CreatePrivateKeyAuth(k)
|
||||
|
||||
return u, nil
|
||||
|
|
|
|||
102
plugin/workingdirbykey/main.go
Normal file
102
plugin/workingdirbykey/main.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tg123/sshpiper/plugin/internal/workingdir"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||
Name: "workingdir",
|
||||
Usage: "sshpiperd workingdir plugin",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "root",
|
||||
Usage: "path to root working directory",
|
||||
Value: "/var/sshpiper",
|
||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_ROOT"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "allow-baduser-name",
|
||||
Usage: "allow bad username",
|
||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_ALLOWBADUSERNAME"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-check-perm",
|
||||
Usage: "disable 0400 checking",
|
||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_NOCHECKPERM"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "strict-hostkey",
|
||||
Usage: "upstream host public key must be in known_hosts file, otherwise drop the connection",
|
||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_STRICTHOSTKEY"},
|
||||
},
|
||||
},
|
||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||
|
||||
root := c.String("root")
|
||||
|
||||
return &libplugin.SshPiperPluginConfig{
|
||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
||||
|
||||
userdir := path.Join(root, conn.User())
|
||||
|
||||
var upstream *libplugin.Upstream
|
||||
|
||||
filepath.Walk(userdir, func(path string, info os.FileInfo, err error) error {
|
||||
|
||||
log.Infof("search public key in path: %v", path)
|
||||
if err != nil {
|
||||
log.Infof("error walking path: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := &workingdir.Workingdir{
|
||||
Path: path,
|
||||
NoCheckPerm: c.Bool("no-check-perm"),
|
||||
Strict: c.Bool("strict-hostkey"),
|
||||
}
|
||||
|
||||
u, err := w.CreateUpstream()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
k, err := w.Mapkey(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Auth = libplugin.CreatePrivateKeyAuth(k)
|
||||
upstream = u
|
||||
return fmt.Errorf("stop")
|
||||
})
|
||||
|
||||
if upstream != nil {
|
||||
return upstream, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no matching public key found in %v", userdir)
|
||||
},
|
||||
|
||||
VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Working Directory for SSHPiper
|
||||
|
||||
`Working Dir` is a `/home`-like directory.
|
||||
SSHPiperd read files from `workingdir/[username]/` to know upstream's configuration.
|
||||
|
||||
e.g.
|
||||
|
||||
```
|
||||
workingdir tree
|
||||
|
||||
.
|
||||
├── github
|
||||
│ └── sshpiper_upstream
|
||||
└── linode
|
||||
└── sshpiper_upstream
|
||||
```
|
||||
|
||||
when `ssh sshpiper_host -l github`,
|
||||
sshpiper reads `workingdir/github/sshpiper_upstream` and the connect to the upstream.
|
||||
|
||||
## User files
|
||||
|
||||
*These file MUST NOT be accessible to group or other. (chmod og-rwx filename)*
|
||||
|
||||
* sshpiper_upstream
|
||||
|
||||
* line starts with `#` are treated as comment
|
||||
* only the first not comment line will be parsed
|
||||
* if no port was given, 22 will be used as default
|
||||
* if `user@` was defined, username to upstream will be the mapped one
|
||||
|
||||
```
|
||||
# comment
|
||||
[user@]upstream[:22]
|
||||
```
|
||||
|
||||
```
|
||||
e.g.
|
||||
|
||||
git@github.com
|
||||
|
||||
google.com:12345
|
||||
|
||||
```
|
||||
|
||||
* authorized_keys
|
||||
|
||||
OpenSSH format `authorized_keys` (see `~/.ssh/authorized_keys`). Used for `publickey sign again(see below)`.
|
||||
|
||||
* id_rsa
|
||||
|
||||
RSA key for `publickey sign again(see below)`.
|
||||
|
||||
* known_hosts
|
||||
|
||||
when `upstream-workingdir-stricthostkey` is set, upstream server's public key must present in known_hosts
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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"`
|
||||
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"`
|
||||
}{}
|
||||
)
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package workingdir
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
)
|
||||
|
||||
func (p *plugin) ListPipe() ([]upstream.Pipe, error) {
|
||||
files, err := ioutil.ReadDir(config.WorkingDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pipes := make([]upstream.Pipe, 0, len(files))
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, file.Name())}
|
||||
data, err := userUpstreamFile.read()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
host, port, mappedUser, err := parseUpstreamFile(string(data))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pipes = append(pipes, upstream.Pipe{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: file.Name(),
|
||||
UpstreamUsername: mappedUser,
|
||||
})
|
||||
}
|
||||
|
||||
return pipes, nil
|
||||
}
|
||||
|
||||
func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error {
|
||||
userdir := path.Join(config.WorkingDir, opt.Username)
|
||||
err := os.MkdirAll(userdir, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir}
|
||||
path := userUpstreamFile.realPath()
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
|
||||
upuser := opt.UpstreamUsername
|
||||
|
||||
if len(upuser) == 0 {
|
||||
upuser = opt.Username
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("%v@%v:%v", upuser, opt.Host, opt.Port)
|
||||
return ioutil.WriteFile(path, []byte(content), 0600)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("upstream file of [%v] alreay exists", opt.Username)
|
||||
}
|
||||
|
||||
func (p *plugin) RemovePipe(name string) error {
|
||||
userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, name)}
|
||||
path := userUpstreamFile.realPath()
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package workingdir
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
)
|
||||
|
||||
var logger *log.Logger
|
||||
|
||||
type plugin struct {
|
||||
}
|
||||
|
||||
func (p *plugin) GetName() string {
|
||||
return "workingdir"
|
||||
}
|
||||
|
||||
func (p *plugin) GetOpts() interface{} {
|
||||
return &config
|
||||
}
|
||||
|
||||
func (p *plugin) GetHandler() upstream.Handler {
|
||||
return findUpstreamFromUserfile
|
||||
}
|
||||
|
||||
func (p *plugin) Init(glogger *log.Logger) error {
|
||||
|
||||
logger = glogger
|
||||
|
||||
logger.Printf("upstream provider: workingdir from path [%v] initializing", config.WorkingDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
upstream.Register("workingdir", &plugin{})
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
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
|
||||
|
||||
if config.MatchPublicKeyInSubDir {
|
||||
old := piper.NextAuthMethods
|
||||
piper.NextAuthMethods = func(conn ssh.ConnMetadata, ctx ssh.ChallengeContext) ([]string, error) {
|
||||
methods, err := old(conn, ctx)
|
||||
methods = append(methods, "publickey")
|
||||
return methods, err
|
||||
}
|
||||
}
|
||||
|
||||
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.Debugf("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.Infof("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)
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
// Copyright 2014, 2015 tgic<farmer1992@gmail.com>. All rights reserved.
|
||||
// this file is governed by MIT-license
|
||||
//
|
||||
// https://github.com/tg123/sshpiper
|
||||
|
||||
package workingdir
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
"github.com/tg123/sshpiper/sshpiperd/v0bridge"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
type userFile struct {
|
||||
filename string
|
||||
userdir string
|
||||
}
|
||||
|
||||
const (
|
||||
userAuthorizedKeysFile = "authorized_keys"
|
||||
userKeyFile = "id_rsa"
|
||||
userUpstreamFile = "sshpiper_upstream"
|
||||
userKnownHosts = "known_hosts"
|
||||
)
|
||||
|
||||
var (
|
||||
usernameRule *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Base username validation on Debians default: https://sources.debian.net/src/adduser/3.113%2Bnmu3/adduser.conf/#L85
|
||||
// -> NAME_REGEX="^[a-z][-a-z0-9_]*\$"
|
||||
// The length is limited to 32 characters. See man 8 useradd: https://linux.die.net/man/8/useradd
|
||||
usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$")
|
||||
}
|
||||
|
||||
func (file userFile) userSpecFile(filename string) string {
|
||||
p := file.userdir
|
||||
if p == "" {
|
||||
p = config.WorkingDir
|
||||
}
|
||||
return path.Join(p, filename)
|
||||
}
|
||||
|
||||
func (file userFile) read() ([]byte, error) {
|
||||
return ioutil.ReadFile(file.userSpecFile(file.filename))
|
||||
}
|
||||
|
||||
func (file userFile) realPath() string {
|
||||
return file.userSpecFile(file.filename)
|
||||
}
|
||||
|
||||
// return error if other and group have access right
|
||||
func (file userFile) checkPerm() error {
|
||||
filename := file.userSpecFile(file.filename)
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.NoCheckPerm {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Mode().Perm()&0077 != 0 {
|
||||
return fmt.Errorf("%v's perm is too open", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// return false if username is not a valid unix user name
|
||||
// this is for security reason
|
||||
func checkUsername(user string) bool {
|
||||
if config.AllowBadUsername {
|
||||
return true
|
||||
}
|
||||
|
||||
return usernameRule.MatchString(user)
|
||||
}
|
||||
|
||||
func parseUpstreamFile(data string) (host string, port int, user string, err error) {
|
||||
r := bufio.NewReader(strings.NewReader(data))
|
||||
for {
|
||||
host, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
if host != "" && host[0] != '#' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t := strings.SplitN(host, "@", 2)
|
||||
|
||||
if len(t) > 1 {
|
||||
user = t[0]
|
||||
host = t[1]
|
||||
}
|
||||
|
||||
host, port, err = upstream.SplitHostPortForSSH(host)
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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, nil, err
|
||||
}
|
||||
|
||||
data, err := userUpstreamFile.read()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
host, port, mappedUser, err := parseUpstreamFile(string(data))
|
||||
if err != nil {
|
||||
return nil, 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, 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, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, &v0bridge.AuthPipe{
|
||||
User: mappedUser,
|
||||
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (v0bridge.AuthPipeType, ssh.AuthMethod, error) {
|
||||
signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key)
|
||||
|
||||
if err != nil || signer == nil {
|
||||
// try one
|
||||
return v0bridge.AuthPipeTypeNone, nil, nil
|
||||
}
|
||||
|
||||
return v0bridge.AuthPipeTypeMap, ssh.PublicKeys(signer), nil
|
||||
},
|
||||
|
||||
UpstreamHostKeyCallback: hostKeyCallback,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}()
|
||||
|
||||
userAuthorizedKeysFile := userFile{filename: userAuthorizedKeysFile, userdir: userdir}
|
||||
err = userAuthorizedKeysFile.checkPerm()
|
||||
|
||||
if os.IsNotExist(err) && len(config.FallbackUsername) > 0 {
|
||||
err = nil
|
||||
user = config.FallbackUsername
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keydata := key.Marshal()
|
||||
|
||||
var rest []byte
|
||||
rest, err = userAuthorizedKeysFile.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userKeyFile := userFile{filename: userKeyFile, userdir: userdir}
|
||||
var authedPubkey ssh.PublicKey
|
||||
|
||||
for len(rest) > 0 {
|
||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(authedPubkey.Marshal(), keydata) {
|
||||
err = userKeyFile.checkPerm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var privateBytes []byte
|
||||
privateBytes, err = userKeyFile.read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var private ssh.Signer
|
||||
private, err = ssh.ParsePrivateKey(privateBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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, conn.RemoteAddr())
|
||||
return private, nil
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("public key auth failed user [%v] from [%v]", conn.User(), conn.RemoteAddr())
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1,383 +0,0 @@
|
|||
// Copyright 2014, 2015 tgic<farmer1992@gmail.com>. All rights reserved.
|
||||
// this file is governed by MIT-license
|
||||
//
|
||||
// https://github.com/tg123/sshpiper
|
||||
|
||||
package workingdir
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/testdata"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
func buildWorkingDir(users []string, t *testing.T) {
|
||||
config.WorkingDir = ""
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "sshpiperd_workingdir")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("setup temp dir:%v", err)
|
||||
}
|
||||
|
||||
config.WorkingDir = dir
|
||||
|
||||
for _, u := range users {
|
||||
if err := os.Mkdir(config.WorkingDir+"/"+u, os.ModePerm); err != nil {
|
||||
t.Fatalf("mkdir dir:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("switch workingdir to %v", config.WorkingDir)
|
||||
}
|
||||
|
||||
func cleanupWorkdir(t *testing.T) {
|
||||
if config.WorkingDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("cleaning workingdir %v", config.WorkingDir)
|
||||
|
||||
os.RemoveAll(config.WorkingDir)
|
||||
}
|
||||
|
||||
func TestReadUserFile(t *testing.T) {
|
||||
user1 := "testuser1"
|
||||
user2 := "testuser2"
|
||||
|
||||
buildWorkingDir([]string{user1, user2}, t)
|
||||
defer cleanupWorkdir(t)
|
||||
|
||||
data1 := []byte("byte[] := data1")
|
||||
data2 := []byte("this is data2")
|
||||
|
||||
f := userFile("f")
|
||||
|
||||
err := ioutil.WriteFile(f.realPath(user1), data1, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(f.realPath(user2), data2, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
d, err := f.read(user1)
|
||||
if err != nil || !bytes.Equal(d, data1) {
|
||||
t.Fatalf("read faild")
|
||||
}
|
||||
|
||||
d, err = f.read(user2)
|
||||
if err != nil || bytes.Equal(d, data1) {
|
||||
t.Fatalf("reading wrong user file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPerm(t *testing.T) {
|
||||
user := "testuser"
|
||||
buildWorkingDir([]string{user}, t)
|
||||
defer cleanupWorkdir(t)
|
||||
|
||||
f := userFile("perm")
|
||||
|
||||
err := ioutil.WriteFile(f.realPath(user), nil, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
err = f.checkPerm(user)
|
||||
if err == nil {
|
||||
t.Fatalf("should fail when read 0777 user file")
|
||||
}
|
||||
|
||||
err = os.Chmod(f.realPath(user), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("cant change file mode %v", err)
|
||||
}
|
||||
|
||||
err = f.checkPerm(user)
|
||||
if err != nil {
|
||||
t.Fatalf("fail when read 0600 user file %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubConnMetadata struct{ user string }
|
||||
|
||||
func (s stubConnMetadata) User() string {
|
||||
return s.user
|
||||
}
|
||||
|
||||
func (s stubConnMetadata) SessionID() []byte { return nil }
|
||||
func (s stubConnMetadata) ClientVersion() []byte { return nil }
|
||||
func (s stubConnMetadata) ServerVersion() []byte { return nil }
|
||||
func (s stubConnMetadata) RemoteAddr() net.Addr { return nil }
|
||||
func (s stubConnMetadata) LocalAddr() net.Addr { return nil }
|
||||
|
||||
func TestParseUpstreamFile(t *testing.T) {
|
||||
|
||||
{
|
||||
|
||||
addr, port, user, err := parseUpstreamFile(`
|
||||
|
||||
a:123
|
||||
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("should not return err: %v", err)
|
||||
}
|
||||
|
||||
if addr != "a" || port != 123 || user != "" {
|
||||
t.Fatalf("parse failed common with port")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
addr, port, user, err := parseUpstreamFile(`
|
||||
a:123
|
||||
b:456
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("should not return err: %v", err)
|
||||
}
|
||||
|
||||
if addr != "a" || port != 123 || user != "" {
|
||||
t.Fatalf("parse multi line")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
addr, port, user, err := parseUpstreamFile(`
|
||||
host
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("should not return err: %v", err)
|
||||
}
|
||||
|
||||
if addr != "host" || port != 22 || user != "" {
|
||||
t.Fatalf("parse no port")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
addr, port, user, err := parseUpstreamFile(`
|
||||
user@github.com
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("should not return err: %v", err)
|
||||
}
|
||||
|
||||
if addr != "github.com" || port != 22 || user != "user" {
|
||||
t.Fatalf("parse no port with user")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
_, _, _, err := parseUpstreamFile(``)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("empty file")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
addr, port, user, err := parseUpstreamFile(`
|
||||
|
||||
# comment
|
||||
user@github.com
|
||||
test@linode.com
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("should not return err: %v", err)
|
||||
}
|
||||
if addr != "github.com" || port != 22 || user != "user" {
|
||||
t.Fatalf("multi line with comment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindUpstreamFromUserfile(t *testing.T) {
|
||||
user := "testuser"
|
||||
buildWorkingDir([]string{user}, t)
|
||||
defer cleanupWorkdir(t)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("cant create fake server: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
go func() {
|
||||
c, err := listener.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("fake server error %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
if _, err := io.Copy(c, c); err != nil {
|
||||
t.Errorf("fake server copy error %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
addr := listener.Addr().String()
|
||||
t.Logf("fake server at %v", addr)
|
||||
|
||||
err = ioutil.WriteFile(userUpstreamFile.realPath(user), []byte(addr), 0777)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("testing file too open")
|
||||
_, _, err = findUpstreamFromUserfile(stubConnMetadata{user}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should return err when file too open")
|
||||
}
|
||||
|
||||
err = os.Chmod(userUpstreamFile.realPath(user), 0400)
|
||||
if err != nil {
|
||||
t.Fatalf("cant change file mode %v", err)
|
||||
}
|
||||
|
||||
t.Logf("testing conn dial to %v", addr)
|
||||
conn, _, err := findUpstreamFromUserfile(stubConnMetadata{user}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("findUpstreamFromUserfile failed %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
d := []byte("hello")
|
||||
|
||||
_, err = conn.Write(d)
|
||||
if err != nil {
|
||||
t.Fatalf("cant write to conn: %v", err)
|
||||
}
|
||||
|
||||
b := make([]byte, len(d))
|
||||
_, err = conn.Read(b)
|
||||
|
||||
if err != nil || !bytes.Equal(b, d) {
|
||||
t.Fatalf("conn to upstream does not work")
|
||||
}
|
||||
|
||||
t.Logf("testing user not found")
|
||||
config.FallbackUsername = ""
|
||||
_, _, err = findUpstreamFromUserfile(stubConnMetadata{"nosuchuser"}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should return err when finding nosuchuser")
|
||||
}
|
||||
|
||||
t.Logf("testing user not found fallback")
|
||||
config.FallbackUsername = user
|
||||
_, _, err = findUpstreamFromUserfile(stubConnMetadata{"nosuchuser"}, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("should return fallbackuser")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapPublicKeyFromUserfile(t *testing.T) {
|
||||
user := "testuser"
|
||||
buildWorkingDir([]string{user}, t)
|
||||
defer cleanupWorkdir(t)
|
||||
|
||||
privateKey, _ := ssh.ParsePrivateKey(testdata.PEMBytes["rsa"])
|
||||
publicKey := privateKey.PublicKey()
|
||||
privateKey2, _ := ssh.ParsePrivateKey(testdata.PEMBytes["dsa"])
|
||||
|
||||
_ = privateKey2
|
||||
|
||||
err := ioutil.WriteFile(userKeyFile.realPath(user), testdata.PEMBytes["rsa"], 0777)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
authKeys := ssh.MarshalAuthorizedKey(publicKey)
|
||||
err = ioutil.WriteFile(userAuthorizedKeysFile.realPath(user), authKeys, 0777)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("testing file too open")
|
||||
|
||||
// UserAuthorizedKeysFile
|
||||
_, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, publicKey)
|
||||
if err == nil {
|
||||
t.Fatalf("should return err when file too open")
|
||||
}
|
||||
|
||||
err = os.Chmod(userAuthorizedKeysFile.realPath(user), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("cant change file mode %v", err)
|
||||
}
|
||||
|
||||
// UserKeyFile
|
||||
_, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, publicKey)
|
||||
if err == nil {
|
||||
t.Fatalf("should return err when file too open")
|
||||
}
|
||||
|
||||
err = os.Chmod(userKeyFile.realPath(user), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("cant change file mode %v", err)
|
||||
}
|
||||
|
||||
t.Logf("testing user not found")
|
||||
config.FallbackUsername = ""
|
||||
_, err = mapPublicKeyFromUserfile(stubConnMetadata{"nosuchuser"}, publicKey)
|
||||
if err == nil {
|
||||
t.Fatalf("should return err when mapping from nosuchuser")
|
||||
}
|
||||
|
||||
t.Logf("testing user not found fallback")
|
||||
config.FallbackUsername = user
|
||||
_, err = mapPublicKeyFromUserfile(stubConnMetadata{"nosuchuser"}, publicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("should return fallbackuser")
|
||||
}
|
||||
|
||||
t.Logf("testing mapping signer")
|
||||
signer, err := mapPublicKeyFromUserfile(stubConnMetadata{user}, privateKey.PublicKey())
|
||||
if err != nil {
|
||||
t.Fatalf("error mapping key %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(signer.PublicKey().Marshal(), privateKey.PublicKey().Marshal()) {
|
||||
t.Fatalf("id_rsa not the same")
|
||||
}
|
||||
|
||||
t.Logf("testing not in UserAuthorizedKeysFile")
|
||||
|
||||
authKeys = ssh.MarshalAuthorizedKey(privateKey2.PublicKey())
|
||||
err = ioutil.WriteFile(userAuthorizedKeysFile.realPath(user), authKeys, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("cant create file: %v", err)
|
||||
}
|
||||
|
||||
signer, err = mapPublicKeyFromUserfile(stubConnMetadata{user}, privateKey.PublicKey())
|
||||
if err != nil {
|
||||
t.Fatalf("cant mapping key: %v", err)
|
||||
}
|
||||
if signer != nil {
|
||||
t.Fatalf("should not map private key when public key not in UserAuthorizedKeysFile")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue