Better Programming

Advice for programmers.

A Simple Cross-Platform SSH Client in 100 Lines of Go

Marten Gartner
Better Programming
Published in
5 min readApr 21, 2023

--

Foto von Adi Goldstein auf Unsplash

Go is a programming language designed for building fast, efficient, and reliable software. Google created it in 2007, and it has become a popular choice for a wide range of applications, including system administration tasks.

One of the benefits of Go is its simplicity and ease of use. Its clean and concise syntax makes it easy to learn and use, even for beginners. Additionally, Go is a compiled language, producing binary executables that can be run on any platform without needing an interpreter. What makes Go really powerful is its ability to compile statically linked binaries by just snapping your fingers (e.g., by setting CGO_ENABLED=0)

The Importance of Executing SSH Commands

Executing commands on remote servers via SSH is a common task in system administration. SSH is a secure and reliable protocol for accessing remote machines and performing administrative tasks. By executing commands remotely, system administrators can manage multiple machines from a central location, reducing the need for manual intervention.

The Need for Cross-Platform SSH Support

While SSH is a powerful tool, it can be challenging to integrate it into an application, particularly when working with different platforms. For example, some platforms may have different SSH client libraries or configurations, making it difficult to execute SSH commands reliably across all platforms.

This is where implementing an SSH client in Go can be useful. Go is a cross-platform language, meaning an application written in Go can be compiled to run on different operating systems and architectures. Implementing an SSH client in Go ensures that your application works consistently across all platforms. So definitely a great choice if you plan to integrate ssh into a cross-platform application. Now, let’s get started!

Implementing an SSH Client in Go

In this article, I’ll show you how to implement an SSH client in Go and execute commands on a remote server. My implementation will use the golang.org/x/crypto/ssh package, which provides a complete implementation of the SSH protocol in Go.

First, let’s start with the SSH client configuration. We start our implementation by loading an ssh config file (usually in ~/.ssh/config) and search for a specific host to connect to. This config file is a great place to configure users, ports, IPs, and jumphosts to connect to particular servers. To load the config and obtain values from it, we use the github.com/kevinburke/ssh_configmodule. It provides a method called Decode that takes an io.Reader as input and returns an ssh_config.Config object, which we can filter through, e.g., the Get method that accepts a host alias and a key to which property should be accessed.

// Load user's SSH config file
f, err := os.Open(os.ExpandEnv("$HOME/.ssh/config"))
sshConfig, err := ssh_config.Decode(f)
if err != nil {
log.Fatalf("Failed to load SSH config: %s", err)
}

// Get the host configuration
user, err := sshConfig.Get(host, "User")
if err != nil {
log.Fatalf("Failed to get ssh User from config: %s", err)
}

hostName, err := sshConfig.Get(host, "HostName")
if err != nil {
log.Fatalf("Failed to get ssh HostName from config: %s", err)
}

port, err := sshConfig.Get(host, "Port")
if err != nil {
log.Fatalf("Failed to get ssh Port from config: %s", err)
}

Next, we create an ssh.ClientConfig struct that contains the authentication information for the SSH client, such as the user, password, and private key. Here's an example of how to create an ssh.ClientConfig:

// Load private key for authentication
keyPath := os.ExpandEnv("$HOME/.ssh/id_rsa")
keyBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
log.Fatalf("Failed to read private key: %s", err)
}

key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte("password"))
if err != nil {
log.Fatalf("Failed to parse private key: %s", err)
}

// Or without password
//key, err := ssh.ParsePrivateKey(keyBytes)
//if err != nil {
// log.Fatalf("Failed to parse private key: %s", err)
//}

// Configure SSH client
sshClientConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}

In this sample, we read the private key file (either password protected or not) and use public-key-auth as the Auth method for our ssh client.

Next, we’ll create an SSH client connection using the ssh.Dial function. This function takes the network type (tcp), the host address, and the ssh.ClientConfig as arguments. Here's an example:

// Connect to the host
address := net.JoinHostPort(hostName, port)
client, err := ssh.Dial("tcp", address, sshClientConfig)
if err != nil {
log.Fatalf("Failed to connect to %s: %s", host, err)
}

fmt.Printf("Connected to %s\n", host)
defer client.Close()

In this example, we’re connecting to the hostName and port we choose. We're also using the defer keyword to ensure the connection is closed when the function returns.

Once we connect, we can create an SSH session using the conn.NewSession function. This function returns an ssh.Session object, which we can use to execute commands on the remote server. Here's an example:

// Create an SSH session
session, err := client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()

// Run the command on the remote machine
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatalf("failed to get stdout: %s", err)
}
if err := session.Start(command); err != nil {
log.Fatalf("failed to start command: %s", err)
}

Finally, we’re reading the output of the command from the SSH session’s stdout stream using the ioutil.ReadAll function, and printing it on the console, as shown below:

output, err := ioutil.ReadAll(stdout)
if err != nil {
log.Fatalf("failed to read stdout: %s", err)
}
if err := session.Wait(); err != nil {
log.Fatalf("command failed: %s", err)
}
fmt.Println(string(output))

Conclusion

This article shows you how to implement an SSH client in Go and execute commands on a remote server. By using the golang.org/x/crypto/ssh package and Go's cross-platform capabilities, we can ensure our SSH client works reliably on different platforms.

We hope this article has helped demonstrate the power and flexibility of Go for system administration tasks. With its simplicity, ease of use, and cross-platform support, Go is an excellent choice for building robust and efficient software for managing remote systems.

In the following, the complete code takes two cmd arguments: a host alias (of the ssh config file) and a command to execute on the remote machine. The whole example, in combination with its instructions, can be found here.

--

--

Marten Gartner
Marten Gartner

Written by Marten Gartner

I’m a dad, software developer and researcher on network performance. Writing about high-speed frontend-dev, neworking, productivity and homeoffice with kids.