go: x/crypto/ssh: Signal method doesn't work

Starting a process over SSH using the /x/crypto/ssh library and then trying to send a signal to it, seems to have no effect. I’ve found a few musings on the internet about users with the same problem.

I’ve attached a full reproducer, including the crappy workaround.

package main

import (
    "bytes"
    "fmt"
    "golang.org/x/crypto/ssh"
    "time"
)

func main() {

    // An SSH client is represented with a ClientConn.
    //
    // To authenticate with the remote server you must pass at least one
    // implementation of AuthMethod via the Auth field in ClientConfig.
    config := &ssh.ClientConfig{
        User: "someuser", // XXX
        Auth: []ssh.AuthMethod{
            ssh.Password("somepass"), // XXX
        },
    }
    client, err := ssh.Dial("tcp", "localhost:22", config)
    if err != nil {
        panic("Failed to dial: " + err.Error())
    }

    // Each ClientConn can support multiple interactive sessions,
    // represented by a Session.
    session, err := client.NewSession()
    if err != nil {
        panic("Failed to create session: " + err.Error())
    }
    defer session.Close()

    // Once a Session is created, you can execute a single command on
    // the remote side using the Run method.
    var b bytes.Buffer
    session.Stdout = &b

    go func() {
        time.Sleep(2 * time.Second)
        // XXX none of these signals work! :(
        //      fmt.Println("Running signal!")
        //      session.Signal(ssh.SIGINT)
        //      session.Signal(ssh.SIGKILL)
        //      session.Signal(ssh.SIGQUIT)
        //      session.Signal(ssh.SIGHUP)
        //      session.Signal(ssh.SIGPIPE)
        //      fmt.Println("Done running signal!")

        s2, err := client.NewSession()
        if err != nil {
            panic("Failed to create session: " + err.Error())
        }
        defer s2.Close()
        var c bytes.Buffer
        s2.Stdout = &c
        if err := s2.Run("echo start && pidof sleep && killall sleep"); err != nil {
            fmt.Println("s2 error!")
        }
        fmt.Println("Sig run done!")
        fmt.Println(c.String())

    }()

    if err := session.Run("echo $0 && /usr/bin/sleep 10s"); err != nil {

        if e, ok := err.(*ssh.ExitError); ok {
            fmt.Printf("Remote: Exit msg: %s", e.Waitmsg.Msg()) // XXX (does this ever return anything useful?)
            fmt.Printf("Remote: Exit signal: %s", e.Waitmsg.Signal())
            fmt.Printf("Remote: Error: Output...\n%s", b.String())
            fmt.Printf("Exited (%d) with: %s", e.Waitmsg.ExitStatus(), e.Error())

        } else if e, ok := err.(*ssh.ExitMissingError); ok {
            fmt.Printf("Exit code missing: %s", e.Error())
        }

        panic("Failed to run: " + err.Error())
    }
    fmt.Println("Done!")
    fmt.Println(b.String())
}

Uncomment the signal you want, and you’ll see that they all do nothing to kill the running sleep command.

Tested with go version: 1.5.4 (but I have no reason to expect this is fixed with newer versions!) on Fedora GNU/Linux 24 as the SSH server, running OpenSSH.

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 1
  • Comments: 17 (6 by maintainers)

Most upvoted comments

I was able to verify that this issue is resolved in OpenSSH 1.7.9 by running the following code against two versions of sshd on Ubuntu 18.04 LTS (bionic).

package main

import (
	"io"
	"log"
	"os"
	"time"

	"golang.org/x/crypto/ssh"
)

func main() {
	// Create client config
	config := &ssh.ClientConfig{
		User: "XXXX",
		Auth: []ssh.AuthMethod{
			ssh.Password("XXXX"),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}
	// Connect to ssh server
	conn, err := ssh.Dial("tcp", "localhost:2222", config)
	if err != nil {
		log.Fatal("unable to connect: ", err)
	}
	defer conn.Close()
	// Create a session
	session, err := conn.NewSession()
	if err != nil {
		log.Fatal("unable to create session: ", err)
	}
	defer session.Close()

	stdout, err := session.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	stderr, err := session.StderrPipe()
	if err != nil {
		log.Fatal(err)
	}

	command := `/bin/bash -c 'catch_interrupt() { echo \"caught SIGINT!\"; sleep 2; } ;
    trap catch_interrupt INT && ssh -V && sleep 5 && echo "no signal caught"'`

	if err := session.Start(command); err != nil {
		log.Fatal(err)
	}

	go func() {
		io.Copy(os.Stderr, stderr)
	}()

	go func() {
		io.Copy(os.Stdout, stdout)
	}()

	go func() {
		time.Sleep(2 * time.Second)
		log.Println("sending signal: ")
		if err := session.Signal(ssh.SIGINT); err != nil {
			log.Fatal(err)
		}
		log.Println("signal sent")
	}()

	if err := session.Wait(); err != nil {
		log.Println(err)
	}
}

Running against Ubuntu Bionic (18.04 LTS) openssh-server 1.7.6

go run main.go
OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n  7 Dec 2017
2019/10/30 14:25:16 sending signal:
2019/10/30 14:25:16 signal sent
no signal caught

Again with pinned Ubuntu Disco(19.04) package openssh-server 1.7.9

go run main.go
OpenSSH_7.9p1 Ubuntu-10, OpenSSL 1.1.1  11 Sep 2018
2019/10/30 14:29:20 sending signal:
2019/10/30 14:29:20 signal sent
"caught SIGINT!"
2019/10/30 14:29:22 Process exited with status 130 from signal INT

As I’ve discovered elsewhere, sending 0x03 (or anything via Signal) only seems to work if you’ve requested that a pseudo TTY be allocated.

I have asked about the open ticket a couple times in the last few months and never got a reply.

https://marc.info/?l=openssh-unix-dev&m=151872213119286&w=2 https://marc.info/?l=openssh-unix-dev&m=152104163506271&w=2

I have asked about the open ticket a couple times in the last few months and never got a reply.

It looks like the fix was finally integrated into openssh version 7.9:

https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/session.c.diff?r1=1.305&r2=1.306&f=h

though I haven’t verified yet that it works.

God I though that was my code’s problem… This lib is awesome, hope this problem can be fixed soon. thank you. I guess i have to hack this problem for now.

I’m hitting this too. I’d even be happy with ssh client workarounds. I’m using to go crypto/ssh package to run interactive sessions and am facing problems with signal handling not propagating and other terminal related hell like getting vi to work on the server side session.