When connecting to a remote daemon through an ssh:// connection,
the CLI connects with the remote host using ssh, executing the
`docker system dial-stdio` command on the remote host to connect
to the daemon API's unix socket.
By default, the `docker system dial-stdio` command connects with the
daemon using the default location (/var/run/docker.sock), or the
location as configured on the remote host.
Commit 25ebf0ec9c (included in docker
CLI v24.0.0-rc.2 and higher) introduced a feature to allow the location
of the socket to be specified through the host connection string, for
example:
DOCKER_HOST='ssh://example.test/run/custom-docker.sock'
The custom path is included as part of the ssh command executed from
the client machine to connect with the remote host. THe example above
would execute the following command from the client machine;
ssh -o ConnectTimeout=30 -T -- example.test docker --host unix:///run/custom-docker.sock system dial-stdio
ssh executes remote commands in a shell environment, and no quoting
was in place, which allowed for a connection string to include additional
content, which would be expanded / executed on the remote machine.
For example, the following example would execute `echo hello > /hello.txt`
on the remote machine;
export DOCKER_HOST='ssh://example.test/var/run/docker.sock $(echo hello > /hello.txt)'
docker info
# (output of docker info from the remote machine)
While this doesn't allow the user to do anything they're not already
able to do so (by directly using the same SSH connection), the behavior
is not expected, so this patch adds quoting to prevent such URLs from
resulting in expansion.
This patch updates the cli/connhelper and cli/connhelper/ssh package to
quote parameters used in the ssh command to prevent code execution and
expansion of variables on the remote machine. Quoting is also applied to
other parameters that are obtained from the DOCKER_HOST url, such as username
and hostname.
- The existing `Spec.Args()` method inthe cli/connhelper/ssh package now
quotes arguments, and returns a nil slice when failing to quote. Users
of this package should therefore check the returned arguments before
consuming. This method did not provide an error-return, and adding
one would be a breaking change.
- A new `Spec.Command` method is introduced, which (unlike the `Spec.Args()`
method) provides an error return. Users are recommended to use this new
method instead of the `Spec.Args()` method.
Some minor additional changes in behavior are included in this patch;
- Connection URLs with a trailing slash (e.g. `ssh://example.test/`)
would previously result in `unix:///` being used as custom socket
path. After this patch, the trailing slash is ignored, and no custom
socket path is used.
- Specifying a remote command is now required. When passing an empty
remote command, `Spec.Args()` now results in a `nil` value to be
returned (or an `no remote command specified` error when using
`Spec.Comnmand()`.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
// Package connhelper provides helpers for connecting to a remote daemon host with custom logic.
|
|
package connhelper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/cli/connhelper/commandconn"
|
|
"github.com/docker/cli/cli/connhelper/ssh"
|
|
)
|
|
|
|
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
|
|
type ConnectionHelper struct {
|
|
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
Host string // dummy URL used for HTTP requests. e.g. "http://docker"
|
|
}
|
|
|
|
// GetConnectionHelper returns Docker-specific connection helper for the given URL.
|
|
// GetConnectionHelper returns nil without error when no helper is registered for the scheme.
|
|
//
|
|
// ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host.
|
|
func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) {
|
|
return getConnectionHelper(daemonURL, nil)
|
|
}
|
|
|
|
// GetConnectionHelperWithSSHOpts returns Docker-specific connection helper for
|
|
// the given URL, and accepts additional options for ssh connections. It returns
|
|
// nil without error when no helper is registered for the scheme.
|
|
//
|
|
// Requires Docker 18.09 or later on the remote host.
|
|
func GetConnectionHelperWithSSHOpts(daemonURL string, sshFlags []string) (*ConnectionHelper, error) {
|
|
return getConnectionHelper(daemonURL, sshFlags)
|
|
}
|
|
|
|
func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper, error) {
|
|
u, err := url.Parse(daemonURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if u.Scheme == "ssh" {
|
|
sp, err := ssh.NewSpec(u)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ssh host connection is not valid: %w", err)
|
|
}
|
|
sshFlags = addSSHTimeout(sshFlags)
|
|
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
|
|
|
remoteCommand := []string{"docker", "system", "dial-stdio"}
|
|
socketPath := sp.Path
|
|
if strings.Trim(sp.Path, "/") != "" {
|
|
remoteCommand = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
|
|
}
|
|
sshArgs, err := sp.Command(sshFlags, remoteCommand...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ConnectionHelper{
|
|
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return commandconn.New(ctx, "ssh", sshArgs...)
|
|
},
|
|
Host: "http://docker.example.com",
|
|
}, nil
|
|
}
|
|
// Future version may support plugins via ~/.docker/config.json. e.g. "dind"
|
|
// See docker/cli#889 for the previous discussion.
|
|
return nil, err
|
|
}
|
|
|
|
// GetCommandConnectionHelper returns Docker-specific connection helper constructed from an arbitrary command.
|
|
func GetCommandConnectionHelper(cmd string, flags ...string) (*ConnectionHelper, error) {
|
|
return &ConnectionHelper{
|
|
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return commandconn.New(ctx, cmd, flags...)
|
|
},
|
|
Host: "http://docker.example.com",
|
|
}, nil
|
|
}
|
|
|
|
func addSSHTimeout(sshFlags []string) []string {
|
|
if !strings.Contains(strings.Join(sshFlags, ""), "ConnectTimeout") {
|
|
sshFlags = append(sshFlags, "-o ConnectTimeout=30")
|
|
}
|
|
return sshFlags
|
|
}
|
|
|
|
// disablePseudoTerminalAllocation disables pseudo-terminal allocation to
|
|
// prevent SSH from executing as a login shell
|
|
func disablePseudoTerminalAllocation(sshFlags []string) []string {
|
|
for _, flag := range sshFlags {
|
|
if flag == "-T" {
|
|
return sshFlags
|
|
}
|
|
}
|
|
return append(sshFlags, "-T")
|
|
}
|