Search code examples
gonetwork-programmingssh

go-ssh: How to gracefully close the connection in a simple server


I don't ask a lot of questions on Stack Overflow, but this is one of these times where I'm miserably stuck.

TLDR I need to know how channel.Close() of golang.org/x/crypto/ssh works and where I am supposed to call it (assuming that's what I need).

I'm writing a 'small' program to automatically synchronize my Git repositories, including stash, extra files, etc. I use go-ssh to handle the most important part on the server. The current state of the project can be found on GitHub

I tried to narrow it down to a simple example. This is one of the few things I adjusted from the official example in the Golang repository. I got rid of the terminal references and made it explicitly handle a (random) command:

go func(in <-chan *ssh.Request) {
    for req := range in {
        switch req.Type {
        case "exec":
            channel.Stderr().Write([]byte("Request received!\n"))
            req.Reply(true, nil)
            // Do some more work here
            channel.Close() // ???
        default:
            req.Reply(false, nil)
        }
    }
    wg.Done()
}(requests)

The Gist is over here. There's also another example from someone else over here.

Whenever I try to connect with something like ssh localhost -T -vvvv -p 2022 git-receive-pack foo to the example, I get the following:

Authenticated to localhost ([::1]:2022) using "publickey".
debug1: channel 0: new session [client-session] (inactive timeout: 0)
debug3: ssh_session2_open: channel_new: 0
debug2: channel 0: send open
debug3: send packet: type 90
debug1: Entering interactive session.
debug1: pledge: filesystem
debug3: client_repledge: enter
debug3: receive packet: type 91
debug2: channel_input_open_confirmation: channel 0: callback start
debug2: fd 3 setting TCP_NODELAY
debug3: set_sock_tos: set socket 3 IPV6_TCLASS 0x20
debug2: client_session2_setup: id 0
debug1: Sending command: git-receive-pack foo
debug2: channel 0: request exec confirm 1
debug3: send packet: type 98
debug3: client_repledge: enter
debug2: channel_input_open_confirmation: channel 0: callback done
debug2: channel 0: open confirm rwindow 2097152 rmax 32768
debug2: channel 0: rcvd ext data 6
debug3: receive packet: type 99
debug2: channel_input_status_confirm: type 99 id 0
debug2: exec request accepted on channel 0
debug3: receive packet: type 97
debug2: channel 0: rcvd close
debug2: channel 0: output open -> drain
debug2: chan_shutdown_read: channel 0: (i0 o1 sock -1 wfd 4 efd 6 [write])
debug2: channel 0: input open -> closed
debug3: channel 0: will not send data after close
debug2: channel 0: obuf_empty delayed efd 6/(6)
Request received!
debug2: channel 0: written 6 to efd 6
debug3: channel 0: will not send data after close
debug2: channel 0: obuf empty
debug2: chan_shutdown_write: channel 0: (i3 o1 sock -1 wfd 5 efd 6 [write])
debug2: channel 0: output drain -> closed
debug2: channel 0: almost dead
debug2: channel 0: gc: notify user
debug2: channel 0: gc: user detached
debug2: channel 0: send_close2
debug2: channel 0: send close for remote id 0
debug3: send packet: type 97
debug2: channel 0: is dead
debug2: channel 0: garbage collecting
debug1: channel 0: free: client-session, nchannels 1
debug3: channel 0: status: The following connections are open:
  #0 client-session (t4 [session] r0 nm0 i3/0 o3/0 e[write]/0 fd -1/-1/6 sock -1 cc -1 nc0 io 0x00/0x00)

debug3: send packet: type 1
Transferred: sent 3624, received 2892 bytes, in 0.0 seconds
Bytes per second: sent 3203530.2, received 2556459.6
debug1: Exit status -1

On my personal project, a 'broken pipe' gets thrown in as well.

Does anyone know how to solve this issue?


Solution

  • From your question, it sounds like maybe you're asking about this bit at the end of the debug output:

    debug1: Exit status -1
    

    This isn't an "error" in the sense that your code is behaving as written. An exit status of -1 is the default behavior of go-ssh if you don't provide an explicit exit code. If you want to return an exit code other than -1 to the client, you can send an exit-status request back to the client, like this:

    go func(in <-chan *ssh.Request) {
        for req := range in {
            switch req.Type {
            case "exec":
                channel.Stderr().Write([]byte("Request received!\n"))
                req.Reply(true, nil)
                // Do some more work here
                status := 0
                channel.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{uint32(status)}))
                channel.Close() // ???
            default:
                req.Reply(false, nil)
            }
        }
        wg.Done()
    }(requests)
    

    With this change in place, the debug output looks like:

    .
    .
    .
    debug3: send packet: type 1
    Transferred: sent 2100, received 2232 bytes, in 0.0 seconds
    Bytes per second: sent 3457951.3, received 3675308.2
    debug1: Exit status 0