SSH, short for Secure Shell, is a network protocol used for securely remote logging into other computers on a network. I believe most backend developers are familiar with SSH. Common shell tools used for logging into servers, such as Xshell, SecureCRT, and iTerm2, are all based on the SSH protocol. In Golang, the crypto/ssh
package provides functionality for implementing an SSH client. In this article, we will explain in detail how to implement an SSH client using Golang.
Creating SSH Client Configuration
First, we need to configure the parameters for the SSH client to connect to the server. The basic configuration includes the username
, authentication method
, and host key callback
. Here is an example code snippet:
package main
import "golang.org/x/crypto/ssh"
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
}
In this configuration, the username is set to "username" and the password is set to "password". The ssh.Password
function is used to create a password authentication method. The HostKeyCallback
function is called each time a new host is connected to, and it is used to verify the server's host key. In this example, ssh.InsecureIgnoreHostKey
is used, which means any host key is accepted. It is not recommended to use this in a production environment because not verifying the host key poses a security risk.
Connecting to the SSH Server
The ssh.Dial
function is used to connect to the remote SSH server. It requires three parameters: the network type (usually "tcp"), the address and port of the server, and the configuration object created earlier. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
}
Connect to the server with the IP address 192.168.3.111 on port 22 (the default port for SSH protocol). If the connection fails, an error will be returned, and you can use log.Fatal
to print the error and exit the program.
Creating an SSH Session
Once the SSH connection is established, you can create an SSH session to communicate with the server. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
}
Use the client.NewSession
function to create a new session, and if it fails, an error will be returned. Use the defer
keyword to ensure that the session is closed after the operations are completed.
Creating a Pseudo Terminal
The SSH protocol allows for the creation of a pseudo terminal, which simulates the behavior of a real terminal and allows for interactive commands, such as a shell or text editor. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
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("linux", 80, 40, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
}
Use the session.RequestPty
function to request a pseudo terminal. It requires four parameters: the terminal type (in this case, "xterm"), the width and height of the terminal, and a map to set terminal modes. In this map, ssh.ECHO
is set to 0, which disables echoing, and the input and output speeds are set to 14.4 kbaud.
Setting Input and Output
Specify the standard input and output for the remote shell. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
"os"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
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("linux", 80, 40, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
//set input and output
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
}
Start Remote Shell
A remote shell is now ready, and you can use the session.Shell
function to start a default shell or use the session.Run
function to execute a specific command. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
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("linux", 80, 40, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
// set input and output
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
if err := session.Shell(); err != nil {
log.Fatal("failed to start shell: ", err)
}
}
Start a default shell, and if it fails to start, an error will be returned.
Waiting for Session to End
Use the session.Wait
function to block until the session ends. Here is an example code snippet:
package main
import (
"golang.org/x/crypto/ssh"
"log"
"os"
)
func main() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", "192.168.3.111:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
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("linux", 80, 40, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
//set input and output
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
if err := session.Shell(); err != nil {
log.Fatal("failed to start shell: ", err)
}
err = session.Wait()
if err != nil {
log.Fatal("Failed to run: " + err.Error())
}
}
With this, you have completed the implementation of a basic SSH client. This client can connect to an SSH server, start a remote shell, and wait for the session to end.
Summary
The SSH client implemented in this article only provides the most basic functionality. To create a feature-rich and user-friendly SSH client, you need to pay attention to many details, such as error handling, reconnection, timeouts, signal handling, and more.