diff --git a/.travis.yml b/.travis.yml index 1a2fb4f3..b48e8fca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ os: - osx go: - - "1.13.x" + - 1.13.x + - 1.14.x env: - GO111MODULE=on diff --git a/README.md b/README.md index cc847eb9..64481784 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Full featured and highly configurable SFTP server - SCP and rsync are supported. - Support for serving local filesystem, S3 Compatible Object Storage and Google Cloud Storage over SFTP/SCP. - Prometheus metrics are exposed. +- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP service without losing the information about the client's address. - REST API for users management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection. - Web based interface to easily manage users and connections. - Easy migration from Linux system user accounts. @@ -159,6 +160,7 @@ The `sftpgo` configuration file contains the following sections: - `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable support for Git repositories over SSH, they need to be installed and in your system's `PATH`. Git commands are not allowed inside virtual folders. - `rsync`. The `rsync` command need to be installed and in your system's `PATH`. We cannot avoid that rsync create symlinks so if the user has the permission to create symlinks we add the option `--safe-links` to the received rsync command if it is not already set. This should prevent to create symlinks that point outside the home dir. If the user cannot create symlinks we add the option `--munge-links`, if it is not already set. This should make symlinks unusable (but manually recoverable). The `rsync` command interacts with the filesystem directly and it is not aware about virtual folders, so it will be automatically disabled for users with virtual folders. - `keyboard_interactive_auth_program`, string. Absolute path to an external program to use for keyboard interactive authentication. See the "Keyboard Interactive Authentication" paragraph for more details. + - `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Set to 1 to enable proxy protocol, default 0. You have to enable the protocol in your proxy configuration too, for example for HAProxy add `send-proxy` or `send-proxy-v2` to each server configuration line - **"data_provider"**, the configuration for the data provider - `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory` - `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. For driver `memory` this is the (optional) path relative to the config dir or the absolute path to the users dump to load. @@ -219,7 +221,8 @@ Here is a full example showing the default config in JSON format: "login_banner_file": "", "setstat_mode": 0, "enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"], - "keyboard_interactive_auth_program": "" + "keyboard_interactive_auth_program": "", + "proxy_protocol": 0 }, "data_provider": { "driver": "sqlite", @@ -909,6 +912,7 @@ There is an open [issue](https://github.com/drakkan/sftpgo/issues/69) with some - [ZeroConf](https://github.com/grandcat/zeroconf) - [SB Admin 2](https://github.com/BlackrockDigital/startbootstrap-sb-admin-2) - [shlex](https://github.com/google/shlex) +- [go-proxyproto](https://github.com/pires/go-proxyproto) Some code was initially taken from [Pterodactyl sftp server](https://github.com/pterodactyl/sftp-server) diff --git a/config/config.go b/config/config.go index 08a09b37..aaaf6678 100644 --- a/config/config.go +++ b/config/config.go @@ -62,6 +62,7 @@ func init() { LoginBannerFile: "", EnabledSSHCommands: sftpd.GetDefaultSSHCommands(), KeyboardInteractiveProgram: "", + ProxyProtocol: 0, }, ProviderConf: dataprovider.Config{ Driver: "sqlite", @@ -85,7 +86,7 @@ func init() { ExternalAuthProgram: "", ExternalAuthScope: 0, CredentialsPath: "credentials", - PreLoginProgram: "", + PreLoginProgram: "", }, HTTPDConfig: httpd.Conf{ BindPort: 8080, diff --git a/go.mod b/go.mod index 5f784120..b7dc28dd 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/nathanaelle/password v1.0.0 github.com/pelletier/go-toml v1.6.0 // indirect + github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0 github.com/pkg/sftp v1.11.0 github.com/prometheus/client_golang v1.4.1 github.com/prometheus/procfs v0.0.10 // indirect diff --git a/go.sum b/go.sum index 090b0d45..96584cb9 100644 --- a/go.sum +++ b/go.sum @@ -164,6 +164,8 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0 h1:iIf32tEZ9PiKn9rPH6kloHsKshbnv8rZ8NWPfJjXj7o= +github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/sftpd/server.go b/sftpd/server.go index c0a98bf8..3e37c519 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -16,6 +16,7 @@ import ( "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/metrics" "github.com/drakkan/sftpgo/utils" + "github.com/pires/go-proxyproto" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) @@ -99,6 +100,15 @@ type Configuration struct { // Absolute path to an external program to use for keyboard interactive authentication. // Leave empty to disable this authentication mode. KeyboardInteractiveProgram string `json:"keyboard_interactive_auth_program" mapstructure:"keyboard_interactive_auth_program"` + // Support for HAProxy PROXY protocol. + // If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable + // the proxy protocol. It provides a convenient way to safely transport connection information + // such as a client's address across multiple layers of NAT or TCP proxies to get the real + // client IP address instead of the proxy IP. + // Set to 1 to enable proxy protocol. + // You have to enable the protocol in your proxy configuration too, for example for HAProxy + // add "send-proxy" or "send-proxy-v2" to each server configuration line + ProxyProtocol int `json:"proxy_protocol" mapstructure:"proxy_protocol"` } // Key contains information about host keys @@ -183,23 +193,38 @@ func (c Configuration) Initialize(configDir string) error { logger.Warn(logSender, "", "error starting listener on address %s:%d: %v", c.BindAddress, c.BindPort, err) return err } + var proxyListener *proxyproto.Listener + if c.ProxyProtocol == 1 { + proxyListener = &proxyproto.Listener{ + Listener: listener, + } + } actions = c.Actions uploadMode = c.UploadMode setstatMode = c.SetstatMode logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String()) - if c.IdleTimeout > 0 { - startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute) - } + c.checkIdleTimer() for { - conn, _ := listener.Accept() - if conn != nil { + var conn net.Conn + if proxyListener != nil { + conn, err = proxyListener.Accept() + } else { + conn, err = listener.Accept() + } + if conn != nil && err == nil { go c.AcceptInboundConnection(conn, serverConfig) } } } +func (c Configuration) checkIdleTimer() { + if c.IdleTimeout > 0 { + startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute) + } +} + func (c Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) { if len(c.KexAlgorithms) > 0 { serverConfig.KeyExchanges = c.KexAlgorithms diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index d5fd50df..db71ab33 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -89,18 +89,18 @@ iixITGvaNZh/tjAAAACW5pY29sYUBwMQE= ) var ( - allPerms = []string{dataprovider.PermAny} - homeBasePath string - scpPath string - gitPath string - sshPath string - pubKeyPath string - privateKeyPath string - gitWrapPath string - extAuthPath string - keyIntAuthPath string - preLoginPath string - logFilePath string + allPerms = []string{dataprovider.PermAny} + homeBasePath string + scpPath string + gitPath string + sshPath string + pubKeyPath string + privateKeyPath string + gitWrapPath string + extAuthPath string + keyIntAuthPath string + preLoginPath string + logFilePath string ) func TestMain(m *testing.M) { @@ -205,6 +205,17 @@ func TestMain(m *testing.M) { waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort)) waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort)) + sftpdConf.BindPort = 2222 + sftpdConf.ProxyProtocol = 1 + go func() { + logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf) + if err := sftpdConf.Initialize(configDir); err != nil { + logger.Error(logSender, "", "could not start SFTP server: %v", err) + } + }() + + waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort)) + exitCode := m.Run() os.Remove(logFilePath) os.Remove(loginBannerFile) @@ -312,6 +323,28 @@ func TestBasicSFTPHandling(t *testing.T) { os.RemoveAll(user.GetHomeDir()) } +func TestProxyProtocol(t *testing.T) { + usePubKey := false + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + if err != nil { + t.Errorf("unable to add user: %v", err) + } + // remove the home dir to test auto creation + os.RemoveAll(user.HomeDir) + client, err := getSftpClientWithAddr(user, usePubKey, "127.0.0.1:2222") + if err != nil { + t.Errorf("unable to create sftp client: %v", err) + } else { + defer client.Close() + _, err = client.Getwd() + if err != nil { + t.Errorf("error mkdir: %v", err) + } + } + httpd.RemoveUser(user, http.StatusOK) + os.RemoveAll(user.GetHomeDir()) +} + func TestUploadResume(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) @@ -4626,7 +4659,7 @@ func runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]by return stdout.Bytes(), err } -func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) { +func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string) (*sftp.Client, error) { var sftpClient *sftp.Client config := &ssh.ClientConfig{ User: user.Username, @@ -4647,7 +4680,7 @@ func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) config.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)} } } - conn, err := ssh.Dial("tcp", sftpServerAddr, config) + conn, err := ssh.Dial("tcp", addr, config) if err != nil { return sftpClient, err } @@ -4655,6 +4688,10 @@ func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) return sftpClient, err } +func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) { + return getSftpClientWithAddr(user, usePubKey, sftpServerAddr) +} + func getKeyboardInteractiveSftpClient(user dataprovider.User, answers []string) (*sftp.Client, error) { var sftpClient *sftp.Client config := &ssh.ClientConfig{ diff --git a/sftpgo.json b/sftpgo.json index 3d84e911..b99b1ff3 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -20,7 +20,8 @@ "login_banner_file": "", "setstat_mode": 0, "enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"], - "keyboard_interactive_auth_program": "" + "keyboard_interactive_auth_program": "", + "proxy_protocol": 0 }, "data_provider": { "driver": "sqlite",