Last active
February 14, 2024 07:10
-
-
Save protosam/53cf7970e17e06135f1622fa9955415f to your computer and use it in GitHub Desktop.
Simple ssh server example in go.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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/creack/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 | |
} | |
go handeConn(tcpConn, config) | |
} | |
} | |
func handeConn(tcpConn net.Conn, config *ssh.ServerConfig) { | |
// 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) | |
return | |
} | |
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 | |
} | |
npty, ntty, err := pty.Open() | |
if err != nil { | |
log.Printf("Could not start pty (%s)", err) | |
} | |
// Fire up bash for this session | |
bash := exec.Command("bash") | |
bash.Stdout = ntty | |
bash.Stdin = ntty | |
bash.Stderr = ntty | |
bash.SysProcAttr = &syscall.SysProcAttr{ | |
Setctty: true, | |
Setsid: true, // TODO: Evaluate me? | |
} | |
// 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") | |
} | |
if err := bash.Start(); err != nil { | |
log.Printf("Failed to start bash (%s)", err) | |
} | |
//pipe session to bash and visa-versa | |
var once sync.Once | |
go func() { | |
io.Copy(connection, npty) | |
once.Do(close) | |
}() | |
go func() { | |
io.Copy(npty, 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(npty.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(npty.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 | |
// corrected from https://gist.github.com/jpillora/b480fde82bff51a06238 |
Did it start a session for you and open a bash terminal?
Exit doesn't happen from this code. Exit happens because the terminal dies and there's nothing left to hold the program open.
ioutil is now deprecated. It should be changed to os.ReadFile
Thanks @benschlueter! I've found myself pretty busy over the past few months. If you want to share an updated example, I'd be happy to accept a commit to this gist.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Once entered into bash shell from from client, "exit" is NOT closing the connection. Could you please fix that?