Support IPv6 in SFTP backend

The previous code was doing its own hostname:port splitting, which
caused IPv6 addresses to be misinterpreted.
This commit is contained in:
greatroar 2020-02-19 15:33:52 +01:00
parent f2bf06a419
commit 6ac6bca7a1
5 changed files with 60 additions and 22 deletions

View file

@ -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 want to specify a path relative to the user's home directory, pass a
relative path to the sftp backend. relative path to the sftp backend.
The backend config string does not allow specifying a port. If you need If you need to specify a port number or IPv6 address, you'll need to use
to contact an sftp server on a different port, you can create an entry URL syntax. E.g., the repository ``/srv/restic-repo`` on ``[::1]`` (localhost)
in the ``ssh`` file, usually located in your user's home directory at at port 2222 with username ``user`` can be specified as
``~/.ssh/config`` or in ``/etc/ssh/ssh_config``:
::
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``:
:: ::

View file

@ -11,7 +11,8 @@ import (
// Config collects all information required to connect to an sftp server. // Config collects all information required to connect to an sftp server.
type Config struct { type Config struct {
User, Host, Path string User, Host, Port, Path string
Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"` Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"`
Command string `option:"command" help:"specify command to create sftp connection"` Command string `option:"command" help:"specify command to create sftp connection"`
} }
@ -21,12 +22,12 @@ func init() {
} }
// ParseConfig parses the string s and extracts the sftp config. The // 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 // and sftp:user@host:directory. The directory will be path Cleaned and can
// be an absolute path if it starts with a '/' (e.g. // be an absolute path if it starts with a '/' (e.g.
// sftp://user@host//absolute and sftp:user@host:/absolute). // sftp://user@host//absolute and sftp:user@host:/absolute).
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (interface{}, error) {
var user, host, dir string var user, host, port, dir string
switch { switch {
case strings.HasPrefix(s, "sftp://"): case strings.HasPrefix(s, "sftp://"):
// parse the "sftp://user@host/path" url format // parse the "sftp://user@host/path" url format
@ -37,7 +38,8 @@ func ParseConfig(s string) (interface{}, error) {
if url.User != nil { if url.User != nil {
user = url.User.Username() user = url.User.Username()
} }
host = url.Host host = url.Hostname()
port = url.Port()
dir = url.Path dir = url.Path
if dir == "" { if dir == "" {
return nil, errors.Errorf("invalid backend %q, no directory specified", s) return nil, errors.Errorf("invalid backend %q, no directory specified", s)
@ -76,6 +78,7 @@ func ParseConfig(s string) (interface{}, error) {
return Config{ return Config{
User: user, User: user,
Host: host, Host: host,
Port: port,
Path: p, Path: p,
}, nil }, nil
} }

View file

@ -23,11 +23,11 @@ var configTests = []struct {
}, },
{ {
"sftp://host:10022//dir/subdir", "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", "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", "sftp://user@host/dir/subdir/../other",
@ -38,6 +38,17 @@ var configTests = []struct {
Config{User: "user", Host: "host", Path: "dir/subdir"}, 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 // second form, user specified sftp:user@host:/dir
{ {
"sftp:user@host:/dir/subdir", "sftp:user@host:/dir/subdir",

View file

@ -8,7 +8,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"strings"
"time" "time"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -190,10 +189,11 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
cmd = "ssh" cmd = "ssh"
hostport := strings.Split(cfg.Host, ":") host, port := cfg.Host, cfg.Port
args = []string{hostport[0]}
if len(hostport) > 1 { args = []string{host}
args = append(args, "-p", hostport[1]) if port != "" {
args = append(args, "-p", port)
} }
if cfg.User != "" { if cfg.User != "" {
args = append(args, "-l") args = append(args, "-l")

View file

@ -21,23 +21,35 @@ var sshcmdTests = []struct {
[]string{"host", "-s", "sftp"}, []string{"host", "-s", "sftp"},
}, },
{ {
Config{Host: "host:10022", Path: "/dir/subdir"}, Config{Host: "host", Port: "10022", Path: "/dir/subdir"},
"ssh", "ssh",
[]string{"host", "-p", "10022", "-s", "sftp"}, []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", "ssh",
[]string{"host", "-p", "10022", "-l", "user", "-s", "sftp"}, []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) { func TestBuildSSHCommand(t *testing.T) {
for _, test := range sshcmdTests { for i, test := range sshcmdTests {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
cmd, args, err := buildSSHCommand(test.cfg) cmd, args, err := buildSSHCommand(test.cfg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatalf("%v in test %d", err, i)
} }
if cmd != test.cmd { if cmd != test.cmd {
@ -45,7 +57,8 @@ func TestBuildSSHCommand(t *testing.T) {
} }
if !reflect.DeepEqual(test.args, args) { 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)
} }
}) })
} }