I am trying to start an interactive SSH session with a remote computer using Golang. I was able to get that without any problems, but the pseudo terminal doesn't seem to be capturing all of the keyboard i/o correctly.
For example, if I run a regular SSH command like,
ssh -i ~/.ssh/some-key.pem username#1.1.1.1
I can exit with a simple Ctrl+d, but for some reason when I run the interactive shell started with Golang it's not working and only prints the actual key characters ^D. Same goes for trying to use the arrow keys. If I run a Ctrl+c it exits the original Golang process and kills the interactive shell rather than executing on the remote machine.
Below is my code for setting up the shell,
func StartInteractiveShell(sshConfig *ssh.ClientConfig, network string, host string, port string) error {
var (
session *ssh.Session
conn *ssh.Client
err error
)
if conn, err = getSshConnection(sshConfig, network, host, port); err != nil {
fmt.Printf("Failed to dial: %s", err)
return err
}
if session, err = getSshSession(conn); err != nil {
fmt.Printf("Failed to create session: %s", err)
return err
}
defer session.Close()
if err = setupPty(session); err != nil {
fmt.Printf("Failed to set up pseudo terminal: %s", err)
return err
}
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
if err = session.Shell(); err != nil {
fmt.Printf("Failed to start interactive shell: %s", err)
return err
}
return session.Wait()
}
func getSshConnection(config *ssh.ClientConfig, network string, host string, port string) (*ssh.Client, error) {
addr := host + ":" + port
return ssh.Dial(network, addr, config)
}
func getSshSession(clientConnection *ssh.Client) (*ssh.Session, error) {
return clientConnection.NewSession()
}
// pty = pseudo terminal
func setupPty(session *ssh.Session) error {
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
session.Close()
fmt.Printf("request for pseudo terminal failed: %s", err)
return err
}
return nil
}
Am I missing something there?
Related
I am working an application written in golang,for which one of the capability will be to SSH into a device and execute a shell script there. With my current implementation,a normal script is getting executed.The problem lies in the execution of a script that requires user input: for e.g the script asks the user, can we proceed with the installation: and the user has to type in Y or N. This is the part where it is failing. Here is my implementation:
func main() {
cmdOutput,err := ExecuteScriptSSH()
if err != nil{
fmt.Printf("Error is %s",err)
}
fmt.Printf("Command Output is %s", cmdOutput)
}
func ExecuteScriptSSH() (string, error) {
script := "#!/bin/sh\n\n ls -l \n\n date"
params := make(map[string]interface{})
params["Username"] = "uname"
params["Password"] = "pwd"
params["IPAddress"] = "ip"
params["Port"] = "22"
connection := NewConnection(params)
client, err := connection.ConnectNonSecure()
if err != nil {
fmt.Printf("unable to Connect:%v ", err)
}
defer client.Close()
ss, err := client.NewSession()
if err != nil {
fmt.Printf("unable to create SSH session:%v ", err)
}
//Converting the Script to string to a shell script file by writing to it.
d1 := []byte(script)
err = os.WriteFile("/tmp/script.sh", d1, 0777)
if err != nil {
fmt.Printf("Error constructing Script file %v", err)
}
// opening the script file
scriptFile, err2 := os.OpenFile("/tmp/script.sh", os.O_RDWR|os.O_CREATE, 0777)
if err2 != nil {
fmt.Printf("Error opening Script file is %s", err)
}
cmdOutput := &bytes.Buffer{}
ss.Stdout = cmdOutput
ss.Stdin = scriptFile
interpreter := "sh"
err = ss.Run(interpreter)
if err != nil {
fmt.Printf("Error in Executing Script file %v", err.Error())
}
return cmdOutput.String(), err
}
type Params struct {
Username string
Password string
IPAddress string
Port string
}
func NewConnection(data map[string]interface{}) *Params {
var params Params
fmt.Printf("Data is %v",data)
err := mapstructure.Decode(data,¶ms)
if err != nil {
panic(err)
}
fmt.Printf("Mapped Connection details %v",params)
return ¶ms
}
func (p *Params) ConnectNonSecure() (*ssh.Client, error){
config := &ssh.ClientConfig{
User: p.Username,
Auth: []ssh.AuthMethod{
ssh.Password(p.Password),
},
// Non-Production-only
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
fmt.Printf("Connection details %v",p)
// Connect to the remote server and perform a handshake
client, err := ssh.Dial("tcp", p.IPAddress+":"+p.Port, config)
if err != nil {
log.Fatalf("unable to connect to: %v %s", err,p.IPAddress)
}
return client, err
}
for simplicity sake I have added a very simple script,but actually the script is huge and requires user confirmation.Any idea on how to solve this issue?
I'm currently writing an app that given a parameter, will run a command on a remote server. I'm using the /x/crypto/ssh package, everything seems to go smoothly if I use one liner commands like "who" or "ls", however, if I run a more complex command such as:
"grep SOMEDATA /var/log/logfile.log"
the program immediately exits and the command execution line with nothing more than "process exited with status 1", and I don't get anything else back.
If I check the history of the user I'm having it SSH into the remote system as, I do not see the command running at all.
Has anyone else run into this type of issue before? Here's a snippet of the code I'm using to execute this (sensitive data removed of course):
func returnData(w http.ResponseWriter, r *http.Request) {
var b bytes.Buffer
hostKey, err := getHostKey("SERVERNAME")
if err != nil {
log.Fatalln(err)
}
err = r.ParseForm()
if err != nil {
log.Fatalln(err)
}
config := &ssh.ClientConfig{
User: "USERNAME",
Auth: []ssh.AuthMethod{
ssh.Password("TESTPASS"),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
}
client, err := ssh.Dial("tcp", "SERVERNAME:22", config)
if err != nil {
log.Fatalln("Creating Client Failed: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatalln("Creating new Session Failed: ", err)
}
session.Stdout = &b
inputData := r.Form["fname"][0]
cmdExecute := fmt.Sprintf(`sudo grep %v /var/log/logfile.log`, inputData)
log.Println(cmdExecute)
if err := session.Run(cmdExecute); err != nil {
log.Fatalln("Getting Data From session Failed: ", err)
log.Fatalln(b.String())
}
//log.Println(hostKey)
defer session.Close()
I have a simple scp function that is just a wrapper over the scp cli tool.
type credential struct {
username string
password string
host string
port string
}
func scpFile(filepath, destpath string, c *credential) error {
cmd := exec.Command("scp", filepath, c.username+"#"+c.host+":"+destpath)
if err := cmd.Run(); err != nil {
return err
}
fmt.Println("done")
return nil
}
This works just fine now I want to add the capability of putting in a password the SSH if scp needs it. This is what I came up with
func scpFile(filepath, destpath string, c *credential) error {
cmd := exec.Command("scp", filepath, c.username+"#"+c.host+":"+destpath)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
defer stdin.Close()
if err := cmd.Start(); err != nil {
return err
}
io.WriteString(stdin, c.password+"\n")
cmd.Wait()
fmt.Println("done")
return nil
}
This does not work as the password prompt just hangs there. I tried adding a 1 second sleep before I re write to stdin thinking maybe I was writing the password to fast but did not make a difference.
So I was able to find a work around by instead of trying to send the password to stdin I create a ssh session and scp a file through the ssh session. Here is the new scpFile function:
func scpFile(filePath, destinationPath string, session *ssh.Session) error {
defer session.Close()
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
s, err := f.Stat()
if err != nil {
return err
}
go func() {
w, _ := session.StdinPipe()
defer w.Close()
fmt.Fprintf(w, "C%#o %d %s\n", s.Mode().Perm(), s.Size(), path.Base(filePath))
io.Copy(w, f)
fmt.Fprint(w, "\x00")
}()
cmd := fmt.Sprintf("scp -t %s", destinationPath)
if err := session.Run(cmd); err != nil {
return err
}
return nil
}
This could probably be made better but the main idea is there
How do I execute a command on a remote machine in a golang CLI? I need to write a golang CLI that can SSH into a remote machine via a key and execute a shell command. Furthermore, I need to be able to do this one hop away. e.g. SSH into a machine (like a cloud bastion) and then SSH into another, internal, machine and execute a shell command.
I haven't (yet) found any examples for this.
You can run commands on a remote machine over SSH using the "golang.org/x/crypto/ssh" package.
Here is an example function demonstrating simple usage of running a single command on a remote machine and returning the output:
//e.g. output, err := remoteRun("root", "MY_IP", "PRIVATE_KEY", "ls")
func remoteRun(user string, addr string, privateKey string, cmd string) (string, error) {
// privateKey could be read from a file, or retrieved from another storage
// source, such as the Secret Service / GNOME Keyring
key, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return "", err
}
// Authentication
config := &ssh.ClientConfig{
User: user,
// https://github.com/golang/go/issues/19767
// as clientConfig is non-permissive by default
// you can set ssh.InsercureIgnoreHostKey to allow any host
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
//alternatively, you could use a password
/*
Auth: []ssh.AuthMethod{
ssh.Password("PASSWORD"),
},
*/
}
// Connect
client, err := ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
if err != nil {
return "", err
}
// Create a session. It is one session per command.
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var b bytes.Buffer // import "bytes"
session.Stdout = &b // get output
// you can also pass what gets input to the stdin, allowing you to pipe
// content from client to server
// session.Stdin = bytes.NewBufferString("My input")
// Finally, run the command
err = session.Run(cmd)
return b.String(), err
}
Try with os/exec https://golang.org/pkg/os/exec/ to execute a ssh
package main
import (
"bytes"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ssh", "remote-machine", "bash-command")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
To jump over machines use the ProxyCommand directive in a ssh config file.
Host remote_machine_name
ProxyCommand ssh -q bastion nc remote_machine_ip 22
The other solutions here will work, but I'll throw out another option you could try: simplessh. I think it is easier to use. For this question, I would use option 3 below where you can ssh using your key.
Option 1: SSH to a machine with a password, then run a command
import (
"log"
"github.com/sfreiberg/simplessh"
)
func main() error {
var client *simplessh.Client
var err error
if client, err = simplessh.ConnectWithPassword("hostname_to_ssh_to", "username", "password"); err != nil {
return err
}
defer client.Close()
// Now run the commands on the remote machine:
if _, err := client.Exec("cat /tmp/somefile"); err != nil {
log.Println(err)
}
return nil
}
Option 2: SSH to a machine using a set of possible passwords, then run a command
import (
"log"
"github.com/sfreiberg/simplessh"
)
type access struct {
login string
password string
}
var loginAccess []access
func init() {
// Initialize all password to try
loginAccess = append(loginAccess, access{"root", "rootpassword1"})
loginAccess = append(loginAccess, access{"someuser", "newpassword"})
}
func main() error {
var client *simplessh.Client
var err error
// Try to connect with first password, then tried second else fails gracefully
for _, credentials := range loginAccess {
if client, err = simplessh.ConnectWithPassword("hostname_to_ssh_to", credentials.login, credentials.password); err == nil {
break
}
}
if err != nil {
return err
}
defer client.Close()
// Now run the commands on the remote machine:
if _, err := client.Exec("cat /tmp/somefile"); err != nil {
log.Println(err)
}
return nil
}
Option 3: SSH to a machine using your key
import (
"log"
"github.com/sfreiberg/simplessh"
)
func SshAndRunCommand() error {
var client *simplessh.Client
var err error
// Option A: Using a specific private key path:
//if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to", "username", "/home/user/.ssh/id_rsa"); err != nil {
// Option B: Using your default private key at $HOME/.ssh/id_rsa:
//if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to", "username"); err != nil {
// Option C: Use the current user to ssh and the default private key file:
if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to"); err != nil {
return err
}
defer client.Close()
// Now run the commands on the remote machine:
if _, err := client.Exec("cat /tmp/somefile"); err != nil {
log.Println(err)
}
return nil
}
golang SSH executes shell command with timeout option
import (
"bytes"
"context"
"errors"
"fmt"
"golang.org/x/crypto/ssh"
"time"
)
func SshRemoteRunCommandWithTimeout(sshClient *ssh.Client, command string, timeout time.Duration) (string, error) {
if timeout < 1 {
return "", errors.New("timeout must be valid")
}
session, err := sshClient.NewSession()
if err != nil {
return "", err
}
defer session.Close()
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
resChan := make(chan string, 1)
errChan := make(chan error, 1)
go func() {
// run shell script
if output, err := session.CombinedOutput(command); err != nil {
errChan <- err
} else {
resChan <- string(output)
}
}()
select {
case err := <-errChan:
return "", err
case ms := <-resChan:
return ms, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
Try the package https://github.com/appleboy/easyssh-proxy
package main
import (
"fmt"
"time"
"github.com/appleboy/easyssh-proxy"
)
func main() {
// Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{
User: "appleboy",
Server: "example.com",
// Optional key or Password without either we try to contact your agent SOCKET
//Password: "password",
// Paste your source content of private key
// Key: `-----BEGIN RSA PRIVATE KEY-----
// MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26
// 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==
// -----END RSA PRIVATE KEY-----
// `,
KeyPath: "/Users/username/.ssh/id_rsa",
Port: "22",
Timeout: 60 * time.Second,
}
// Call Run method with command you want to run on remote server.
stdout, stderr, done, err := ssh.Run("ls -al", 60*time.Second)
// Handle errors
if err != nil {
panic("Can't run remote command: " + err.Error())
} else {
fmt.Println("don is :", done, "stdout is :", stdout, "; stderr is :", stderr)
}
}
See more example.
I am trying to automate debugging of docker containers on coreos. I want to have a script that connects to a host via ssh and exectues nsenter. That would be very convenient to jump directly into a container from my OSX box without doing a lot of stuff manually. I know that entering containers that way can be nasty, but if things are getting tough I would like to use such a tool. So here is what I have so far in golang.
I am able to create a interactive shell. Here I have the problem that things like reverse searching bash history using ctrl+R breaks the session. That code is commented below, thus not executed.
However, I am also able to execute a single command, here nsenter, but I receive the error stdin: is not a tty and nothing more happens. I am interested to know why stdin in my programm is not a tty and how I can achieve this.
Thanks
package main
import (
"code.google.com/p/go.crypto/ssh"
"io/ioutil"
"log"
"os"
)
func privateKey() ssh.Signer {
buf, err := ioutil.ReadFile("./id_rsa")
if err != nil {
panic(err)
}
key, err := ssh.ParsePrivateKey(buf)
if err != nil {
panic(err)
}
return key
}
func main() {
privateKey := privateKey()
// Create client config
config := &ssh.ClientConfig{
User: "core",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey),
},
}
// Connect to ssh server
conn, err := ssh.Dial("tcp", "myhost.com:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Create a session
session, err := conn.NewSession()
if err != nil {
log.Fatalf("unable to create session: %s", err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin // How can session.Stdin be a tty?
//////////////////////////////////////////////////////////////////////
// Stuff for interactive shell
// Set up terminal modes
//modes := ssh.TerminalModes{
// ssh.ECHO: 1, // enable echoing
// ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
// ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
//}
// Request pseudo terminal
//if err := session.RequestPty("xterm-256color", 80, 40, modes); err != nil {
// log.Fatalf("request for pseudo terminal failed: %s", err)
//}
// Start remote shell
//if err := session.Shell(); err != nil {
// log.Fatalf("failed to start shell: %s", err)
//}
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// Stuff for executing remote command
// 2202 in my example is actually the pid of a running container
if err := session.Run("sudo nsenter --target 2202 --mount --uts --ipc --net --pid"); err != nil {
panic("Failed to run: " + err.Error())
}
//////////////////////////////////////////////////////////////////////
session.Wait()
}
Super cool, I got it working, but there is still a magic I cannot comprehend. However, I changed my code as followed. The basic change leading to the correct pty behaviour, was the usage of the package "code.google.com/p/go.crypto/ssh/terminal". Using its MakeRaw(fd) seems to lead to side effects that enable the correct pty behaviour. Also thanks to the fleet project where I found the working example https://github.com/coreos/fleet/blob/master/ssh/ssh.go.
// The following two lines makes the terminal work properly because of
// side-effects I don't understand.
fd := int(os.Stdin.Fd())
oldState, err := terminal.MakeRaw(fd)
if err != nil {
panic(err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
panic(err)
}
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 1, // enable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm-256color", termHeight, termWidth, modes); err != nil {
log.Fatalf("request for pseudo terminal failed: %s", err)
}
if err := session.Run("sudo nsenter --target 2202 --mount --uts --ipc --net --pid"); err != nil {
// if the session terminated normally, err should be ExitError; in that
// case, return nil error and actual exit status of command
if exitErr, ok := err.(*ssh.ExitError); ok {
fmt.Printf("exit code: %#v\n", exitErr.ExitStatus())
} else {
panic("Failed to run: " + err.Error())
}
}
session.Close()
terminal.Restore(fd, oldState)