summaryrefslogtreecommitdiffhomepage
path: root/ssh/tailssh/session.go
blob: d88df14559bd4b7463fe37be4be533c70a6cedb9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9

package tailssh

import (
	"context"
	"errors"
	"io"
	"net"
	"time"

	"tailscale.com/tailcfg"
	"tailscale.com/tempfork/gliderlabs/ssh"
)

var errNoDeadline = errors.New("tailssh.Session: deadlines not supported")

// Signal represents an SSH signal (e.g. "INT", "TERM").
type Signal = ssh.Signal

// Pty represents a PTY request and configuration.
type Pty struct {
	// Term is the TERM environment variable value.
	Term string

	// Window is the initial window size.
	Window Window

	// Modes are the RFC 4254 terminal modes as opcode/value pairs.
	Modes map[uint8]uint32
}

// Window represents the size of a PTY window.
type Window struct {
	Width        int
	Height       int
	WidthPixels  int
	HeightPixels int
}

// PeerIdentity contains the Tailscale identity of the connecting SSH peer.
type PeerIdentity struct {
	Node        tailcfg.NodeView
	UserProfile tailcfg.UserProfile
}

// Session wraps a gliderlabs ssh.Session with Tailscale peer identity
// information. It implements net.Conn so callers that only need Read/Write/Close
// can use it directly. Callers that need SSH-specific functionality can
// type-assert from the net.Conn returned by the listener's Accept.
type Session struct {
	// sess is the underlying gliderlabs SSH session.
	sess ssh.Session

	// peer is the Tailscale identity of the remote peer.
	peer PeerIdentity

	// done is closed when the session handler should return,
	// unblocking the gliderlabs handler goroutine.
	done chan struct{}
}

// newSession creates a new Session wrapping the given gliderlabs session and
// peer identity. The done channel is closed by the session consumer to signal
// that the handler goroutine may return.
func newSession(sess ssh.Session, peer PeerIdentity, done chan struct{}) *Session {
	return &Session{
		sess: sess,
		peer: peer,
		done: done,
	}
}

// Read reads from the SSH channel (stdin from the client).
func (s *Session) Read(p []byte) (int, error) {
	return s.sess.Read(p)
}

// Write writes to the SSH channel (stdout to the client).
func (s *Session) Write(p []byte) (int, error) {
	return s.sess.Write(p)
}

// Close signals the session handler to return and closes the underlying channel.
func (s *Session) Close() error {
	select {
	case <-s.done:
	default:
		close(s.done)
	}
	return nil
}

// RemoteAddr returns the net.Addr of the client side of the connection.
func (s *Session) RemoteAddr() net.Addr {
	return s.sess.RemoteAddr()
}

// LocalAddr returns the net.Addr of the server side of the connection.
func (s *Session) LocalAddr() net.Addr {
	return s.sess.LocalAddr()
}

// SetDeadline is not supported and returns an error.
func (s *Session) SetDeadline(t time.Time) error {
	return errNoDeadline
}

// SetReadDeadline is not supported and returns an error.
func (s *Session) SetReadDeadline(t time.Time) error {
	return errNoDeadline
}

// SetWriteDeadline is not supported and returns an error.
func (s *Session) SetWriteDeadline(t time.Time) error {
	return errNoDeadline
}

// User returns the SSH username.
func (s *Session) User() string {
	return s.sess.User()
}

// PeerIdentity returns the Tailscale identity of the remote peer.
func (s *Session) PeerIdentity() PeerIdentity {
	return s.peer
}

// Environ returns a copy of the environment variables set by the client.
func (s *Session) Environ() []string {
	return s.sess.Environ()
}

// RawCommand returns the exact command string provided by the client.
func (s *Session) RawCommand() string {
	return s.sess.RawCommand()
}

// Subsystem returns the subsystem requested by the client.
func (s *Session) Subsystem() string {
	return s.sess.Subsystem()
}

// Pty returns PTY information, a channel of window size changes, and whether
// a PTY was requested. The returned types use this package's Pty and Window
// types rather than the internal gliderlabs types.
func (s *Session) Pty() (Pty, <-chan Window, bool) {
	gPty, gWinCh, ok := s.sess.Pty()
	if !ok {
		return Pty{}, nil, false
	}
	p := Pty{
		Term: gPty.Term,
		Window: Window{
			Width:        gPty.Window.Width,
			Height:       gPty.Window.Height,
			WidthPixels:  gPty.Window.WidthPixels,
			HeightPixels: gPty.Window.HeightPixels,
		},
	}
	if gPty.Modes != nil {
		p.Modes = make(map[uint8]uint32, len(gPty.Modes))
		for k, v := range gPty.Modes {
			p.Modes[k] = v
		}
	}

	// Convert the gliderlabs Window channel to our Window type.
	winCh := make(chan Window, 1)
	go func() {
		defer close(winCh)
		for gw := range gWinCh {
			winCh <- Window{
				Width:        gw.Width,
				Height:       gw.Height,
				WidthPixels:  gw.WidthPixels,
				HeightPixels: gw.HeightPixels,
			}
		}
	}()

	return p, winCh, true
}

// Signals registers a channel to receive signals from the client.
// Pass nil to unregister.
func (s *Session) Signals(c chan<- Signal) {
	s.sess.Signals(c)
}

// Exit sends an exit status to the client and closes the session.
func (s *Session) Exit(code int) error {
	err := s.sess.Exit(code)
	s.Close()
	return err
}

// Stderr returns an io.Writer for the SSH stderr channel.
func (s *Session) Stderr() io.Writer {
	return s.sess.Stderr()
}

// Context returns the session's context, which is canceled when the client
// disconnects.
func (s *Session) Context() context.Context {
	return s.sess.Context()
}