Fixes golang/go#3810. This change introduces an atomic boolean to guard the close of the clientChan. Previously the client code was quite lax with the ordering of the close messages and could allow window adjustment or EOF messages to leak after Close had been signaled. Consolidating the changes to the serverChan will be handled in a following CL. R=agl, kardianos, gustav.paul CC=golang-dev https://golang.org/cl/6405064
507 lines
13 KiB
Go
507 lines
13 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"sync"
|
|
)
|
|
|
|
// clientVersion is the fixed identification string that the client will use.
|
|
var clientVersion = []byte("SSH-2.0-Go\r\n")
|
|
|
|
// ClientConn represents the client side of an SSH connection.
|
|
type ClientConn struct {
|
|
*transport
|
|
config *ClientConfig
|
|
chanList // channels associated with this connection
|
|
forwardList // forwarded tcpip connections from the remote side
|
|
globalRequest
|
|
}
|
|
|
|
type globalRequest struct {
|
|
sync.Mutex
|
|
response chan interface{}
|
|
}
|
|
|
|
// Client returns a new SSH client connection using c as the underlying transport.
|
|
func Client(c net.Conn, config *ClientConfig) (*ClientConn, error) {
|
|
conn := &ClientConn{
|
|
transport: newTransport(c, config.rand()),
|
|
config: config,
|
|
globalRequest: globalRequest{response: make(chan interface{}, 1)},
|
|
}
|
|
if err := conn.handshake(); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
go conn.mainLoop()
|
|
return conn, nil
|
|
}
|
|
|
|
// handshake performs the client side key exchange. See RFC 4253 Section 7.
|
|
func (c *ClientConn) handshake() error {
|
|
var magics handshakeMagics
|
|
|
|
if _, err := c.Write(clientVersion); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Flush(); err != nil {
|
|
return err
|
|
}
|
|
magics.clientVersion = clientVersion[:len(clientVersion)-2]
|
|
|
|
// read remote server version
|
|
version, err := readVersion(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
magics.serverVersion = version
|
|
clientKexInit := kexInitMsg{
|
|
KexAlgos: supportedKexAlgos,
|
|
ServerHostKeyAlgos: supportedHostKeyAlgos,
|
|
CiphersClientServer: c.config.Crypto.ciphers(),
|
|
CiphersServerClient: c.config.Crypto.ciphers(),
|
|
MACsClientServer: c.config.Crypto.macs(),
|
|
MACsServerClient: c.config.Crypto.macs(),
|
|
CompressionClientServer: supportedCompressions,
|
|
CompressionServerClient: supportedCompressions,
|
|
}
|
|
kexInitPacket := marshal(msgKexInit, clientKexInit)
|
|
magics.clientKexInit = kexInitPacket
|
|
|
|
if err := c.writePacket(kexInitPacket); err != nil {
|
|
return err
|
|
}
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
magics.serverKexInit = packet
|
|
|
|
var serverKexInit kexInitMsg
|
|
if err = unmarshal(&serverKexInit, packet, msgKexInit); err != nil {
|
|
return err
|
|
}
|
|
|
|
kexAlgo, hostKeyAlgo, ok := findAgreedAlgorithms(c.transport, &clientKexInit, &serverKexInit)
|
|
if !ok {
|
|
return errors.New("ssh: no common algorithms")
|
|
}
|
|
|
|
if serverKexInit.FirstKexFollows && kexAlgo != serverKexInit.KexAlgos[0] {
|
|
// The server sent a Kex message for the wrong algorithm,
|
|
// which we have to ignore.
|
|
if _, err := c.readPacket(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var H, K []byte
|
|
var hashFunc crypto.Hash
|
|
switch kexAlgo {
|
|
case kexAlgoDH14SHA1:
|
|
hashFunc = crypto.SHA1
|
|
dhGroup14Once.Do(initDHGroup14)
|
|
H, K, err = c.kexDH(dhGroup14, hashFunc, &magics, hostKeyAlgo)
|
|
case keyAlgoDH1SHA1:
|
|
hashFunc = crypto.SHA1
|
|
dhGroup1Once.Do(initDHGroup1)
|
|
H, K, err = c.kexDH(dhGroup1, hashFunc, &magics, hostKeyAlgo)
|
|
default:
|
|
err = fmt.Errorf("ssh: unexpected key exchange algorithm %v", kexAlgo)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = c.writePacket([]byte{msgNewKeys}); err != nil {
|
|
return err
|
|
}
|
|
if err = c.transport.writer.setupKeys(clientKeys, K, H, H, hashFunc); err != nil {
|
|
return err
|
|
}
|
|
if packet, err = c.readPacket(); err != nil {
|
|
return err
|
|
}
|
|
if packet[0] != msgNewKeys {
|
|
return UnexpectedMessageError{msgNewKeys, packet[0]}
|
|
}
|
|
if err := c.transport.reader.setupKeys(serverKeys, K, H, H, hashFunc); err != nil {
|
|
return err
|
|
}
|
|
return c.authenticate(H)
|
|
}
|
|
|
|
// kexDH performs Diffie-Hellman key agreement on a ClientConn. The
|
|
// returned values are given the same names as in RFC 4253, section 8.
|
|
func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) ([]byte, []byte, error) {
|
|
x, err := rand.Int(c.config.rand(), group.p)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
X := new(big.Int).Exp(group.g, x, group.p)
|
|
kexDHInit := kexDHInitMsg{
|
|
X: X,
|
|
}
|
|
if err := c.writePacket(marshal(msgKexDHInit, kexDHInit)); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var kexDHReply kexDHReplyMsg
|
|
if err = unmarshal(&kexDHReply, packet, msgKexDHReply); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
kInt, err := group.diffieHellman(kexDHReply.Y, x)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
h := hashFunc.New()
|
|
writeString(h, magics.clientVersion)
|
|
writeString(h, magics.serverVersion)
|
|
writeString(h, magics.clientKexInit)
|
|
writeString(h, magics.serverKexInit)
|
|
writeString(h, kexDHReply.HostKey)
|
|
writeInt(h, X)
|
|
writeInt(h, kexDHReply.Y)
|
|
K := make([]byte, intLength(kInt))
|
|
marshalInt(K, kInt)
|
|
h.Write(K)
|
|
|
|
H := h.Sum(nil)
|
|
|
|
return H, K, nil
|
|
}
|
|
|
|
// mainLoop reads incoming messages and routes channel messages
|
|
// to their respective ClientChans.
|
|
func (c *ClientConn) mainLoop() {
|
|
defer func() {
|
|
c.Close()
|
|
c.closeAll()
|
|
}()
|
|
|
|
for {
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
break
|
|
}
|
|
// TODO(dfc) A note on blocking channel use.
|
|
// The msg, data and dataExt channels of a clientChan can
|
|
// cause this loop to block indefinately if the consumer does
|
|
// not service them.
|
|
switch packet[0] {
|
|
case msgChannelData:
|
|
if len(packet) < 9 {
|
|
// malformed data packet
|
|
return
|
|
}
|
|
remoteId := binary.BigEndian.Uint32(packet[1:5])
|
|
length := binary.BigEndian.Uint32(packet[5:9])
|
|
packet = packet[9:]
|
|
|
|
if length != uint32(len(packet)) {
|
|
return
|
|
}
|
|
ch, ok := c.getChan(remoteId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.stdout.write(packet)
|
|
case msgChannelExtendedData:
|
|
if len(packet) < 13 {
|
|
// malformed data packet
|
|
return
|
|
}
|
|
remoteId := binary.BigEndian.Uint32(packet[1:5])
|
|
datatype := binary.BigEndian.Uint32(packet[5:9])
|
|
length := binary.BigEndian.Uint32(packet[9:13])
|
|
packet = packet[13:]
|
|
|
|
if length != uint32(len(packet)) {
|
|
return
|
|
}
|
|
// RFC 4254 5.2 defines data_type_code 1 to be data destined
|
|
// for stderr on interactive sessions. Other data types are
|
|
// silently discarded.
|
|
if datatype == 1 {
|
|
ch, ok := c.getChan(remoteId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.stderr.write(packet)
|
|
}
|
|
default:
|
|
switch msg := decode(packet).(type) {
|
|
case *channelOpenMsg:
|
|
c.handleChanOpen(msg)
|
|
case *channelOpenConfirmMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.msg <- msg
|
|
case *channelOpenFailureMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.msg <- msg
|
|
case *channelCloseMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.Close()
|
|
// TODO(dfc) may need to optimisically remove the
|
|
// channel before closing
|
|
c.chanList.remove(msg.PeersId)
|
|
case *channelEOFMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.stdout.eof()
|
|
// RFC 4254 is mute on how EOF affects dataExt messages but
|
|
// it is logical to signal EOF at the same time.
|
|
ch.stderr.eof()
|
|
case *channelRequestSuccessMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.msg <- msg
|
|
case *channelRequestFailureMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.msg <- msg
|
|
case *channelRequestMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
ch.msg <- msg
|
|
case *windowAdjustMsg:
|
|
ch, ok := c.getChan(msg.PeersId)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !ch.remoteWin.add(msg.AdditionalBytes) {
|
|
// invalid window update
|
|
return
|
|
}
|
|
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
|
|
c.globalRequest.response <- msg
|
|
case *disconnectMsg:
|
|
return
|
|
default:
|
|
fmt.Printf("mainLoop: unhandled message %T: %v\n", msg, msg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle channel open messages from the remote side.
|
|
func (c *ClientConn) handleChanOpen(msg *channelOpenMsg) {
|
|
switch msg.ChanType {
|
|
case "forwarded-tcpip":
|
|
laddr, rest, ok := parseTCPAddr(msg.TypeSpecificData)
|
|
if !ok {
|
|
// invalid request
|
|
c.sendConnectionFailed(msg.PeersId)
|
|
return
|
|
}
|
|
l, ok := c.forwardList.lookup(laddr)
|
|
if !ok {
|
|
fmt.Println("could not find forward list entry for", laddr)
|
|
// Section 7.2, implementations MUST reject suprious incoming
|
|
// connections.
|
|
c.sendConnectionFailed(msg.PeersId)
|
|
return
|
|
}
|
|
raddr, rest, ok := parseTCPAddr(rest)
|
|
if !ok {
|
|
// invalid request
|
|
c.sendConnectionFailed(msg.PeersId)
|
|
return
|
|
}
|
|
ch := c.newChan(c.transport)
|
|
ch.remoteId = msg.PeersId
|
|
ch.remoteWin.add(msg.PeersWindow)
|
|
|
|
m := channelOpenConfirmMsg{
|
|
PeersId: ch.remoteId,
|
|
MyId: ch.localId,
|
|
MyWindow: 1 << 14,
|
|
MaxPacketSize: 1 << 15, // RFC 4253 6.1
|
|
}
|
|
c.writePacket(marshal(msgChannelOpenConfirm, m))
|
|
l <- forward{ch, raddr}
|
|
default:
|
|
// unknown channel type
|
|
m := channelOpenFailureMsg{
|
|
PeersId: msg.PeersId,
|
|
Reason: UnknownChannelType,
|
|
Message: fmt.Sprintf("unknown channel type: %v", msg.ChanType),
|
|
Language: "en_US.UTF-8",
|
|
}
|
|
c.writePacket(marshal(msgChannelOpenFailure, m))
|
|
}
|
|
}
|
|
|
|
// sendGlobalRequest sends a global request message as specified
|
|
// in RFC4254 section 4. To correctly synchronise messages, a lock
|
|
// is held internally until a response is returned.
|
|
func (c *ClientConn) sendGlobalRequest(m interface{}) (*globalRequestSuccessMsg, error) {
|
|
c.globalRequest.Lock()
|
|
defer c.globalRequest.Unlock()
|
|
if err := c.writePacket(marshal(msgGlobalRequest, m)); err != nil {
|
|
return nil, err
|
|
}
|
|
r := <-c.globalRequest.response
|
|
if r, ok := r.(*globalRequestSuccessMsg); ok {
|
|
return r, nil
|
|
}
|
|
return nil, errors.New("request failed")
|
|
}
|
|
|
|
// sendConnectionFailed rejects an incoming channel identified
|
|
// by remoteId.
|
|
func (c *ClientConn) sendConnectionFailed(remoteId uint32) error {
|
|
m := channelOpenFailureMsg{
|
|
PeersId: remoteId,
|
|
Reason: ConnectionFailed,
|
|
Message: "invalid request",
|
|
Language: "en_US.UTF-8",
|
|
}
|
|
return c.writePacket(marshal(msgChannelOpenFailure, m))
|
|
}
|
|
|
|
// parseTCPAddr parses the originating address from the remote into a *net.TCPAddr.
|
|
// RFC 4254 section 7.2 is mute on what to do if parsing fails but the forwardlist
|
|
// requires a valid *net.TCPAddr to operate, so we enforce that restriction here.
|
|
func parseTCPAddr(b []byte) (*net.TCPAddr, []byte, bool) {
|
|
addr, b, ok := parseString(b)
|
|
if !ok {
|
|
return nil, b, false
|
|
}
|
|
port, b, ok := parseUint32(b)
|
|
if !ok {
|
|
return nil, b, false
|
|
}
|
|
ip := net.ParseIP(string(addr))
|
|
if ip == nil {
|
|
return nil, b, false
|
|
}
|
|
return &net.TCPAddr{ip, int(port)}, b, true
|
|
}
|
|
|
|
// Dial connects to the given network address using net.Dial and
|
|
// then initiates a SSH handshake, returning the resulting client connection.
|
|
func Dial(network, addr string, config *ClientConfig) (*ClientConn, error) {
|
|
conn, err := net.Dial(network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Client(conn, config)
|
|
}
|
|
|
|
// A ClientConfig structure is used to configure a ClientConn. After one has
|
|
// been passed to an SSH function it must not be modified.
|
|
type ClientConfig struct {
|
|
// Rand provides the source of entropy for key exchange. If Rand is
|
|
// nil, the cryptographic random reader in package crypto/rand will
|
|
// be used.
|
|
Rand io.Reader
|
|
|
|
// The username to authenticate.
|
|
User string
|
|
|
|
// A slice of ClientAuth methods. Only the first instance
|
|
// of a particular RFC 4252 method will be used during authentication.
|
|
Auth []ClientAuth
|
|
|
|
// Cryptographic-related configuration.
|
|
Crypto CryptoConfig
|
|
}
|
|
|
|
func (c *ClientConfig) rand() io.Reader {
|
|
if c.Rand == nil {
|
|
return rand.Reader
|
|
}
|
|
return c.Rand
|
|
}
|
|
|
|
// Thread safe channel list.
|
|
type chanList struct {
|
|
// protects concurrent access to chans
|
|
sync.Mutex
|
|
// chans are indexed by the local id of the channel, clientChan.localId.
|
|
// The PeersId value of messages received by ClientConn.mainLoop is
|
|
// used to locate the right local clientChan in this slice.
|
|
chans []*clientChan
|
|
}
|
|
|
|
// Allocate a new ClientChan with the next avail local id.
|
|
func (c *chanList) newChan(t *transport) *clientChan {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for i := range c.chans {
|
|
if c.chans[i] == nil {
|
|
ch := newClientChan(t, uint32(i))
|
|
c.chans[i] = ch
|
|
return ch
|
|
}
|
|
}
|
|
i := len(c.chans)
|
|
ch := newClientChan(t, uint32(i))
|
|
c.chans = append(c.chans, ch)
|
|
return ch
|
|
}
|
|
|
|
func (c *chanList) getChan(id uint32) (*clientChan, bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if id >= uint32(len(c.chans)) {
|
|
return nil, false
|
|
}
|
|
return c.chans[id], true
|
|
}
|
|
|
|
func (c *chanList) remove(id uint32) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.chans[id] = nil
|
|
}
|
|
|
|
func (c *chanList) closeAll() {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
for _, ch := range c.chans {
|
|
if ch == nil {
|
|
continue
|
|
}
|
|
ch.Close()
|
|
}
|
|
}
|