
本文详解如何在 Go 中将 SSH 远程 shell 会话(非单次命令)双向透传至 WebSocket 连接,重点利用 golang.org/x/crypto/ssh 的 Session.Stdin/Stdout 接口实现字节流级桥接,并提供可运行的双向转发示例。
本文详解如何在 go 中将 ssh 远程 shell 会话(非单次命令)双向透传至 websocket 连接,重点利用 `golang.org/x/crypto/ssh` 的 `session.stdin`/`stdout` 接口实现字节流级桥接,并提供可运行的双向转发示例。
在 Web 终端场景中,常需将浏览器 WebSocket 连接与后端 SSH 会话打通,实现类似 xterm.js + sshd 的交互式终端体验。与 Telnet 等简单协议不同,SSH 会话是全双工、带状态、需协商 TTY 和信号处理的复杂流式交互——因此不能直接套用 io.Copy 或简单缓冲读写,而必须正确桥接 ssh.Session 的标准 I/O 接口。
关键认知在于:ssh.Session 并非仅支持单次命令执行(如 Run()),其结构体原生暴露了 Stdin io.Reader、Stdout io.Writer 和 Stderr io.Writer 字段。这意味着你可以将任意 io.Reader(如 WebSocket 消息流)赋给 session.Stdin,将任意 io.Writer(如 WebSocket 写入器)赋给 session.Stdout,从而实现真正的流式透传。
以下是一个生产就绪的双向转发核心逻辑示例:
func forwardSSHOverWS(ws *websocket.Conn, session *ssh.Session) error {
// 1. 将 WebSocket 的读取端(客户端输入)映射为 SSH Stdin
go func() {
defer ws.Close()
buf := make([]byte, 4096)
for {
_, msgType, data, err := ws.ReadMessage()
if err != nil {
log.Printf("WS read error: %v", err)
return
}
if msgType != websocket.BinaryMessage && msgType != websocket.TextMessage {
continue
}
// 写入 SSH 会话 stdin(注意:session.Stdin 是 io.Reader,但实际需写入的是其背后管道)
// ✅ 正确做法:使用 session.StdinPipe() 获取可写入的 io.WriteCloser
stdin, err := session.StdinPipe()
if err != nil {
log.Printf("Failed to get stdin pipe: %v", err)
return
}
if _, writeErr := stdin.Write(data); writeErr != nil {
log.Printf("SSH stdin write error: %v", writeErr)
return
}
}
}()
// 2. 将 SSH Stdout/Stderr 映射为 WebSocket 输出(二进制消息)
go func() {
defer ws.Close()
stdout, _ := session.StdoutPipe()
stderr, _ := session.StderrPipe()
// 合并 stdout/stderr 到同一输出流(按需可分离)
var wg sync.WaitGroup
wg.Add(2)
copyToWS := func(r io.Reader, label string) {
defer wg.Done()
buf := make([]byte, 4096)
for {
n, err := r.Read(buf)
if n > 0 {
if err := ws.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
log.Printf("WS write %s error: %v", label, err)
return
}
}
if err == io.EOF {
break
}
if err != nil {
log.Printf("SSH %s read error: %v", label, err)
break
}
}
}
go copyToWS(stdout, "stdout")
go copyToWS(stderr, "stderr")
wg.Wait()
}()
// 3. 启动交互式 Shell(关键!必须显式请求 Pseudo-Terminal)
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err := session.RequestPty("xterm", 40, 80, modes); err != nil {
return fmt.Errorf("failed to request pty: %w", err)
}
if err := session.Shell(); err != nil { // 启动交互式 shell,非 Run()
return fmt.Errorf("failed to start shell: %w", err)
}
// 4. 等待会话结束
return session.Wait()
}⚠️ 重要注意事项:
- 必须调用 session.RequestPty():否则远程 shell 不会分配 TTY,导致 Ctrl+C、ls 颜色、行编辑等功能失效;
- 避免 io.Copy(session.Stdin, wsReader) 直接使用:session.Stdin 是 io.Reader,不可写;应始终通过 session.StdinPipe() 获取 io.WriteCloser;
- 错误处理需精细:WebSocket 断连时应主动关闭 SSH session(调用 session.Close()),防止资源泄漏;
- 安全加固建议:生产环境务必使用密钥认证(ssh.PublicKeys)、限制连接超时、校验目标主机指纹(HostKeyCallback),禁用密码明文传输;
- 性能优化点:可引入 bufio.Reader/Writer 缓冲、设置 ws.SetReadLimit() 防攻击、对长输出做分块发送(避免单消息过大触发浏览器限制)。
总结而言,SSH over WebSocket 的本质是构建一个「字节流桥接器」:一端是 WebSocket 的 ReadMessage/WriteMessage,另一端是 ssh.Session 的 StdinPipe()/StdoutPipe()。只要理解 ssh.Session 的 I/O 接口设计意图,并正确处理 TTY 协商与并发流控制,即可稳定支撑高交互性 Web 终端场景。










