Skip to content

Commit 3371dc1

Browse files
committed
⚽ producer/sshclient: fix agent socket management
1 parent 9e03260 commit 3371dc1

1 file changed

Lines changed: 88 additions & 15 deletions

File tree

producer/sshclient/sshclient.go

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ type SourceConfig struct {
5555
// Auth is the list of authentication methods for SSH.
5656
Auth []ssh.AuthMethod
5757

58+
// AgentClient is an optional SSH agent client for authentication.
59+
AgentClient *SSHAgentClient
60+
5861
// HostKeyCallback is the callback function for verifying the server's host key.
5962
HostKeyCallback ssh.HostKeyCallback
6063

@@ -79,6 +82,7 @@ func (cfg *SourceConfig) NewSource() *Source {
7982
macs: cfg.MACs,
8083
user: cfg.User,
8184
auth: cfg.Auth,
85+
agentClient: cfg.AgentClient,
8286
hostKeyCallback: cfg.HostKeyCallback,
8387
hostKeyAlgorithms: cfg.HostKeyAlgorithms,
8488
}
@@ -108,6 +112,7 @@ type Source struct {
108112
macs []string
109113
user string
110114
auth []ssh.AuthMethod
115+
agentClient *SSHAgentClient
111116
hostKeyCallback ssh.HostKeyCallback
112117
hostKeyAlgorithms []string
113118
}
@@ -135,6 +140,10 @@ func (s *Source) Snapshot(ctx context.Context) (producer.Message, error) {
135140
}()
136141

137142
if s.client == nil {
143+
// Horrible hack due to [agent]'s lack of context support.
144+
if s.agentClient != nil {
145+
s.agentClient.SetContext(ctx)
146+
}
138147
c, chans, req, err := ssh.NewClientConn(s.netConn, s.address, &ssh.ClientConfig{
139148
Config: ssh.Config{
140149
KeyExchanges: s.keyExchanges,
@@ -146,6 +155,10 @@ func (s *Source) Snapshot(ctx context.Context) (producer.Message, error) {
146155
HostKeyCallback: s.hostKeyCallback,
147156
HostKeyAlgorithms: s.hostKeyAlgorithms,
148157
})
158+
// Same horrible hack :P
159+
if s.agentClient != nil {
160+
s.agentClient.StopContextAfterFunc()
161+
}
149162
if err != nil {
150163
// On error, ssh.NewClientConn closes the connection for you.
151164
return producer.Message{}, fmt.Errorf("failed to establish SSH connection: %w", err)
@@ -307,7 +320,10 @@ func (cfg *ProducerConfig) NewProducer(logger *tslog.Logger) (producer.Producer,
307320
keyAuth = ssh.PublicKeys(signers...)
308321
}
309322

310-
var agentAuth ssh.AuthMethod
323+
var (
324+
agentAuth ssh.AuthMethod
325+
agentClient *SSHAgentClient
326+
)
311327
if cfg.IdentityAgent != "none" {
312328
var agentPath string
313329
if cfg.IdentityAgent != "" {
@@ -317,20 +333,8 @@ func (cfg *ProducerConfig) NewProducer(logger *tslog.Logger) (producer.Producer,
317333
}
318334

319335
if agentPath != "" {
320-
agentAuth = ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
321-
conn, err := net.Dial("unix", agentPath)
322-
if err != nil {
323-
return nil, fmt.Errorf("failed to connect to SSH agent at %q: %w", agentPath, err)
324-
}
325-
defer conn.Close()
326-
327-
agentClient := agent.NewClient(conn)
328-
signers, err := agentClient.Signers()
329-
if err != nil {
330-
return nil, fmt.Errorf("failed to get signers from SSH agent: %w", err)
331-
}
332-
return signers, nil
333-
})
336+
agentClient = NewSSHAgentClient(agentPath)
337+
agentAuth = ssh.PublicKeysCallback(agentClient.Signers)
334338
}
335339
}
336340

@@ -414,6 +418,7 @@ func (cfg *ProducerConfig) NewProducer(logger *tslog.Logger) (producer.Producer,
414418
MACs: macs,
415419
User: cfg.User,
416420
Auth: authMethods,
421+
AgentClient: agentClient,
417422
HostKeyCallback: hostKeyCallback,
418423
HostKeyAlgorithms: hostKeyAlgorithms,
419424
}
@@ -483,3 +488,71 @@ func (cfg *SSHKeyConfig) Signer() (ssh.Signer, error) {
483488

484489
return signer, nil
485490
}
491+
492+
// SSHAgentClient is a wrapper around the SSH agent client provided by the [agent] package.
493+
// It manages the connection lifetime and provides [context.Context] cancellation support.
494+
type SSHAgentClient struct {
495+
client agent.ExtendedAgent
496+
netConn net.Conn
497+
ctx context.Context
498+
path string
499+
stopCtxAfterFunc func()
500+
}
501+
502+
// NewSSHAgentClient returns a new SSH agent client for the given socket path.
503+
func NewSSHAgentClient(path string) *SSHAgentClient {
504+
return &SSHAgentClient{
505+
path: path,
506+
stopCtxAfterFunc: func() {},
507+
}
508+
}
509+
510+
func (c *SSHAgentClient) resetClient() {
511+
_ = c.netConn.Close()
512+
c.client = nil
513+
}
514+
515+
// SetContext sets the context for the underlying connection used by the SSH agent client.
516+
func (c *SSHAgentClient) SetContext(ctx context.Context) {
517+
c.ctx = ctx
518+
}
519+
520+
// Signers returns the list of signers from the SSH agent.
521+
//
522+
// SetContext must be called first. After authentication is complete, StopContextAfterFunc
523+
// must be called to release resources associated with the context.
524+
func (c *SSHAgentClient) Signers() ([]ssh.Signer, error) {
525+
if c.client == nil {
526+
var dialer net.Dialer
527+
nc, err := dialer.DialContext(c.ctx, "unix", c.path)
528+
if err != nil {
529+
return nil, fmt.Errorf("failed to connect to SSH agent at %q: %w", c.path, err)
530+
}
531+
c.netConn = nc
532+
c.client = agent.NewClient(nc)
533+
}
534+
535+
stop := context.AfterFunc(c.ctx, func() {
536+
_ = c.netConn.SetDeadline(aLongTimeAgo)
537+
})
538+
c.stopCtxAfterFunc = func() {
539+
if !stop() {
540+
c.resetClient()
541+
}
542+
c.stopCtxAfterFunc = func() {}
543+
}
544+
545+
signers, err := c.client.Signers()
546+
if err != nil {
547+
if _, ok := errors.AsType[*net.OpError](err); ok {
548+
c.resetClient()
549+
}
550+
return nil, fmt.Errorf("failed to get signers from SSH agent: %w", err)
551+
}
552+
return signers, nil
553+
}
554+
555+
// StopContextAfterFunc releases resources associated with the context.
556+
func (c *SSHAgentClient) StopContextAfterFunc() {
557+
c.stopCtxAfterFunc()
558+
}

0 commit comments

Comments
 (0)