Go SSH Server:如何将stdout/stderr返回给SSH客户端?
问题:自研Go SSH服务器命令执行后客户端挂起
我正在开发一个配合自研Go SSH客户端的极简SSH服务器,接近完成时遇到瓶颈:执行客户端请求中的命令后,通过StdoutPipe/StderrPipe获取输出并写入channel,但数据无法返回给客户端,客户端卡在session.Run处等待。已确认客户端功能正常(与标准OpenSSH服务器交互时所有命令均可成功执行),因此问题出在服务器代码中。
原服务器通道处理代码(精简版)
func handleChannel(newChannel ssh.NewChannel) { // Error out channels other than 'session' if newChannel.ChannelType() != "session" { logError("SSH channel error", fmt.Errorf("unauthorized channel type requested: %s", newChannel.ChannelType()), false) return } // Accept the channel channel, requests, err := newChannel.Accept() if err != nil { logError("SSH channel error", fmt.Errorf("could not accept channel: %v", err), false) return } defer channel.Close() fmt.Printf("DEBUG: Accepted new channel (type=%s)\n", newChannel.ChannelType()) // Loop client requests - Only allow SFTP or Exec for req := range requests { fmt.Printf(" DEBUG: Received Request (type=%s)\n", req.Type) switch req.Type { case "exec": command, err := StripPayloadHeader(req.Payload) if err != nil { logError("SSH request error", fmt.Errorf("exec: failed to strip request payload header: %v", err), false) continue } if req.WantReply { fmt.Printf(" DEBUG: Sending (reply) confirmation after command request\n") req.Reply(true, nil) } err = executeCommand(channel, command) if err != nil { logError("SSH request error", fmt.Errorf("failed command execution: %v", err), false) continue } fmt.Printf(" DEBUG: Sending (reply) confirmation after command execution\n") req.Reply(true, nil) case "subsystem": subsystem, err := StripPayloadHeader(req.Payload) if err != nil { logError("SSH request error", fmt.Errorf("subsystem: failed to strip request payload header: %v", err), false) continue } if subsystem != "sftp" { req.Reply(false, nil) logError("SSH request error", fmt.Errorf("received unauthorized subsystem %s", subsystem), false) continue } if req.WantReply { fmt.Printf(" DEBUG: Sending (reply) confirmation after sftp request\n") req.Reply(true, nil) } fmt.Printf(" DEBUG: Starting SFTP server\n") err = HandleSFTP(channel) fmt.Printf(" DEBUG: Finished SFTP server\n") if err != nil { logError("SSH request error", fmt.Errorf("failed sftp: %v", err), false) continue } fmt.Printf(" DEBUG: Sending (reply) confirmation after sftp completion\n") req.Reply(true, nil) default: req.Reply(false, nil) // Reject unknown requests } fmt.Printf(" DEBUG: Finished Request (type=%s)\n", req.Type) } // Close the session fmt.Printf("Closing channel\n") channel.Close() } func executeCommand(channel ssh.Channel, receivedCommand string) error { // Parse command for exe and args args := strings.Fields(receivedCommand) // Prep command and args for execution cmd := exec.Command(args[0], args[1:]...) // Pipes for receiving cmd outs stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } // Writer for cmd stdout/stderr into channel var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() io.Copy(channel.Stderr(), stderr) }() go func() { defer wg.Done() io.Copy(channel, stdout) }() // Run command and wait for output fmt.Printf(" DEBUG: Running Command: %s\n", cmd) err = cmd.Run() if err != nil { return err } wg.Wait() fmt.Printf(" DEBUG: Channel Contents post command (string): %s\n DEBUG: Channel bytes: %v\n", channel, channel) return nil }
原客户端命令执行代码片段
func RunSSHCommand(client *ssh.Client, command string) (string, error) { // Open new session session, err := client.NewSession() if err != nil { return "", fmt.Errorf("failed to create session: %v", err) } defer session.Close() // Command output stdout, err := session.StdoutPipe() if err != nil { return "", fmt.Errorf("failed to get stdout pipe: %v", err) } // Command Error stderr, err := session.StderrPipe() if err != nil { return "", fmt.Errorf("failed to get stderr pipe: %v", err) } // Run the command fmt.Printf("DEBUG: Run command: %s\n", command) if err := session.Run(command); err != nil { return "", fmt.Errorf("failed to run command: %v", err) } fmt.Printf("DEBUG: Reading from stdout...\n") CommandOutput, err := io.ReadAll(stdout) fmt.Printf("DEBUG: Stdout from Channel: %s\n", stdout) fmt.Printf("DEBUG: Io ReadAll from stdout: %s\n", CommandOutput) if err != nil { return "", fmt.Errorf("error reading from io.Reader: %v", err) } CommandError, err := io.ReadAll(stderr) fmt.Printf("DEBUG: Command Err %s\n", CommandError) if err != nil { return "", fmt.Errorf("error reading from io.Reader: %v", err) } // Only return the error if there is one if string(CommandError) != "" { return string(CommandOutput), fmt.Errorf("%v", string(CommandError)) } return string(CommandOutput), nil }
我通过goroutine+io.Copy将stdout/stderr写入channel,并用WaitGroup确保输出写入后再关闭通道,但调试显示channel已有输出,客户端仍挂起。曾考虑使用ssh Reply函数,但文档说明“通道特定请求的回复会忽略payload”,无法采用该方式。
基于@LeGEC修复方案更新后的服务器代码片段
func handleChannel(newChannel ssh.NewChannel) { // Accept the channel channel, requests, err := newChannel.Accept() if err != nil { logError("SSH channel error", fmt.Errorf("could not accept channel: %v", err), false) return } defer channel.Close() // Loop client requests - Only allow SFTP or Exec for req := range requests { switch req.Type { case "exec": command, err := StripPayloadHeader(req.Payload) if err != nil { logError("SSH request error", fmt.Errorf("exec: failed to strip request payload header: %v", err), false) break } if req.WantReply { req.Reply(true, nil) } err = executeCommand(channel, command) if err != nil { logError("SSH request error", fmt.Errorf("failed command execution: %v", err), false) break } case "subsystem": subsystem, err := StripPayloadHeader(req.Payload) if err != nil { logError("SSH request error", fmt.Errorf("subsystem: failed to strip request payload header: %v", err), false) break } if subsystem != "sftp" { req.Reply(false, nil) logError("SSH request error", fmt.Errorf("received unauthorized subsystem %s", subsystem), false) break } if req.WantReply { req.Reply(true, nil) } // Handle SFTP err = HandleSFTP(channel) if err != nil { logError("SSH request error", fmt.Errorf("failed sftp: %v", err), false) break } default: req.Reply(false, nil) // Reject unknown requests } channel.Close() } } func executeCommand(channel ssh.Channel, receivedCommand string) error { // Parse command for exe and args commandArray := strings.Fields(receivedCommand) commandBinary := commandArray[0] // Prep command and args for execution cmd := exec.Command(commandBinary, commandArray[1:]...) // Init command output buffers var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Run the command and set exit code err := cmd.Run() // Determine exit code to send back var exitCode int if err != nil { if exitError, ok := err.(*exec.ExitError); ok { // Command failed with a non-zero exit code exitCode = exitError.ExitCode() } else { if strings.Contains(err.Error(), "executable file not found in ") { exitCode = 127 // Command not found stderr.WriteString(err.Error()) } else { exitCode = 126 // Command exists but cannot execute stderr.WriteString("Command exists but cannot execute\n") } } } else { exitCode = 0 // Command executed successfully } // Send command output back through channel io.Copy(channel, &stdout) io.Copy(channel.Stderr(), &stderr) // Send exit status back through channel exitStatus := make([]byte, 4) binary.BigEndian.PutUint32(exitStatus, uint32(exitCode)) channel.SendRequest("exit-status", false, exitStatus) // Return any errors if err != nil { return err } return nil }
客户端代码未改动。
内容的提问来源于stack exchange,提问作者bdrun33
相关产品推荐
相关产品推荐

