Skip to content

Commit 5d1ffcd

Browse files
committed
feat: add support for X11 forwarding
1 parent 2b9d128 commit 5d1ffcd

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

agent/agentssh/agentssh.go

+4
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ func NewServer(ctx context.Context, logger slog.Logger, maxTimeout time.Duration
8484
forwardHandler := &ssh.ForwardedTCPHandler{}
8585
unixForwardHandler := &forwardedUnixHandler{log: logger}
8686

87+
x11Handler := &x11Handler{log: logger}
88+
8789
s := &Server{
8890
listeners: make(map[net.Listener]struct{}),
8991
conns: make(map[net.Conn]struct{}),
@@ -96,6 +98,7 @@ func NewServer(ctx context.Context, logger slog.Logger, maxTimeout time.Duration
9698
"direct-tcpip": ssh.DirectTCPIPHandler,
9799
"direct-streamlocal@openssh.com": directStreamLocalHandler,
98100
"session": ssh.DefaultSessionHandler,
101+
"x11": x11Handler.channel,
99102
},
100103
ConnectionFailedCallback: func(_ net.Conn, err error) {
101104
s.logger.Info(ctx, "ssh connection ended", slog.Error(err))
@@ -124,6 +127,7 @@ func NewServer(ctx context.Context, logger slog.Logger, maxTimeout time.Duration
124127
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
125128
"streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
126129
"cancel-streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
130+
"x11-req": x11Handler.request,
127131
},
128132
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
129133
return &gossh.ServerConfig{

agent/agentssh/x11.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package agentssh
2+
3+
import (
4+
"encoding/binary"
5+
"encoding/hex"
6+
"fmt"
7+
"net"
8+
"os"
9+
"path/filepath"
10+
"sync"
11+
12+
"github.com/gliderlabs/ssh"
13+
gossh "golang.org/x/crypto/ssh"
14+
"golang.org/x/xerrors"
15+
16+
"cdr.dev/slog"
17+
)
18+
19+
// x11ReqPayload describes the extra data sent in a x11-req request.
20+
// https://www.rfc-editor.org/rfc/rfc4254#section-6.3
21+
type x11ReqPayload struct {
22+
SingleConnection bool
23+
AuthProtocol string
24+
AuthCookie string
25+
ScreenNumber uint32
26+
}
27+
28+
type x11Handler struct {
29+
mutex sync.Mutex
30+
log slog.Logger
31+
32+
forwardingEnabled bool
33+
screenNumber uint32
34+
}
35+
36+
func (h *x11Handler) request(ctx ssh.Context, _ *ssh.Server, req *gossh.Request) (bool, []byte) {
37+
var reqPayload x11ReqPayload
38+
err := gossh.Unmarshal(req.Payload, &reqPayload)
39+
if err != nil {
40+
h.log.Warn(ctx, "parse x11-req request payload from client", slog.Error(err))
41+
return false, nil
42+
}
43+
44+
err = addXauthEntry(fmt.Sprintf("localhost:%d.0", reqPayload.ScreenNumber), reqPayload.AuthProtocol, reqPayload.AuthCookie)
45+
if err != nil {
46+
h.log.Warn(ctx, "failed to add Xauthority entry", slog.Error(err))
47+
return false, nil
48+
}
49+
50+
h.mutex.Lock()
51+
h.forwardingEnabled = true
52+
h.screenNumber = reqPayload.ScreenNumber
53+
h.mutex.Unlock()
54+
return true, nil
55+
}
56+
57+
func (h *x11Handler) channel(_ *ssh.Server, _ *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
58+
h.mutex.Lock()
59+
enabled := h.forwardingEnabled
60+
h.mutex.Unlock()
61+
if !enabled {
62+
_ = newChan.Reject(gossh.Prohibited, "X11 forwarding not requested")
63+
return
64+
}
65+
66+
x11Conn, err := net.Dial("unix", fmt.Sprintf(filepath.Join(os.TempDir(), ".X11-unix", "X%d"), h.screenNumber))
67+
if err != nil {
68+
_ = newChan.Reject(gossh.ConnectionFailed, "Failed to connect to local X11 server")
69+
return
70+
}
71+
72+
channel, requests, err := newChan.Accept()
73+
if err != nil {
74+
_ = x11Conn.Close()
75+
return
76+
}
77+
go gossh.DiscardRequests(requests)
78+
Bicopy(ctx, channel, x11Conn)
79+
}
80+
81+
func addXauthEntry(display string, authProtocol string, authCookie string) error {
82+
// Get the Xauthority file path
83+
homeDir, err := os.UserHomeDir()
84+
if err != nil {
85+
return xerrors.Errorf("failed to get user home directory: %w", err)
86+
}
87+
88+
xauthPath := filepath.Join(homeDir, ".Xauthority")
89+
90+
// Open or create the Xauthority file
91+
file, err := os.OpenFile(xauthPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
92+
if err != nil {
93+
return xerrors.Errorf("failed to open Xauthority file: %w", err)
94+
}
95+
defer file.Close()
96+
97+
// Convert the authCookie from hex string to byte slice
98+
authCookieBytes, err := hex.DecodeString(authCookie)
99+
if err != nil {
100+
return xerrors.Errorf("failed to decode auth cookie: %w", err)
101+
}
102+
103+
// Write Xauthority entry
104+
family := uint16(0x0100) // FamilyLocal
105+
err = binary.Write(file, binary.BigEndian, family)
106+
if err != nil {
107+
return xerrors.Errorf("failed to write family: %w", err)
108+
}
109+
110+
err = binary.Write(file, binary.BigEndian, uint16(len(display)))
111+
if err != nil {
112+
return xerrors.Errorf("failed to write display length: %w", err)
113+
}
114+
_, err = file.WriteString(display)
115+
if err != nil {
116+
return xerrors.Errorf("failed to write display: %w", err)
117+
}
118+
119+
err = binary.Write(file, binary.BigEndian, uint16(len(authProtocol)))
120+
if err != nil {
121+
return xerrors.Errorf("failed to write auth protocol length: %w", err)
122+
}
123+
_, err = file.WriteString(authProtocol)
124+
if err != nil {
125+
return xerrors.Errorf("failed to write auth protocol: %w", err)
126+
}
127+
128+
err = binary.Write(file, binary.BigEndian, uint16(len(authCookieBytes)))
129+
if err != nil {
130+
return xerrors.Errorf("failed to write auth cookie length: %w", err)
131+
}
132+
_, err = file.Write(authCookieBytes)
133+
if err != nil {
134+
return xerrors.Errorf("failed to write auth cookie: %w", err)
135+
}
136+
137+
return nil
138+
}

0 commit comments

Comments
 (0)