mirror of
https://github.com/restic/restic.git
synced 2026-04-21 14:17:12 -04:00
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:
parent
13cb90b83a
commit
1637d96d62
6 changed files with 176 additions and 48 deletions
14
changelog/unreleased/issue-5313
Normal file
14
changelog/unreleased/issue-5313
Normal 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
|
||||
|
|
@ -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
|
||||
***********
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue