@@ -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