From 347812d1d36f4b840302a84168aca9b07195e556 Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 29 Jul 2023 22:02:08 -0400 Subject: [PATCH] ftp,sftp: add socks_proxy support for SOCKS5 proxies Fixes #3558 --- backend/ftp/ftp.go | 21 +++++++++++++++++- backend/sftp/sftp.go | 12 +++++++++++ backend/sftp/ssh_internal.go | 15 +++++++++++-- lib/proxy/socks.go | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 lib/proxy/socks.go diff --git a/backend/ftp/ftp.go b/backend/ftp/ftp.go index 6a2758911..1aa646bef 100644 --- a/backend/ftp/ftp.go +++ b/backend/ftp/ftp.go @@ -28,6 +28,7 @@ import ( "github.com/rclone/rclone/lib/encoder" "github.com/rclone/rclone/lib/env" "github.com/rclone/rclone/lib/pacer" + "github.com/rclone/rclone/lib/proxy" "github.com/rclone/rclone/lib/readers" ) @@ -174,6 +175,18 @@ Enabled by default. Use 0 to disable.`, If this is set and no password is supplied then rclone will ask for a password `, Advanced: true, + }, { + Name: "socks_proxy", + Default: "", + Help: `Socks 5 proxy host. + + Supports the format user:pass@host:port, user@host:port, host:port. + + Example: + + myUser:myPass@localhost:9005 + `, + Advanced: true, }, { Name: config.ConfigEncoding, Help: config.ConfigEncodingHelp, @@ -218,6 +231,7 @@ type Options struct { ShutTimeout fs.Duration `config:"shut_timeout"` AskPassword bool `config:"ask_password"` Enc encoder.MultiEncoder `config:"encoding"` + SocksProxy string `config:"socks_proxy"` } // Fs represents a remote FTP server @@ -359,7 +373,12 @@ func (f *Fs) ftpConnection(ctx context.Context) (c *ftp.ServerConn, err error) { defer func() { fs.Debugf(f, "> dial: conn=%T, err=%v", conn, err) }() - conn, err = fshttp.NewDialer(ctx).Dial(network, address) + baseDialer := fshttp.NewDialer(ctx) + if f.opt.SocksProxy != "" { + conn, err = proxy.SOCKS5Dial(network, address, f.opt.SocksProxy, baseDialer) + } else { + conn, err = baseDialer.Dial(network, address) + } if err != nil { return nil, err } diff --git a/backend/sftp/sftp.go b/backend/sftp/sftp.go index 0f914cdfa..13b30c539 100644 --- a/backend/sftp/sftp.go +++ b/backend/sftp/sftp.go @@ -416,6 +416,17 @@ An example setting might be: Note that when using an external ssh binary rclone makes a new ssh connection for every hash it calculates. `, + }, { + Name: "socks_proxy", + Default: "", + Help: `Socks 5 proxy host. + +Supports the format user:pass@host:port, user@host:port, host:port. + +Example: + + myUser:myPass@localhost:9005 + `, Advanced: true, }}, } @@ -457,6 +468,7 @@ type Options struct { MACs fs.SpaceSepList `config:"macs"` HostKeyAlgorithms fs.SpaceSepList `config:"host_key_algorithms"` SSH fs.SpaceSepList `config:"ssh"` + SocksProxy string `config:"socks_proxy"` } // Fs stores the interface to the remote SFTP files diff --git a/backend/sftp/ssh_internal.go b/backend/sftp/ssh_internal.go index 4237a8e28..2352601e3 100644 --- a/backend/sftp/ssh_internal.go +++ b/backend/sftp/ssh_internal.go @@ -6,9 +6,11 @@ package sftp import ( "context" "io" + "net" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/fshttp" + "github.com/rclone/rclone/lib/proxy" "golang.org/x/crypto/ssh" ) @@ -22,8 +24,17 @@ type sshClientInternal struct { // convenience function that connects to the given network address, // initiates the SSH handshake, and then sets up a Client. func (f *Fs) newSSHClientInternal(ctx context.Context, network, addr string, sshConfig *ssh.ClientConfig) (sshClient, error) { - dialer := fshttp.NewDialer(ctx) - conn, err := dialer.Dial(network, addr) + + baseDialer := fshttp.NewDialer(ctx) + var ( + conn net.Conn + err error + ) + if f.opt.SocksProxy != "" { + conn, err = proxy.SOCKS5Dial(network, addr, f.opt.SocksProxy, baseDialer) + } else { + conn, err = baseDialer.Dial(network, addr) + } if err != nil { return nil, err } diff --git a/lib/proxy/socks.go b/lib/proxy/socks.go new file mode 100644 index 000000000..607bf43a7 --- /dev/null +++ b/lib/proxy/socks.go @@ -0,0 +1,41 @@ +package proxy + +import ( + "fmt" + "net" + "strings" + + "golang.org/x/net/proxy" +) + +// SOCKS5Dial dials a net.Conn using a SOCKS5 proxy server. +// The socks5Proxy address can be in the form of [user:password@]host:port, [user@]host:port or just host:port if no auth is required. +// It will optionally take a proxyDialer to dial the SOCKS5 proxy server. If nil is passed, it will use the default net.Dialer. +func SOCKS5Dial(network, addr, socks5Proxy string, proxyDialer proxy.Dialer) (net.Conn, error) { + + if proxyDialer == nil { + proxyDialer = &net.Dialer{} + } + var ( + proxyAddress string + proxyAuth *proxy.Auth + ) + if credsAndHost := strings.SplitN(socks5Proxy, "@", 2); len(credsAndHost) == 2 { + proxyCreds := strings.SplitN(credsAndHost[0], ":", 2) + proxyAuth = &proxy.Auth{ + User: proxyCreds[0], + } + if len(proxyCreds) == 2 { + proxyAuth.Password = proxyCreds[1] + } + proxyAddress = credsAndHost[1] + } else { + proxyAddress = credsAndHost[0] + } + proxyDialer, err := proxy.SOCKS5("tcp", proxyAddress, proxyAuth, proxyDialer) + if err != nil { + return nil, fmt.Errorf("failed to create proxy dialer: %w", err) + } + return proxyDialer.Dial(network, addr) + +}