ssh executing nsenter as remote command with interactive shell in golang to debug docker container - shell

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)

Related

Reading os.OpenFile in Golang while still being written?

I have code that is writing to a logfile while executing a system command. E.g.
logfile, err := os.OpenFile(THIS_LOG_FILE, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
cmd.Stderr = logfile
cmd.Stdout = logfile
go func() {
err := cmd.Run()
if err != nil {
// WANT TO LOG ERROR HERE
}
}()
At the "// WANT TO LOG" line, I'd like to output the content to the standard logger, in addition to the previously assigned logfile destination. Is there a way to capture this in memory? Or should I just write everything to an in-memory buffer and flush at the end?
To clarify, in capturing the output of the command in memory, I can parse it and take action in the running program (handling errors/etc). When I write to the log file, that information is lost.
My issue is that, theoretically, I could read that back in from the file I just wrote, but that seems wasteful (and prone to failure if the command failed).
If I understand correctly, you want to write the content of stdout/stderror to a file while executing a shell command.
Since stdout and stderror are implemented the ReadCloser interface, you can merge them by io.MultiReader and perform io.Copy from source to destination.
The following snippet implements the pipeline
package main
import (
"io"
"log"
"os"
"os/exec"
)
func main() {
// prepare the command
cmd := exec.Command("your-shell-command.sh")
// get the stdout and stderr stream
erc, err := cmd.StderrPipe()
if err != nil {
log.Fatalln("Failed to get stderr reader: ", err)
}
orc, err := cmd.StdoutPipe()
if err != nil {
log.Fatalln("Failed to get stdout reader: ", err)
}
// combine stdout and stderror ReadCloser
rc := io.MultiReader(erc, orc)
// Prepare the writer
f, err := os.Create("output.log")
if err != nil {
log.Fatalln("Failed to create file")
}
defer f.Close()
// Command.Start starts a new go routine
if err := cmd.Start(); err != nil {
log.Println("Failed to start the command")
}
// add the TeeReader.
var buf bytes.Buffer
tr := io.TeeReader(rc, &buf)
if _, err := io.Copy(f, tr); err != nil {
logger.Fatalf("Failed to stream to file: %s", err)
}
if err := cmd.Wait(); err != nil {
log.Println("Failed to wait the command to execute: ", err)
}
}

How to simulate a keypress when using ssh and interactive shell

Stuck on keypress
I'm trying to create a script that makes a back-up from a HP proCurve switch. For this I'm using the package golang.org/x/crypto/ssh.
Golang is not new to me and I have quite a bit of "Go" knowledge. But I get stuck after establishing the connection. The switch is asking me to press any key to continue but I don't know how to simulate a keypress. (see image below)
Current code
This is the code that I'm currently using:
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
"golang.org/x/crypto/ssh"
)
type password string
func main() {
host := "192.168.2.43:22"
user := "admin"
pass := "admin"
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(pass),
},
Config: ssh.Config{
KeyExchanges: []string{"diffie-hellman-group-exchange-sha1", "diffie-hellman-group1-sha1"},
},
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
}
conn, err := ssh.Dial("tcp", host, config)
if err != nil {
panic("Failed to dial: " + err.Error())
}
defer conn.Close()
// Each ClientConn can support multiple interactive sessions,
// represented by a Session.
session, err := conn.NewSession()
if err != nil {
panic("Failed to create session: " + err.Error())
}
defer session.Close()
// Set IO
session.Stdout = os.Stdout
session.Stderr = os.Stderr
in, _ := session.StdinPipe()
// Set up terminal modes
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
}
// Request pseudo terminal
if err := session.RequestPty("xterm", 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)
}
// Accepting commands
for {
reader := bufio.NewReader(os.Stdin)
str, _ := reader.ReadString('\n')
fmt.Fprint(in, str)
// Solutition to keypress to continue
fmt.Fprint(in, " \n")
fmt.Fprint(in, "show run \n")
}
}
What I try to achieve
But when I'm pressing any key manually, the script will run all the commands perfectly. So does anyone know how I can simulate any key within the code below:
for {
reader := bufio.NewReader(os.Stdin)
str, _ := reader.ReadString('\n')
fmt.Fprint(in, str)
// Solutition to keypress to continue
fmt.Fprint(in, " \n")
fmt.Fprint(in, "show run \n")
}
You are reading from the standard input before you ever write to the ssh connection. As a result, this screen can only by skipped by manual intervention.
The reading of the command line's standard input should come after any initial commands you want to run on the host, so it would be organized like this:
// Requires keypress to continue
fmt.Fprint(in, " \n")
// List out useful information
fmt.Fprint(in, "show run \n")
// Forward user commands to the remote shell
for {
reader := bufio.NewReader(os.Stdin)
str, _ := reader.ReadString('\n')
fmt.Fprint(in, str)
}

GoLang SSH Commands with slashes seem to instantly fail

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()

Interactive secure shell in Golang not capturing all keyboard

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?

How do I execute a command on a remote machine in a golang CLI?

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.

Resources