#5648·kopia

SFTP: ssh.ClientConfig.Timeout is never set, leaving connection attempts unbounded

Author: mitarCreated Sep 15, 2026Updated Sep 15, 2026

createSSHConfig builds the client config with only User, Auth and HostKeyCallback, leaving Timeout at its zero value. ssh.Dial passes that straight to net.DialTimeout, where zero means no timeout, so each connection attempt falls back to the kernel's tcp_syn_retries (~127s with the default of 6).

That cost is then multiplied twice over:

  • connection.UsingConnection retries the dial through retry.WithExponentialBackoff, with maxAttempts = 10
  • retryingStorage wraps every blob operation in another 10 attempts

So a single blob operation against an unreachable SFTP host can spend roughly 100 x 127s (~3.5 hours) before giving up, and a snapshot performs many of them.

Reproduction

Tested with kopia 0.23.1 (build 72ec08fd8edb86c67ed27099bf1b955e1f308ffa) on Linux 6.8.

  1. Create an SFTP repository against a reachable host.
  2. Make the server unreachable (drop, do not reject): iptables -A OUTPUT -p tcp --dport 22 -j DROP
  3. kopia snapshot create <dir>

Observed: the command does not terminate. In my test it was still running after two hours, repeatedly logging

connection failed: error establishing connecting: unable to dial [127.0.0.1:22]:
&ssh.ClientConfig{... Timeout:0}: dial tcp 127.0.0.1:22: connect: connection timed out, will retry

Note Timeout:0 in the dumped config.

Expected: the operation fails within a bounded, ideally configurable, period.

Suggested fix

Add a connect-timeout option to sftp.Options, plumb it into ssh.ClientConfig.Timeout, and give it a non-zero default. The external SSH path can already get this through -o ConnectTimeout= in --ssh-args; the native Go path has no equivalent.

Note

Making this bounded matters more than it might appear, because the run also cannot be stopped with SIGTERM while it is in this state (the terminate handler sets a cooperative flag that the retry loop never reads), so timeout, a manual kill, and service shutdown all fail to interrupt it. See my comment here.