GO SSH 服务器 - 如何将 stdout/stderr 写回 ssh 客户端?

问题描述 投票:0回答:1

我正在编写一个极其准系统的 SSH 服务器(以补充我的 go SSH 客户端),我几乎完成了,除了我碰壁了。

运行命令后,客户端发送其请求负载并获取 StdoutPipe/StderrPipe,似乎将它们写入通道不会将其发送回客户端(客户端只是挂起等待其 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
}

因此,我准备好两个管道,为每个 stdout/stderr 启动一个 io.copy go 例程,将管道内容写入通道,然后运行命令(我在那里有 waitgroup 以确保输出被写入通道关闭之前的通道)。

我将打印放入其中,看看是否可能没有发生写入,但输出显示命令 stdout 存在于通道中,但客户端仍挂在其 session.Run 上。

这是服务器端的测试运行:

DEBUG: Accepted new channel (type=session)
 DEBUG: Received Request (type=exec)
 DEBUG: Sending (reply) confirmation after command request
  DEBUG: Running Command: /usr/bin/sha256sum /etc/rsyslog.conf
  DEBUG: Channel Contents post command (string): &{session  %!s(uint32=0) %!s(uint32=0) %!s(uint32=32768) %!s(uint32=32768) %!s(*ssh.mux=&{0xc0000ba9c0 {{0 0} [0xc000122240] 0} 0xc000186000 {0 0} 0xc000186070 0xc0001860e0 0xc000070080 <nil>}) %!s(bool=true) %!s(ssh.channelDirection=0) %!s(chan interface {}=0xc00011e3f0) {%!s(int32=0) %!s(uint32=0)} %!s(chan *ssh.Request=0xc00011e380) %!s(bool=false) {%!s(*sync.Cond=&{{} 0xc000096d30 {0 0 0 <nil> <nil>} 824634410864}) %!s(uint32=2097068) %!s(int=0) %!s(bool=false)} %!s(*ssh.buffer=&{0xc0000a8780 0xc0000b4ba0 0xc0000b4ba0 true}) %!s(*ssh.buffer=&{0xc0000a87c0 0xc0000b4be0 0xc0000b4be0 true}) {%!s(int32=0) %!s(uint32=0)} %!s(uint32=2097152) %!s(uint32=0) {%!s(int32=0) %!s(uint32=0)} %!s(bool=false) map[%!s(uint32=0):^Tf55121cb4505fb91348d412abeceb8845f876b784628f03843f0c3213ca22766  /etc/rsyslog.conf
]}
  DEBUG: Channel bytes: &{session [] 0 0 32768 32768 0xc000186150 true 0 0xc00011e3f0 {0 0} 0xc00011e380 false {0xc0000a8740 2097068 0 false} 0xc0000b4bc0 0xc0000b4c00 {0 0} 2097152 0 {0 0} false map[0:[94 0 0 0 0 0 0 0 84 102 53 53 49 50 49 99 98 52 53 48 53 102 98 57 49 51 52 56 100 52 49 50 97 98 101 99 101 98 56 56 52 53 102 56 55 54 98 55 56 52 54 50 56 102 48 51 56 52 51 102 48 99 51 50 49 51 99 97 50 50 55 54 54 32 32 47 101 116 99 47 114 115 121 115 108 111 103 46 99 111 110 102 10]]}
 DEBUG: Sending (reply) confirmation after command execution
 DEBUG: Finished Request (type=exec)

客户端的调试点击

DEBUG: Run command: sha256sum /etc/rsyslog.conf
然后挂在那里。

我假设写入通道会将数据返回给客户端,但现在我确信我在这里遗漏了一些东西。

我确实认为这里可能会使用 ssh Reply 函数,因为它需要一个有效负载。但文档说:

The payload argument is ignored for replies to channel-specific requests
。我从客户端收到的请求牢牢地在这个单一通道中(对于这个循环迭代),所以我相当确定我不能使用 Reply。

这让我回到了在频道中写作的话题。我不确定为什么内容(在频道中清楚地存在)没有被发送回客户端?

go ssh channel
1个回答
0
投票

在您发布的代码中:没有一个参与者(客户端或服务器)调用

.Close()
,并且都运行等待另一端完成的操作 - io.ReadAll 在客户端,
for req := range requests {
在服务器端.


两侧其中一侧应表明已完成。

在客户端启动会话并尝试运行 1 或 2 个或

n
命令的情况下,我会研究客户端如何无法检测到命令已完成,并在完成后关闭会话这是命令列表。

© www.soinside.com 2019 - 2024. All rights reserved.