diff --git a/docs/content/docs.md b/docs/content/docs.md index 218757a86..b053a8f89 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -249,6 +249,13 @@ If running rclone from a script you might want to use today's date as the directory name passed to `--backup-dir` to store the old files, or you might want to pass `--suffix` with today's date. +### --bind string ### + +Local address to bind to for outgoing connections. This can be an +IPv4 address (1.2.3.4), an IPv6 address (1234::789A) or host name. If +the host name doesn't resolve or resoves to more than one IP address +it will give an error. + ### --bwlimit=BANDWIDTH_SPEC ### This option controls the bandwidth limit. Limits can be specified diff --git a/docs/content/ftp.md b/docs/content/ftp.md index 6ea0f0a8e..4d69c5210 100644 --- a/docs/content/ftp.md +++ b/docs/content/ftp.md @@ -126,4 +126,6 @@ with it: `--dump-headers`, `--dump-bodies`, `--dump-auth` Note that `--timeout` isn't supported (but `--contimeout` is). +Note that `--bind` isn't supported. + FTP could support server side move but doesn't yet. diff --git a/fs/config.go b/fs/config.go index 8f2a57258..1e732c9f9 100644 --- a/fs/config.go +++ b/fs/config.go @@ -14,6 +14,7 @@ import ( "io" "io/ioutil" "log" + "net" "os" "os/user" "path/filepath" @@ -98,6 +99,7 @@ var ( useListR = BoolP("fast-list", "", false, "Use recursive list if available. Uses more memory but fewer transactions.") tpsLimit = Float64P("tpslimit", "", 0, "Limit HTTP transactions per second to this.") tpsLimitBurst = IntP("tpslimit-burst", "", 1, "Max burst of transactions for --tpslimit.") + bindAddr = StringP("bind", "", "", "Local address to bind to for outgoing connections, IPv4, IPv4 or name.") logLevel = LogLevelNotice statsLogLevel = LogLevelInfo bwLimit BwTimetable @@ -232,6 +234,7 @@ type ConfigInfo struct { BufferSize SizeSuffix TPSLimit float64 TPSLimitBurst int + BindAddr net.IP } // Return the path to the configuration file @@ -398,6 +401,17 @@ func LoadConfig() { log.Fatalf(`Can only use --suffix with --backup-dir.`) } + if *bindAddr != "" { + addrs, err := net.LookupIP(*bindAddr) + if err != nil { + log.Fatalf("--bind: Failed to parse %q as IP address: %v", *bindAddr, err) + } + if len(addrs) != 1 { + log.Fatalf("--bind: Expecting 1 IP address for %q but got %d", *bindAddr, len(addrs)) + } + Config.BindAddr = addrs[0] + } + // Load configuration file. var err error configData, err = loadConfigFile() diff --git a/fs/http.go b/fs/http.go index adecd127e..b6db85601 100644 --- a/fs/http.go +++ b/fs/http.go @@ -272,3 +272,16 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error } return resp, err } + +// NewDialer creates a net.Dialer structure with Timeout, Keepalive +// and LocalAddr set from rclone flags. +func (ci *ConfigInfo) NewDialer() *net.Dialer { + dialer := &net.Dialer{ + Timeout: ci.ConnectTimeout, + KeepAlive: 30 * time.Second, + } + if ci.BindAddr != nil { + dialer.LocalAddr = &net.TCPAddr{IP: ci.BindAddr} + } + return dialer +} diff --git a/fs/http_new.go b/fs/http_new.go index 4099ffa6f..68d8deecc 100644 --- a/fs/http_new.go +++ b/fs/http_new.go @@ -12,23 +12,18 @@ import ( ) // dial with context and timeouts -func dialContextTimeout(ctx context.Context, network, address string, connectTimeout, timeout time.Duration) (net.Conn, error) { - dialer := net.Dialer{ - Timeout: connectTimeout, - KeepAlive: 30 * time.Second, - } +func (ci *ConfigInfo) dialContextTimeout(ctx context.Context, network, address string) (net.Conn, error) { + dialer := ci.NewDialer() c, err := dialer.DialContext(ctx, network, address) if err != nil { return c, err } - return newTimeoutConn(c, timeout), nil + return newTimeoutConn(c, ci.Timeout), nil } // Initialise the http.Transport for go1.7+ func (ci *ConfigInfo) initTransport(t *http.Transport) { - t.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { - return dialContextTimeout(ctx, network, address, ci.ConnectTimeout, ci.Timeout) - } + t.DialContext = ci.dialContextTimeout t.IdleConnTimeout = 60 * time.Second t.ExpectContinueTimeout = ci.ConnectTimeout } diff --git a/fs/http_old.go b/fs/http_old.go index 2fc9df019..9226b5b9b 100644 --- a/fs/http_old.go +++ b/fs/http_old.go @@ -7,25 +7,19 @@ package fs import ( "net" "net/http" - "time" ) // dial with timeouts -func dialTimeout(network, address string, connectTimeout, timeout time.Duration) (net.Conn, error) { - dialer := net.Dialer{ - Timeout: connectTimeout, - KeepAlive: 30 * time.Second, - } +func (ci *ConfigInfo) dialTimeout(network, address string) (net.Conn, error) { + dialer := ci.NewDialer() c, err := dialer.Dial(network, address) if err != nil { return c, err } - return newTimeoutConn(c, timeout), nil + return newTimeoutConn(c, ci.Timeout), nil } // Initialise the http.Transport for pre go1.7 func (ci *ConfigInfo) initTransport(t *http.Transport) { - t.Dial = func(network, address string) (net.Conn, error) { - return dialTimeout(network, address, ci.ConnectTimeout, ci.Timeout) - } + t.Dial = dialTimeout } diff --git a/sftp/sftp.go b/sftp/sftp.go index ef64f3f20..8c3fe67be 100644 --- a/sftp/sftp.go +++ b/sftp/sftp.go @@ -79,6 +79,22 @@ type ObjectReader struct { sftpFile *sftp.File } +// Dial starts a client connection to the given SSH server. It is a +// convenience function that connects to the given network address, +// initiates the SSH handshake, and then sets up a Client. +func Dial(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + dialer := fs.Config.NewDialer() + conn, err := dialer.Dial(network, addr) + if err != nil { + return nil, err + } + c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) + if err != nil { + return nil, err + } + return ssh.NewClient(c, chans, reqs), nil +} + // NewFs creates a new Fs object from the name and root. It connects to // the host specified in the config file. func NewFs(name, root string) (fs.Fs, error) { @@ -135,7 +151,7 @@ func NewFs(name, root string) (fs.Fs, error) { config.Auth = append(config.Auth, ssh.Password(clearpass)) } - sshClient, err := ssh.Dial("tcp", host+":"+port, config) + sshClient, err := Dial("tcp", host+":"+port, config) if err != nil { return nil, errors.Wrap(err, "couldn't connect ssh") }