A Simple Cross-Platform SSH Client in 100 Lines of Go
SSH in Go: Simple and powerful
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_config
module. 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.