Golang SSH to Cisco Wireless Controller and Run Commands - go

I am trying to SSH to a Cisco wireless controller through Go, using Go's golang.org/x/crypto/ssh library, to programmatically configure access points. The problem I'm running into is correctly parsing the controller CLI in Go. For example, this is the typical SSH login to the controller:
$ ssh <controller_ip>
(Cisco Controller)
User: username
Password:****************
(Cisco Controller) >
I am trying to figure out how to send the username and then the password after the SSH session is established in Go. So far, I am able to successfully SSH to the controller, but the program exits at the username prompt, like this:
$ go run main.go
(Cisco Controller)
User:
How would I go about sending the username when prompted, then repeating that for the password prompt?
No errors are being thrown or exit codes are being given, so I'm not sure why the program is exiting immediately at the username prompt. But Even if it wasn't exiting that way, I'm still unsure of how to send the username and password when the controller's CLI is expecting it.
Here is my code:
package main
import (
"golang.org/x/crypto/ssh"
"log"
"io/ioutil"
"os"
"strings"
"path/filepath"
"bufio"
"fmt"
"errors"
"time"
)
const (
HOST = "host"
)
func main() {
hostKey, err := checkHostKey(HOST)
if err != nil {
log.Fatal(err)
}
key, err := ioutil.ReadFile("/Users/user/.ssh/id_rsa")
if err != nil {
log.Fatalf("unable to read private key: %v", err)
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
log.Fatalf("unable to parse private key: %v", err)
}
// Create client config
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
// Use the PublicKeys method for remote authentication.
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
Timeout: time.Second * 5,
}
// Connect to the remote server and perform the SSH handshake.
client, err := ssh.Dial("tcp", HOST+":22", config)
if err != nil {
log.Fatalf("unable to connect: %v", err)
}
defer client.Close()
// Create a session
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
modes := ssh.TerminalModes{
ssh.ECHO: 0,
ssh.TTY_OP_ISPEED: 9600,
ssh.TTY_OP_OSPEED: 9600,
}
if err := session.RequestPty("xterm", 0, 200, modes); err != nil {
log.Fatal(err)
}
if err := session.Shell(); err != nil {
log.Fatal(err)
}
buf := make([]byte, 1000)
n, err := stdout.Read(buf) //this reads the ssh terminal welcome message
loadStr := ""
if err == nil {
loadStr = string(buf[:n])
}
for (err == nil) && (!strings.Contains(loadStr, "(Cisco Controller)")) {
n, err = stdout.Read(buf)
loadStr += string(buf[:n])
}
fmt.Println(loadStr)
if _, err := stdin.Write([]byte("show ap summary\r")); err != nil {
panic("Failed to run: " + err.Error())
}
}
func checkHostKey(host string) (ssh.PublicKey, error) {
file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var hostKey ssh.PublicKey
for scanner.Scan() {
fields := strings.Split(scanner.Text(), " ")
if len(fields) != 3 {
continue
}
if strings.Contains(fields[0], host) {
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
if err != nil {
return nil, errors.New(fmt.Sprintf("error parsing %q: %v", fields[2], err))
}
break
}
}
if hostKey == nil {
return nil, errors.New(fmt.Sprintf("no hostkey for %s", host))
}
return hostKey, nil
}

Finally got it working. Here is my new code inspired by this post:
package main
import (
"golang.org/x/crypto/ssh"
"log"
"io/ioutil"
"os"
"strings"
"path/filepath"
"bufio"
"fmt"
"errors"
"time"
)
func main() {
client, err := authenticate("10.4.112.11", "mwalto7", "lion$Tiger$Bear$")
if err != nil {
log.Fatalf("unable to connect: %v", err)
}
defer client.Close()
// Create a session
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
if err := session.Shell(); err != nil {
log.Fatal(err)
}
for _, cmd := range os.Args[1:] {
stdin.Write([]byte(cmd + "\n"))
}
stdin.Write([]byte("logout\n"))
stdin.Write([]byte("N\n"))
session.Wait()
}
func authenticate(host, username, password string) (ssh.Client, error) {
hostKey, err := checkHostKey(host)
if err != nil {
log.Fatal(err)
}
key, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa"))
if err != nil {
log.Fatalf("unable to read private key: %v", err)
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
log.Fatalf("unable to parse private key: %v", err)
}
// Create client config
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
// Use the PublicKeys method for remote authentication.
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
Timeout: time.Second * 5,
}
// Connect to the remote server and perform the SSH handshake.
client, err := ssh.Dial("tcp", host+":22", config)
return *client, err
}
func checkHostKey(host string) (ssh.PublicKey, error) {
file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var hostKey ssh.PublicKey
for scanner.Scan() {
fields := strings.Split(scanner.Text(), " ")
if len(fields) != 3 {
continue
}
if strings.Contains(fields[0], host) {
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
if err != nil {
return nil, errors.New(fmt.Sprintf("error parsing %q: %v", fields[2], err))
}
break
}
}
if hostKey == nil {
return nil, errors.New(fmt.Sprintf("no hostkey for %s", host))
}
return hostKey, nil
}

Related

ssh server in go : how to offer public key types different than rsa?

I’m trying to create a ssh server in go using the x/crypto/ssh module but i can’t manage to make the public key authentification work.
I tried the ExampleNewServerConn() function in the ssh/example_test.go file (in the https://go.googlesource.com/crypto repo) but the public key method doesn’t work, it looks like the server isn’t advertising the right algorithms because i get this line when trying to connect with a ssh client :
debug1: send_pubkey_test: no mutual signature algorithm
If i add -o PubkeyAcceptedKeyTypes=+ssh-rsa the public key login works, but this rsa method is deprecated, i would like to use another public key type, how can i do that ?
Thanks in advance.
Edit : here is the code that i used to test
package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"golang.org/x/crypto/ssh"
terminal "golang.org/x/term"
)
func main() {
authorizedKeysBytes, err := ioutil.ReadFile("authorized_keys")
if err != nil {
log.Fatalf("Failed to load authorized_keys, err: %v", err)
}
authorizedKeysMap := map[string]bool{}
for len(authorizedKeysBytes) > 0 {
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
if err != nil {
log.Fatal(err)
}
authorizedKeysMap[string(pubKey.Marshal())] = true
authorizedKeysBytes = rest
}
config := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if c.User() == "testuser" && string(pass) == "tiger" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
if authorizedKeysMap[string(pubKey.Marshal())] {
return &ssh.Permissions{
// Record the public key used for authentication.
Extensions: map[string]string{
"pubkey-fp": ssh.FingerprintSHA256(pubKey),
},
}, nil
}
return nil, fmt.Errorf("unknown public key for %q", c.User())
},
}
privateBytes, err := ioutil.ReadFile("id_rsa")
if err != nil {
log.Fatal("Failed to load private key: ", err)
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
log.Fatal("Failed to parse private key: ", err)
}
config.AddHostKey(private)
listener, err := net.Listen("tcp", "0.0.0.0:2022")
if err != nil {
log.Fatal("failed to listen for connection: ", err)
}
nConn, err := listener.Accept()
if err != nil {
log.Fatal("failed to accept incoming connection: ", err)
}
conn, chans, reqs, err := ssh.NewServerConn(nConn, config)
if err != nil {
log.Fatal("failed to handshake: ", err)
}
log.Printf("logged in with key %s", conn.Permissions.Extensions["pubkey-fp"])
go ssh.DiscardRequests(reqs)
for newChannel := range chans {
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
log.Fatalf("Could not accept channel: %v", err)
}
go func(in <-chan *ssh.Request) {
for req := range in {
req.Reply(req.Type == "shell", nil)
}
}(requests)
term := terminal.NewTerminal(channel, "> ")
go func() {
defer channel.Close()
for {
line, err := term.ReadLine()
if err != nil {
break
}
fmt.Println(line)
}
}()
}
}
I found why the client and the server can’t communicate, the rsa-sha2 algorithms are not yet implemented in the x/crypto library. There is an issue about it on github : https://github.com/golang/go/issues/49952 .
A temporary solution is to add
replace golang.org/x/crypto => github.com/rmohr/crypto v0.0.0-20211203105847-e4ed9664ac54
at the end of your go.mod file, it uses a x/crypto fork from #rmohr that works with rsa-sha2.
This is the easy way to do it, let letsencrypt handle the certificates for you :)
func main() {
r := mux.NewRouter()
r.HandleFunc("/index", index)
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("www.example.com"), // replace with your domain
Cache: autocert.DirCache("certs"),
}
srv := &http.Server{
Handler: r,
Addr: ":https",
WriteTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
},
}
go http.ListenAndServe(":http", certManager.HTTPHandler(nil)) //nolint
log.Fatal(srv.ListenAndServeTLS("", ""))
}

Golang concurrent SSH connections

I have multiple servers that I'm trying to establish SSH connections to, and I'm spawning a new goroutine for every SSH connection I have to establish. I then send some commands. This program sort of works, but just exits after the first connection.
What am I doing wrong here?
package main
import (
"bufio"
"log"
"os"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
file, err := os.Open("servers.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
go ssh_login(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
func ssh_login(host string) {
port := "22"
user := "testing"
pass := "testing"
cmd := "uname -a"
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(pass),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 1 * time.Second,
}
client, err := ssh.Dial("tcp", host+":"+port, config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
sess, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer sess.Close()
sess.Stdout = os.Stdout
sess.Stderr = os.Stderr
err = sess.Run(cmd)
if err != nil {
log.Fatal(err)
}
}
What am I doing wrong?

Operation timeout with SSH connection

I am trying to connect to a remote EC2-server through Go code using a PEM key provided by AWS. I am able to log in to the server through the command line using the PEM key.
I have done the following so far.
package main
import (
"io"
"io/ioutil"
"os"
"time"
"golang.org/x/crypto/ssh"
)
func publicKey(path string) ssh.AuthMethod {
key, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
panic(err)
}
return ssh.PublicKeys(signer)
}
func runCommand(cmd string, conn *ssh.Client) {
sess, err := conn.NewSession()
if err != nil {
panic(err)
}
defer sess.Close()
sessStdOut, err := sess.StdoutPipe()
if err != nil {
panic(err)
}
go io.Copy(os.Stdout, sessStdOut)
sessStderr, err := sess.StderrPipe()
if err != nil {
panic(err)
}
go io.Copy(os.Stderr, sessStderr)
err = sess.Run(cmd) // eg., /usr/bin/whoami
if err != nil {
panic(err)
}
}
func main() {
config := &ssh.ClientConfig{
User: "ec2-user",
Auth: []ssh.AuthMethod{
publicKey("mykey"),
},
Timeout: 15 * time.Second,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
conn, err := ssh.Dial("tcp", "remote-server:22", config)
if err != nil {
panic(err.Error())
}
defer conn.Close()
runCommand("whoami", conn)
}
I keep getting the following error. What am I missing?
panic: ssh: handshake failed: read tcp "localhost"->"remotehost:22": read: operation timed out
In the above message I have replaced the IP addresses of the actual machines with bogus names.
I was able to resolve this since I was using an incorrect port to connect to :). I should be using port 22 for SSH, but was using some other service port.
Thanks.

Upload a file from aws s3 to linux server directly with private key

There are some larger size files in AWS S3. I need to copy those files to a remote server.
I've tried with the below code, but I'm not able to transfer the file. Is this right way or I need to do in any other ways.
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/pkg/errors"
"github.com/tmc/scp"
"golang.org/x/crypto/ssh"
"io"
"io/ioutil"
"devtools/objectstorage"
"log"
"os"
)
func main() {
var osSecretKey = "secret-key"
var osAccessKeyID = "access-key"
var osRegion = "region"
var osUrl = "url"
var osBucketName = "bucket"
accpusr := "username"
accphost := "server"
sshport := "22"
scpDestinationPath := "/destination/path"
s3Config := objectstorage.S3Config{
AccessKeyID: osAccessKeyID,
SecretAccessKey: osSecretKey,
Region: osRegion,
Bucket: osBucketName,
URL: osUrl,
}
var s3FileName = "sample.txt"
var storage, err = objectstorage.New(s3Config)
if err != nil {
log.Fatal("failed to create s3 session", err)
}
dlObj, err := storage.Download(s3FileName)
if err != nil {
log.Fatal("failed to download s3 file", err)
}
log.Println("downloaded object")
err = Send(accpusr, accphost, sshport, scpDestinationPath, s3FileName, *dlObj.ContentLength, dlObj.Body)
if err != nil {
log.Fatal("failed to copy file", err)
}
log.Println("file copied successfully")
}
func Send(remoteUser, remoteHost, remoteSSHPort, remoteFolder, remoteFileName string, size int64, r io.Reader) error {
session, err := connect(remoteUser, remoteHost, remoteSSHPort)
if err != nil {
return err
}
defer session.Close()
err = scp.Copy(size, os.FileMode.Perm(0770), remoteFileName, r, remoteFolder, session)
if err != nil {
errMsg := fmt.Sprintf("Error sending file: %s", err.Error())
return errors.New(errMsg)
}
return nil
}
func connect(remoteUser string, remoteHost string, remoteSSHPort string) (*ssh.Session, error) {
//retrieve public key from private key
key, err := retrieveKey()
if err != nil {
return nil, err
}
var sshConfig ssh.Config
sshConfig.SetDefaults()
sshConfig.Ciphers = append(sshConfig.Ciphers, "aes256-cbc")
// Define the Client Config as:
config := &ssh.ClientConfig{
Config: sshConfig,
User: remoteUser,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", remoteHost, remoteSSHPort), config)
if err != nil {
errMsg := fmt.Sprintf("Error connecting to client: %s", err.Error())
return nil, errors.New(errMsg)
}
session, err := client.NewSession()
if err != nil {
errMsg := fmt.Sprintf("Error creating session: %s", err.Error())
return nil, errors.New(errMsg)
}
return session, nil
}
func retrieveKey() (key ssh.Signer, err error) {
scpCert := ""
buf, err := ioutil.ReadFile(fmt.Sprintf("%s", scpCert))
if err != nil {
errMsg := fmt.Sprintf("Error retrieving key: %s", err.Error())
return nil, errors.New(errMsg)
}
key, err = ssh.ParsePrivateKey(buf)
if err != nil {
errMsg := fmt.Sprintf("Error parsing key: %s", err.Error())
return nil, errors.New(errMsg)
}
return key, nil
}
objectstorage.New will return aws s3 session.
storage.Download will return s3.GetObjectOutput.

How to connect remote ssh server with socks proxy?

package main
import (
"errors"
"fmt"
"net"
"golang.org/x/crypto/ssh"
)
var (
socks string = "127.0.0.1:8000"
server string = "192.168.1.1:2222"
cmd string = ""
login string = "root"
password string = ""
)
func main() {
c, err := netConn(socks)
if err != nil {
fmt.Println(err)
}
conf := &ssh.ClientConfig{
User: login,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
}
client, err := Dialer(conf, c, server)
if err != nil {
fmt.Println(err)
}
session, err := client.NewSession()
defer session.Close()
if err != nil {
fmt.Println(err)
}
session.Run(cmd)
}
func netConn(addr string) (net.Conn, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to connect to %v", err))
}
return conn, nil
}
func Dialer(conf *ssh.ClientConfig, c net.Conn, host string) (*ssh.Client, error) {
conn, chans, reqs, err := ssh.NewClientConn(c, host, conf)
if err != nil {
return nil, err
}
return ssh.NewClient(conn, chans, reqs), nil
}
Client panic
ssh: handshake failed: EOF
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x80f3e2d]
goroutine 1 [running]:
panic(0x8201b60, 0x18522030)
/usr/lib/go/src/runtime/panic.go:464 +0x326
golang.org/x/crypto/ssh.(*Client).NewSession(0x0, 0x1, 0x0, 0x0)
/home/user/go/src/golang.org/x/crypto/ssh/client.go:129 +0xcd
main.main()
/home/user/go/src/ss/i.go:34 +0x276
And socks server in log
[ERR] socks: Unsupported SOCKS version: [83]
How i can make it posible? Thanks.
You can use the x/net/proxy package as follows:
package main
import (
"log"
"golang.org/x/crypto/ssh"
"golang.org/x/net/proxy"
)
func main() {
sshConfig := &ssh.ClientConfig{
User: "user",
// Auth: .... fill out with keys etc as normal
}
client, err := proxiedSSHClient("127.0.0.1:8000", "127.0.0.1:22", sshConfig)
if err != nil {
log.Fatal(err)
}
// get a session etc...
}
func proxiedSSHClient(proxyAddress, sshServerAddress string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
dialer, err := proxy.SOCKS5("tcp", proxyAddress, nil, proxy.Direct)
if err != nil {
return nil, err
}
conn, err := dialer.Dial("tcp", sshServerAddress)
if err != nil {
return nil, err
}
c, chans, reqs, err := ssh.NewClientConn(conn, sshServerAddress, sshConfig)
if err != nil {
return nil, err
}
return ssh.NewClient(c, chans, reqs), nil
}
As you can see from the ssh package API, the function
func Dial(network, addr string, config *ClientConfig) (*Client, error)
directly returns an object of type ssh.Client.
Therefore you can shorten a lot your code with:
func main() {
conf := &ssh.ClientConfig{
User: login,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
}
client, err := ssh.Dial("tcp", server, conf)
if err != nil {
fmt.Println(err)
}
defer client.Close()
session, err := client.NewSession()
defer session.Close()
if err != nil {
fmt.Println(err)
}
session.Run(cmd)
}
Hope this helps !

Resources