@@ -3,11 +3,13 @@ package utils
33import (
44 "context"
55 "encoding/base64"
6+ "errors"
67 "fmt"
78 "log/slog"
8- "sync/atomic "
9+ "sync"
910 "time"
1011
12+ "go.temporal.io/sdk/log"
1113 "golang.org/x/crypto/ssh"
1214
1315 "github.com/PeerDB-io/peerdb/flow/generated/protos"
@@ -16,13 +18,17 @@ import (
1618 "github.com/PeerDB-io/peerdb/flow/shared/exceptions"
1719)
1820
19- const SSHKeepaliveInterval = 15 * time .Second
21+ const (
22+ SSHKeepaliveInterval = 15 * time .Second
23+ SSHRequestTimeout = 10 * time .Second
24+ )
2025
2126type SSHTunnel struct {
2227 * ssh.Client
23- logger * slog.Logger
24- keepaliveChan atomic.Pointer [chan struct {}]
25- badTunnel bool
28+ logger log.Logger
29+ watch chan struct {}
30+ err error
31+ closeOnce sync.Once
2632}
2733
2834// GetSSHClientConfig returns an *ssh.ClientConfig based on provided credentials.
@@ -76,128 +82,98 @@ func NewSSHTunnel(
7682 ctx context.Context ,
7783 sshConfig * protos.SSHConfig ,
7884) (* SSHTunnel , error ) {
79- if sshConfig != nil {
80- logger := internal .LoggerFromCtx (ctx )
81- sshServer := shared .JoinHostPort (sshConfig .Host , sshConfig .Port )
82- clientConfig , err := GetSSHClientConfig (sshConfig )
83- if err != nil {
84- logger .Error ("Failed to get SSH client config" , slog .Any ("error" , err ))
85- return nil , err
86- }
85+ if sshConfig == nil {
86+ return nil , nil
87+ }
8788
88- logger .Info ("Setting up SSH connection" , slog .String ("Server" , sshServer ))
89- client , err := ssh .Dial ("tcp" , sshServer , clientConfig )
90- if err != nil {
91- return nil , exceptions .NewSSHTunnelSetupError (err )
92- }
89+ logger := internal .LoggerFromCtx (ctx )
90+ sshServer := shared .JoinHostPort (sshConfig .Host , sshConfig .Port )
91+ clientConfig , err := GetSSHClientConfig (sshConfig )
92+ if err != nil {
93+ logger .Error ("Failed to get SSH client config" , slog .Any ("error" , err ))
94+ return nil , err
95+ }
9396
94- return & SSHTunnel {
95- Client : client ,
96- logger : internal . SlogLoggerFromCtx ( ctx ),
97- }, nil
97+ logger . Info ( "Setting up SSH connection" , slog . String ( "Server" , sshServer ))
98+ client , err := ssh . Dial ( "tcp" , sshServer , clientConfig )
99+ if err != nil {
100+ return nil , exceptions . NewSSHTunnelSetupError ( err )
98101 }
99102
100- return nil , nil
103+ tunnel := & SSHTunnel {
104+ Client : client ,
105+ logger : internal .SlogLoggerFromCtx (ctx ),
106+ watch : make (chan struct {}),
107+ }
108+
109+ go tunnel .runKeepaliveLoop ()
110+
111+ return tunnel , nil
112+ }
113+
114+ func (tunnel * SSHTunnel ) Watch () <- chan struct {} {
115+ if tunnel == nil || tunnel .Client == nil {
116+ return nil
117+ }
118+ return tunnel .watch
101119}
102120
103121func (tunnel * SSHTunnel ) Close () error {
104- if tunnel != nil && tunnel .Client != nil {
105- if keepaliveChan := tunnel .keepaliveChan .Swap (nil ); keepaliveChan != nil {
106- close (* keepaliveChan )
107- }
108- tunnel .badTunnel = true
109- return tunnel .Client .Close ()
122+ if tunnel == nil || tunnel .Client == nil {
123+ return nil
110124 }
111- return nil
125+ tunnel .shutdown (nil )
126+ return tunnel .err
127+ }
128+
129+ func (tunnel * SSHTunnel ) shutdown (err error ) {
130+ tunnel .closeOnce .Do (func () {
131+ tunnel .err = err
132+ close (tunnel .watch )
133+ if closeErr := tunnel .Client .Close (); closeErr != nil && tunnel .err == nil {
134+ tunnel .err = closeErr
135+ }
136+ })
112137}
113138
114- func (tunnel * SSHTunnel ) runKeepaliveLoop (
115- ctx context.Context , stopChan <- chan struct {}, onFailure func (),
116- ) {
139+ func (tunnel * SSHTunnel ) runKeepaliveLoop () {
117140 ticker := time .NewTicker (SSHKeepaliveInterval )
118141 defer ticker .Stop ()
119- logger := tunnel .logger
120- // in case request hangs, we want to detect that and not send another request
121- requestSent := atomic.Bool {}
122- var keepaliveErr error
123- // closed by request making goroutine to signal error, keepaliveErr
124- errChan := make (chan struct {})
125-
126142 for {
127143 select {
144+ case <- tunnel .watch :
145+ // tunnel closed
146+ return
128147 case <- ticker .C :
129- if requestSent .Load () {
130- // Previous keepalive request didn't return yet, something's wrong
131- logger .ErrorContext (ctx , "Previous keepalive request still pending, marking tunnel as bad" )
132- if keepaliveChan := tunnel .keepaliveChan .Swap (nil ); keepaliveChan != nil {
133- close (* keepaliveChan )
134- }
135- tunnel .badTunnel = true
136- if onFailure != nil {
137- onFailure ()
138- }
148+ success := tunnel .sendKeepAlive ()
149+ if ! success {
139150 return
140151 }
141- go func () {
142- requestSent .Store (true )
143- _ , _ , err := tunnel .Client .SendRequest ("keepalive@openssh.com" , true , nil )
144- requestSent .Store (false )
145- if err != nil {
146- keepaliveErr = err
147- close (errChan )
148- }
149- }()
150- case <- ctx .Done ():
151- if keepaliveChan := tunnel .keepaliveChan .Swap (nil ); keepaliveChan != nil {
152- close (* keepaliveChan )
153- }
154- return
155- case <- stopChan :
156- // channel closed from outside
157- return
158- case <- errChan :
159- logger .ErrorContext (ctx , "Keepalive request failed, marking tunnel as bad" , slog .Any ("error" , keepaliveErr ))
160- if keepaliveChan := tunnel .keepaliveChan .Swap (nil ); keepaliveChan != nil {
161- close (* keepaliveChan )
162- }
163- tunnel .badTunnel = true
164- if onFailure != nil {
165- onFailure ()
166- }
167- return
168152 }
169153 }
170154}
171155
172- func (tunnel * SSHTunnel ) StartKeepalive (ctx context.Context , onFailure func ()) {
173- if tunnel == nil || tunnel .Client == nil || tunnel .badTunnel {
174- return
175- }
176- if tunnel .keepaliveChan .Load () != nil {
177- // Already started
178- return
179- }
180- stopChan := make (chan struct {})
181- tunnel .keepaliveChan .Store (& stopChan )
182-
183- go tunnel .runKeepaliveLoop (ctx , stopChan , onFailure )
184- }
185-
186- // returns a channel that is closed if the SSH keepalive fails,
187- // or nil if no SSH tunnel is configured
188- func (tunnel * SSHTunnel ) GetKeepaliveChan (ctx context.Context ) <- chan struct {} {
189- if tunnel == nil || tunnel .Client == nil || tunnel .badTunnel {
190- // nil channel would be of no consequence in a select
191- // UNLESS it's the only branch in a select, in which case it would block forever
192- return nil
193- }
194- if keepaliveChan := tunnel .keepaliveChan .Load (); keepaliveChan != nil {
195- // Already started
196- return * keepaliveChan
156+ func (tunnel * SSHTunnel ) sendKeepAlive () bool {
157+ sent := make (chan error , 1 )
158+ go func () {
159+ _ , _ , err := tunnel .SendRequest ("keepalive@openssh.com" , true , nil )
160+ sent <- err
161+ }()
162+ timer := time .NewTimer (SSHRequestTimeout )
163+ defer timer .Stop ()
164+ select {
165+ case err := <- sent :
166+ if err != nil {
167+ tunnel .logger .Error ("SSH keepalive failed, tearing down tunnel" , slog .Any ("error" , err ))
168+ tunnel .shutdown (err )
169+ return false
170+ }
171+ return true
172+ case <- timer .C :
173+ tunnel .logger .Error ("SSH keepalive request timed out, tearing down tunnel" )
174+ tunnel .shutdown (errors .New ("SSH keepalive request timed out" ))
175+ return false
176+ case <- tunnel .watch :
177+ return false
197178 }
198- keepaliveChan := make (chan struct {})
199- tunnel .keepaliveChan .Store (& keepaliveChan )
200-
201- go tunnel .runKeepaliveLoop (ctx , keepaliveChan , nil )
202- return keepaliveChan
203179}
0 commit comments