-
Notifications
You must be signed in to change notification settings - Fork 894
feat: add support for X11 forwarding #7205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package agentssh | ||
|
||
import ( | ||
"context" | ||
"encoding/binary" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"time" | ||
|
||
"github.com/gliderlabs/ssh" | ||
"github.com/gofrs/flock" | ||
"github.com/spf13/afero" | ||
gossh "golang.org/x/crypto/ssh" | ||
"golang.org/x/xerrors" | ||
|
||
"cdr.dev/slog" | ||
) | ||
|
||
// x11Callback is called when the client requests X11 forwarding. | ||
// It adds an Xauthority entry to the Xauthority file. | ||
func (s *Server) x11Callback(ctx ssh.Context, x11 ssh.X11) bool { | ||
hostname, err := os.Hostname() | ||
if err != nil { | ||
s.logger.Warn(ctx, "failed to get hostname", slog.Error(err)) | ||
return false | ||
} | ||
|
||
err = s.fs.MkdirAll(s.x11SocketDir, 0o700) | ||
if err != nil { | ||
s.logger.Warn(ctx, "failed to make the x11 socket dir", slog.F("dir", s.x11SocketDir), slog.Error(err)) | ||
return false | ||
} | ||
|
||
err = addXauthEntry(ctx, s.fs, hostname, strconv.Itoa(int(x11.ScreenNumber)), x11.AuthProtocol, x11.AuthCookie) | ||
if err != nil { | ||
s.logger.Warn(ctx, "failed to add Xauthority entry", slog.Error(err)) | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
// x11Handler is called when a session has requested X11 forwarding. | ||
// It listens for X11 connections and forwards them to the client. | ||
func (s *Server) x11Handler(ctx ssh.Context, x11 ssh.X11) bool { | ||
serverConn, valid := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn) | ||
if !valid { | ||
s.logger.Warn(ctx, "failed to get server connection") | ||
return false | ||
} | ||
listener, err := net.Listen("unix", filepath.Join(s.x11SocketDir, fmt.Sprintf("X%d", x11.ScreenNumber))) | ||
if err != nil { | ||
s.logger.Warn(ctx, "failed to listen for X11", slog.Error(err)) | ||
return false | ||
} | ||
s.trackListener(listener, true) | ||
|
||
go func() { | ||
defer s.trackListener(listener, false) | ||
handledFirstConnection := false | ||
|
||
for { | ||
conn, err := listener.Accept() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
if err != nil { | ||
if errors.Is(err, net.ErrClosed) { | ||
return | ||
} | ||
s.logger.Warn(ctx, "failed to accept X11 connection", slog.Error(err)) | ||
return | ||
} | ||
if x11.SingleConnection && handledFirstConnection { | ||
s.logger.Warn(ctx, "X11 connection rejected because single connection is enabled") | ||
_ = conn.Close() | ||
continue | ||
} | ||
handledFirstConnection = true | ||
|
||
unixConn, ok := conn.(*net.UnixConn) | ||
if !ok { | ||
s.logger.Warn(ctx, fmt.Sprintf("failed to cast connection to UnixConn. got: %T", conn)) | ||
return | ||
} | ||
unixAddr, ok := unixConn.LocalAddr().(*net.UnixAddr) | ||
if !ok { | ||
s.logger.Warn(ctx, fmt.Sprintf("failed to cast local address to UnixAddr. got: %T", unixConn.LocalAddr())) | ||
return | ||
} | ||
|
||
channel, reqs, err := serverConn.OpenChannel("x11", gossh.Marshal(struct { | ||
OriginatorAddress string | ||
OriginatorPort uint32 | ||
}{ | ||
OriginatorAddress: unixAddr.Name, | ||
OriginatorPort: 0, | ||
})) | ||
if err != nil { | ||
s.logger.Warn(ctx, "failed to open X11 channel", slog.Error(err)) | ||
return | ||
} | ||
go gossh.DiscardRequests(reqs) | ||
go Bicopy(ctx, conn, channel) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to track that conn and channel are actually closed by the time |
||
} | ||
}() | ||
return true | ||
} | ||
|
||
// addXauthEntry adds an Xauthority entry to the Xauthority file. | ||
// The Xauthority file is located at ~/.Xauthority. | ||
func addXauthEntry(ctx context.Context, fs afero.Fs, host string, display string, authProtocol string, authCookie string) error { | ||
// Get the Xauthority file path | ||
homeDir, err := os.UserHomeDir() | ||
if err != nil { | ||
return xerrors.Errorf("failed to get user home directory: %w", err) | ||
} | ||
|
||
xauthPath := filepath.Join(homeDir, ".Xauthority") | ||
|
||
lock := flock.New(xauthPath) | ||
ok, err := lock.TryLockContext(ctx, 100*time.Millisecond) | ||
if !ok { | ||
return xerrors.Errorf("failed to lock Xauthority file: %w", err) | ||
} | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
defer lock.Close() | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Open or create the Xauthority file | ||
file, err := fs.OpenFile(xauthPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we consider flock-ing the file so we're the only one writing to it? Not sure if xauth respects that though. How about multiple SSH connections with X11 forwarding? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how this works either actually... I suppose we should flock? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a flock! |
||
if err != nil { | ||
return xerrors.Errorf("failed to open Xauthority file: %w", err) | ||
} | ||
defer file.Close() | ||
|
||
// Convert the authCookie from hex string to byte slice | ||
authCookieBytes, err := hex.DecodeString(authCookie) | ||
if err != nil { | ||
return xerrors.Errorf("failed to decode auth cookie: %w", err) | ||
} | ||
|
||
// Write Xauthority entry | ||
family := uint16(0x0100) // FamilyLocal | ||
err = binary.Write(file, binary.BigEndian, family) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write family: %w", err) | ||
} | ||
|
||
err = binary.Write(file, binary.BigEndian, uint16(len(host))) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write host length: %w", err) | ||
} | ||
_, err = file.WriteString(host) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write host: %w", err) | ||
} | ||
|
||
err = binary.Write(file, binary.BigEndian, uint16(len(display))) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write display length: %w", err) | ||
} | ||
_, err = file.WriteString(display) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write display: %w", err) | ||
} | ||
|
||
err = binary.Write(file, binary.BigEndian, uint16(len(authProtocol))) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write auth protocol length: %w", err) | ||
} | ||
_, err = file.WriteString(authProtocol) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write auth protocol: %w", err) | ||
} | ||
|
||
err = binary.Write(file, binary.BigEndian, uint16(len(authCookieBytes))) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write auth cookie length: %w", err) | ||
} | ||
_, err = file.Write(authCookieBytes) | ||
if err != nil { | ||
return xerrors.Errorf("failed to write auth cookie: %w", err) | ||
} | ||
|
||
return nil | ||
} |
Uh oh!
There was an error while loading. Please reload this page.