diff --git a/changelog/unreleased/pull-2592 b/changelog/unreleased/pull-2592 new file mode 100644 index 000000000..6f19dba98 --- /dev/null +++ b/changelog/unreleased/pull-2592 @@ -0,0 +1,6 @@ +Bugfix: SFTP backend supports IPv6 addresses + +The SFTP backend now supports IPv6 addresses natively, without relying on +aliases in the external SSH configuration. + +https://github.com/restic/restic/pull/2592 diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst index 9133c70a3..50f41fd9a 100644 --- a/doc/030_preparing_a_new_repo.rst +++ b/doc/030_preparing_a_new_repo.rst @@ -86,10 +86,21 @@ specify the user this way: ``user@domain@host``. want to specify a path relative to the user's home directory, pass a relative path to the sftp backend. -The backend config string does not allow specifying a port. If you need -to contact an sftp server on a different port, you can create an entry -in the ``ssh`` file, usually located in your user's home directory at -``~/.ssh/config`` or in ``/etc/ssh/ssh_config``: +If you need to specify a port number or IPv6 address, you'll need to use +URL syntax. E.g., the repository ``/srv/restic-repo`` on ``[::1]`` (localhost) +at port 2222 with username ``user`` can be specified as + +:: + + sftp://user@[::1]:2222//srv/restic-repo + +Note the double slash: the first slash separates the connection settings from +the path, while the second is the start of the path. To specify a relative +path, use one slash. + +Alternatively, you can create an entry in the ``ssh`` configuration file, +usually located in your home directory at ``~/.ssh/config`` or in +``/etc/ssh/ssh_config``: :: diff --git a/internal/backend/sftp/config.go b/internal/backend/sftp/config.go index 2860a479e..d5e0e5182 100644 --- a/internal/backend/sftp/config.go +++ b/internal/backend/sftp/config.go @@ -11,9 +11,10 @@ import ( // Config collects all information required to connect to an sftp server. type Config struct { - User, Host, Path string - Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"` - Command string `option:"command" help:"specify command to create sftp connection"` + User, Host, Port, Path string + + Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"` + Command string `option:"command" help:"specify command to create sftp connection"` } func init() { @@ -21,12 +22,12 @@ func init() { } // ParseConfig parses the string s and extracts the sftp config. The -// supported configuration formats are sftp://user@host/directory +// supported configuration formats are sftp://user@host[:port]/directory // and sftp:user@host:directory. The directory will be path Cleaned and can // be an absolute path if it starts with a '/' (e.g. // sftp://user@host//absolute and sftp:user@host:/absolute). func ParseConfig(s string) (interface{}, error) { - var user, host, dir string + var user, host, port, dir string switch { case strings.HasPrefix(s, "sftp://"): // parse the "sftp://user@host/path" url format @@ -37,7 +38,8 @@ func ParseConfig(s string) (interface{}, error) { if url.User != nil { user = url.User.Username() } - host = url.Host + host = url.Hostname() + port = url.Port() dir = url.Path if dir == "" { return nil, errors.Errorf("invalid backend %q, no directory specified", s) @@ -76,6 +78,7 @@ func ParseConfig(s string) (interface{}, error) { return Config{ User: user, Host: host, + Port: port, Path: p, }, nil } diff --git a/internal/backend/sftp/config_test.go b/internal/backend/sftp/config_test.go index acf07e4e7..d785a4113 100644 --- a/internal/backend/sftp/config_test.go +++ b/internal/backend/sftp/config_test.go @@ -23,11 +23,11 @@ var configTests = []struct { }, { "sftp://host:10022//dir/subdir", - Config{Host: "host:10022", Path: "/dir/subdir"}, + Config{Host: "host", Port: "10022", Path: "/dir/subdir"}, }, { "sftp://user@host:10022//dir/subdir", - Config{User: "user", Host: "host:10022", Path: "/dir/subdir"}, + Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir"}, }, { "sftp://user@host/dir/subdir/../other", @@ -38,6 +38,17 @@ var configTests = []struct { Config{User: "user", Host: "host", Path: "dir/subdir"}, }, + // IPv6 address. + { + "sftp://user@[::1]/dir", + Config{User: "user", Host: "::1", Path: "dir"}, + }, + // IPv6 address with port. + { + "sftp://user@[::1]:22/dir", + Config{User: "user", Host: "::1", Port: "22", Path: "dir"}, + }, + // second form, user specified sftp:user@host:/dir { "sftp:user@host:/dir/subdir", diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index bebcb4c84..b0758dcf3 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path" - "strings" "time" "github.com/restic/restic/internal/errors" @@ -190,10 +189,8 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) { cmd = "ssh" - hostport := strings.Split(cfg.Host, ":") - args = []string{hostport[0]} - if len(hostport) > 1 { - args = append(args, "-p", hostport[1]) + if cfg.Port != "" { + args = append(args, "-p", cfg.Port) } if cfg.User != "" { args = append(args, "-l") @@ -201,6 +198,8 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) { } args = append(args, "-s") args = append(args, "sftp") + + args = append(args, "--", cfg.Host) return cmd, args, nil } diff --git a/internal/backend/sftp/sshcmd_test.go b/internal/backend/sftp/sshcmd_test.go index dea811a35..63d926a37 100644 --- a/internal/backend/sftp/sshcmd_test.go +++ b/internal/backend/sftp/sshcmd_test.go @@ -13,31 +13,43 @@ var sshcmdTests = []struct { { Config{User: "user", Host: "host", Path: "dir/subdir"}, "ssh", - []string{"host", "-l", "user", "-s", "sftp"}, + []string{"-l", "user", "-s", "sftp", "--", "host"}, }, { Config{Host: "host", Path: "dir/subdir"}, "ssh", - []string{"host", "-s", "sftp"}, + []string{"-s", "sftp", "--", "host"}, }, { - Config{Host: "host:10022", Path: "/dir/subdir"}, + Config{Host: "host", Port: "10022", Path: "/dir/subdir"}, "ssh", - []string{"host", "-p", "10022", "-s", "sftp"}, + []string{"-p", "10022", "-s", "sftp", "--", "host"}, }, { - Config{User: "user", Host: "host:10022", Path: "/dir/subdir"}, + Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir"}, "ssh", - []string{"host", "-p", "10022", "-l", "user", "-s", "sftp"}, + []string{"-p", "10022", "-l", "user", "-s", "sftp", "--", "host"}, + }, + { + // IPv6 address. + Config{User: "user", Host: "::1", Path: "dir"}, + "ssh", + []string{"-l", "user", "-s", "sftp", "--", "::1"}, + }, + { + // IPv6 address with zone and port. + Config{User: "user", Host: "::1%lo0", Port: "22", Path: "dir"}, + "ssh", + []string{"-p", "22", "-l", "user", "-s", "sftp", "--", "::1%lo0"}, }, } func TestBuildSSHCommand(t *testing.T) { - for _, test := range sshcmdTests { + for i, test := range sshcmdTests { t.Run("", func(t *testing.T) { cmd, args, err := buildSSHCommand(test.cfg) if err != nil { - t.Fatal(err) + t.Fatalf("%v in test %d", err, i) } if cmd != test.cmd { @@ -45,7 +57,8 @@ func TestBuildSSHCommand(t *testing.T) { } if !reflect.DeepEqual(test.args, args) { - t.Fatalf("wrong args, want:\n %v\ngot:\n %v", test.args, args) + t.Fatalf("wrong args in test %d, want:\n %v\ngot:\n %v", + i, test.args, args) } }) }