Go 语言中通过 WebSocket 实现 SSH 会话透传的完整实践指南

1次阅读

Go 语言中通过 WebSocket 实现 SSH 会话透传的完整实践指南

本文详解如何在 go 中结合 golang.org/x/crypto/sshwebsocket,安全、双向地透传交互式 SSH shell 会话,避免误用管道(StdinPipe/StdoutPipe),正确利用 session.Stdin/Stdout 接口实现字节流级实时转发。

本文详解如何在 go 中结合 `golang.org/x/crypto/ssh` 与 websocket,安全、双向地透传交互式 ssh shell 会话,避免误用管道(stdinpipe/stdoutpipe),正确利用 `session.stdin`/`stdout` 接口实现字节流级实时转发。

在构建 Web 终端(如基于浏览器的 SSH 客户端)时,一个常见误区是试图将 session.StdoutPipe() 和 session.StdinPipe() 类比为普通 TCP 连接中的 io.Reader/io.Writer,进而手动拼接缓冲区并发送 WebSocket 消息。但 StdinPipe() 返回的是只读通道(用于读取远程命令输出),而 StdoutPipe() 是只写通道(用于向远程进程注入输入)——这与 session.Stdin/session.Stdout 的语义恰好相反,且不支持并发读写,极易导致死锁或数据丢失。

正确做法是:直接将 session.Stdin 设为从 WebSocket 接收数据的 reader(如 io.MultiReader 或自定义 reader),并将 session.Stdout 和 session.Stderr 指向能实时推送至 WebSocket 的 writer(如带缓冲的 io.WriteCloser 封装)。整个 SSH 会话应以 交互式 shell 模式 启动,并启用伪终端(PTY),确保远程 Shell 正确响应控制字符(如 Ctrl+C、Tab 补全、ANSI 转义序列)。

以下是一个生产就绪的核心转发逻辑示例:

func handleSSHOverWS(ws *websocket.Conn, sshConfig *ssh.ClientConfig, host string) error {     // 1. 建立 SSH 连接     client, err := ssh.Dial("tcp", host, sshConfig)     if err != nil {         return fmt.Errorf("SSH dial failed: %w", err)     }     defer client.Close()      // 2. 创建交互式 Session 并请求 PTY     session, err := client.NewSession()     if err != nil {         return fmt.Errorf("failed to create session: %w", err)     }     defer session.Close()      // 请求伪终端(关键!否则无法运行 /bin/bash 等交互式 shell)     modes := ssh.TerminalModes{         ssh.ECHO:          0,     // disable local echo (handled by browser)         ssh.TTY_OP_ISPEED: 14400, // input speed         ssh.TTY_OP_OSPEED: 14400, // output speed     }     if err := session.RequestPty("xterm-256color", 80, 24, modes); err != nil {         return fmt.Errorf("failed to request pty: %w", err)     }      // 3. 启动交互式 shell(非单条命令)     if err := session.Shell(); err != nil {         return fmt.Errorf("failed to start shell: %w", err)     }      // 4. 双向流转发:WebSocket ↔ SSH Session     done := make(chan error, 2)      // WebSocket → SSH Stdin     go func() {         defer close(done)         buf := make([]byte, 4096)         for {             _, msg, err := ws.ReadMessage()             if err != nil {                 done <- fmt.Errorf("WS read error: %w", err)                 return             }             // 将 WebSocket 收到的字节(如键盘输入)写入 SSH stdin             if _, writeErr := session.Stdin.Write(msg); writeErr != nil {                 done <- fmt.Errorf("SSH stdin write error: %w", writeErr)                 return             }         }     }()      // SSH Stdout/Stderr → WebSocket     go func() {         defer close(done)         var wg sync.WaitGroup         wg.Add(2)          // 转发 stdout         go func() {             defer wg.Done()             io.copy(&wsWriter{ws: ws}, session.Stdout)         }()          // 转发 stderr(可合并到 stdout 或单独处理)         go func() {             defer wg.Done()             io.Copy(&wsWriter{ws: ws}, session.Stderr)         }()          wg.Wait()     }()      // 等待任一方向出错或连接关闭     select {     case err := <-done:         return err     case <-time.After(10 * time.Minute): // 可选超时保护         return errors.New("session timeout")     } }  // wsWriter 是一个适配器,将 io.Write 推送为 WebSocket BinaryMessage type wsWriter struct {     ws *websocket.Conn }  func (w *wsWriter) Write(p []byte) (n int, err error) {     if len(p) == 0 {         return 0, nil     }     if err = w.ws.WriteMessage(websocket.BinaryMessage, p); err != nil {         return 0, err     }     return len(p), nil }

⚠️ 关键注意事项

  • 必须启用 PTY:session.RequestPty() 是交互式 shell 正常工作的前提,否则 session.Shell() 可能静默失败或返回非交互行为;
  • 避免 io.Copy 直接套用 StdinPipe:StdinPipe() 返回的是供你 读取 远程输出的 reader,而非写入本地输入的 writer —— 这正是原问题的根本混淆点;
  • 错误处理需覆盖全链路:SSH 连接、Session 创建、PTY 请求、Shell 启动、WebSocket 读写均可能失败,建议统一使用 errgroup.Group 或 context.WithTimeout 增强可靠性;
  • 安全性增强建议:生产环境应使用 ssh.PublicKeys 替代密码认证;对 WebSocket 连接启用 JWT 鉴权;限制 SSH 目标主机白名单;对用户输入做基础过滤(如禁用 x00 空字节);
  • 性能优化:高频率小包(如按键事件)可启用 WebSocket 消息合并或添加简单缓冲(如 bufio.Writer 封装 wsWriter)。

总结而言,Go 中实现 WebSocket + SSH 透传的本质,是将 WebSocket 连接视为“网络层抽象”,而 ssh.Session 的 Stdin/Stdout/Stderr 字段则作为标准 I/O 接口桥接层——无需手动拆解字节流或模拟协议帧,只需专注流的双向绑定与错误传播,即可构建稳定、低延迟的 Web Terminal 基础设施。

text=ZqhQzanResources