backend/sftp: Add sftp.server-alive-interval and sftp.server-alive-count-max options

Restic previously did not set the `ServerAliveInterval` and `ServerAliveCountMax`
SSH options when connecting to an SFTP server, resulting in backups being
interrupted on some connections. It now sets these options to the values
specified by the `sftp.server-alive-interval` and `sftp.server-alive-count-max`
configuration options, to avoid connections being interrupted unnecessarily.
The default values are 25 seconds for `ServerAliveInterval` and
200 for `ServerAliveCountMax`. If either is set to -1,
the corresponding SSH option is not set and the value from the SSH
configuration is used.
This commit is contained in:
merlinz01 2025-03-25 15:03:13 -04:00
parent 13cb90b83a
commit 1637d96d62
6 changed files with 176 additions and 48 deletions

View file

@ -0,0 +1,14 @@
Enhancement: Add `sftp.server-alive-interval` and `sftp.server-alive-count-max` options
Restic previously did not set the `ServerAliveInterval` and `ServerAliveCountMax`
SSH options when connecting to an SFTP server, resulting in backups being
interrupted on some connections. It now sets these options to the values
specified by the `sftp.server-alive-interval` and `sftp.server-alive-count-max`
configuration options, to avoid connections being interrupted unnecessarily.
The default values are 25 seconds for `ServerAliveInterval` and
200 for `ServerAliveCountMax`. If either is set to -1,
the corresponding SSH option is not set and the value from the SSH
configuration is used.
https://github.com/restic/restic/issues/5313
https://forum.restic.net/t/robustness-of-repository-issue-resolved/1475

View file

@ -178,15 +178,13 @@ setting the arguments passed to the default SSH command (ignored when
.. note:: Please be aware that SFTP servers close connections when no data is
received by the client. This can happen when restic is processing huge
amounts of unchanged data. To avoid this issue add the following lines
to the client's .ssh/config file:
amounts of unchanged data. Restic sets the `ServerAliveInterval` and
`ServerAliveCountMax` options for `ssh` to keep the connection alive. If you
experience connection issues, you can adjust these settings with Restic's
`sftp.server-alive-interval` and `sftp.server-alive-count-max` options.
::
ServerAliveInterval 60
ServerAliveCountMax 240
REST Server
***********

View file

@ -17,13 +17,21 @@ type Config struct {
Args string `option:"args" help:"specify arguments for ssh"`
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
// SSH options
ServerAliveInterval int `option:"server-alive-interval" help:"set the interval to send keepalive messages, or -1 to not set (default: 25)"`
ServerAliveCountMax int `option:"server-alive-count-max" help:"set the number of unacknowledged keepalive messages allowed before disconnecting, or -1 to not set (default: 200)"`
}
var defaultConfig = Config{
Connections: 5,
ServerAliveInterval: 25,
ServerAliveCountMax: 200,
}
// NewConfig returns a new config with default options applied.
func NewConfig() Config {
return Config{
Connections: 5,
}
return defaultConfig
}
func init() {

View file

@ -9,69 +9,129 @@ import (
var configTests = []test.ConfigTestData[Config]{
// first form, user specified sftp://user@host/dir
{
S: "sftp://user@host/dir/subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://user@host/dir/subdir",
Cfg: Config{
User: "user", Host: "host", Path: "dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://host/dir/subdir",
Cfg: Config{Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://host/dir/subdir",
Cfg: Config{
Host: "host", Path: "dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://host//dir/subdir",
Cfg: Config{Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp://host//dir/subdir",
Cfg: Config{
Host: "host", Path: "/dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://host:10022//dir/subdir",
Cfg: Config{Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
S: "sftp://host:10022//dir/subdir",
Cfg: Config{
Host: "host", Port: "10022", Path: "/dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://user@host:10022//dir/subdir",
Cfg: Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
S: "sftp://user@host:10022//dir/subdir",
Cfg: Config{
User: "user", Host: "host", Port: "10022", Path: "/dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://user@host/dir/subdir/../other",
Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
S: "sftp://user@host/dir/subdir/../other",
Cfg: Config{
User: "user", Host: "host", Path: "dir/other",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp://user@host/dir///subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://user@host/dir///subdir",
Cfg: Config{
User: "user", Host: "host", Path: "dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
// IPv6 address.
{
S: "sftp://user@[::1]/dir",
Cfg: Config{User: "user", Host: "::1", Path: "dir", Connections: 5},
S: "sftp://user@[::1]/dir",
Cfg: Config{
User: "user", Host: "::1", Path: "dir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
// IPv6 address with port.
{
S: "sftp://user@[::1]:22/dir",
Cfg: Config{User: "user", Host: "::1", Port: "22", Path: "dir", Connections: 5},
S: "sftp://user@[::1]:22/dir",
Cfg: Config{
User: "user", Host: "::1", Port: "22", Path: "dir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
// second form, user specified sftp:user@host:/dir
{
S: "sftp:user@host:/dir/subdir",
Cfg: Config{User: "user", Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp:user@host:/dir/subdir",
Cfg: Config{
User: "user", Host: "host", Path: "/dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp:user@domain@host:/dir/subdir",
Cfg: Config{User: "user@domain", Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp:user@domain@host:/dir/subdir",
Cfg: Config{
User: "user@domain", Host: "host", Path: "/dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp:host:../dir/subdir",
Cfg: Config{Host: "host", Path: "../dir/subdir", Connections: 5},
S: "sftp:host:../dir/subdir",
Cfg: Config{
Host: "host", Path: "../dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp:user@host:dir/subdir:suffix",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir:suffix", Connections: 5},
S: "sftp:user@host:dir/subdir:suffix",
Cfg: Config{
User: "user", Host: "host", Path: "dir/subdir:suffix",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp:user@host:dir/subdir/../other",
Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
S: "sftp:user@host:dir/subdir/../other",
Cfg: Config{
User: "user", Host: "host", Path: "dir/other",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
{
S: "sftp:user@host:dir///subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp:user@host:dir///subdir",
Cfg: Config{
User: "user", Host: "host", Path: "dir/subdir",
Connections: defaultConfig.Connections,
ServerAliveInterval: defaultConfig.ServerAliveInterval,
ServerAliveCountMax: defaultConfig.ServerAliveCountMax},
},
}

View file

@ -224,6 +224,15 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
if cfg.User != "" {
args = append(args, "-l", cfg.User)
}
if cfg.ServerAliveInterval >= 0 {
args = append(args, "-o", fmt.Sprintf("ServerAliveInterval=%d", cfg.ServerAliveInterval))
}
if cfg.ServerAliveCountMax == 0 {
return "", nil, errors.New("sftp.server-alive-count-max cannot be 0")
}
if cfg.ServerAliveCountMax > 0 {
args = append(args, "-o", fmt.Sprintf("ServerAliveCountMax=%d", cfg.ServerAliveCountMax))
}
if cfg.Args != "" {
a, err := backend.SplitShellStrings(cfg.Args)

View file

@ -12,55 +12,91 @@ var sshcmdTests = []struct {
err string
}{
{
Config{User: "user", Host: "host", Path: "dir/subdir"},
Config{User: "user", Host: "host", Path: "dir/subdir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-l", "user", "-s", "sftp"},
"",
},
{
Config{Host: "host", Path: "dir/subdir"},
Config{Host: "host", Path: "dir/subdir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-s", "sftp"},
"",
},
{
Config{Host: "host", Port: "10022", Path: "/dir/subdir"},
Config{Host: "host", Port: "10022", Path: "/dir/subdir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-p", "10022", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir"},
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-p", "10022", "-l", "user", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Args: "-i /path/to/id_rsa"},
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Args: "-i /path/to/id_rsa", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-p", "10022", "-l", "user", "-i", "/path/to/id_rsa", "-s", "sftp"},
"",
},
{
Config{Command: "ssh something", Args: "-i /path/to/id_rsa"},
Config{Command: "ssh something", Args: "-i /path/to/id_rsa", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"",
nil,
"cannot specify both sftp.command and sftp.args options",
},
{
// IPv6 address.
Config{User: "user", Host: "::1", Path: "dir"},
Config{User: "user", Host: "::1", Path: "dir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"::1", "-l", "user", "-s", "sftp"},
"",
},
{
// IPv6 address with zone and port.
Config{User: "user", Host: "::1%lo0", Port: "22", Path: "dir"},
Config{User: "user", Host: "::1%lo0", Port: "22", Path: "dir", ServerAliveInterval: -1, ServerAliveCountMax: -1},
"ssh",
[]string{"::1%lo0", "-p", "22", "-l", "user", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: 99, ServerAliveCountMax: -1},
"ssh",
[]string{"host", "-l", "user", "-o", "ServerAliveInterval=99", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: -1, ServerAliveCountMax: 99},
"ssh",
[]string{"host", "-l", "user", "-o", "ServerAliveCountMax=99", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: 99, ServerAliveCountMax: 99},
"ssh",
[]string{"host", "-l", "user", "-o", "ServerAliveInterval=99", "-o", "ServerAliveCountMax=99", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: 99, ServerAliveCountMax: 99, Args: "-i /path/to/id_rsa"},
"ssh",
[]string{"host", "-l", "user", "-o", "ServerAliveInterval=99", "-o", "ServerAliveCountMax=99", "-i", "/path/to/id_rsa", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: 0, ServerAliveCountMax: 99},
"ssh",
[]string{"host", "-l", "user", "-o", "ServerAliveInterval=0", "-o", "ServerAliveCountMax=99", "-s", "sftp"},
"",
},
{
Config{User: "user", Host: "host", Path: "dir", ServerAliveInterval: 99, ServerAliveCountMax: 0},
"",
nil,
"sftp.server-alive-count-max cannot be 0",
},
}
func TestBuildSSHCommand(t *testing.T) {
@ -68,6 +104,9 @@ func TestBuildSSHCommand(t *testing.T) {
t.Run("", func(t *testing.T) {
cmd, args, err := buildSSHCommand(test.cfg)
if test.err != "" {
if err == nil {
t.Fatalf("expected error %v got nil", test.err)
}
if err.Error() != test.err {
t.Fatalf("expected error %v got %v", test.err, err.Error())
}