You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by zh...@apache.org on 2025/02/03 07:18:07 UTC

(dubbo-kubernetes) branch master updated: [dubboctl] add docker api agent and ssh logic (#575)

This is an automated email from the ASF dual-hosted git repository.

zhongxjian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new c8808ca0 [dubboctl] add docker api agent and ssh logic (#575)
c8808ca0 is described below

commit c8808ca03cc5a0c6557bbcd3c1b1748ded61ce59
Author: Jian Zhong <11...@qq.com>
AuthorDate: Mon Feb 3 15:18:02 2025 +0800

    [dubboctl] add docker api agent and ssh logic (#575)
---
 dubboctl/pkg/mirror/ssh/agent.go    |  12 ++
 dubboctl/pkg/mirror/ssh/dialer.go   | 392 ++++++++++++++++++++++++++++++++++++
 dubboctl/pkg/mirror/ssh/terminal.go |   1 -
 3 files changed, 404 insertions(+), 1 deletion(-)

diff --git a/dubboctl/pkg/mirror/ssh/agent.go b/dubboctl/pkg/mirror/ssh/agent.go
new file mode 100644
index 00000000..ae47b58c
--- /dev/null
+++ b/dubboctl/pkg/mirror/ssh/agent.go
@@ -0,0 +1,12 @@
+//go:build !windows
+// +build !windows
+
+package ssh
+
+import (
+	"net"
+)
+
+func dialSSHAgentConnection(sock string) (agentConn net.Conn, error error) {
+	return net.Dial("unix", sock)
+}
diff --git a/dubboctl/pkg/mirror/ssh/dialer.go b/dubboctl/pkg/mirror/ssh/dialer.go
index c181288d..dc5ea822 100644
--- a/dubboctl/pkg/mirror/ssh/dialer.go
+++ b/dubboctl/pkg/mirror/ssh/dialer.go
@@ -1,9 +1,33 @@
 package ssh
 
 import (
+	"bufio"
+	"bytes"
 	"context"
+	"errors"
+	"fmt"
+	"github.com/docker/cli/cli/connhelper"
+	"github.com/docker/docker/pkg/homedir"
 	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/agent"
+	"golang.org/x/crypto/ssh/knownhosts"
 	"net"
+	nurl "net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+var (
+	ErrBadServerKeyMsg     = "server key for given host differs from key in known_host"
+	ErrUnknownServerKeyMsg = "server key not found in known_hosts"
+)
+
+// I would expose those but since ssh pkg doesn't do correct error wrapping it would be entirely futile
+var (
+	errBadServerKey     = errors.New(ErrBadServerKeyMsg)
+	errUnknownServerKey = errors.New(ErrUnknownServerKeyMsg)
 )
 
 type (
@@ -26,11 +50,379 @@ type dialer struct {
 	addr      string
 }
 
+func (d *dialer) DialContext(ctx context.Context, n, a string) (net.Conn, error) {
+	conn, err := d.Dial(d.network, d.addr)
+	if err != nil {
+		return nil, err
+	}
+	go func() {
+		if ctx != nil {
+			<-ctx.Done()
+			conn.Close()
+		}
+	}()
+	return conn, nil
+}
+
+func (d *dialer) Dial(n, a string) (net.Conn, error) {
+	return d.sshClient.Dial(d.network, d.addr)
+}
+
+func (d *dialer) Close() error {
+	return d.sshClient.Close()
+}
+
 type ContextDialer interface {
 	DialContext(ctx context.Context, network, address string) (net.Conn, error)
 }
 type DialContextFn = func(ctx context.Context, network, addr string) (net.Conn, error)
 
+func NewDialContext(url *nurl.URL, config Config) (ContextDialer, string, error) {
+	sshConfig, err := NewSSHClientConfig(url, config)
+	if err != nil {
+		return nil, "", err
+	}
+
+	port := url.Port()
+	if port == "" {
+		port = "22"
+	}
+	host := url.Hostname()
+
+	sshClient, err := ssh.Dial("tcp", net.JoinHostPort(host, port), sshConfig)
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to dial ssh: %w", err)
+	}
+	defer func() {
+		if sshClient != nil {
+			sshClient.Close()
+		}
+	}()
+
+	var remoteDockerHost string
+	if url.Path != "" {
+		remoteDockerHost = fmt.Sprintf(`unix://%s`, url.Path)
+	} else {
+		remoteDockerHost, err = getRemoteDockerHost(sshClient)
+		if err != nil {
+			return nil, "", err
+		}
+	}
+
+	network, addr, err := getNetworkAndAddress(remoteDockerHost)
+	if err != nil {
+		return nil, "", err
+	}
+
+	if network == "npipe" {
+		// ssh tunneling doesn't support tunneling of Windows' named pipes
+		dialContext, err := stdioDialContext(url, sshClient, config.Identity)
+		return contextDialerFn(dialContext), remoteDockerHost, err
+	}
+
+	d := dialer{sshClient: sshClient, addr: addr, network: network}
+	// moving ownership of sshClient from this function to the returned structure
+	sshClient = nil
+
+	return &d, remoteDockerHost, nil
+}
+
+func getRemoteDockerHost(sshClient *ssh.Client) (remoteDockerHost string, err error) {
+	session, err := sshClient.NewSession()
+	if err != nil {
+		return
+	}
+	defer session.Close()
+
+	out, err := session.CombinedOutput("set")
+	if err != nil {
+		return
+	}
+
+	remoteDockerHost = "unix:///var/run/docker.sock"
+	isWin, err := isWindowsMachine(sshClient)
+	if err != nil {
+		return
+	}
+
+	if isWin {
+		remoteDockerHost = "npipe:////./pipe/docker_engine"
+	}
+
+	scanner := bufio.NewScanner(bytes.NewBuffer(out))
+	for scanner.Scan() {
+		if strings.HasPrefix(scanner.Text(), "DOCKER_HOST=") {
+			parts := strings.SplitN(scanner.Text(), "=", 2)
+			remoteDockerHost = strings.Trim(parts[1], `"'`)
+			break
+		}
+	}
+
+	return remoteDockerHost, err
+}
+
+func isWindowsMachine(sshClient *ssh.Client) (bool, error) {
+	session, err := sshClient.NewSession()
+	if err != nil {
+		return false, err
+	}
+	defer session.Close()
+
+	out, err := session.CombinedOutput("systeminfo")
+	if err == nil && strings.Contains(string(out), "Windows") {
+		return true, nil
+	}
+	return false, nil
+}
+
+func stdioDialContext(url *nurl.URL, sshClient *ssh.Client, identity string) (DialContextFn, error) {
+	session, err := sshClient.NewSession()
+	if err != nil {
+		return nil, err
+	}
+	defer session.Close()
+
+	out, err := session.CombinedOutput("docker system dial-stdio --help")
+	if err != nil {
+		return nil, fmt.Errorf("cannot use dial-stdio: %w (%q)", err, out)
+	}
+
+	var opts []string
+	if identity != "" {
+		opts = append(opts, "-i", identity)
+	}
+
+	connHelper, err := connhelper.GetConnectionHelperWithSSHOpts(url.String(), opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return connHelper.Dialer, nil
+}
+
+func getNetworkAndAddress(remoteDockerHost string) (network string, addr string, err error) {
+	remoteDockerHostURL, err := nurl.Parse(remoteDockerHost)
+	if err != nil {
+		return
+	}
+	switch remoteDockerHostURL.Scheme {
+	case "unix", "npipe":
+		addr = remoteDockerHostURL.Path
+	case "fd":
+		remoteDockerHostURL.Scheme = "tcp" // don't know why it works that way
+		fallthrough
+	case "tcp":
+		addr = remoteDockerHostURL.Host
+	default:
+		return "", "", errors.New("scheme is not supported")
+	}
+	network = remoteDockerHostURL.Scheme
+
+	return network, addr, err
+}
+
+var knownKeyNames = []string{"id_rsa", "id_dsa", "id_ecdsa", "id_ecdsa_sk", "id_ed25519", "id_ed25519_sk"}
+
+func NewSSHClientConfig(url *nurl.URL, credentialsConfig Config) (*ssh.ClientConfig, error) {
+	var (
+		authMethods []ssh.AuthMethod
+		signers     []ssh.Signer
+		err         error
+	)
+
+	if pw, found := url.User.Password(); found {
+		authMethods = append(authMethods, ssh.Password(pw))
+	}
+
+	// add signer from explicit identity parameter
+	if credentialsConfig.Identity != "" {
+		s, err := publicKey(credentialsConfig.Identity, []byte(credentialsConfig.Identity), credentialsConfig.PassPhraseCallback)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse identity file: %w", err)
+		}
+		signers = append(signers, s)
+	}
+
+	// add signers from ssh-agent
+	if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found && sock != "" {
+		var agentSigners []ssh.Signer
+		var agentConn net.Conn
+		agentConn, err = dialSSHAgentConnection(sock)
+		if err != nil {
+			return nil, fmt.Errorf("failed to connect to ssh-agent's socket: %w", err)
+		}
+		agentSigners, err = agent.NewClient(agentConn).Signers()
+		if err != nil {
+			return nil, fmt.Errorf("failed to get signers from ssh-agent: %w", err)
+		}
+		signers = append(signers, agentSigners...)
+	}
+
+	// if there is no explicit identity file nor keys from ssh-agent then
+	// add keys with standard name from ~/.ssh/
+	if len(signers) == 0 {
+		var defaultKeyPaths []string
+		if home, err := os.UserHomeDir(); err == nil {
+			for _, keyName := range knownKeyNames {
+				p := filepath.Join(home, ".ssh", keyName)
+
+				fi, err := os.Stat(p)
+				if err != nil {
+					continue
+				}
+				if fi.Mode().IsRegular() {
+					defaultKeyPaths = append(defaultKeyPaths, p)
+				}
+			}
+		}
+
+		if len(defaultKeyPaths) == 1 {
+			s, err := publicKey(defaultKeyPaths[0], []byte(credentialsConfig.PassPhrase), credentialsConfig.PassPhraseCallback)
+			if err != nil {
+				return nil, err
+			}
+			signers = append(signers, s)
+		}
+	}
+
+	if len(signers) > 0 {
+		dedup := make(map[string]ssh.Signer)
+		// Dedup signers based on fingerprint, ssh-agent keys override explicit identity
+		for _, s := range signers {
+			fp := ssh.FingerprintSHA256(s.PublicKey())
+			// if _, found := dedup[fp]; found {
+			//	key updated
+			// }
+			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 len(authMethods) == 0 && credentialsConfig.PasswordCallback != nil {
+		authMethods = append(authMethods, ssh.PasswordCallback(credentialsConfig.PasswordCallback))
+	}
+
+	const sshTimeout = 5
+	clientConfig := &ssh.ClientConfig{
+		User:            url.User.Username(),
+		Auth:            authMethods,
+		HostKeyCallback: createHostKeyCallback(credentialsConfig.HostKeyCallback),
+		HostKeyAlgorithms: []string{
+			ssh.KeyAlgoECDSA256,
+			ssh.KeyAlgoECDSA384,
+			ssh.KeyAlgoECDSA521,
+			ssh.KeyAlgoED25519,
+			ssh.KeyAlgoRSASHA256,
+			ssh.KeyAlgoRSASHA512,
+			ssh.KeyAlgoRSA,
+			ssh.KeyAlgoDSA,
+		},
+		Timeout: sshTimeout * time.Second,
+	}
+
+	return clientConfig, nil
+}
+
+func publicKey(path string, passphrase []byte, passPhraseCallback PassPhraseCallback) (ssh.Signer, error) {
+	key, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read key file: %w", err)
+	}
+
+	signer, err := ssh.ParsePrivateKey(key)
+	if err != nil {
+		var missingPhraseError *ssh.PassphraseMissingError
+		if ok := errors.As(err, &missingPhraseError); !ok {
+			return nil, fmt.Errorf("failed to parse private key: %w", err)
+		}
+
+		if len(passphrase) == 0 && passPhraseCallback != nil {
+			b, err := passPhraseCallback()
+			if err != nil {
+				return nil, err
+			}
+			passphrase = []byte(b)
+		}
+
+		return ssh.ParsePrivateKeyWithPassphrase(key, passphrase)
+	}
+
+	return signer, nil
+}
+
+func createHostKeyCallback(hostKeyCallback HostKeyCallback) func(hostPort string, remote net.Addr, key ssh.PublicKey) error {
+	return func(hostPort string, remote net.Addr, pubKey ssh.PublicKey) error {
+		host, port := hostPort, "22"
+		if _h, _p, err := net.SplitHostPort(host); err == nil {
+			host, port = _h, _p
+		}
+
+		knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts")
+
+		_, err := os.Stat(knownHosts)
+		if err != nil && errors.Is(err, os.ErrNotExist) {
+			if hostKeyCallback != nil && hostKeyCallback(hostPort, pubKey) == nil {
+				return nil
+			}
+			return errUnknownServerKey
+		}
+
+		f, err := os.Open(knownHosts)
+		if err != nil {
+			return fmt.Errorf("failed to open known_hosts: %w", err)
+		}
+		defer f.Close()
+
+		hashhost := knownhosts.HashHostname(host)
+
+		var errs []error
+		scanner := bufio.NewScanner(f)
+		for scanner.Scan() {
+			_, hostPorts, _key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+
+			for _, hp := range hostPorts {
+				h, p := hp, "22"
+				if _h, _p, err := net.SplitHostPort(hp); err == nil {
+					h, p = _h, _p
+				}
+
+				if (h == host || h == hashhost) && port == p {
+					if pubKey.Type() != _key.Type() {
+						errs = append(errs, fmt.Errorf("missmatch in type of a key"))
+						continue
+					}
+					if bytes.Equal(_key.Marshal(), pubKey.Marshal()) {
+						return nil
+					}
+
+					return errBadServerKey
+				}
+			}
+		}
+
+		if hostKeyCallback != nil && hostKeyCallback(hostPort, pubKey) == nil {
+			return nil
+		}
+
+		if len(errs) > 0 {
+			return fmt.Errorf("server is not trusted (%v)", errs)
+		}
+
+		return errUnknownServerKey
+	}
+}
+
 type contextDialerFn DialContextFn
 
 func (n contextDialerFn) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
diff --git a/dubboctl/pkg/mirror/ssh/terminal.go b/dubboctl/pkg/mirror/ssh/terminal.go
index 0f96f41a..dd608308 100644
--- a/dubboctl/pkg/mirror/ssh/terminal.go
+++ b/dubboctl/pkg/mirror/ssh/terminal.go
@@ -40,7 +40,6 @@ func readSecret(prompt string) (pw []byte, err error) {
 			return pw, err
 		}
 	}
-
 }
 
 func NewPasswordCbk() PasswordCallback {