From db3ef2c02ee6b911919c748976dd61f55d42174f Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sat, 12 Mar 2022 17:12:09 -0800 Subject: [PATCH 1/4] add support for terminal opcodes Updates tailscale/tailscale#4146 Signed-off-by: Maisem Ali --- session.go | 2 +- ssh.go | 34 +++++++++++++-- util.go | 125 ++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 131 insertions(+), 30 deletions(-) diff --git a/session.go b/session.go index fbe5692..a2bc187 100644 --- a/session.go +++ b/session.go @@ -347,7 +347,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) { req.Reply(false, nil) continue } - win, ok := parseWinchRequest(req.Payload) + win, _, ok := parseWindow(req.Payload) if ok { sess.pty.Window = win sess.winch <- win diff --git a/ssh.go b/ssh.go index fbeb150..7dc76b3 100644 --- a/ssh.go +++ b/ssh.go @@ -69,16 +69,44 @@ type ServerConfigCallback func(ctx Context) *gossh.ServerConfig type ConnectionFailedCallback func(conn net.Conn, err error) // Window represents the size of a PTY window. +// +// From https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 +// +// Zero dimension parameters MUST be ignored. The character/row dimensions +// override the pixel dimensions (when nonzero). Pixel dimensions refer +// to the drawable area of the window. type Window struct { - Width int + // Width is the number of columns. + // It overrides WidthPixels. + Width int + // Height is the number of rows. + // It overrides HeightPixels. Height int + + // WidthPixels is the drawable width of the window, in pixels. + WidthPixels int + // HeightPixels is the drawable height of the window, in pixels. + HeightPixels int } // Pty represents a PTY request and configuration. type Pty struct { - Term string + // Term is the TERM environment variable value. + Term string + + // Window is the Window sent as part of the pty-req. Window Window - // HELP WANTED: terminal modes! + + // Modes represent a mapping of Terminal Mode opcode to value as it was + // requested by the client as part of the pty-req. These are outlined as + // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8. + // + // The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.). + // Boolean opcodes have values 0 or 1. + // + // Note: golang.org/x/crypto/ssh currently (2022-03-12) doesn't have a + // definition for opcode 42 "iutf8" which was introduced in https://datatracker.ietf.org/doc/html/rfc8160. + Modes gossh.TerminalModes } // Serve accepts incoming SSH connections on the listener l, creating a new diff --git a/util.go b/util.go index 015a44e..a5e6699 100644 --- a/util.go +++ b/util.go @@ -16,61 +16,134 @@ func generateSigner() (ssh.Signer, error) { return ssh.NewSignerFromKey(key) } -func parsePtyRequest(s []byte) (pty Pty, ok bool) { - term, s, ok := parseString(s) +func parsePtyRequest(payload []byte) (pty Pty, ok bool) { + // From https://datatracker.ietf.org/doc/html/rfc4254 + // 6.2. Requesting a Pseudo-Terminal + // A pseudo-terminal can be allocated for the session by sending the + // following message. + // byte SSH_MSG_CHANNEL_REQUEST + // uint32 recipient channel + // string "pty-req" + // boolean want_reply + // string TERM environment variable value (e.g., vt100) + // uint32 terminal width, characters (e.g., 80) + // uint32 terminal height, rows (e.g., 24) + // uint32 terminal width, pixels (e.g., 640) + // uint32 terminal height, pixels (e.g., 480) + // string encoded terminal modes + + // The payload starts from the TERM variable. + term, rem, ok := parseString(payload) if !ok { return } - width32, s, ok := parseUint32(s) + win, rem, ok := parseWindow(rem) if !ok { return } - height32, _, ok := parseUint32(s) + modes, ok := parseTerminalModes(rem) if !ok { return } pty = Pty{ - Term: term, - Window: Window{ - Width: int(width32), - Height: int(height32), - }, + Term: term, + Window: win, + Modes: modes, } return } -func parseWinchRequest(s []byte) (win Window, ok bool) { - width32, s, ok := parseUint32(s) - if width32 < 1 { - ok = false +func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) { + // From https://datatracker.ietf.org/doc/html/rfc4254 + // 8. Encoding of Terminal Modes + // + // All 'encoded terminal modes' (as passed in a pty request) are encoded + // into a byte stream. It is intended that the coding be portable + // across different environments. The stream consists of opcode- + // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159 + // have a single uint32 argument. Opcodes 160 to 255 are not yet + // defined, and cause parsing to stop (they should only be used after + // any other data). The stream is terminated by opcode TTY_OP_END + // (0x00). + // + // The client SHOULD put any modes it knows about in the stream, and the + // server MAY ignore any modes it does not know about. This allows some + // degree of machine-independence, at least between systems that use a + // POSIX-like tty interface. The protocol can support other systems as + // well, but the client may need to fill reasonable values for a number + // of parameters so the server pty gets set to a reasonable mode (the + // server leaves all unspecified mode bits in their default values, and + // only some combinations make sense). + _, rem, ok := parseUint32(in) + if !ok { + return + } + const ttyOpEnd = 0 + for len(rem) > 0 { + if modes == nil { + modes = make(ssh.TerminalModes) + } + code := uint8(rem[0]) + rem = rem[1:] + if code == ttyOpEnd || code > 160 { + break + } + var val uint32 + val, rem, ok = parseUint32(rem) + if !ok { + return + } + modes[code] = val + } + ok = true + return +} + +func parseWindow(s []byte) (win Window, rem []byte, ok bool) { + // 6.7. Window Dimension Change Message + // When the window (terminal) size changes on the client side, it MAY + // send a message to the other side to inform it of the new dimensions. + + // byte SSH_MSG_CHANNEL_REQUEST + // uint32 recipient channel + // string "window-change" + // boolean FALSE + // uint32 terminal width, columns + // uint32 terminal height, rows + // uint32 terminal width, pixels + // uint32 terminal height, pixels + wCols, rem, ok := parseUint32(s) + if !ok { + return } + hRows, rem, ok := parseUint32(rem) if !ok { return } - height32, _, ok := parseUint32(s) - if height32 < 1 { - ok = false + wPixels, rem, ok := parseUint32(rem) + if !ok { + return } + hPixels, rem, ok := parseUint32(rem) if !ok { return } win = Window{ - Width: int(width32), - Height: int(height32), + Width: int(wCols), + Height: int(hRows), + WidthPixels: int(wPixels), + HeightPixels: int(hPixels), } return } -func parseString(in []byte) (out string, rest []byte, ok bool) { - if len(in) < 4 { - return - } - length := binary.BigEndian.Uint32(in) - if uint32(len(in)) < 4+length { +func parseString(in []byte) (out string, rem []byte, ok bool) { + length, rem, ok := parseUint32(in) + if uint32(len(rem)) < length || !ok { + ok = false return } - out = string(in[4 : 4+length]) - rest = in[4+length:] + out, rem = string(rem[:length]), rem[length:] ok = true return } From 851f95c51b530b53755d41e5ac7e2208a3a9b9fb Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sat, 12 Mar 2022 17:14:58 -0800 Subject: [PATCH 2/4] go.mod: change name to github.com/tailscale/ssh Signed-off-by: Maisem Ali --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6d83084..eee9a4f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/gliderlabs/ssh +module github.com/tailscale/ssh go 1.12 From 4f6e0b53ea32a4030adcd068d1f680d65077639e Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sat, 12 Mar 2022 17:18:23 -0800 Subject: [PATCH 3/4] document behavior of NL to CRNL translation in Write and add a way to disable it. Updates tailscale/tailscale#4146 Signed-off-by: Maisem Ali --- session.go | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/session.go b/session.go index a2bc187..a7a9a3e 100644 --- a/session.go +++ b/session.go @@ -83,6 +83,13 @@ type Session interface { // the request handling loop. Registering nil will unregister the channel. // During the time that no channel is registered, breaks are ignored. Break(c chan<- bool) + + // DisablePTYEmulation disables the session's default minimal PTY emulation. + // If you're setting the pty's termios settings from the Pty request, use + // this method to avoid corruption. + // Currently (2022-03-12) the only emulation implemented is NL-to-CRNL translation (`\n`=>`\r\n`). + // A call of DisablePTYEmulation must precede any call to Write. + DisablePTYEmulation() } // maxSigBufSize is how many signals will be buffered @@ -110,26 +117,31 @@ func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.Ne type session struct { sync.Mutex gossh.Channel - conn *gossh.ServerConn - handler Handler - subsystemHandlers map[string]SubsystemHandler - handled bool - exited bool - pty *Pty - winch chan Window - env []string - ptyCb PtyCallback - sessReqCb SessionRequestCallback - rawCmd string - subsystem string - ctx Context - sigCh chan<- Signal - sigBuf []Signal - breakCh chan<- bool + conn *gossh.ServerConn + handler Handler + subsystemHandlers map[string]SubsystemHandler + handled bool + exited bool + pty *Pty + winch chan Window + env []string + ptyCb PtyCallback + sessReqCb SessionRequestCallback + rawCmd string + subsystem string + ctx Context + sigCh chan<- Signal + sigBuf []Signal + breakCh chan<- bool + disablePtyEmulation bool +} + +func (sess *session) DisablePTYEmulation() { + sess.disablePtyEmulation = true } func (sess *session) Write(p []byte) (n int, err error) { - if sess.pty != nil { + if sess.pty != nil && !sess.disablePtyEmulation { m := len(p) // normalize \n to \r\n when pty is accepted. // this is a hardcoded shortcut since we don't support terminal modes. From be8b7add4057ef5a8e458b42331a7633c06d026a Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sat, 12 Mar 2022 17:31:26 -0800 Subject: [PATCH 4/4] address comments from #1 Signed-off-by: Maisem Ali --- ssh.go | 5 +---- util.go | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ssh.go b/ssh.go index 7dc76b3..8bb02a3 100644 --- a/ssh.go +++ b/ssh.go @@ -70,7 +70,7 @@ type ConnectionFailedCallback func(conn net.Conn, err error) // Window represents the size of a PTY window. // -// From https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 +// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 // // Zero dimension parameters MUST be ignored. The character/row dimensions // override the pixel dimensions (when nonzero). Pixel dimensions refer @@ -103,9 +103,6 @@ type Pty struct { // // The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.). // Boolean opcodes have values 0 or 1. - // - // Note: golang.org/x/crypto/ssh currently (2022-03-12) doesn't have a - // definition for opcode 42 "iutf8" which was introduced in https://datatracker.ietf.org/doc/html/rfc8160. Modes gossh.TerminalModes } diff --git a/util.go b/util.go index a5e6699..3bee06d 100644 --- a/util.go +++ b/util.go @@ -17,7 +17,7 @@ func generateSigner() (ssh.Signer, error) { } func parsePtyRequest(payload []byte) (pty Pty, ok bool) { - // From https://datatracker.ietf.org/doc/html/rfc4254 + // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2 // 6.2. Requesting a Pseudo-Terminal // A pseudo-terminal can be allocated for the session by sending the // following message. @@ -54,7 +54,7 @@ func parsePtyRequest(payload []byte) (pty Pty, ok bool) { } func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) { - // From https://datatracker.ietf.org/doc/html/rfc4254 + // See https://datatracker.ietf.org/doc/html/rfc4254#section-8 // 8. Encoding of Terminal Modes // // All 'encoded terminal modes' (as passed in a pty request) are encoded @@ -100,7 +100,8 @@ func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) { } func parseWindow(s []byte) (win Window, rem []byte, ok bool) { - // 6.7. Window Dimension Change Message + // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.7 + // 6.7. Window Dimension Change Message // When the window (terminal) size changes on the client side, it MAY // send a message to the other side to inform it of the new dimensions.