Skip to content

Instantly share code, notes, and snippets.

@jpillora jpillora/sshd.go
Last active Sep 15, 2019

Embed
What would you like to do?
Go SSH server complete example - Read more here https://blog.gopheracademy.com/go-and-ssh/
// A small SSH daemon providing bash sessions
//
// Server:
// cd my/new/dir/
// #generate server keypair
// ssh-keygen -t rsa
// go get -v .
// go run sshd.go
//
// Client:
// ssh foo@localhost -p 2200 #pass=bar
package main
import (
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os/exec"
"sync"
"syscall"
"unsafe"
"github.com/kr/pty"
"golang.org/x/crypto/ssh"
)
func main() {
// In the latest version of crypto/ssh (after Go 1.3), the SSH server type has been removed
// in favour of an SSH connection type. A ssh.ServerConn is created by passing an existing
// net.Conn and a ssh.ServerConfig to ssh.NewServerConn, in effect, upgrading the net.Conn
// into an ssh.ServerConn
config := &ssh.ServerConfig{
//Define a function to run when a client attempts a password login
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
// Should use constant-time compare (or better, salt+hash) in a production setting.
if c.User() == "foo" && string(pass) == "bar" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
// You may also explicitly allow anonymous client authentication, though anon bash
// sessions may not be a wise idea
// NoClientAuth: true,
}
// You can generate a keypair with 'ssh-keygen -t rsa'
privateBytes, err := ioutil.ReadFile("id_rsa")
if err != nil {
log.Fatal("Failed to load private key (./id_rsa)")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
log.Fatal("Failed to parse private key")
}
config.AddHostKey(private)
// Once a ServerConfig has been configured, connections can be accepted.
listener, err := net.Listen("tcp", "0.0.0.0:2200")
if err != nil {
log.Fatalf("Failed to listen on 2200 (%s)", err)
}
// Accept all connections
log.Print("Listening on 2200...")
for {
tcpConn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept incoming connection (%s)", err)
continue
}
// Before use, a handshake must be performed on the incoming net.Conn.
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config)
if err != nil {
log.Printf("Failed to handshake (%s)", err)
continue
}
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
// Discard all global out-of-band Requests
go ssh.DiscardRequests(reqs)
// Accept all channels
go handleChannels(chans)
}
}
func handleChannels(chans <-chan ssh.NewChannel) {
// Service the incoming Channel channel in go routine
for newChannel := range chans {
go handleChannel(newChannel)
}
}
func handleChannel(newChannel ssh.NewChannel) {
// Since we're handling a shell, we expect a
// channel type of "session". The also describes
// "x11", "direct-tcpip" and "forwarded-tcpip"
// channel types.
if t := newChannel.ChannelType(); t != "session" {
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
return
}
// At this point, we have the opportunity to reject the client's
// request for another logical connection
connection, requests, err := newChannel.Accept()
if err != nil {
log.Printf("Could not accept channel (%s)", err)
return
}
// Fire up bash for this session
bash := exec.Command("bash")
// Prepare teardown function
close := func() {
connection.Close()
_, err := bash.Process.Wait()
if err != nil {
log.Printf("Failed to exit bash (%s)", err)
}
log.Printf("Session closed")
}
// Allocate a terminal for this channel
log.Print("Creating pty...")
bashf, err := pty.Start(bash)
if err != nil {
log.Printf("Could not start pty (%s)", err)
close()
return
}
//pipe session to bash and visa-versa
var once sync.Once
go func() {
io.Copy(connection, bashf)
once.Do(close)
}()
go func() {
io.Copy(bashf, connection)
once.Do(close)
}()
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
go func() {
for req := range requests {
switch req.Type {
case "shell":
// We only accept the default shell
// (i.e. no command in the Payload)
if len(req.Payload) == 0 {
req.Reply(true, nil)
}
case "pty-req":
termLen := req.Payload[3]
w, h := parseDims(req.Payload[termLen+4:])
SetWinsize(bashf.Fd(), w, h)
// Responding true (OK) here will let the client
// know we have a pty ready for input
req.Reply(true, nil)
case "window-change":
w, h := parseDims(req.Payload)
SetWinsize(bashf.Fd(), w, h)
}
}
}()
}
// =======================
// parseDims extracts terminal dimensions (width x height) from the provided buffer.
func parseDims(b []byte) (uint32, uint32) {
w := binary.BigEndian.Uint32(b)
h := binary.BigEndian.Uint32(b[4:])
return w, h
}
// ======================
// Winsize stores the Height and Width of a terminal.
type Winsize struct {
Height uint16
Width uint16
x uint16 // unused
y uint16 // unused
}
// SetWinsize sets the size of the given pty.
func SetWinsize(fd uintptr, w, h uint32) {
ws := &Winsize{Width: uint16(w), Height: uint16(h)}
syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
}
// Borrowed from https://github.com/creack/termios/blob/master/win/win.go
@jpillora

This comment has been minimized.

Copy link
Owner Author

commented Dec 2, 2014

Server:

cd my/new/dir/
ssh-keygen -t rsa #generate server keypair
go get -v .
go run sshd.go

Client:

ssh foo@localhost -p 2022 #pass=bar
@cocodrino

This comment has been minimized.

Copy link

commented Jan 5, 2016

hi

ssh foo@localhost -p 2022 #pass=bar

the port is 2200 not 2022

I'm getting this error when I try connect my client, do you know what it means??

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
7d:46:4a:90:d0:be:1d:02:49:f4:b2:ec:e6:f2:d8:80.
Please contact your system administrator.
Add correct host key in /home/yo/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/yo/.ssh/known_hosts:23
  remove with: ssh-keygen -f "/home/yo/.ssh/known_hosts" -R [localhost]:2200
RSA host key for [localhost]:2200 has changed and you have requested strict checking.
Host key verification failed.

thank you!!!

@jpillora

This comment has been minimized.

Copy link
Owner Author

commented Jul 15, 2016

means you've connected to a different server (different key) before over localhost and it thinks your traffic is being intercepted, just follow the instructions to remove the message

@anjanb

This comment has been minimized.

Copy link

commented Sep 29, 2016

What would it take to implement this for windows ? My windows servers would not necessarily have bash. They would have cmd.exe as the shell. Can we figure out what the shell is from the environment and then launch the shell ?

@literadix

This comment has been minimized.

Copy link

commented Apr 20, 2017

Hello,

this is a great example. Thank you very much. Could you provide an example, where instead of bash execution the lines are parsed (scanner ?) and then echoed back to the user terminal client ? Thank you very much in advance.

Maciej

@coolbrg

This comment has been minimized.

Copy link

commented Apr 5, 2018

@jpillora, Can you give the example PublicKeyCallback ?

@muratsplat

This comment has been minimized.

Copy link

commented May 20, 2018

Great example, thank you

@truthadjustr

This comment has been minimized.

Copy link

commented Aug 24, 2018

go build and having this error:
# command-line-arguments .\sshd.go:134:16: undefined: pty.Start .\sshd.go:199:17: not enough arguments in call to syscall.Syscall .\sshd.go:199:18: undefined: syscall.SYS_IOCTL .\sshd.go:199:49: undefined: syscall.TIOCSWINSZ

@wk8

This comment has been minimized.

Copy link

commented Jun 20, 2019

In case @anjanb (or anyone else) is interested in using this on a Windows box, this works:

package main

import (
	"fmt"
	"io"
	"log"
	"os/exec"

	"github.com/gliderlabs/ssh"
)

func main() {
	ssh.Handle(func(s ssh.Session) {
		_, _, isPty := s.Pty()
		if isPty {
			fmt.Println("PTY requested")

			cmd := exec.Command("powershell")
			stdin, err := cmd.StdinPipe()
			if err != nil {
				panic(err)
			}
			stdout, err := cmd.StdoutPipe()
			if err != nil {
				panic(err)
			}
			stderr, err := cmd.StderrPipe()
			if err != nil {
				panic(err)
			}

			go func() {
				io.Copy(stdin, s)
			}()
			go func() {
				io.Copy(s, stdout)
			}()
			go func() {
				io.Copy(s, stderr)
			}()

			err = cmd.Run()
			if err == nil {
				log.Println("session ended normally")
				s.Exit(0)
			} else {
				log.Printf("session ended with an error: %v\n", err)

				exitCode := 1
				if exitError, ok := err.(*exec.ExitError); ok {
					exitCode = exitError.ExitCode()
					log.Printf("exit code: %d\n", exitCode)
				}

				s.Exit(exitCode)
			}
		} else {
			io.WriteString(s, "No PTY requested.\n")
			s.Exit(1)
		}
	})

	log.Println("starting ssh server on port 2824...")
	log.Fatal(ssh.ListenAndServe(":2824", nil))
}

(it's not a full-fledged PTY, sure, but at least it will work to some extent)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.