SSH连接失败常因TCP握手无超时、私钥格式错误(需PEM而非OpenSSH新格式)、Session未正确关闭导致;须显式设置net.Dialer超时、转换私钥格式、复用Client并及时Close Session。

ssh.Dial 连接失败但没报错,实际卡在 TCP 握手
Go 的 ssh.Dial 默认不设超时,一旦目标服务器端口不通、防火墙拦截或 DNS 解析慢,就会无响应地挂起几十秒。这不是 SSH 协议问题,是底层 net.Dial 在等 TCP 连接建立。
- 必须显式传入带超时的
net.Dialer,不能只靠ssh.ClientConfig里的 Timeout -
ssh.ClientConfig.Timeout只控制认证阶段(如密码/密钥交换),不影响 TCP 连接本身 - 示例写法:
dialer := &net.Dialer{Timeout: 5 * time.Second} client, err := ssh.Dial("tcp", "192.168.1.100:22", config, dialer)
执行命令后 stdout 读不到输出,或者程序卡死
常见于用 session.Run 执行带交互、后台进程或未正确关闭 stdin 的命令(比如 top、tail -f、nohup ./app &)。Run 会等待命令彻底退出,而这类命令不会自然结束。
- 改用
session.Start+session.Wait,便于控制流程 - 务必调用
session.CombinedOutput或分别读session.StdoutPipe/session.StderrPipe,不能直接读os.Stdout - 如果命令本身需要输入(如 sudo 密码),
session.Stdin需提前设置,否则阻塞 - 别忘了
defer session.Close(),否则连接资源泄漏
私钥认证失败:“unable to parse private key” 或 “no auth passed”
crypto/ssh 对私钥格式非常敏感——它只接受 PEM 编码的 OpenSSH 格式(即以 -----BEGIN RSA PRIVATE KEY----- 开头),不支持新版 OpenSSH 默认生成的 -----BEGIN OPENSSH PRIVATE KEY-----(即新 OpenSSH 私钥格式)。
- 用
ssh-keygen -p -m PEM -f id_rsa把新格式转成 PEM(会提示输原密码) - 加载私钥时别用
ioutil.ReadFile(已弃用),改用os.ReadFile - 解析私钥必须用
ssh.ParsePrivateKey,不能用ssh.ParseRawPrivateKey(后者不处理加密私钥) - 如果私钥有密码,需先解密:
block, _ := pem.Decode(keyData) if block == nil { /* error */ } priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) // RSA // 或 x509.ParsePKCS8PrivateKey / ssh.ParseRawPrivateKey 等,依类型而定
并发执行多条命令时出现 connection reset 或 EOF
一个 *ssh.Client 实例可复用多个 *ssh.Session,但每个 Session 是单次命令通道。若并发量大又不复用 client,容易触发服务端连接数限制或 TCP TIME_WAIT 耗尽。
立即学习“go语言免费学习笔记(深入)”;
- 不要为每次命令都
ssh.Dial一次;应复用*ssh.Client,按需client.NewSession() - 每个
Session必须显式session.Close(),否则底层 channel 不释放 - 注意
Client本身不是 goroutine 安全的,但NewSession是;并发调用没问题 - 长时间空闲连接可能被中间设备断开,可加简单心跳:
client.SendRequest("ping", false, nil)
实际跑通的关键往往就卡在 PEM 私钥格式、TCP Dial 超时、以及 Session 生命周期管理这三处。少一个,轻则超时失败,重则静默卡死,还不好定位。










