]> Cypherpunks repositories - gostls13.git/commitdiff
exp/ssh: introduce Session to replace Cmd for interactive commands
authorDave Cheney <dave@cheney.net>
Mon, 24 Oct 2011 23:13:55 +0000 (19:13 -0400)
committerAdam Langley <agl@golang.org>
Mon, 24 Oct 2011 23:13:55 +0000 (19:13 -0400)
This CL replaces the Cmd type with a Session type representing
interactive channels. This lays the foundation for supporting
other kinds of channels like direct-tcpip or x11.

client.go:
* replace chanlist map with slice.
* generalize stdout and stderr into a single type.
* unexport ClientChan to clientChan.

doc.go:
* update ServerConfig/ServerConn documentation.
* update Client example for Session.

message.go:
* make channelExtendedData more like channelData.

session.go:
* added Session which replaces Cmd.

R=agl, rsc, n13m3y3r, gustavo
CC=golang-dev
https://golang.org/cl/5302054

src/pkg/exp/ssh/Makefile
src/pkg/exp/ssh/client.go
src/pkg/exp/ssh/doc.go
src/pkg/exp/ssh/messages.go
src/pkg/exp/ssh/session.go [new file with mode: 0644]

index 1084d029db403fa671481a284ee2d09a6052f353..8e007a4b2168f4df13f5e4b1db8bb766bbb35fb7 100644 (file)
@@ -13,5 +13,6 @@ GOFILES=\
        transport.go\
        server.go\
        server_shell.go\
+       session.go\
 
 include ../../../Make.pkg
index b3d7708a265ffd02f8aa21bb307a9812e9cfbc77..9223b6c3cf21b60aad69586e4e40353048eeb4c4 100644 (file)
@@ -8,7 +8,6 @@ import (
        "big"
        "crypto"
        "crypto/rand"
-       "encoding/binary"
        "fmt"
        "io"
        "os"
@@ -31,10 +30,6 @@ func Client(c net.Conn, config *ClientConfig) (*ClientConn, os.Error) {
        conn := &ClientConn{
                transport: newTransport(c, config.rand()),
                config:    config,
-               chanlist: chanlist{
-                       Mutex: new(sync.Mutex),
-                       chans: make(map[uint32]*ClientChan),
-               },
        }
        if err := conn.handshake(); err != nil {
                conn.Close()
@@ -233,18 +228,17 @@ func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handsha
        return H, K, nil
 }
 
-// OpenChan opens a new client channel. The most common session type is "session". 
+// openChan opens a new client channel. The most common session type is "session". 
 // The full set of valid session types are listed in RFC 4250 4.9.1.
-func (c *ClientConn) OpenChan(typ string) (*ClientChan, os.Error) {
-       ch, id := c.newChan(c.transport)
+func (c *ClientConn) openChan(typ string) (*clientChan, os.Error) {
+       ch := c.newChan(c.transport)
        if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{
                ChanType:      typ,
-               PeersId:       id,
-               PeersWindow:   8192,
-               MaxPacketSize: 16384,
+               PeersId:       ch.id,
+               PeersWindow:   1 << 14,
+               MaxPacketSize: 1 << 15, // RFC 4253 6.1
        })); err != nil {
-               // remove channel reference
-               c.chanlist.remove(id)
+               c.chanlist.remove(ch.id)
                return nil, err
        }
        // wait for response
@@ -252,10 +246,10 @@ func (c *ClientConn) OpenChan(typ string) (*ClientChan, os.Error) {
        case *channelOpenConfirmMsg:
                ch.peersId = msg.MyId
        case *channelOpenFailureMsg:
-               c.chanlist.remove(id)
+               c.chanlist.remove(ch.id)
                return nil, os.NewError(msg.Message)
        default:
-               c.chanlist.remove(id)
+               c.chanlist.remove(ch.id)
                return nil, os.NewError("Unexpected packet")
        }
        return ch, nil
@@ -271,6 +265,10 @@ func (c *ClientConn) mainLoop() {
                        c.Close()
                        return
                }
+               // TODO(dfc) A note on blocking channel use. 
+               // The msg, win, data and dataExt channels of a clientChan can 
+               // cause this loop to block indefinately if the consumer does 
+               // not service them. 
                switch msg := decode(packet).(type) {
                case *channelOpenMsg:
                        c.getChan(msg.PeersId).msg <- msg
@@ -280,9 +278,9 @@ func (c *ClientConn) mainLoop() {
                        c.getChan(msg.PeersId).msg <- msg
                case *channelCloseMsg:
                        ch := c.getChan(msg.PeersId)
-                       close(ch.stdinWriter.win)
-                       close(ch.stdoutReader.data)
-                       close(ch.stderrReader.dataExt)
+                       close(ch.win)
+                       close(ch.data)
+                       close(ch.dataExt)
                        c.chanlist.remove(msg.PeersId)
                case *channelEOFMsg:
                        c.getChan(msg.PeersId).msg <- msg
@@ -293,13 +291,16 @@ func (c *ClientConn) mainLoop() {
                case *channelRequestMsg:
                        c.getChan(msg.PeersId).msg <- msg
                case *windowAdjustMsg:
-                       c.getChan(msg.PeersId).stdinWriter.win <- int(msg.AdditionalBytes)
+                       c.getChan(msg.PeersId).win <- int(msg.AdditionalBytes)
                case *channelData:
-                       c.getChan(msg.PeersId).stdoutReader.data <- msg.Payload
+                       c.getChan(msg.PeersId).data <- msg.Payload
                case *channelExtendedData:
-                       // TODO(dfc) should this send be non blocking. RFC 4254 5.2 suggests
-                       // ext data consumes window size, does that need to be handled as well ?
-                       c.getChan(msg.PeersId).stderrReader.dataExt <- msg.Data
+                       // 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 msg.Datatype == 1 {
+                               c.getChan(msg.PeersId).dataExt <- msg.Payload
+                       }
                default:
                        fmt.Printf("mainLoop: unhandled %#v\n", msg)
                }
@@ -338,207 +339,95 @@ func (c *ClientConfig) rand() io.Reader {
        return c.Rand
 }
 
-// A ClientChan represents a single RFC 4254 channel that is multiplexed 
+// A clientChan represents a single RFC 4254 channel that is multiplexed 
 // over a single SSH connection.
-type ClientChan struct {
+type clientChan struct {
        packetWriter
-       *stdinWriter  // used by Exec and Shell
-       *stdoutReader // used by Exec and Shell
-       *stderrReader // used by Exec and Shell
-       id, peersId   uint32
-       msg           chan interface{} // incoming messages 
+       id, peersId uint32
+       data        chan []byte      // receives the payload of channelData messages
+       dataExt     chan []byte      // receives the payload of channelExtendedData messages
+       win         chan int         // receives window adjustments
+       msg         chan interface{} // incoming messages
 }
 
-func newClientChan(t *transport, id uint32) *ClientChan {
-       // TODO(DFC) allocating stdin/out/err on ClientChan creation is
-       // wasteful, but ClientConn.mainLoop() needs a way of finding 
-       // those channels before Exec/Shell is called because the remote 
-       // may send window adjustments at any time.
-       return &ClientChan{
+func newClientChan(t *transport, id uint32) *clientChan {
+       return &clientChan{
                packetWriter: t,
-               stdinWriter: &stdinWriter{
-                       packetWriter: t,
-                       id:           id,
-                       win:          make(chan int, 16),
-               },
-               stdoutReader: &stdoutReader{
-                       packetWriter: t,
-                       id:           id,
-                       win:          8192,
-                       data:         make(chan []byte, 16),
-               },
-               stderrReader: &stderrReader{
-                       dataExt: make(chan string, 16),
-               },
-               id:  id,
-               msg: make(chan interface{}, 16),
+               id:           id,
+               data:         make(chan []byte, 16),
+               dataExt:      make(chan []byte, 16),
+               win:          make(chan int, 16),
+               msg:          make(chan interface{}, 16),
        }
 }
 
 // Close closes the channel. This does not close the underlying connection.
-func (c *ClientChan) Close() os.Error {
+func (c *clientChan) Close() os.Error {
        return c.writePacket(marshal(msgChannelClose, channelCloseMsg{
                PeersId: c.id,
        }))
 }
 
-// Setenv sets an environment variable that will be applied to any
-// command executed by Shell or Exec.
-func (c *ClientChan) Setenv(name, value string) os.Error {
-       namLen := stringLength([]byte(name))
-       valLen := stringLength([]byte(value))
-       payload := make([]byte, namLen+valLen)
-       marshalString(payload[:namLen], []byte(name))
-       marshalString(payload[namLen:], []byte(value))
-
-       return c.sendChanReq(channelRequestMsg{
-               PeersId:             c.id,
-               Request:             "env",
-               WantReply:           true,
-               RequestSpecificData: payload,
-       })
-}
-
-func (c *ClientChan) sendChanReq(req channelRequestMsg) os.Error {
+func (c *clientChan) sendChanReq(req channelRequestMsg) os.Error {
        if err := c.writePacket(marshal(msgChannelRequest, req)); err != nil {
                return err
        }
-       for {
-               switch msg := (<-c.msg).(type) {
-               case *channelRequestSuccessMsg:
-                       return nil
-               case *channelRequestFailureMsg:
-                       return os.NewError(req.Request)
-               default:
-                       return fmt.Errorf("%#v", msg)
-               }
+       msg := <-c.msg
+       if _, ok := msg.(*channelRequestSuccessMsg); ok {
+               return nil
        }
-       panic("unreachable")
-}
-
-// An empty mode list (a string of 1 character, opcode 0), see RFC 4254 Section 8.
-var emptyModeList = []byte{0, 0, 0, 1, 0}
-
-// RequstPty requests a pty to be allocated on the remote side of this channel.
-func (c *ClientChan) RequestPty(term string, h, w int) os.Error {
-       buf := make([]byte, 4+len(term)+16+len(emptyModeList))
-       b := marshalString(buf, []byte(term))
-       binary.BigEndian.PutUint32(b, uint32(h))
-       binary.BigEndian.PutUint32(b[4:], uint32(w))
-       binary.BigEndian.PutUint32(b[8:], uint32(h*8))
-       binary.BigEndian.PutUint32(b[12:], uint32(w*8))
-       copy(b[16:], emptyModeList)
-
-       return c.sendChanReq(channelRequestMsg{
-               PeersId:             c.id,
-               Request:             "pty-req",
-               WantReply:           true,
-               RequestSpecificData: buf,
-       })
-}
-
-// Exec runs cmd on the remote host.
-// Typically, the remote server passes cmd to the shell for interpretation.
-func (c *ClientChan) Exec(cmd string) (*Cmd, os.Error) {
-       cmdLen := stringLength([]byte(cmd))
-       payload := make([]byte, cmdLen)
-       marshalString(payload, []byte(cmd))
-       err := c.sendChanReq(channelRequestMsg{
-               PeersId:             c.id,
-               Request:             "exec",
-               WantReply:           true,
-               RequestSpecificData: payload,
-       })
-       return &Cmd{
-               c.stdinWriter,
-               c.stdoutReader,
-               c.stderrReader,
-       }, err
-}
-
-// Shell starts a login shell on the remote host.
-func (c *ClientChan) Shell() (*Cmd, os.Error) {
-       err := c.sendChanReq(channelRequestMsg{
-               PeersId:   c.id,
-               Request:   "shell",
-               WantReply: true,
-       })
-       return &Cmd{
-               c.stdinWriter,
-               c.stdoutReader,
-               c.stderrReader,
-       }, err
-
+       return fmt.Errorf("failed to complete request: %s, %#v", req.Request, msg)
 }
 
 // Thread safe channel list.
 type chanlist struct {
-       *sync.Mutex
-       // TODO(dfc) should could be converted to a slice
-       chans map[uint32]*ClientChan
+       // protects concurrent access to chans
+       sync.Mutex
+       // chans are indexed by the local id of the channel, clientChan.id.
+       // 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, uint32) {
+func (c *chanlist) newChan(t *transport) *clientChan {
        c.Lock()
        defer c.Unlock()
-
-       for i := uint32(0); i < 1<<31; i++ {
-               if _, ok := c.chans[i]; !ok {
-                       ch := newClientChan(t, i)
+       for i := range c.chans {
+               if c.chans[i] == nil {
+                       ch := newClientChan(t, uint32(i))
                        c.chans[i] = ch
-                       return ch, uint32(i)
+                       return ch
                }
        }
-       panic("unable to find free channel")
+       i := len(c.chans)
+       ch := newClientChan(t, uint32(i))
+       c.chans = append(c.chans, ch)
+       return ch
 }
 
-func (c *chanlist) getChan(id uint32) *ClientChan {
+func (c *chanlist) getChan(id uint32) *clientChan {
        c.Lock()
        defer c.Unlock()
-       return c.chans[id]
+       return c.chans[int(id)]
 }
 
 func (c *chanlist) remove(id uint32) {
        c.Lock()
        defer c.Unlock()
-       delete(c.chans, id)
+       c.chans[int(id)] = nil
 }
 
-// A Cmd represents a connection to a remote command or shell
-// Closing Cmd.Stdin will be observed by the remote process.
-type Cmd struct {
-       // Writes to Stdin are made available to the command's standard input.
-       // Closing Stdin causes the command to observe an EOF on its standard input.
-       Stdin io.WriteCloser
-
-       // Reads from Stdout consume the command's standard output.
-       // There is a fixed amount of buffering of the command's standard output.
-       // Failing to read from Stdout will eventually cause the command to block
-       // when writing to its standard output.  Closing Stdout unblocks any
-       // such writes and makes them return errors.
-       Stdout io.ReadCloser
-
-       // Reads from Stderr consume the command's standard error.
-       // The SSH protocol assumes it can always send standard error;
-       // the command will never block writing to its standard error.
-       // However, failure to read from Stderr will eventually cause the
-       // SSH protocol to jam, so it is important to arrange for reading
-       // from Stderr, even if by
-       //    go io.Copy(ioutil.Discard, cmd.Stderr)
-       Stderr io.Reader
-}
-
-// A stdinWriter represents the stdin of a remote process.
-type stdinWriter struct {
+// A chanWriter represents the stdin of a remote process.
+type chanWriter struct {
        win          chan int // receives window adjustments
-       id           uint32
-       rwin         int // current rwin size
-       packetWriter     // for sending channelDataMsg
+       id           uint32   // this channel's id
+       rwin         int      // current rwin size
+       packetWriter          // for sending channelDataMsg
 }
 
 // Write writes data to the remote process's standard input.
-func (w *stdinWriter) Write(data []byte) (n int, err os.Error) {
+func (w *chanWriter) Write(data []byte) (n int, err os.Error) {
        for {
                if w.rwin == 0 {
                        win, ok := <-w.win
@@ -560,69 +449,42 @@ func (w *stdinWriter) Write(data []byte) (n int, err os.Error) {
        panic("unreachable")
 }
 
-func (w *stdinWriter) Close() os.Error {
+func (w *chanWriter) Close() os.Error {
        return w.writePacket(marshal(msgChannelEOF, channelEOFMsg{w.id}))
 }
 
-// A stdoutReader represents the stdout of a remote process.
-type stdoutReader struct {
+// A chanReader represents stdout or stderr of a remote process.
+type chanReader struct {
        // TODO(dfc) a fixed size channel may not be the right data structure.
        // If writes to this channel block, they will block mainLoop, making
        // it unable to receive new messages from the remote side.
        data         chan []byte // receives data from remote
        id           uint32
-       win          int // current win size
-       packetWriter     // for sending windowAdjustMsg
+       packetWriter // for sending windowAdjustMsg
        buf          []byte
 }
 
-// Read reads data from the remote process's standard output.
-func (r *stdoutReader) Read(data []byte) (int, os.Error) {
+// Read reads data from the remote process's stdout or stderr.
+func (r *chanReader) Read(data []byte) (int, os.Error) {
        var ok bool
        for {
                if len(r.buf) > 0 {
                        n := copy(data, r.buf)
                        r.buf = r.buf[n:]
-                       r.win += n
                        msg := windowAdjustMsg{
                                PeersId:         r.id,
                                AdditionalBytes: uint32(n),
                        }
-                       err := r.writePacket(marshal(msgChannelWindowAdjust, msg))
-                       return n, err
+                       return n, r.writePacket(marshal(msgChannelWindowAdjust, msg))
                }
                r.buf, ok = <-r.data
                if !ok {
                        return 0, os.EOF
                }
-               r.win -= len(r.buf)
        }
        panic("unreachable")
 }
 
-func (r *stdoutReader) Close() os.Error {
+func (r *chanReader) Close() os.Error {
        return r.writePacket(marshal(msgChannelEOF, channelEOFMsg{r.id}))
 }
-
-// A stderrReader represents the stderr of a remote process.
-type stderrReader struct {
-       dataExt chan string // receives dataExt from remote
-       buf     []byte      // buffer current dataExt
-}
-
-// Read reads a line of data from the remote process's stderr.
-func (r *stderrReader) Read(data []byte) (int, os.Error) {
-       for {
-               if len(r.buf) > 0 {
-                       n := copy(data, r.buf)
-                       r.buf = r.buf[n:]
-                       return n, nil
-               }
-               buf, ok := <-r.dataExt
-               if !ok {
-                       return 0, os.EOF
-               }
-               r.buf = []byte(buf)
-       }
-       panic("unreachable")
-}
index a2ec3faca74014a56b054ebde8c9ff936cc27ec1..fc842b0c1d247420892415067a6b5bc790140e10 100644 (file)
@@ -11,26 +11,29 @@ protocol is a remote shell and this is specifically implemented.  However,
 the multiplexed nature of SSH is exposed to users that wish to support
 others.
 
-An SSH server is represented by a Server, which manages a number of
-ServerConnections and handles authentication.
+An SSH server is represented by a ServerConfig, which holds certificate
+details and handles authentication of ServerConns.
 
-       var s Server
-       s.PubKeyCallback = pubKeyAuth
-       s.PasswordCallback = passwordAuth
+       config := new(ServerConfig)
+       config.PubKeyCallback = pubKeyAuth
+       config.PasswordCallback = passwordAuth
 
        pemBytes, err := ioutil.ReadFile("id_rsa")
        if err != nil {
                panic("Failed to load private key")
        }
-       err = s.SetRSAPrivateKey(pemBytes)
+       err = config.SetRSAPrivateKey(pemBytes)
        if err != nil {
                panic("Failed to parse private key")
        }
 
-Once a Server has been set up, connections can be attached.
+Once a ServerConfig has been configured, connections can be accepted.
 
-       var sConn ServerConnection
-       sConn.Server = &s
+       listener := Listen("tcp", "0.0.0.0:2022", config)
+       sConn, err := listener.Accept()
+       if err != nil {
+               panic("failed to accept incoming connection")
+       }
        err = sConn.Handshake(conn)
        if err != nil {
                panic("failed to handshake")
@@ -38,7 +41,6 @@ Once a Server has been set up, connections can be attached.
 
 An SSH connection multiplexes several channels, which must be accepted themselves:
 
-
        for {
                channel, err := sConn.Accept()
                if err != nil {
@@ -85,17 +87,19 @@ authentication method is supported.
        }
        client, err := Dial("yourserver.com:22", config)
 
-Each ClientConn can support multiple channels, represented by ClientChan. Each
-channel should be of a type specified in rfc4250, 4.9.1.
+Each ClientConn can support multiple interactive sessions, represented by a Session. 
 
-       ch, err := client.OpenChan("session")
+       session, err := client.NewSession()
 
-Once the ClientChan is opened, you can execute a single command on the remote side 
+Once a Session is created, you can execute a single command on the remote side 
 using the Exec method.
 
-       cmd, err := ch.Exec("/usr/bin/whoami")
-       reader := bufio.NewReader(cmd.Stdin)
+       if err := session.Exec("/usr/bin/whoami"); err != nil {
+               panic("Failed to exec: " + err.String())
+       }
+       reader := bufio.NewReader(session.Stdin)
        line, _, _ := reader.ReadLine()
        fmt.Println(line)
+       session.Close()
 */
 package ssh
index 1d0bc5774265350f008a2a0d9c87b333af7489c4..7771f2b242e662287c3d64d0b6a9826e916dbca5 100644 (file)
@@ -154,7 +154,7 @@ type channelData struct {
 type channelExtendedData struct {
        PeersId  uint32
        Datatype uint32
-       Data     string
+       Payload  []byte `ssh:"rest"`
 }
 
 type channelRequestMsg struct {
diff --git a/src/pkg/exp/ssh/session.go b/src/pkg/exp/ssh/session.go
new file mode 100644 (file)
index 0000000..13df2f0
--- /dev/null
@@ -0,0 +1,132 @@
+// 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
+
+// Session implements an interactive session described in
+// "RFC 4254, section 6".
+
+import (
+       "encoding/binary"
+       "io"
+       "os"
+)
+
+// A Session represents a connection to a remote command or shell.
+type Session struct {
+       // Writes to Stdin are made available to the remote command's standard input.
+       // Closing Stdin causes the command to observe an EOF on its standard input.
+       Stdin io.WriteCloser
+
+       // Reads from Stdout and Stderr consume from the remote command's standard
+       // output and error streams, respectively.
+       // There is a fixed amount of buffering that is shared for the two streams.
+       // Failing to read from either may eventually cause the command to block.
+       // Closing Stdout unblocks such writes and causes them to return errors.
+       Stdout io.ReadCloser
+       Stderr io.Reader
+
+       *clientChan // the channel backing this session
+
+       started bool // started is set to true once a Shell or Exec is invoked.
+}
+
+// Setenv sets an environment variable that will be applied to any
+// command executed by Shell or Exec.
+func (s *Session) Setenv(name, value string) os.Error {
+       n, v := []byte(name), []byte(value)
+       nlen, vlen := stringLength(n), stringLength(v)
+       payload := make([]byte, nlen+vlen)
+       marshalString(payload[:nlen], n)
+       marshalString(payload[nlen:], v)
+
+       return s.sendChanReq(channelRequestMsg{
+               PeersId:             s.id,
+               Request:             "env",
+               WantReply:           true,
+               RequestSpecificData: payload,
+       })
+}
+
+// An empty mode list (a string of 1 character, opcode 0), see RFC 4254 Section 8.
+var emptyModeList = []byte{0, 0, 0, 1, 0}
+
+// RequestPty requests the association of a pty with the session on the remote host.
+func (s *Session) RequestPty(term string, h, w int) os.Error {
+       buf := make([]byte, 4+len(term)+16+len(emptyModeList))
+       b := marshalString(buf, []byte(term))
+       binary.BigEndian.PutUint32(b, uint32(h))
+       binary.BigEndian.PutUint32(b[4:], uint32(w))
+       binary.BigEndian.PutUint32(b[8:], uint32(h*8))
+       binary.BigEndian.PutUint32(b[12:], uint32(w*8))
+       copy(b[16:], emptyModeList)
+
+       return s.sendChanReq(channelRequestMsg{
+               PeersId:             s.id,
+               Request:             "pty-req",
+               WantReply:           true,
+               RequestSpecificData: buf,
+       })
+}
+
+// Exec runs cmd on the remote host. Typically, the remote 
+// server passes cmd to the shell for interpretation. 
+// A Session only accepts one call to Exec or Shell.
+func (s *Session) Exec(cmd string) os.Error {
+       if s.started {
+               return os.NewError("session already started")
+       }
+       cmdLen := stringLength([]byte(cmd))
+       payload := make([]byte, cmdLen)
+       marshalString(payload, []byte(cmd))
+       s.started = true
+
+       return s.sendChanReq(channelRequestMsg{
+               PeersId:             s.id,
+               Request:             "exec",
+               WantReply:           true,
+               RequestSpecificData: payload,
+       })
+}
+
+// Shell starts a login shell on the remote host. A Session only 
+// accepts one call to Exec or Shell.
+func (s *Session) Shell() os.Error {
+       if s.started {
+               return os.NewError("session already started")
+       }
+       s.started = true
+
+       return s.sendChanReq(channelRequestMsg{
+               PeersId:   s.id,
+               Request:   "shell",
+               WantReply: true,
+       })
+}
+
+// NewSession returns a new interactive session on the remote host.
+func (c *ClientConn) NewSession() (*Session, os.Error) {
+       ch, err := c.openChan("session")
+       if err != nil {
+               return nil, err
+       }
+       return &Session{
+               Stdin: &chanWriter{
+                       packetWriter: ch,
+                       id:           ch.id,
+                       win:          ch.win,
+               },
+               Stdout: &chanReader{
+                       packetWriter: ch,
+                       id:           ch.id,
+                       data:         ch.data,
+               },
+               Stderr: &chanReader{
+                       packetWriter: ch,
+                       id:           ch.id,
+                       data:         ch.dataExt,
+               },
+               clientChan: ch,
+       }, nil
+}