-
Star
(189)
You must be signed in to star a gist -
Fork
(55)
You must be signed in to fork a gist
-
-
Save jpillora/b480fde82bff51a06238 to your computer and use it in GitHub Desktop.
| // A simple SSH server providing bash sessions | |
| // | |
| // Requires: | |
| // go get github.com/kr/pty | |
| // go get golang.org/x/crypto/ssh | |
| // | |
| // Server: | |
| // ssh-keygen -t rsa #generate server keypair | |
| // go run sshd.go | |
| // | |
| // Client: | |
| // ssh foo@localhost -p 2022 #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() { | |
| // An SSH server is represented by a ServerConfig, which holds | |
| // certificate details and handles authentication of ServerConns. | |
| config := &ssh.ServerConfig{ | |
| 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 can generate a keypair with 'ssh-keygen -t rsa -C "[email protected]"' | |
| 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:2022") | |
| if err != nil { | |
| log.Fatal("failed to listen on 2022") | |
| } | |
| // Accept all connections | |
| log.Print("listening on 2022...") | |
| 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()) | |
| // Print incoming [out-of-band] Requests | |
| go handleRequests(reqs) | |
| // Accept all channels | |
| go handleChannels(chans) | |
| } | |
| } | |
| func handleRequests(reqs <-chan *ssh.Request) { | |
| for req := range reqs { | |
| log.Printf("recieved request: %+v", req) | |
| } | |
| } | |
| func handleChannels(chans <-chan ssh.NewChannel) { | |
| // Service the incoming Channel channel. | |
| for newChannel := range chans { | |
| // Channels have a type, depending on the application level | |
| // protocol intended. In the case of a shell, the type is | |
| // "session" and ServerShell may be used to present a simple | |
| // terminal interface. | |
| if t := newChannel.ChannelType(); t != "session" { | |
| newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) | |
| continue | |
| } | |
| channel, requests, err := newChannel.Accept() | |
| if err != nil { | |
| log.Printf("could not accept channel (%s)", err) | |
| continue | |
| } | |
| // allocate a terminal for this channel | |
| log.Print("creating pty...") | |
| //fire up bash for this session | |
| c := exec.Command("bash") | |
| f, err := pty.Start(c) | |
| if err != nil { | |
| log.Printf("could not start pty (%s)", err) | |
| continue | |
| } | |
| //teardown session | |
| var once sync.Once | |
| close := func() { | |
| channel.Close() | |
| log.Printf("session closed") | |
| } | |
| //link session and bash streams | |
| go func() { | |
| io.Copy(channel, f) | |
| once.Do(close) | |
| }() | |
| go func() { | |
| io.Copy(f, channel) | |
| once.Do(close) | |
| }() | |
| // Sessions have out-of-band requests such as "shell", | |
| // "pty-req" and "env". Here we handle only the | |
| // "shell" request. | |
| go func(in <-chan *ssh.Request) { | |
| for req := range in { | |
| ok := false | |
| switch req.Type { | |
| case "shell": | |
| // We don't accept any commands (Payload), | |
| // only the default shell. | |
| if len(req.Payload) == 0 { | |
| ok = true | |
| } | |
| case "pty-req": | |
| // Responding 'ok' here will let the client | |
| // know we have a pty ready for input | |
| ok = true | |
| // Parse body... | |
| termLen := req.Payload[3] | |
| termEnv := string(req.Payload[4 : termLen+4]) | |
| w, h := parseDims(req.Payload[termLen+4:]) | |
| SetWinsize(f.Fd(), w, h) | |
| log.Printf("pty-req '%s'", termEnv) | |
| case "window-change": | |
| w, h := parseDims(req.Payload) | |
| SetWinsize(f.Fd(), w, h) | |
| continue //no response | |
| } | |
| if !ok { | |
| log.Printf("declining %s request...", req.Type) | |
| } | |
| req.Reply(ok, nil) | |
| } | |
| }(requests) | |
| } | |
| } | |
| // ======================= | |
| // Window size helpers | |
| 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) { | |
| log.Printf("window resize %dx%d", w, h) | |
| ws := &Winsize{Width: uint16(w), Height: uint16(h)} | |
| syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) | |
| } |
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!!!
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
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 ?
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
@jpillora, Can you give the example PublicKeyCallback ?
Great example, thank you
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
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)
go buildand 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
Same err occur to me, may be you fixed it? how? thx.
Note that this code has a potential denial of service attack in it - the ssh.NewServerConn call should be called in a go routine as it may take an arbitrary amount of time. In fact one user can take infinitely long doing the authentication, stopping any more calls to Accept and any new connections.
I made an issue to make this clearer in the go docs: see golang/go#43521 for details.
@ncw Good point, I guess there's a few ways to combat this
- you can do is rate-limit/conn-limit by IP, though this has its own draw backs, carrier grade NAT (mobile networks) can host many legitimate users on a single IP
- you can enable low auth handshake timeouts, though this could mean users with unstable connections are unreasonably disconnected
FYI, not sure if this gist works anymore, see https://github.com/jpillora/sshd-lite for a working ssh server (as of Feb 2021)
Corrections made for Go 1.15: https://gist.github.com/protosam/53cf7970e17e06135f1622fa9955415f
Also think I fixed the denial of service attack mentioned by @ncw
Server:
Client: