Skip to content

Commit

Permalink
ssh package
Browse files Browse the repository at this point in the history
initial implementation of the ssh package including:

- ConnectionEngine Interface
- ConnectionCreate()
- ConnectionDial()
- ConnectionScp()

the way this works, is there are publically accessible functions `Create` `Dial` and `Scp`
which determine whether or not you have ssh, or scp binaries installed. If you do, the package uses those directly.
if not, the package uses the golang ssh of sftp packages to pass files.

defaulting to native ssh/scp is still up in the air, but I feel like this makes the most sense unless we want to
provide some sort of toggle

closes #1091

Signed-off-by: Charlie Doern <[email protected]>
  • Loading branch information
cdoern committed Jul 25, 2022
1 parent 415ead5 commit c36c2e9
Show file tree
Hide file tree
Showing 140 changed files with 27,605 additions and 3,487 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ require (
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/opencontainers/runtime-tools v0.9.0
github.com/opencontainers/selinux v1.10.1
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.5
github.com/pmezard/go-difflib v1.0.0
github.com/seccomp/libseccomp-golang v0.10.0
github.com/sirupsen/logrus v1.8.1
Expand All @@ -40,6 +42,7 @@ require (
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/sys v0.0.0-20220624220833-87e55d714810
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
Expand Down Expand Up @@ -72,6 +75,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.15.7 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/letsencrypt/boulder v0.0.0-20220331220046-b23ab962616e // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand All @@ -83,7 +87,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/proglottis/gpgme v0.1.3 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
Expand All @@ -105,7 +108,6 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect
Expand Down
6 changes: 5 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,7 @@ github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQ
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -1249,6 +1250,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -1651,8 +1654,9 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down
13 changes: 13 additions & 0 deletions pkg/ssh/engine_connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ssh

import (
"context"

"github.com/containers/common/pkg/ssh/types"
)

type ConnectionEngine interface {
ConnectionCreate(ctx context.Context, options types.ConnectionCreateOptions) (*types.ConnectionCreateReport, error)
ConnectionDial(ctx context.Context, options types.ConnectionDialOptions) (*types.ConnectionDialReport, error)
ConnectionScp(ctx context.Context, options types.ConnectionScpOptions) (*types.ConnectionScpReport, error)
}
290 changes: 290 additions & 0 deletions pkg/ssh/golang/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package golang

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/url"
"os"
"os/user"
"path/filepath"
"strings"
"time"

"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"

"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/ssh/types"
"github.com/containers/common/pkg/ssh/utils"
"github.com/sirupsen/logrus"
)

func (ic ConnectionEngine) ConnectionCreate(ctx context.Context, options types.ConnectionCreateOptions) (*types.ConnectionCreateReport, error) {
dst, uri, err := utils.Validate(options.User, options.Path, options.Port, options.Identity)
if err != nil {
return nil, err
}
if uri.Path == "" || uri.Path == "/" {
if uri.Path, err = getUDS(uri, options.Identity); err != nil {
return nil, err
}
}
cfg, err := config.ReadCustomConfig()
if err != nil {
return nil, err
}
if cfg.Engine.ServiceDestinations == nil {
cfg.Engine.ServiceDestinations = map[string]config.Destination{
options.Name: *dst,
}
cfg.Engine.ActiveService = options.Name
} else {
cfg.Engine.ServiceDestinations[options.Name] = *dst
}
err = cfg.Write()
if err != nil {
return nil, err
}

return &types.ConnectionCreateReport{}, nil
}

func (ic ConnectionEngine) ConnectionDial(ctx context.Context, options types.ConnectionDialOptions) (*types.ConnectionDialReport, error) {
_, uri, err := utils.Validate(options.User, options.Host, options.Port, options.Identity)
if err != nil {
return nil, err
}
cfg, err := ValidateAndConfigure(uri, options.Identity)
if err != nil {
return nil, err
}
dialAdd, err := ssh.Dial("tcp", uri.Host, cfg) // dial the client
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}

out, err := ExecRemoteCommand(dialAdd, strings.Join(options.Args, " "))
if err != nil {
return nil, err
}

return &types.ConnectionDialReport{Response: string(out)}, nil
}

func (ic ConnectionEngine) ConnectionScp(ctx context.Context, options types.ConnectionScpOptions) (*types.ConnectionScpReport, error) {
host, remote_file, local_file, swap, err := utils.ParseScpArgs(options)
if err != nil {
return nil, err
}

_, uri, err := utils.Validate(options.User, host, options.Port, options.Identity)
if err != nil {
return nil, err
}
cfg, err := ValidateAndConfigure(uri, options.Identity)
if err != nil {
return nil, err
}
dial, err := ssh.Dial("tcp", uri.Host, cfg) // dial the client
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}
sc, err := sftp.NewClient(dial)
if err != nil {
return nil, err
}

f, err := os.OpenFile(local_file, (os.O_RDWR | os.O_CREATE), 0644)
if err != nil {
return nil, err
}
parent := filepath.Dir(remote_file)
path := string(filepath.Separator)
dirs := strings.Split(parent, path)
for _, dir := range dirs {
path = filepath.Join(path, dir)
// ignore errors due to most of the dirs already existing
_ = sc.Mkdir(path)
}

remote, err := sc.OpenFile(remote_file, (os.O_RDWR | os.O_CREATE))
if err != nil {
return nil, err
}
defer remote.Close()

if !swap {
_, err = io.Copy(remote, f)
if err != nil {
return nil, err
}
} else {
_, err = io.Copy(f, remote)
if err != nil {
return nil, err
}
}

return &types.ConnectionScpReport{Response: remote.Name()}, nil
}

// ExecRemoteCommand takes a ssh client connection and a command to run and executes the
// command on the specified client. The function returns the Stdout from the client or the Stderr
func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) {
sess, err := dial.NewSession() // new ssh client session
if err != nil {
return nil, err
}
defer sess.Close()

var buffer bytes.Buffer
var bufferErr bytes.Buffer
sess.Stdout = &buffer // output from client funneled into buffer
sess.Stderr = &bufferErr // err form client funneled into buffer
if err := sess.Run(run); err != nil { // run the command on the ssh client
return nil, fmt.Errorf("%v: %w", bufferErr.String(), err)
}
return buffer.Bytes(), nil
}

func GetUserInfo(uri *url.URL) (*url.Userinfo, error) {
var (
usr *user.User
err error
)
if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found {
usr, err = user.LookupId(u)
if err != nil {
return nil, fmt.Errorf("failed to lookup rootless user: %w", err)
}
} else {
usr, err = user.Current()
if err != nil {
return nil, fmt.Errorf("failed to obtain current user: %w", err)
}
}

pw, set := uri.User.Password()
if set {
return url.UserPassword(usr.Username, pw), nil
}
return url.User(usr.Username), nil
}

// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid
// iden iden can be blank to mean no identity key
// once the function validates the information it creates and returns an ssh.ClientConfig.
func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) {
var signers []ssh.Signer
passwd, passwdSet := uri.User.Password()
if iden != "" { // iden might be blank if coming from image scp or if no validation is needed
value := iden
s, err := utils.PublicKey(value, []byte(passwd))
if err != nil {
return nil, fmt.Errorf("failed to read identity %q: %w", value, err)
}
signers = append(signers, s)
logrus.Debugf("SSH Ident Key %q %s %s", value, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
} else if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { // validate ssh information, specifically the unix file socket used by the ssh agent.
logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)

c, err := net.Dial("unix", sock)
if err != nil {
return nil, err
}
agentSigners, err := agent.NewClient(c).Signers()
if err != nil {
return nil, err
}

signers = append(signers, agentSigners...)

if logrus.IsLevelEnabled(logrus.DebugLevel) {
for _, s := range agentSigners {
logrus.Debugf("SSH Agent Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
}
}
}
var authMethods []ssh.AuthMethod // now we validate and check for the authorization methods, most notaibly public key authorization
if len(signers) > 0 {
dedup := make(map[string]ssh.Signer)
for _, s := range signers {
fp := ssh.FingerprintSHA256(s.PublicKey())
if _, found := dedup[fp]; found {
logrus.Debugf("Dedup SSH Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
}
dedup[fp] = s
}

var uniq []ssh.Signer
for _, s := range dedup {
uniq = append(uniq, s)
}
authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
return uniq, nil
}))
}
if passwdSet { // if password authentication is given and valid, add to the list
authMethods = append(authMethods, ssh.Password(passwd))
}
if len(authMethods) == 0 {
authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) {
pass, err := utils.ReadPassword(fmt.Sprintf("%s's login password:", uri.User.Username()))
return string(pass), err
}))
}
tick, err := time.ParseDuration("40s")
if err != nil {
return nil, err
}
cfg := &ssh.ClientConfig{
User: uri.User.Username(),
Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: tick,
}
return cfg, nil
}

func getUDS(uri *url.URL, iden string) (string, error) {
cfg, err := ValidateAndConfigure(uri, iden)
if err != nil {
return "", fmt.Errorf("failed to validate: %w", err)
}
dial, err := ssh.Dial("tcp", uri.Host, cfg)
if err != nil {
return "", fmt.Errorf("failed to connect: %w", err)
}
defer dial.Close()

session, err := dial.NewSession()
if err != nil {
return "", fmt.Errorf("failed to create new ssh session on %q: %w", uri.Host, err)
}
defer session.Close()

// Override podman binary for testing etc
podman := "podman"
if v, found := os.LookupEnv("PODMAN_BINARY"); found {
podman = v
}
infoJSON, err := ExecRemoteCommand(dial, podman+" info --format=json")
if err != nil {
return "", err
}

var info types.Info
if err := json.Unmarshal(infoJSON, &info); err != nil {
return "", fmt.Errorf("failed to parse 'podman info' results: %w", err)
}

if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 {
return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host)
}
return info.Host.RemoteSocket.Path, nil
}
11 changes: 11 additions & 0 deletions pkg/ssh/golang/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package golang

import "github.com/containers/common/pkg/ssh/types"

type ConnectionEngine struct {
Kind types.EngineMode
}

func MakeConnectionEngine() ConnectionEngine {
return ConnectionEngine{Kind: types.GolangMode}
}
Loading

0 comments on commit c36c2e9

Please sign in to comment.