├── README.md ├── channels ├── client.go └── server.go └── sshd ├── client.sh ├── id_rsa ├── id_rsa.pub └── server.go /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Go and the Secure Shell protocol 3 | 4 | http://blog.gopheracademy.com/go-and-ssh/ 5 | 6 | See directories for SSH examples 7 | 8 | #### MIT License 9 | 10 | Copyright © 2014 Jaime Pillora <dev@jpillora.com> 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | 'Software'), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 26 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 27 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 28 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 29 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /channels/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | func main() { 12 | config := &ssh.ClientConfig{ 13 | ClientVersion: "Go SSH Client v1337", 14 | User: "jpillora", 15 | Auth: []ssh.AuthMethod{ssh.Password("t0ps3cr3t")}, 16 | } 17 | 18 | client, err := ssh.Dial("tcp", "localhost:2200", config) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | log.Printf("connected") 24 | 25 | go open(client, "foo") 26 | 27 | time.Sleep(1 * time.Second) 28 | go open(client, "bar") 29 | 30 | time.Sleep(1 * time.Second) 31 | go open(client, "bazz") 32 | 33 | client.Wait() 34 | 35 | log.Printf("disconnected") 36 | } 37 | 38 | func open(client *ssh.Client, chanType string) { 39 | 40 | //open channel 41 | channel, requests, err := client.OpenChannel(chanType, s("%s extra data", chanType)) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | //requests must be serviced 47 | go ssh.DiscardRequests(requests) 48 | 49 | //send data forever... 50 | n := 1 51 | for { 52 | _, err := channel.Write(s("#%d send data channel ", n)) 53 | if err != nil { 54 | break 55 | } 56 | n++ 57 | time.Sleep(3 * time.Second) 58 | } 59 | } 60 | 61 | func s(f string, args ...interface{}) []byte { 62 | return []byte(fmt.Sprintf(f, args...)) 63 | } 64 | -------------------------------------------------------------------------------- /channels/server.go: -------------------------------------------------------------------------------- 1 | // A simple SSH server providing bash sessions 2 | package main 3 | 4 | import ( 5 | "log" 6 | "net" 7 | 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | func main() { 12 | // Simple server config with hard-coded key 13 | config := &ssh.ServerConfig{NoClientAuth: true} 14 | private, _ := ssh.ParsePrivateKey([]byte(key)) 15 | config.AddHostKey(private) 16 | 17 | // Once a ServerConfig has been configured, connections can be accepted. 18 | listener, err := net.Listen("tcp", "0.0.0.0:2200") 19 | if err != nil { 20 | log.Fatal("Failed to listen on 2200") 21 | } 22 | 23 | // Accept all connections 24 | log.Print("Listening on 2200...") 25 | for { 26 | tcpConn, err := listener.Accept() 27 | if err != nil { 28 | log.Printf("Failed to accept incoming connection (%s)", err) 29 | continue 30 | } 31 | // Before use, a handshake must be performed on the incoming net.Conn. 32 | sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config) 33 | if err != nil { 34 | log.Printf("Failed to handshake (%s)", err) 35 | continue 36 | } 37 | 38 | log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) 39 | // Discard all global out-of-band Requests 40 | go ssh.DiscardRequests(reqs) 41 | // Accept all channels 42 | go handleChannels(chans) 43 | } 44 | } 45 | 46 | func handleChannels(chans <-chan ssh.NewChannel) { 47 | for newChannel := range chans { 48 | go handleChannel(newChannel) 49 | } 50 | } 51 | 52 | func handleChannel(newChannel ssh.NewChannel) { 53 | 54 | channel, requests, err := newChannel.Accept() 55 | if err != nil { 56 | log.Printf("could not accept channel (%s)", err) 57 | return 58 | } 59 | 60 | chanType := newChannel.ChannelType() 61 | extraData := newChannel.ExtraData() 62 | 63 | log.Printf("open channel [%s] '%s'", chanType, extraData) 64 | 65 | //requests must be serviced 66 | go ssh.DiscardRequests(requests) 67 | 68 | //channel 69 | buff := make([]byte, 256) 70 | for { 71 | n, err := channel.Read(buff) 72 | if err != nil { 73 | break 74 | } 75 | b := buff[:n] 76 | log.Printf("[%s] %s", chanType, string(b)) 77 | } 78 | } 79 | 80 | //dont do this IRL :) 81 | const key = ` 82 | -----BEGIN RSA PRIVATE KEY----- 83 | MIIEowIBAAKCAQEAzNO5vZPpP7WgXA3Ck5NeCq85i1v2JCB5vM0udK+oWrCQpMdy 84 | oKZlxC8z8n/mSsylm+2xEm+kAFxyvB9ae/Pr8Lh0czePw473Qx2v78E/HdouXn3w 85 | xEHG12IoDUdC7Rt4faxNdfsebd/wWybHEV6vOEDDkxmppJ1y6Cbgx6a59X0wqW54 86 | bTKy5D98iLMzSvWi6AUS3I/hP53f7mNK7cTPqHTdVOwICgCGHOI1hcDKwMafj590 87 | +3H/F5ACYRl9Keuij09zsk+QkI+7HJN5HUtq9mjJ9Mw4vo9LzqIWTOWncEvX5b2f 88 | 99GOOlsBNh91L3PNwQdf1M++CM6F0HTv5p8ioQIDAQABAoIBACwruJF2dUWE8IkJ 89 | ep2CmTQqp3kzIriVvEsH4G3Pd7ne+8JdNI4KdEXDfCteg5Y73bbrolT8eFyPkzqY 90 | dFXou0fVL1+tarZcfVwe6dMFVIwmgftko2hfWvcVttduN7OUSf6oCqhXuC8vrNCr 91 | YyCOz7CM3uA5F4llXuNLhwvnG5EhxHk/AVN0SUbJbfKD5DEpqFM33PuITAuIPuSi 92 | Td2qa84WitZ12hBJqtZGngujE/bMZNaY0Lk6EM4L2p47+//z3raScQT2B+eF/LnR 93 | Jn32YaI7np7Y4D7RbW6QZBB/sOkrvtX51tIHIQEYdn4zlfT8+tNeVo9jn0QM77Ky 94 | FcY4a8ECgYEA5vF+P5MeSa+QsUVgK3HY4MuNNRKw4daIFJr/keYLyUwfPYQsdu5V 95 | ZXfJPkQ/y1Xlgek6E/eiiaiJN91hZEkoF6fkXcORCCmjr19FfssC++arTKk/UPxT 96 | y946yFscsZXosssCON7CskGLCiPMn7YwdwQiJ9uvKIxwB2ChfJ/trSkCgYEA4wzY 97 | rp5Pz3lbXg6P7xqYibnIH847PW9GVMGNl6pXfhUkP3NqFD+Oc41S/wD/vv1SVSZ7 98 | 2ih56E7vctxtxc9b5wWcZfzRUbBWrSKwWO1ImqsBdFapxtoOynDL0uHnXaDrQCvW 99 | UsI44d92gmO+MMYst9//I/sLRTrwYrrIvJOVALkCgYEAg0uqVeSDJKtOnKnveeOY 100 | xHyVBCZjL5Hy/Zv9Tmo2KzQ+0o9xZBAttqk6XU8Z4bUs7QW2giGYY6DQmlUfCI/a 101 | 3lASMgh8TOK3b32/mc07HhFPNB9IovdBgLcQPlYmYwPyLqvh0Ik8sXE35gTiUa6X 102 | sSJFdNmdpHTrQBZ82MhnrLkCgYB8wG06HKALhkmOd3/cR4eyfNKZry3bho1lOmf7 103 | AkxKaYFeH6MUdwtlMCx/EmRy4ytev+NjLcQ1wVFNkhH6kwGTAQE7BFtagAJP5PRy 104 | GAZBfV4yNv/X0642yx0ixJ7kUeuQecWr+S1Z5fdukzFICUs+yKOeeGxr4IN+K9Tp 105 | 0EkZeQKBgF58RcI6PZD7mayf0Z58gd+zb2WXL1rTGErYsbVgxkc/TFRaZYK0cb+n 106 | V6WZNy6k5Amx54pv59U34sEiGqFb8xo9Q0o+jcdrirTJKvuJuGh5Hm/4jjRvu4O3 107 | 1Qr6yBnUTsDcXkDy8G0oenhDMceZEbIz+WOqmxKx7eGl0OxE0CNt 108 | -----END RSA PRIVATE KEY----- 109 | ` 110 | -------------------------------------------------------------------------------- /sshd/client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ssh foo@localhost -p 2200 -------------------------------------------------------------------------------- /sshd/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAzNO5vZPpP7WgXA3Ck5NeCq85i1v2JCB5vM0udK+oWrCQpMdy 3 | oKZlxC8z8n/mSsylm+2xEm+kAFxyvB9ae/Pr8Lh0czePw473Qx2v78E/HdouXn3w 4 | xEHG12IoDUdC7Rt4faxNdfsebd/wWybHEV6vOEDDkxmppJ1y6Cbgx6a59X0wqW54 5 | bTKy5D98iLMzSvWi6AUS3I/hP53f7mNK7cTPqHTdVOwICgCGHOI1hcDKwMafj590 6 | +3H/F5ACYRl9Keuij09zsk+QkI+7HJN5HUtq9mjJ9Mw4vo9LzqIWTOWncEvX5b2f 7 | 99GOOlsBNh91L3PNwQdf1M++CM6F0HTv5p8ioQIDAQABAoIBACwruJF2dUWE8IkJ 8 | ep2CmTQqp3kzIriVvEsH4G3Pd7ne+8JdNI4KdEXDfCteg5Y73bbrolT8eFyPkzqY 9 | dFXou0fVL1+tarZcfVwe6dMFVIwmgftko2hfWvcVttduN7OUSf6oCqhXuC8vrNCr 10 | YyCOz7CM3uA5F4llXuNLhwvnG5EhxHk/AVN0SUbJbfKD5DEpqFM33PuITAuIPuSi 11 | Td2qa84WitZ12hBJqtZGngujE/bMZNaY0Lk6EM4L2p47+//z3raScQT2B+eF/LnR 12 | Jn32YaI7np7Y4D7RbW6QZBB/sOkrvtX51tIHIQEYdn4zlfT8+tNeVo9jn0QM77Ky 13 | FcY4a8ECgYEA5vF+P5MeSa+QsUVgK3HY4MuNNRKw4daIFJr/keYLyUwfPYQsdu5V 14 | ZXfJPkQ/y1Xlgek6E/eiiaiJN91hZEkoF6fkXcORCCmjr19FfssC++arTKk/UPxT 15 | y946yFscsZXosssCON7CskGLCiPMn7YwdwQiJ9uvKIxwB2ChfJ/trSkCgYEA4wzY 16 | rp5Pz3lbXg6P7xqYibnIH847PW9GVMGNl6pXfhUkP3NqFD+Oc41S/wD/vv1SVSZ7 17 | 2ih56E7vctxtxc9b5wWcZfzRUbBWrSKwWO1ImqsBdFapxtoOynDL0uHnXaDrQCvW 18 | UsI44d92gmO+MMYst9//I/sLRTrwYrrIvJOVALkCgYEAg0uqVeSDJKtOnKnveeOY 19 | xHyVBCZjL5Hy/Zv9Tmo2KzQ+0o9xZBAttqk6XU8Z4bUs7QW2giGYY6DQmlUfCI/a 20 | 3lASMgh8TOK3b32/mc07HhFPNB9IovdBgLcQPlYmYwPyLqvh0Ik8sXE35gTiUa6X 21 | sSJFdNmdpHTrQBZ82MhnrLkCgYB8wG06HKALhkmOd3/cR4eyfNKZry3bho1lOmf7 22 | AkxKaYFeH6MUdwtlMCx/EmRy4ytev+NjLcQ1wVFNkhH6kwGTAQE7BFtagAJP5PRy 23 | GAZBfV4yNv/X0642yx0ixJ7kUeuQecWr+S1Z5fdukzFICUs+yKOeeGxr4IN+K9Tp 24 | 0EkZeQKBgF58RcI6PZD7mayf0Z58gd+zb2WXL1rTGErYsbVgxkc/TFRaZYK0cb+n 25 | V6WZNy6k5Amx54pv59U34sEiGqFb8xo9Q0o+jcdrirTJKvuJuGh5Hm/4jjRvu4O3 26 | 1Qr6yBnUTsDcXkDy8G0oenhDMceZEbIz+WOqmxKx7eGl0OxE0CNt 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /sshd/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDM07m9k+k/taBcDcKTk14KrzmLW/YkIHm8zS50r6hasJCkx3KgpmXELzPyf+ZKzKWb7bESb6QAXHK8H1p78+vwuHRzN4/DjvdDHa/vwT8d2i5effDEQcbXYigNR0LtG3h9rE11+x5t3/BbJscRXq84QMOTGamknXLoJuDHprn1fTCpbnhtMrLkP3yIszNK9aLoBRLcj+E/nd/uY0rtxM+odN1U7AgKAIYc4jWFwMrAxp+Pn3T7cf8XkAJhGX0p66KPT3OyT5CQj7sck3kdS2r2aMn0zDi+j0vOohZM5adwS9flvZ/30Y46WwE2H3Uvc83BB1/Uz74IzoXQdO/mnyKh test@example.com 2 | -------------------------------------------------------------------------------- /sshd/server.go: -------------------------------------------------------------------------------- 1 | // A small SSH daemon providing bash sessions 2 | // 3 | // Server: 4 | // cd my/new/dir/ 5 | // #generate server keypair 6 | // ssh-keygen -t rsa 7 | // go get -v . 8 | // go run sshd.go 9 | // 10 | // Client: 11 | // ssh foo@localhost -p 2200 #pass=bar 12 | 13 | package main 14 | 15 | import ( 16 | "encoding/binary" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "log" 21 | "net" 22 | "os/exec" 23 | "sync" 24 | "syscall" 25 | "unsafe" 26 | 27 | "github.com/kr/pty" 28 | "golang.org/x/crypto/ssh" 29 | ) 30 | 31 | func main() { 32 | 33 | // In the latest version of crypto/ssh (after Go 1.3), the SSH server type has been removed 34 | // in favour of an SSH connection type. A ssh.ServerConn is created by passing an existing 35 | // net.Conn and a ssh.ServerConfig to ssh.NewServerConn, in effect, upgrading the net.Conn 36 | // into an ssh.ServerConn 37 | 38 | config := &ssh.ServerConfig{ 39 | //Define a function to run when a client attempts a password login 40 | PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { 41 | // Should use constant-time compare (or better, salt+hash) in a production setting. 42 | if c.User() == "foo" && string(pass) == "bar" { 43 | return nil, nil 44 | } 45 | return nil, fmt.Errorf("password rejected for %q", c.User()) 46 | }, 47 | // You may also explicitly allow anonymous client authentication, though anon bash 48 | // sessions may not be a wise idea 49 | // NoClientAuth: true, 50 | } 51 | 52 | // You can generate a keypair with 'ssh-keygen -t rsa' 53 | privateBytes, err := ioutil.ReadFile("id_rsa") 54 | if err != nil { 55 | log.Fatal("Failed to load private key (./id_rsa)") 56 | } 57 | 58 | private, err := ssh.ParsePrivateKey(privateBytes) 59 | if err != nil { 60 | log.Fatal("Failed to parse private key") 61 | } 62 | 63 | config.AddHostKey(private) 64 | 65 | // Once a ServerConfig has been configured, connections can be accepted. 66 | listener, err := net.Listen("tcp", "0.0.0.0:2200") 67 | if err != nil { 68 | log.Fatalf("Failed to listen on 2200 (%s)", err) 69 | } 70 | 71 | // Accept all connections 72 | log.Print("Listening on 2200...") 73 | for { 74 | tcpConn, err := listener.Accept() 75 | if err != nil { 76 | log.Printf("Failed to accept incoming connection (%s)", err) 77 | continue 78 | } 79 | // Before use, a handshake must be performed on the incoming net.Conn. 80 | sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config) 81 | if err != nil { 82 | log.Printf("Failed to handshake (%s)", err) 83 | continue 84 | } 85 | 86 | log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) 87 | // Discard all global out-of-band Requests 88 | go ssh.DiscardRequests(reqs) 89 | // Accept all channels 90 | go handleChannels(chans) 91 | } 92 | } 93 | 94 | func handleChannels(chans <-chan ssh.NewChannel) { 95 | // Service the incoming Channel channel in go routine 96 | for newChannel := range chans { 97 | go handleChannel(newChannel) 98 | } 99 | } 100 | 101 | func handleChannel(newChannel ssh.NewChannel) { 102 | // Since we're handling a shell, we expect a 103 | // channel type of "session". The also describes 104 | // "x11", "direct-tcpip" and "forwarded-tcpip" 105 | // channel types. 106 | if t := newChannel.ChannelType(); t != "session" { 107 | newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) 108 | return 109 | } 110 | 111 | // At this point, we have the opportunity to reject the client's 112 | // request for another logical connection 113 | connection, requests, err := newChannel.Accept() 114 | if err != nil { 115 | log.Printf("Could not accept channel (%s)", err) 116 | return 117 | } 118 | 119 | // Fire up bash for this session 120 | bash := exec.Command("bash") 121 | 122 | // Prepare teardown function 123 | close := func() { 124 | connection.Close() 125 | _, err := bash.Process.Wait() 126 | if err != nil { 127 | log.Printf("Failed to exit bash (%s)", err) 128 | } 129 | log.Printf("Session closed") 130 | } 131 | 132 | // Allocate a terminal for this channel 133 | log.Print("Creating pty...") 134 | bashf, err := pty.Start(bash) 135 | if err != nil { 136 | log.Printf("Could not start pty (%s)", err) 137 | close() 138 | return 139 | } 140 | 141 | //pipe session to bash and visa-versa 142 | var once sync.Once 143 | go func() { 144 | io.Copy(connection, bashf) 145 | once.Do(close) 146 | }() 147 | go func() { 148 | io.Copy(bashf, connection) 149 | once.Do(close) 150 | }() 151 | 152 | // Sessions have out-of-band requests such as "shell", "pty-req" and "env" 153 | go func() { 154 | for req := range requests { 155 | switch req.Type { 156 | case "shell": 157 | // We only accept the default shell 158 | // (i.e. no command in the Payload) 159 | if len(req.Payload) == 0 { 160 | req.Reply(true, nil) 161 | } 162 | case "pty-req": 163 | termLen := req.Payload[3] 164 | w, h := parseDims(req.Payload[termLen+4:]) 165 | SetWinsize(bashf.Fd(), w, h) 166 | // Responding true (OK) here will let the client 167 | // know we have a pty ready for input 168 | req.Reply(true, nil) 169 | case "window-change": 170 | w, h := parseDims(req.Payload) 171 | SetWinsize(bashf.Fd(), w, h) 172 | } 173 | } 174 | }() 175 | } 176 | 177 | // ======================= 178 | 179 | // parseDims extracts terminal dimensions (width x height) from the provided buffer. 180 | func parseDims(b []byte) (uint32, uint32) { 181 | w := binary.BigEndian.Uint32(b) 182 | h := binary.BigEndian.Uint32(b[4:]) 183 | return w, h 184 | } 185 | 186 | // ====================== 187 | 188 | // Winsize stores the Height and Width of a terminal. 189 | type Winsize struct { 190 | Height uint16 191 | Width uint16 192 | x uint16 // unused 193 | y uint16 // unused 194 | } 195 | 196 | // SetWinsize sets the size of the given pty. 197 | func SetWinsize(fd uintptr, w, h uint32) { 198 | ws := &Winsize{Width: uint16(w), Height: uint16(h)} 199 | syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) 200 | } 201 | 202 | // Borrowed from https://github.com/creack/termios/blob/master/win/win.go 203 | --------------------------------------------------------------------------------