From 6ac6bca7a1fad9f88a0c85971fb1e8b7270e169b Mon Sep 17 00:00:00 2001 From: greatroar <@> Date: Wed, 19 Feb 2020 15:33:52 +0100 Subject: [PATCH] Support IPv6 in SFTP backend The previous code was doing its own hostname:port splitting, which caused IPv6 addresses to be misinterpreted. --- doc/030_preparing_a_new_repo.rst | 19 +++++++++++++++---- internal/backend/sftp/config.go | 15 +++++++++------ internal/backend/sftp/config_test.go | 15 +++++++++++++-- internal/backend/sftp/sftp.go | 10 +++++----- internal/backend/sftp/sshcmd_test.go | 23 ++++++++++++++++++----- 5 files changed, 60 insertions(+), 22 deletions(-) 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..fc2de79d8 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,11 @@ 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]) + host, port := cfg.Host, cfg.Port + + args = []string{host} + if port != "" { + args = append(args, "-p", port) } if cfg.User != "" { args = append(args, "-l") diff --git a/internal/backend/sftp/sshcmd_test.go b/internal/backend/sftp/sshcmd_test.go index dea811a35..822f28b5d 100644 --- a/internal/backend/sftp/sshcmd_test.go +++ b/internal/backend/sftp/sshcmd_test.go @@ -21,23 +21,35 @@ var sshcmdTests = []struct { []string{"host", "-s", "sftp"}, }, { - Config{Host: "host:10022", Path: "/dir/subdir"}, + Config{Host: "host", Port: "10022", Path: "/dir/subdir"}, "ssh", []string{"host", "-p", "10022", "-s", "sftp"}, }, { - 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"}, }, + { + // IPv6 address. + Config{User: "user", Host: "::1", Path: "dir"}, + "ssh", + []string{"::1", "-l", "user", "-s", "sftp"}, + }, + { + // IPv6 address with zone and port. + Config{User: "user", Host: "::1%lo0", Port: "22", Path: "dir"}, + "ssh", + []string{"::1%lo0", "-p", "22", "-l", "user", "-s", "sftp"}, + }, } 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) } }) }