@@ -220,13 +220,14 @@ def connect( # type: ignore[override]
220220 session = transport .open_session ()
221221 AgentRequestHandler (session )
222222
223- def gateway (self , hostname , host_port , target , target_port ):
223+ def gateway (self , hostname , host_port , target , target_port , timeout = None ):
224224 transport = self .get_transport ()
225225 assert transport is not None , "No transport"
226226 return transport .open_channel (
227227 "direct-tcpip" ,
228228 (target , target_port ),
229229 (hostname , host_port ),
230+ timeout = timeout ,
230231 )
231232
232233 def parse_config (
@@ -284,6 +285,12 @@ def parse_config(
284285 if "port" in host_config :
285286 cfg ["port" ] = int (host_config ["port" ])
286287
288+ # Respect ``ConnectTimeout`` from ssh_config (issue #971): without this,
289+ # paramiko waits on its own default and a ProxyJump hop can hang for
290+ # minutes before failing.
291+ if "connecttimeout" in host_config and "timeout" not in cfg :
292+ cfg ["timeout" ] = int (host_config ["connecttimeout" ])
293+
287294 if "serveraliveinterval" in host_config :
288295 keep_alive = int (host_config ["serveraliveinterval" ])
289296
@@ -298,14 +305,26 @@ def parse_config(
298305 elif "proxyjump" in host_config :
299306 hops = host_config ["proxyjump" ].split ("," )
300307 sock = None
308+ # Propagate the target's timeout down so hop connections and the
309+ # direct-tcpip channel don't hang forever when the network misbehaves
310+ # (issue #971). Individual hops can still override via their own
311+ # ``ConnectTimeout`` in ssh_config.
312+ target_timeout = cfg .get ("timeout" )
301313
302314 for i , hop in enumerate (hops ):
303315 hop_hostname , hop_config = self .derive_shorthand (ssh_config , hop )
304316 logger .debug ("SSH ProxyJump through %s:%s" , hop_hostname , hop_config ["port" ])
305317
318+ hop_connect_kwargs = dict (hop_config )
319+ if "timeout" not in hop_connect_kwargs and target_timeout is not None :
320+ hop_connect_kwargs ["timeout" ] = target_timeout
321+
306322 c = SSHClient ()
307323 c .connect (
308- hop_hostname , _pyinfra_ssh_config_file = ssh_config_file , sock = sock , ** hop_config
324+ hop_hostname ,
325+ _pyinfra_ssh_config_file = ssh_config_file ,
326+ sock = sock ,
327+ ** hop_connect_kwargs ,
309328 )
310329
311330 if i == len (hops ) - 1 :
@@ -314,7 +333,13 @@ def parse_config(
314333 else :
315334 target , target_config = self .derive_shorthand (ssh_config , hops [i + 1 ])
316335
317- sock = c .gateway (hostname , cfg ["port" ], target , target_config ["port" ])
336+ sock = c .gateway (
337+ hostname ,
338+ cfg ["port" ],
339+ target ,
340+ target_config ["port" ],
341+ timeout = target_timeout ,
342+ )
318343 cfg ["sock" ] = sock
319344
320345 return (
@@ -354,6 +379,8 @@ def derive_shorthand(ssh_config, host_string):
354379 "port" : base_config .get ("port" , 22 ),
355380 "username" : base_config .get ("user" ),
356381 }
382+ if "connecttimeout" in base_config :
383+ config ["timeout" ] = int (base_config ["connecttimeout" ])
357384 config .update (shorthand_config )
358385
359386 return hostname , config
0 commit comments