Skip to content

Commit 49c7414

Browse files
committed
feat: add support for X11 forwarding
1 parent fcea999 commit 49c7414

File tree

5 files changed

+96
-3
lines changed

5 files changed

+96
-3
lines changed

server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Server struct {
4242
PasswordHandler PasswordHandler // password authentication handler
4343
PublicKeyHandler PublicKeyHandler // public key authentication handler
4444
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
45+
X11Callback X11Callback //callback for allowing X11 forwarding, denies all if nil
4546
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
4647
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
4748
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil

session.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ type Session interface {
6969
// of whether or not a PTY was accepted for this session.
7070
Pty() (Pty, <-chan Window, bool)
7171

72+
// X11 returns X11 forwarding information and a boolean of whether or not X11
73+
// forwarding was accepted for this session.
74+
X11() (X11, bool)
75+
7276
// Signals registers a channel to receive signals sent from the client. The
7377
// channel must handle signal sends or it will block the SSH request loop.
7478
// Registering nil will unregister the channel from signal sends. During the
@@ -106,6 +110,7 @@ func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.Ne
106110
conn: conn,
107111
handler: srv.Handler,
108112
ptyCb: srv.PtyCallback,
113+
x11Cb: srv.X11Callback,
109114
sessReqCb: srv.SessionRequestCallback,
110115
subsystemHandlers: srv.SubsystemHandlers,
111116
ctx: ctx,
@@ -122,9 +127,11 @@ type session struct {
122127
handled bool
123128
exited bool
124129
pty *Pty
130+
x11 *X11
125131
winch chan Window
126132
env []string
127133
ptyCb PtyCallback
134+
x11Cb X11Callback
128135
sessReqCb SessionRequestCallback
129136
rawCmd string
130137
subsystem string
@@ -226,6 +233,13 @@ func (sess *session) Pty() (Pty, <-chan Window, bool) {
226233
return Pty{}, sess.winch, false
227234
}
228235

236+
func (sess *session) X11() (X11, bool) {
237+
if sess.x11 != nil {
238+
return *sess.x11, true
239+
}
240+
return X11{}, false
241+
}
242+
229243
func (sess *session) Signals(c chan<- Signal) {
230244
sess.Lock()
231245
defer sess.Unlock()
@@ -353,6 +367,23 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
353367
close(sess.winch)
354368
}()
355369
req.Reply(ok, nil)
370+
case "x11-req":
371+
if sess.handled || sess.x11 != nil {
372+
req.Reply(false, nil)
373+
continue
374+
}
375+
x11Req, ok := parseX11Request(req.Payload)
376+
if !ok {
377+
req.Reply(false, nil)
378+
continue
379+
}
380+
sess.x11 = &x11Req
381+
if sess.x11Cb != nil {
382+
ok := sess.x11Cb(sess.ctx, x11Req)
383+
req.Reply(ok, nil)
384+
continue
385+
}
386+
req.Reply(false, nil)
356387
case "window-change":
357388
if sess.pty == nil {
358389
req.Reply(false, nil)

session_test.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"testing"
99

10+
"golang.org/x/crypto/ssh"
1011
gossh "golang.org/x/crypto/ssh"
1112
)
1213

@@ -228,11 +229,52 @@ func TestPty(t *testing.T) {
228229
<-done
229230
}
230231

232+
func TestX11(t *testing.T) {
233+
t.Parallel()
234+
done := make(chan struct{})
235+
session, _, cleanup := newTestSession(t, &Server{
236+
X11Callback: func(ctx Context, x11 X11) bool {
237+
return true
238+
},
239+
Handler: func(s Session) {
240+
fmt.Printf("We got here!\n")
241+
x11Req, isX11 := s.X11()
242+
if !isX11 {
243+
t.Fatalf("expected x11 but none requested")
244+
}
245+
if !x11Req.SingleConnection {
246+
t.Fatalf("expected single connection but got %#v", x11Req.SingleConnection)
247+
}
248+
close(done)
249+
},
250+
}, nil)
251+
defer cleanup()
252+
253+
reply, err := session.SendRequest("x11-req", true, ssh.Marshal(X11{
254+
SingleConnection: true,
255+
AuthProtocol: "MIT-MAGIC-COOKIE-1",
256+
AuthCookie: "deadbeef",
257+
ScreenNumber: 1,
258+
}))
259+
if err != nil {
260+
t.Fatalf("expected nil but got %v", err)
261+
}
262+
if !reply {
263+
t.Fatalf("expected true but got %v", reply)
264+
}
265+
err = session.Shell()
266+
if err != nil {
267+
t.Fatalf("expected nil but got %v", err)
268+
}
269+
session.Close()
270+
<-done
271+
}
272+
231273
func TestPtyResize(t *testing.T) {
232274
t.Parallel()
233-
winch0 := Window{40, 80}
234-
winch1 := Window{80, 160}
235-
winch2 := Window{20, 40}
275+
winch0 := Window{40, 80, 0, 0}
276+
winch1 := Window{80, 160, 0, 0}
277+
winch2 := Window{20, 40, 0, 0}
236278
winches := make(chan Window)
237279
done := make(chan bool)
238280
session, _, cleanup := newTestSession(t, &Server{

ssh.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInter
4747
// PtyCallback is a hook for allowing PTY sessions.
4848
type PtyCallback func(ctx Context, pty Pty) bool
4949

50+
// X11Callback is a hook for allowing X11 forwarding.
51+
type X11Callback func(ctx Context, x11 X11) bool
52+
5053
// SessionRequestCallback is a callback for allowing or denying SSH sessions.
5154
type SessionRequestCallback func(sess Session, requestType string) bool
5255

@@ -106,6 +109,16 @@ type Pty struct {
106109
Modes gossh.TerminalModes
107110
}
108111

112+
// X11 represents a X11 forwarding request.
113+
type X11 struct {
114+
// SingleConnection is whether the X11 connection should be closed after
115+
// the first use.
116+
SingleConnection bool
117+
AuthProtocol string
118+
AuthCookie string
119+
ScreenNumber uint32
120+
}
121+
109122
// Serve accepts incoming SSH connections on the listener l, creating a new
110123
// connection goroutine for each. The connection goroutines read requests and
111124
// then calls handler to handle sessions. Handler is typically nil, in which

util.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func parsePtyRequest(payload []byte) (pty Pty, ok bool) {
5353
return
5454
}
5555

56+
// parseX11Request parses an X11 forwarding request.
57+
// See https://www.rfc-editor.org/rfc/rfc4254#section-6.3
58+
func parseX11Request(payload []byte) (x11 X11, ok bool) {
59+
return x11, ssh.Unmarshal(payload, &x11) == nil
60+
}
61+
5662
func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) {
5763
// See https://datatracker.ietf.org/doc/html/rfc4254#section-8
5864
// 8. Encoding of Terminal Modes

0 commit comments

Comments
 (0)