// Package sshutil 提供 SSH 连接测试能力,用于远程节点凭证验证。 // 借鉴 aiops pkg/remoteexec 的连接逻辑,精简为节点连通性测试专用。 // 基于 golang.org/x/crypto/ssh,支持密码认证与私钥认证。 package sshutil import ( "fmt" "net" "strings" "time" "golang.org/x/crypto/ssh" ) // Config SSH 连接测试配置 type Config struct { Host string // 目标 IP 或域名 Port int // SSH 端口(默认 22) User string // SSH 用户(默认 root) AuthType string // password / key Password string // 密码认证 KeyData string // 私钥内容 } // Result 连接测试结果 type Result struct { Success bool `json:"success"` Message string `json:"message"` // 成功时为 uname 输出,失败时为友好错误 Output string `json:"output"` // 原始命令输出 Took int64 `json:"took_ms"` // 耗时(毫秒) } // TestConnection 测试 SSH 连接并执行探测命令,返回友好结果。 // 探测命令:echo vpn-node-ok && uname -a(与 aiops build-node 一致) func TestConnection(cfg Config) Result { start := time.Now() if cfg.Port == 0 { cfg.Port = 22 } if cfg.User == "" { cfg.User = "root" } authMethods, err := buildAuth(cfg) if err != nil { return Result{Success: false, Message: err.Error(), Took: time.Since(start).Milliseconds()} } addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port)) sshConfig := &ssh.ClientConfig{ User: cfg.User, Auth: authMethods, Timeout: 10 * time.Second, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 内网管理工具,简化主机密钥校验 } client, err := ssh.Dial("tcp", addr, sshConfig) if err != nil { return Result{Success: false, Message: formatSSHError(err, addr), Took: time.Since(start).Milliseconds()} } defer client.Close() // 执行探测命令 session, err := client.NewSession() if err != nil { return Result{Success: false, Message: fmt.Sprintf("创建会话失败: %v", err), Took: time.Since(start).Milliseconds()} } defer session.Close() out, err := session.CombinedOutput("echo vpn-node-ok && uname -a") output := strings.TrimSpace(string(out)) if err != nil { return Result{Success: false, Message: fmt.Sprintf("命令执行失败: %v", err), Output: output, Took: time.Since(start).Milliseconds()} } return Result{ Success: true, Message: output, Output: output, Took: time.Since(start).Milliseconds(), } } // buildAuth 根据认证方式构建 ssh.AuthMethod 列表。 // 密钥认证解析失败时,若有密码则兜底用密码(aiops 模式)。 func buildAuth(cfg Config) ([]ssh.AuthMethod, error) { switch cfg.AuthType { case "key": if cfg.KeyData == "" { return nil, fmt.Errorf("私钥认证模式但未提供私钥") } signer, err := ssh.ParsePrivateKey([]byte(cfg.KeyData)) if err != nil { // 私钥解析失败,尝试密码兜底 if cfg.Password != "" { return []ssh.AuthMethod{ssh.Password(cfg.Password)}, nil } return nil, fmt.Errorf("私钥解析失败: %v", err) } return []ssh.AuthMethod{ssh.PublicKeys(signer)}, nil default: // password if cfg.Password == "" { return nil, fmt.Errorf("密码认证模式但未提供密码") } return []ssh.AuthMethod{ssh.Password(cfg.Password)}, nil } } // formatSSHError 将底层 SSH 错误映射为用户友好的中文提示 func formatSSHError(err error, addr string) string { msg := err.Error() switch { case strings.Contains(msg, "i/o timeout"), strings.Contains(msg, "deadline exceeded"): return fmt.Sprintf("连接超时:无法在 10 秒内连接到 %s,请检查地址/端口/防火墙", addr) case strings.Contains(msg, "connection refused"): return fmt.Sprintf("连接被拒绝:%s 端口未开放 SSH 服务", addr) case strings.Contains(msg, "no route to host"): return fmt.Sprintf("网络不可达:%s 地址不存在或路由不通", addr) case strings.Contains(msg, "authentication failed"), strings.Contains(msg, "unable to authenticate"): return "认证失败:用户名/密码/私钥不正确" case strings.Contains(msg, "handshake"): return fmt.Sprintf("SSH 握手失败:%s 可能不是标准 SSH 服务", addr) case strings.Contains(msg, "dns"), strings.Contains(msg, "no such host"): return fmt.Sprintf("域名解析失败:%s", addr) default: return fmt.Sprintf("连接失败:%s", msg) } }