
本文详解如何使用 Go 的 golang.org/x/crypto/ssh 包,通过跳板机(Bastion)建立到私有子网中目标服务器的 SSH 连接,并执行命令、上传文件,全程无需 nc 或 shell 代理命令,纯原生 Go 实现。
本文详解如何使用 go 的 `golang.org/x/crypto/ssh` 包,通过跳板机(bastion)建立到私有子网中目标服务器的 ssh 连接,并执行命令、上传文件,全程无需 `nc` 或 shell 代理命令,纯原生 go 实现。
在云环境(如 AWS VPC)中,出于安全考虑,服务实例常部署于私有子网,无法直接从公网访问;而跳板机(Bastion)作为唯一可公开访问的入口,承担代理转发职责。与 OpenSSH 的 -o ProxyCommand=... 方式不同,Go 的 x/crypto/ssh 提供了更底层、更可控的隧道能力:利用已建立的 SSH 连接调用 Client.Dial(),在远端(Bastion)主动发起到目标主机的 TCP 连接——这本质上构建了一条“SSH over SSH”隧道,无需依赖 netcat 或 socat 等外部工具。
以下是一个完整、可运行的示例,涵盖连接建立、远程命令执行和 SFTP 文件上传三类核心操作:
package main
import (
"io"
"log"
"os"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
func main() {
// 1. 准备认证配置(支持私钥 + 主机密钥验证)
signer, err := ssh.ParsePrivateKey([]byte(`-----BEGIN OPENSSH PRIVATE KEY-----
...your-privkey-here...
-----END OPENSSH PRIVATE KEY-----`))
if err != nil {
log.Fatal("failed to parse private key:", err)
}
// 使用 known_hosts 进行主机验证(推荐生产环境启用)
hostKeyCallback, err := knownhosts.New("/home/user/.ssh/known_hosts")
if err != nil {
log.Fatal("failed to load known_hosts:", err)
}
config := &ssh.ClientConfig{
User: "ubuntu",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: hostKeyCallback,
Timeout: 30 * time.Second,
}
// 2. 连接到跳板机(Bastion)
bastionAddr := "bastion.example.com:22" // 替换为实际跳板机地址
bClient, err := ssh.Dial("tcp", bastionAddr, config)
if err != nil {
log.Fatal("failed to dial bastion:", err)
}
defer bClient.Close()
// 3. 通过跳板机 Dial 到私有实例(Service Instance)
serviceAddr := "10.0.1.5:22" // 私有子网内目标 IP:Port(Bastion 可直连)
conn, err := bClient.Dial("tcp", serviceAddr)
if err != nil {
log.Fatal("failed to dial service instance via bastion:", err)
}
defer conn.Close()
// 4. 将隧道连接升级为新的 SSH 客户端连接
ncc, chans, reqs, err := ssh.NewClientConn(conn, serviceAddr, config)
if err != nil {
log.Fatal("failed to create client connection:", err)
}
sClient := ssh.NewClient(ncc, chans, reqs)
defer sClient.Close()
// ✅ 场景 1:执行远程命令(如 ls -l)
session, err := sClient.NewSession()
if err != nil {
log.Fatal("failed to create session:", err)
}
defer session.Close()
out, err := session.Output("ls -l /tmp")
if err != nil {
log.Fatal("command failed:", err)
}
log.Printf("Command output:\n%s", out)
// ✅ 场景 2:上传文件(使用 SFTP)
sftpClient, err := sftp.NewClient(sClient)
if err != nil {
log.Fatal("failed to start SFTP client:", err)
}
defer sftpClient.Close()
srcFile, err := os.Open("/path/on/local/laptop/file.txt")
if err != nil {
log.Fatal("failed to open local file:", err)
}
defer srcFile.Close()
dstFile, err := sftpClient.Create("/tmp/uploaded-file.txt")
if err != nil {
log.Fatal("failed to create remote file:", err)
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
if err != nil {
log.Fatal("failed to upload file:", err)
}
log.Println("✅ File uploaded successfully.")
}? 关键注意事项:
- 主机密钥验证不可省略:务必使用 knownhosts.New() 或自定义 HostKeyCallback,避免中间人攻击;禁用 InsecureIgnoreHostKey() 仅限测试。
- 连接生命周期管理:每个 ssh.Client 和 net.Conn 都需显式 Close(),否则将导致资源泄漏。
- 私钥格式兼容性:ssh.ParsePrivateKey 支持 OpenSSH 格式(BEGIN OPENSSH PRIVATE KEY),若为 PEM 格式(BEGIN RSA PRIVATE KEY),需用 ssh.ParseRawPrivateKey 并处理加密逻辑。
- 网络可达性前提:跳板机必须能通过内网 IP 直连私有实例(如 AWS 安全组放行 10.0.1.0/24:22),且私有实例未绑定公网 IP。
- 错误处理建议:生产代码中应封装重试逻辑、超时控制及结构化错误返回(如自定义 error 类型),而非直接 log.Fatal。
该方案完全复现了 OpenSSH 的 ProxyCommand 行为,但以纯 Go、零外部依赖的方式实现,具备更好的可观测性、可嵌入性和跨平台一致性,是构建自动化运维工具、CI/CD 后端或云管理平台的理想选择。


















