Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the possibility to connect via ssh to a remote docker node. #143

Merged
merged 2 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
135 changes: 120 additions & 15 deletions docker/connection.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package docker

import (
"context"
"crypto/tls"
"golang.org/x/crypto/ssh"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/docker/cli/opts"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
"github.com/kevinburke/ssh_config"
homedir "github.com/mitchellh/go-homedir"
drytls "github.com/moncho/dry/tls"
"github.com/moncho/dry/version"
Expand Down Expand Up @@ -92,44 +99,142 @@ func ConnectToDaemon(env Env) (*DockerDaemon, error) {
if err != nil {
return nil, errors.Wrap(err, "Invalid Host")
}
var tlsConfig *tls.Config
var options *drytls.Options
//If a path to certificates is given use the path to read certificates from
if dockerCertPath := env.DockerCertPath; dockerCertPath != "" {
options := drytls.Options{
options = &drytls.Options{
CAFile: filepath.Join(dockerCertPath, "ca.pem"),
CertFile: filepath.Join(dockerCertPath, "cert.pem"),
KeyFile: filepath.Join(dockerCertPath, "key.pem"),
InsecureSkipVerify: env.DockerTLSVerify,
}
tlsConfig, err = drytls.Client(options)
if err != nil {
return nil, errors.Wrap(err, "TLS setup error")
}
} else if env.DockerTLSVerify {
//No cert path is given but TLS verify is set, default location for
//docker certs will be used.
//See https://docs.docker.com/engine/security/https/#secure-by-default
//Fixes #23
options := drytls.Options{
options = &drytls.Options{
CAFile: filepath.Join(defaultDockerPath, "ca.pem"),
CertFile: filepath.Join(defaultDockerPath, "cert.pem"),
KeyFile: filepath.Join(defaultDockerPath, "key.pem"),
InsecureSkipVerify: env.DockerTLSVerify,
}
env.DockerCertPath = defaultDockerPath
tlsConfig, err = drytls.Client(options)
}

var opt []client.Opt
if options != nil {
opt = append(opt, client.WithTLSClientConfig(options.CAFile, options.CertFile, options.KeyFile))
}

if host != "" && strings.Index(host, "ssh") == 0 {
//if it starts with ssh, its an ssh connection, and we need to handle this specially
//github.com/docker/docker does not handle ssh, as an upgrade to go-connections need to be made
//see https://github.com/docker/go-connections/pull/39
url, err := url.Parse(host)
if err != nil {
return nil, errors.Wrap(err, "TLS setup error")
return nil, err
}

pass, _ := url.User.Password()
sshConfig, err := configureSshTransport(url.Host, url.User.Username(), pass)
if err != nil {
return nil, err
}
opt = append(opt, client.WithDialContext(
func(ctx context.Context, network, addr string) (net.Conn, error) {
return connectSshTransport(url.Host, url.Path, sshConfig)
}))
} else if host != "" {
//default uses the docker library to connect to hosts
opt = append(opt, client.WithHost(host))
}
httpClient, err := newHTTPClient(host, tlsConfig)

client, err := client.NewClientWithOpts(opt...)
if err != nil {
return nil, errors.Wrap(err, "HttpClient creation error")
return nil, errors.Wrap(err, "Error creating client")
}
return connect(client, env)

client, err := client.NewClient(host, env.DockerAPIVersion, httpClient, headers)
if err == nil {
return connect(client, env)
}

func configureSshTransport(host string, user string, pass string) (*ssh.ClientConfig, error) {
dirname, err := homedir.Dir()
if err != nil {
return nil, err
}

var methods []ssh.AuthMethod

foundIdentityFile := false
files := ssh_config.GetAll(host, "IdentityFile")
for _, v := range files {
//see https://github.com/docker/go-connections/pull/39#issuecomment-312765226
if _, err := os.Stat(v); err == nil {
methods, err = readPk(v, methods, dirname)
if err != nil {
return nil, err
}
foundIdentityFile = true
}
}

if !foundIdentityFile {
pkFilenames, err := ioutil.ReadDir(dirname + "/.ssh/")
if err != nil {
return nil, err
}

for _, pkFilename := range pkFilenames {
if strings.Index(pkFilename.Name(), "id_") == 0 && !strings.HasSuffix(pkFilename.Name(), ".pub") {
methods, err = readPk(pkFilename.Name(), methods, dirname+"/.ssh/")
if err != nil {
return nil, err
}
}
}
}
if pass != "" {
methods = append(methods, []ssh.AuthMethod{ssh.Password(pass)}...)
}
return nil, errors.Wrap(err, "Error creating client")

return &ssh.ClientConfig{
User: user,
Auth: methods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, nil
}

func readPk(pkFilename string, auth []ssh.AuthMethod, dirname string) ([]ssh.AuthMethod, error) {
pk, err := ioutil.ReadFile(dirname + pkFilename)
if err != nil {
return nil, nil
}
signer, err := ssh.ParsePrivateKey(pk)
if err != nil {
return nil, err
}
auth = append(auth, []ssh.AuthMethod{ssh.PublicKeys(signer)}...)
return auth, nil
}

func connectSshTransport(host string, path string, sshConfig *ssh.ClientConfig) (net.Conn, error) {
remoteConn, err := net.Dial("tcp", host)
if err != nil {
return nil, err
}

ncc, chans, reqs, err := ssh.NewClientConn(remoteConn, "", sshConfig)

if err != nil {
return nil, err
}

sClient := ssh.NewClient(ncc, chans, reqs)
c, err := sClient.Dial("unix", path)
if err != nil {
return nil, err
}

return c, nil
}
2 changes: 1 addition & 1 deletion docker/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (daemon *DockerDaemon) RunImage(image dockerTypes.ImageSummary, command str
return pkgError.Wrap(err, "Error configuring container")
}

cCreated, err := daemon.client.ContainerCreate(ctx, &cc, &hc, nil, "")
cCreated, err := daemon.client.ContainerCreate(ctx, &cc, &hc, nil, nil, "")

if err != nil {
return pkgError.Wrap(err, fmt.Sprintf("Cannot create container for image %s", imageName))
Expand Down
3 changes: 2 additions & 1 deletion docker/mock/docker_api_client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mock

import (
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"strconv"

"golang.org/x/net/context"
Expand Down Expand Up @@ -55,7 +56,7 @@ func (m ContainerAPIClientMock) ContainerInspect(ctx context.Context, container
}

//ContainerCreate mocks container creation
func (mock ImageAPIClientMock) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) {
func (mock ImageAPIClientMock) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{ID: "NewContainer"}, nil
}

Expand Down
23 changes: 9 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,38 @@ module github.com/moncho/dry
go 1.14

require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.4.9 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/containerd/containerd v1.3.3 // indirect
github.com/docker/cli v0.0.0-20200331182946-6e98ebc89a68
github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478
github.com/docker/docker v1.4.2-0.20200309214505-aa6a9891b09c
github.com/docker/docker v20.10.9+incompatible
github.com/docker/go-connections v0.4.1-0.20180821093606-97c2040d34df
github.com/docker/go-units v0.4.0
github.com/docker/libnetwork v0.0.0-20180222171459-0ae9b6f38f24 // indirect
github.com/gdamore/tcell v1.3.0
github.com/gizak/termui v0.0.0-20190118200331-b3075f731367
github.com/gogo/protobuf v1.1.1 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.6
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kevinburke/ssh_config v1.1.0
github.com/mattn/go-runewidth v0.0.4
github.com/mitchellh/go-homedir v0.0.0-20160621174243-756f7b183b7a
github.com/mitchellh/go-wordwrap v1.0.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.1
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.1
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/stretchr/testify v1.3.0 // indirect
github.com/vishvananda/netlink v1.0.0 // indirect
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
go.uber.org/goleak v0.10.0
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/grpc v1.18.0 // indirect
gotest.tools v2.1.0+incompatible // indirect
gotest.tools/v3 v3.0.2 // indirect
)