introduce Yaml upstream (#49)
YAML upstream provides a more flexible way to config mappings
This commit is contained in:
parent
4317868ccc
commit
0f58c3b449
14 changed files with 820 additions and 49 deletions
3
go.mod
3
go.mod
|
|
@ -4,7 +4,7 @@ go 1.12
|
|||
|
||||
replace (
|
||||
github.com/jessevdk/go-flags => github.com/tg123/go-flags v1.4.0-globalref
|
||||
golang.org/x/crypto => github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515
|
||||
golang.org/x/crypto => github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -29,4 +29,5 @@ require (
|
|||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -77,8 +77,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tg123/go-flags v1.4.0-globalref h1:pfUF3Mdnw5gZ5izA7s95Yrp+AetZPlnO9x39RByCi98=
|
||||
github.com/tg123/go-flags v1.4.0-globalref/go.mod h1:G60U6XrJAj49cFQ8MY2Wr+SEjylerbSqj0I8FZy2tFE=
|
||||
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515 h1:ag4BBUO+Ixmiz5vpSxBVlmvGqQfOlY+0NGa2QLR1yyo=
|
||||
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2 h1:vGv7a9lvys1S0eng/lz3p56uQbAES4H1D0OU1rp8Bh4=
|
||||
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
|
|
@ -88,3 +88,7 @@ golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -19,28 +19,22 @@ services:
|
|||
- names:/names
|
||||
- ./test.sh:/test.sh
|
||||
- ./wait.sh:/wait.sh
|
||||
links:
|
||||
- "piper:piper"
|
||||
- "piper_sqlite:piper_sqlite"
|
||||
- "piper_mysql:piper_mysql"
|
||||
- "piper_pg:piper_pg"
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "/test.sh"
|
||||
depends_on:
|
||||
- piper
|
||||
- piper_yaml
|
||||
- piper_mysql
|
||||
- piper_pg
|
||||
- piper_mssql
|
||||
- piper_sqlite
|
||||
piper:
|
||||
environment:
|
||||
- "SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM=true"
|
||||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
links:
|
||||
- "host1:host1"
|
||||
- "host2:host2"
|
||||
volumes:
|
||||
- workingdir:/var/sshpiper
|
||||
- ./piper.sh:/piper.sh
|
||||
|
|
@ -51,15 +45,27 @@ services:
|
|||
depends_on:
|
||||
- host1
|
||||
- host2
|
||||
piper_yaml:
|
||||
environment:
|
||||
- "SSHPIPERD_UPSTREAM_DRIVER=yaml"
|
||||
- "SSHPIPERD_UPSTREAM_YAML_NOCHECKPERM=true"
|
||||
- "SSHPIPERD_UPSTREAM_YAML_FILE=/sshpiperd.yaml"
|
||||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
volumes:
|
||||
- localkey:/local
|
||||
- workingdir:/var/sshpiper
|
||||
- ./sshpiperd.yaml:/sshpiperd.yaml
|
||||
depends_on:
|
||||
- host1
|
||||
- host2
|
||||
piper_sqlite:
|
||||
environment:
|
||||
- "SSHPIPERD_UPSTREAM_DRIVER=sqlite"
|
||||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
links:
|
||||
- "host1:host1"
|
||||
- "host2:host2"
|
||||
volumes:
|
||||
- workingdir:/var/sshpiper
|
||||
- ./piper.sh:/piper.sh
|
||||
|
|
@ -79,10 +85,6 @@ services:
|
|||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
links:
|
||||
- "host1:host1"
|
||||
- "host2:host2"
|
||||
- "mysql:mysql"
|
||||
volumes:
|
||||
- ./piper.sh:/piper.sh
|
||||
- workingdir:/var/sshpiper
|
||||
|
|
@ -111,10 +113,6 @@ services:
|
|||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
links:
|
||||
- "host1:host1"
|
||||
- "host2:host2"
|
||||
- "postgres:postgres"
|
||||
volumes:
|
||||
- ./piper.sh:/piper.sh
|
||||
- workingdir:/var/sshpiper
|
||||
|
|
@ -145,10 +143,6 @@ services:
|
|||
- "SSHPIPERD_AUDITOR=typescript-logger"
|
||||
- "SSHPIPERD_BANNERTEXT=hellopiper"
|
||||
build: ../..
|
||||
links:
|
||||
- "host1:host1"
|
||||
- "host2:host2"
|
||||
- "mssql:mssql"
|
||||
volumes:
|
||||
- ./piper.sh:/piper.sh
|
||||
- workingdir:/var/sshpiper
|
||||
|
|
|
|||
81
sshpiperd/e2e/sshpiperd.yaml
Normal file
81
sshpiperd/e2e/sshpiperd.yaml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
version: 1
|
||||
pipes:
|
||||
- username: passthrough
|
||||
upstream_host: host2
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
- username: mappasspass
|
||||
upstream_host: host2
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: password
|
||||
password: pass
|
||||
to:
|
||||
type: password
|
||||
password: root
|
||||
- username: mappasskey
|
||||
upstream_host: host1
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: password
|
||||
password: pass
|
||||
to:
|
||||
type: privatekey
|
||||
private_key: /var/sshpiper/host1/id_rsa
|
||||
- username: mapkeypass
|
||||
upstream_host: host2
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: publickey
|
||||
authorized_keys: /local/id_rsa2.pub
|
||||
to:
|
||||
type: password
|
||||
password: root
|
||||
- username: mapkeykey
|
||||
upstream_host: host1
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: publickey
|
||||
authorized_keys: /local/id_rsa2.pub
|
||||
to:
|
||||
type: privatekey
|
||||
private_key: /var/sshpiper/host1/id_rsa
|
||||
- username: mapkeykey2
|
||||
upstream_host: host1
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: publickey
|
||||
allow_any_public_key: true
|
||||
to:
|
||||
type: privatekey
|
||||
key_map:
|
||||
- authorized_keys: /local/id_rsa2.pub
|
||||
private_key: /var/sshpiper/host1/id_rsa
|
||||
- username: ^regex.*$
|
||||
username_regex_match: true
|
||||
upstream_host: host2
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
- username: ^host.*$
|
||||
username_regex_match: true
|
||||
upstream_host: host1
|
||||
ignore_hostkey: true
|
||||
authmap:
|
||||
mapped_username: root
|
||||
from:
|
||||
- type: any
|
||||
to:
|
||||
type: privatekey
|
||||
private_key: /var/sshpiper/$USER/id_rsa
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
|
||||
/wait.sh piper 2222
|
||||
/wait.sh piper_yaml 2222
|
||||
/wait.sh piper_sqlite 2222
|
||||
/wait.sh piper_mysql 2222
|
||||
/wait.sh piper_pg 2222
|
||||
/wait.sh piper_mssql 2222
|
||||
|
|
@ -14,6 +17,9 @@ mkdir -p /workingdir/host{1,2}
|
|||
ssh-keygen -N '' -f /local/id_rsa
|
||||
ssh-keygen -N '' -f /workingdir/host1/id_rsa #TODO pipe cmd
|
||||
|
||||
ssh-keygen -N '' -f /local/id_rsa2
|
||||
|
||||
|
||||
/bin/cp /local/id_rsa.pub /workingdir/host1/authorized_keys
|
||||
/bin/cp /workingdir/host1/id_rsa.pub /host1/authorized_keys
|
||||
|
||||
|
|
@ -23,11 +29,13 @@ succ="\033[0;32mSUCC\033[0m"
|
|||
runtest(){
|
||||
casename=$1
|
||||
host=$2
|
||||
user=$3
|
||||
cmd=$4
|
||||
|
||||
rnd=`head -c 20 /dev/urandom | base64`
|
||||
echo $rnd > /names/$host
|
||||
rm -f /tmp/$host.stderr
|
||||
t=$($3 2>/tmp/$host.stderr)
|
||||
t=$($cmd 2>/tmp/$host.stderr)
|
||||
|
||||
if [ "$t" != "$rnd" ];then
|
||||
echo -e $casename $fail
|
||||
|
|
@ -36,7 +44,7 @@ runtest(){
|
|||
echo -e $casename $succ
|
||||
fi
|
||||
|
||||
grep $rnd /workingdir/$host/*
|
||||
grep $rnd /workingdir/$user/*
|
||||
|
||||
if [ $? -ne 0 ];then
|
||||
echo -e "grep typescript logger" $fail
|
||||
|
|
@ -51,11 +59,20 @@ runtest(){
|
|||
fi
|
||||
}
|
||||
|
||||
runtest "host1 with public key:" "host1" "ssh host1@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa cat /names/host1"
|
||||
runtest "host2 with password:" "host2" "sshpass -p root ssh host2@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "host1 with public key:" "host1" "host1" "ssh host1@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa cat /names/host1"
|
||||
runtest "host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
|
||||
runtest "sqlite host2 with password:" "host2" "sshpass -p root ssh host2@piper_sqlite -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "mysql host2 with password:" "host2" "sshpass -p root ssh host2@piper_mysql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "pg host2 with password:" "host2" "sshpass -p root ssh host2@piper_pg -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "msql host2 with password:" "host2" "sshpass -p root ssh host2@piper_mssql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "sqlite host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_sqlite -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "mysql host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_mysql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "pg host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_pg -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "msql host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_mssql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
|
||||
|
||||
runtest "yaml host2 with password passthrough:" "host2" "passthrough" "sshpass -p root ssh passthrough@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "yaml host2 with password mappasspass:" "host2" "mappasspass" "sshpass -p pass ssh mappasspass@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "yaml host1 with password mappasskey:" "host1" "mappasskey" "sshpass -p pass ssh mappasskey@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host1"
|
||||
runtest "yaml host2 with password mapkeypass:" "host2" "mapkeypass" "ssh mapkeypass@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host2"
|
||||
runtest "yaml host2 with key mapkeykey:" "host1" "mapkeykey" "ssh mapkeykey@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host1"
|
||||
runtest "yaml host2 with key mapkeykey2:" "host1" "mapkeykey2" "ssh mapkeykey2@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host1"
|
||||
runtest "yaml host2 with password regex:" "host2" "regex000" "sshpass -p root ssh regex000@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
|
||||
runtest "yaml host1 with none host1:" "host1" "host1" "ssh host1@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host1"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
_ "github.com/tg123/sshpiper/sshpiperd/upstream/database"
|
||||
_ "github.com/tg123/sshpiper/sshpiperd/upstream/workingdir"
|
||||
_ "github.com/tg123/sshpiper/sshpiperd/upstream/yaml"
|
||||
|
||||
_ "github.com/tg123/sshpiper/sshpiperd/challenger/authy"
|
||||
_ "github.com/tg123/sshpiper/sshpiperd/challenger/azdevicecode"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name: sshpiperd
|
||||
version: 0.4.5
|
||||
version: 0.5.0
|
||||
summary: The missing reverse proxy for ssh scp
|
||||
description: |
|
||||
SSH Piper works as a proxy-like ware, and route connections by username, src ip , etc.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"net"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
upstreamprovider "github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
)
|
||||
|
||||
func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
|
||||
|
|
@ -26,7 +28,7 @@ func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.Additi
|
|||
|
||||
logger.Printf("mapping user [%v] to [%v@%v]", user, upuser, addr)
|
||||
|
||||
c, err := dial(addr)
|
||||
c, err := upstreamprovider.DialForSSH(addr)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -115,15 +117,3 @@ func lookupConfigValue(db *gorm.DB, entry string) (string, error) {
|
|||
|
||||
return c.Value, nil
|
||||
}
|
||||
|
||||
func dial(addr string) (net.Conn, error) {
|
||||
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil && addr != "" {
|
||||
// test valid after concat :22
|
||||
if _, _, err := net.SplitHostPort(addr + ":22"); err == nil {
|
||||
addr += ":22"
|
||||
}
|
||||
}
|
||||
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,3 +102,16 @@ func SplitHostPortForSSH(addr string) (host string, port int, err error) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// DialForSSH is the modified version of net.Dial, would add ":22" automaticlly
|
||||
func DialForSSH(addr string) (net.Conn, error) {
|
||||
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil && addr != "" {
|
||||
// test valid after concat :22
|
||||
if _, _, err := net.SplitHostPort(addr + ":22"); err == nil {
|
||||
addr += ":22"
|
||||
}
|
||||
}
|
||||
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
|
|
|
|||
250
sshpiperd/upstream/yaml/pipemgr.go
Normal file
250
sshpiperd/upstream/yaml/pipemgr.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Return All pipes inside upstream
|
||||
func (p *plugin) ListPipe() ([]upstream.Pipe, error) {
|
||||
|
||||
config, err := p.loadConfig()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println(string(out))
|
||||
|
||||
var pipes []upstream.Pipe
|
||||
|
||||
for _, pipe := range config.Pipes {
|
||||
host, port, err := upstream.SplitHostPortForSSH(pipe.UpstreamHost)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mappeduser := pipe.Authmap.MappedUsername
|
||||
|
||||
if mappeduser == "" {
|
||||
mappeduser = pipe.Username
|
||||
}
|
||||
|
||||
pipes = append(pipes, upstream.Pipe{
|
||||
Host: host,
|
||||
Port: port,
|
||||
UpstreamUsername: mappeduser,
|
||||
Username: pipe.Username,
|
||||
})
|
||||
}
|
||||
|
||||
return pipes, nil
|
||||
}
|
||||
|
||||
func findnode(root *yaml.Node, test func(*yaml.Node) bool) *yaml.Node {
|
||||
var q []*yaml.Node
|
||||
|
||||
q = append(q, root)
|
||||
|
||||
for len(q) > 0 {
|
||||
e := q[0]
|
||||
q = q[1:]
|
||||
|
||||
if test(e) {
|
||||
return e
|
||||
}
|
||||
|
||||
for _, n := range e.Content {
|
||||
q = append(q, n)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findByMapKey(m *yaml.Node, k string) (*yaml.Node, int) {
|
||||
for i := 1; i < len(m.Content); i += 2 {
|
||||
if m.Content[i-1].Value == k {
|
||||
return m.Content[i], i
|
||||
}
|
||||
}
|
||||
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
func toYamlNode(s interface{}) (*yaml.Node, error) {
|
||||
t := yaml.Node{}
|
||||
|
||||
out, err := yaml.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(out, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t.Content[0], nil
|
||||
}
|
||||
|
||||
func (p *plugin) loadConfigRaw() ([]byte, *yaml.Node, error) {
|
||||
var config yaml.Node
|
||||
|
||||
configbyte, err := ioutil.ReadFile(p.Config.File)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(configbyte, &config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return configbyte, &config, nil
|
||||
}
|
||||
|
||||
func (p *plugin) writeConfig(config []byte) error {
|
||||
return ioutil.WriteFile(p.Config.File, config, 0600)
|
||||
}
|
||||
|
||||
func toPipeConfig(opt upstream.CreatePipeOption) pipeConfig {
|
||||
p := pipeConfig{
|
||||
Username: opt.Username,
|
||||
UpstreamHost: fmt.Sprintf("%v:%v", opt.Host, opt.Port),
|
||||
}
|
||||
|
||||
if len(opt.UpstreamUsername) > 0 {
|
||||
p.Authmap.MappedUsername = opt.UpstreamUsername
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Create a pipe inside upstream
|
||||
func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error {
|
||||
configbyte, config, err := p.loadConfigRaw()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(config.Content) == 0 {
|
||||
var buf bytes.Buffer
|
||||
buf.Write(configbyte)
|
||||
fmt.Fprintln(&buf)
|
||||
|
||||
out, err := yaml.Marshal(piperConfig{
|
||||
Version: 1,
|
||||
Pipes: []pipeConfig{toPipeConfig(opt)},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Write(out)
|
||||
|
||||
return p.writeConfig(buf.Bytes())
|
||||
}
|
||||
|
||||
pipes, idx := findByMapKey(config.Content[0], "pipes")
|
||||
|
||||
if idx > 0 && pipes.Tag == "!!null" {
|
||||
// replace null
|
||||
t, err := toYamlNode([]pipeConfig{toPipeConfig(opt)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.Content[0].Content[idx] = t
|
||||
|
||||
out, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.writeConfig(out)
|
||||
}
|
||||
|
||||
if pipes.Kind != yaml.SequenceNode {
|
||||
return fmt.Errorf("pipes should be !!seq")
|
||||
}
|
||||
|
||||
for _, pnode := range pipes.Content {
|
||||
pipe, _ := findByMapKey(pnode, "username")
|
||||
|
||||
if pipe != nil && pipe.Value == opt.Username {
|
||||
return fmt.Errorf("username [%v] already exists", opt.Username)
|
||||
}
|
||||
}
|
||||
|
||||
// append
|
||||
|
||||
{
|
||||
t, err := toYamlNode(toPipeConfig(opt))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pipes.Content = append(pipes.Content, t)
|
||||
|
||||
out, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.writeConfig(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a pipe from upstream
|
||||
func (p *plugin) RemovePipe(name string) error {
|
||||
_, config, err := p.loadConfigRaw()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(config.Content) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pipes, idx := findByMapKey(config.Content[0], "pipes")
|
||||
|
||||
if idx > 0 && pipes.Tag == "!!null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pipes.Kind != yaml.SequenceNode {
|
||||
return fmt.Errorf("pipes should be !!seq")
|
||||
}
|
||||
|
||||
for i, pnode := range pipes.Content {
|
||||
pipe, _ := findByMapKey(pnode, "username")
|
||||
|
||||
if pipe != nil && pipe.Value == name {
|
||||
rest := pipes.Content[i+1:]
|
||||
pipes.Content = append(pipes.Content[:i], rest...)
|
||||
|
||||
out, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.writeConfig(out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
41
sshpiperd/upstream/yaml/plugin.go
Normal file
41
sshpiperd/upstream/yaml/plugin.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package yaml
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
)
|
||||
|
||||
type plugin struct {
|
||||
Config struct {
|
||||
File string `long:"upstream-yaml-file" default:"/var/sshpiper/sshpiperd.yaml" description:"Yaml config file path" env:"SSHPIPERD_UPSTREAM_YAML_FILE" ini-name:"upstream-yaml-file"`
|
||||
NoCheckPerm bool `long:"upstream-yaml-nocheckperm" description:"Disable 0400 checking when using config file" env:"SSHPIPERD_UPSTREAM_YAML_NOCHECKPERM" ini-name:"upstream-yaml-nocheckperm"`
|
||||
}
|
||||
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// The name of the Plugin
|
||||
func (p *plugin) GetName() string {
|
||||
return "yaml"
|
||||
}
|
||||
|
||||
// A ref to a struct which holds the options for the plugins
|
||||
// will be populated by cmd or other plugin runners
|
||||
func (p *plugin) GetOpts() interface{} {
|
||||
return &p.Config
|
||||
}
|
||||
|
||||
// Will be called before the Plugin is used to ensure the Plugin is ready
|
||||
func (p *plugin) Init(logger *log.Logger) error {
|
||||
p.logger = logger
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *plugin) GetHandler() upstream.Handler {
|
||||
return p.findUpstream
|
||||
}
|
||||
|
||||
func init() {
|
||||
upstream.Register("yaml", &plugin{})
|
||||
}
|
||||
0
sshpiperd/upstream/yaml/schema.json
Normal file
0
sshpiperd/upstream/yaml/schema.json
Normal file
379
sshpiperd/upstream/yaml/yaml.go
Normal file
379
sshpiperd/upstream/yaml/yaml.go
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/tg123/sshpiper/sshpiperd/upstream"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type pipeConfig struct {
|
||||
Username string `yaml:"username"`
|
||||
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
|
||||
UpstreamHost string `yaml:"upstream_host"`
|
||||
Authmap struct {
|
||||
MappedUsername string `yaml:"mapped_username,omitempty"`
|
||||
From []struct {
|
||||
Type string `yaml:"type"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
AuthorizedKeys string `yaml:"authorized_keys,omitempty"`
|
||||
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"`
|
||||
AllowAnyPublicKey bool `yaml:"allow_any_public_key,omitempty"`
|
||||
} `yaml:"from,flow"`
|
||||
|
||||
To struct {
|
||||
Type string `yaml:"type"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
PrivateKey string `yaml:"private_key,omitempty"`
|
||||
PrivateKeyData string `yaml:"private_key_data,omitempty"`
|
||||
KeyMap []struct {
|
||||
AuthorizedKeys string `yaml:"authorized_keys,omitempty"`
|
||||
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"`
|
||||
PrivateKey string `yaml:"private_key,omitempty"`
|
||||
PrivateKeyData string `yaml:"private_key_data,omitempty"`
|
||||
} `yaml:"key_map,flow"`
|
||||
} `yaml:"to,flow"`
|
||||
|
||||
NoPassthrough bool `yaml:"no_passthrough,omitempty"`
|
||||
} `yaml:"authmap,omitempty,flow"`
|
||||
|
||||
KnownHosts string `yaml:"known_hosts,omitempty"`
|
||||
KnownHostsData string `yaml:"known_hosts_data,omitempty"`
|
||||
IgnoreHostkey bool `yaml:"ignore_hostkey,omitempty"`
|
||||
}
|
||||
|
||||
type piperConfig struct {
|
||||
Version int `yaml:"version"`
|
||||
Pipes []pipeConfig `yaml:"pipes,flow"`
|
||||
}
|
||||
|
||||
func (p *plugin) checkPerm() error {
|
||||
filename := p.Config.File
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Config.NoCheckPerm {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Mode().Perm()&0077 != 0 {
|
||||
return fmt.Errorf("%v's perm is too open", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *plugin) loadConfig() (piperConfig, error) {
|
||||
var config piperConfig
|
||||
|
||||
err := p.checkPerm()
|
||||
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
configbyte, err := ioutil.ReadFile(p.Config.File)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(configbyte, &config)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type createPipeCtx struct {
|
||||
pipe pipeConfig
|
||||
conn ssh.ConnMetadata
|
||||
challengeContext ssh.AdditionalChallengeContext
|
||||
}
|
||||
|
||||
func (p *plugin) loadFileOrDecode(file string, base64data string, ctx createPipeCtx) ([]byte, error) {
|
||||
if file != "" {
|
||||
|
||||
file = os.Expand(file, func(placeholderName string) string {
|
||||
switch placeholderName {
|
||||
case "USER":
|
||||
return ctx.conn.User()
|
||||
case "MAPPED_USER":
|
||||
return ctx.pipe.Authmap.MappedUsername
|
||||
}
|
||||
|
||||
return os.Getenv(placeholderName)
|
||||
})
|
||||
|
||||
if !filepath.IsAbs(file) {
|
||||
file = filepath.Join(filepath.Dir(p.Config.File), file)
|
||||
}
|
||||
|
||||
return ioutil.ReadFile(file)
|
||||
}
|
||||
|
||||
if base64data != "" {
|
||||
return base64.StdEncoding.DecodeString(base64data)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *plugin) createAuthPipe(pipe pipeConfig, conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (*ssh.AuthPipe, error) {
|
||||
ctx := createPipeCtx{pipe, conn, challengeContext}
|
||||
|
||||
hostKeyCallback := ssh.InsecureIgnoreHostKey()
|
||||
if !pipe.IgnoreHostkey {
|
||||
|
||||
data, err := p.loadFileOrDecode(pipe.KnownHosts, pipe.KnownHostsData, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("no known hosts spicified")
|
||||
}
|
||||
|
||||
hostKeyCallback, err = knownhosts.NewFromReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
to := func(key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
|
||||
|
||||
switch pipe.Authmap.To.Type {
|
||||
case "none":
|
||||
return ssh.AuthPipeTypeNone, nil, nil
|
||||
|
||||
case "password":
|
||||
return ssh.AuthPipeTypeMap, ssh.Password(pipe.Authmap.To.Password), nil
|
||||
|
||||
case "privatekey":
|
||||
|
||||
privateBytes, err := p.loadFileOrDecode(pipe.Authmap.To.PrivateKey, pipe.Authmap.To.PrivateKeyData, ctx)
|
||||
if err != nil {
|
||||
return ssh.AuthPipeTypeDiscard, nil, err
|
||||
}
|
||||
|
||||
// did not find to 1 private key try key map
|
||||
if len(privateBytes) == 0 && key != nil {
|
||||
for _, privkey := range pipe.Authmap.To.KeyMap {
|
||||
rest, err := p.loadFileOrDecode(privkey.AuthorizedKeys, privkey.AuthorizedKeysData, ctx)
|
||||
if err != nil {
|
||||
return ssh.AuthPipeTypeDiscard, nil, err
|
||||
}
|
||||
|
||||
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, err = p.loadFileOrDecode(privkey.PrivateKey, privkey.PrivateKeyData, ctx)
|
||||
|
||||
if err != nil {
|
||||
return ssh.AuthPipeTypeDiscard, nil, err
|
||||
}
|
||||
|
||||
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:
|
||||
p.logger.Printf("unsupport type [%v] fallback to passthrough", pipe.Authmap.To.Type)
|
||||
}
|
||||
|
||||
if pipe.Authmap.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.Authmap.MappedUsername,
|
||||
|
||||
UpstreamHostKeyCallback: hostKeyCallback,
|
||||
}
|
||||
|
||||
for _, from := range pipe.Authmap.From {
|
||||
switch from.Type {
|
||||
case "none":
|
||||
|
||||
if a.NoneAuthCallback == nil {
|
||||
a.NoneAuthCallback = func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) {
|
||||
return to(nil)
|
||||
}
|
||||
}
|
||||
|
||||
case "password":
|
||||
allowPasswords[from.Password] = 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.Authmap.NoPassthrough {
|
||||
return ssh.AuthPipeTypeDiscard, nil, nil
|
||||
}
|
||||
|
||||
return ssh.AuthPipeTypePassThrough, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
case "publickey":
|
||||
|
||||
allowAnyPubKey = allowAnyPubKey || from.AllowAnyPublicKey
|
||||
|
||||
if !allowAnyPubKey {
|
||||
|
||||
rest, err := p.loadFileOrDecode(from.AuthorizedKeys, from.AuthorizedKeysData, ctx)
|
||||
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
|
||||
}
|
||||
|
||||
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.Authmap.NoPassthrough {
|
||||
return ssh.AuthPipeTypeDiscard, nil, nil
|
||||
}
|
||||
|
||||
// will fail but discard will lead a timeout
|
||||
return ssh.AuthPipeTypePassThrough, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
case "any":
|
||||
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:
|
||||
p.logger.Printf("unsupport type [%v], ignore section", from.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
|
||||
user := conn.User()
|
||||
|
||||
config, err := p.loadConfig()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, pipe := range config.Pipes {
|
||||
matched := pipe.Username == user
|
||||
|
||||
if pipe.UsernameRegexMatch {
|
||||
matched, _ = regexp.MatchString(pipe.Username, user)
|
||||
}
|
||||
|
||||
if matched {
|
||||
|
||||
p.logger.Printf("mapping [%v] to [%v]", user, pipe.Username)
|
||||
|
||||
c, err := upstream.DialForSSH(pipe.UpstreamHost)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
a, err := p.createAuthPipe(pipe, conn, challengeContext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c, a, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("username not [%v] found", user)
|
||||
}
|
||||
2
ver
2
ver
|
|
@ -1 +1 @@
|
|||
v0.4.5dev
|
||||
v0.5.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue