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 <[email protected]>
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 {