ソースを参照

ftpd: allow hostnames as passive IP

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 2 年 前
コミット
a23fdea9e3

+ 1 - 0
docs/full-configuration.md

@@ -165,6 +165,7 @@ The configuration file contains the following sections:
     - `passive_ip_overrides`, list of struct that allows to return a different passive ip based on the client IP address. Each struct has the following fields:
       - `networks`, list of strings. Each string must define a network in CIDR notation, for example 192.168.1.0/24.
       - `ip`, string. Passive IP to return if the client IP address belongs to the defined networks. Empty means autodetect.
+    - `passive_host`, string. Hostname for passive connections. This hostname will be resolved each time a passive connection is requested and this can, depending on the DNS configuration, take a noticeable amount of time. Enable this setting only if you have a dynamic IP address. Default: "".
     - `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
     - `passive_connections_security`, integer. Defines the security checks for passive data connections. Set to `0` to require matching peer IP addresses of control and data connection. Set to `1` to disable any checks. Please note that if you run the FTP service behind a proxy you must enable the proxy protocol for control and data connections. Default: `0`.

+ 1 - 1
go.mod

@@ -20,7 +20,7 @@ require (
 	github.com/bmatcuk/doublestar/v4 v4.6.0
 	github.com/cockroachdb/cockroach-go/v2 v2.2.20
 	github.com/coreos/go-oidc/v3 v3.5.0
-	github.com/drakkan/webdav v0.0.0-20230124152008-9aaec6ea77c9
+	github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
 	github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
 	github.com/fclairamb/ftpserverlib v0.21.0
 	github.com/fclairamb/go-log v0.4.1

+ 2 - 2
go.sum

@@ -853,8 +853,8 @@ github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558 h1:M4nv9gf47uCKouIe
 github.com/drakkan/crypto v0.0.0-20230209112458-e15d12511558/go.mod h1:20JIOkADKNe0e6yKflVpVinG/uP19j94rhQlU7Ea/hQ=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
-github.com/drakkan/webdav v0.0.0-20230124152008-9aaec6ea77c9 h1:zHUGiI7ide7ZHNHnfa7n0a7dl2FCcgfgFeixctI7SX4=
-github.com/drakkan/webdav v0.0.0-20230124152008-9aaec6ea77c9/go.mod h1:8opebuqUyBXrvl7Vo/S1Zzl9U0G1X2Ceud440eVuhUE=
+github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 h1:tdkLkSKtYd3WSDsZXGJDKsakiNstLQJPN5HjnqCkf2c=
+github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=

+ 46 - 29
internal/config/config.go

@@ -75,6 +75,7 @@ var (
 		MinTLSVersion:              12,
 		ForcePassiveIP:             "",
 		PassiveIPOverrides:         nil,
+		PassiveHost:                "",
 		ClientAuthType:             0,
 		TLSCipherSuites:            nil,
 		PassiveConnectionsSecurity: 0,
@@ -1116,85 +1117,97 @@ func getDefaultFTPDBinding(idx int) ftpd.Binding {
 	return binding
 }
 
-func getFTPDBindingFromEnv(idx int) {
-	binding := getDefaultFTPDBinding(idx)
+func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
 	isSet := false
 
-	port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx))
+	certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE", idx))
 	if ok {
-		binding.Port = int(port)
+		binding.CertificateFile = certificateFile
 		isSet = true
 	}
 
-	address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx))
+	certificateKeyFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE", idx))
 	if ok {
-		binding.Address = address
+		binding.CertificateKeyFile = certificateKeyFile
 		isSet = true
 	}
 
-	applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
+	tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
 	if ok {
-		binding.ApplyProxyConfig = applyProxyConfig
+		binding.TLSMode = int(tlsMode)
 		isSet = true
 	}
 
-	certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE", idx))
+	tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx))
 	if ok {
-		binding.CertificateFile = certificateFile
+		binding.MinTLSVersion = int(tlsVer)
 		isSet = true
 	}
 
-	certificateKeyFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE", idx))
+	tlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_CIPHER_SUITES", idx))
 	if ok {
-		binding.CertificateKeyFile = certificateKeyFile
+		binding.TLSCipherSuites = tlsCiphers
 		isSet = true
 	}
 
-	tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
+	clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
 	if ok {
-		binding.TLSMode = int(tlsMode)
+		binding.ClientAuthType = int(clientAuthType)
 		isSet = true
 	}
 
-	tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx))
+	pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx))
 	if ok {
-		binding.MinTLSVersion = int(tlsVer)
+		binding.PassiveConnectionsSecurity = int(pasvSecurity)
 		isSet = true
 	}
 
-	passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx))
+	activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx))
 	if ok {
-		binding.ForcePassiveIP = passiveIP
+		binding.ActiveConnectionsSecurity = int(activeSecurity)
 		isSet = true
 	}
 
-	passiveIPOverrides := getFTPDPassiveIPOverridesFromEnv(idx)
-	if len(passiveIPOverrides) > 0 {
-		binding.PassiveIPOverrides = passiveIPOverrides
+	return isSet
+}
+
+func getFTPDBindingFromEnv(idx int) {
+	binding := getDefaultFTPDBinding(idx)
+	isSet := false
+
+	port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx))
+	if ok {
+		binding.Port = int(port)
 		isSet = true
 	}
 
-	clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
+	address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx))
 	if ok {
-		binding.ClientAuthType = int(clientAuthType)
+		binding.Address = address
 		isSet = true
 	}
 
-	tlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_CIPHER_SUITES", idx))
+	applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
 	if ok {
-		binding.TLSCipherSuites = tlsCiphers
+		binding.ApplyProxyConfig = applyProxyConfig
 		isSet = true
 	}
 
-	pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx))
+	passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx))
 	if ok {
-		binding.PassiveConnectionsSecurity = int(pasvSecurity)
+		binding.ForcePassiveIP = passiveIP
 		isSet = true
 	}
 
-	activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx))
+	passiveIPOverrides := getFTPDPassiveIPOverridesFromEnv(idx)
+	if len(passiveIPOverrides) > 0 {
+		binding.PassiveIPOverrides = passiveIPOverrides
+		isSet = true
+	}
+
+	passiveHost, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_HOST", idx))
 	if ok {
-		binding.ActiveConnectionsSecurity = int(activeSecurity)
+		binding.PassiveHost = passiveHost
 		isSet = true
 	}
 
@@ -1204,6 +1217,10 @@ func getFTPDBindingFromEnv(idx int) {
 		isSet = true
 	}
 
+	if getFTPDBindingSecurityFromEnv(idx, &binding) {
+		isSet = true
+	}
+
 	applyFTPDBindingFromEnv(idx, isSet, binding)
 }
 

+ 4 - 0
internal/config/config_test.go

@@ -947,6 +947,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP", "127.0.1.2")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP", "172.16.1.1")
+	os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST", "127.0.1.3")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_CIPHER_SUITES", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_CONNECTIONS_SECURITY", "1")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS", "127.0.1.1")
@@ -969,6 +970,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP")
+		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_CIPHER_SUITES")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS")
@@ -996,6 +998,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
 	require.Len(t, bindings[0].PassiveIPOverrides, 0)
+	require.Equal(t, "127.0.1.3", bindings[0].PassiveHost)
 	require.Equal(t, 0, bindings[0].ClientAuthType)
 	require.Len(t, bindings[0].TLSCipherSuites, 2)
 	require.Equal(t, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", bindings[0].TLSCipherSuites[0])
@@ -1009,6 +1012,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 1, bindings[1].TLSMode)
 	require.Equal(t, 13, bindings[1].MinTLSVersion)
 	require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
+	require.Empty(t, bindings[1].PassiveHost)
 	require.Len(t, bindings[1].PassiveIPOverrides, 1)
 	require.Equal(t, "192.168.1.1", bindings[1].PassiveIPOverrides[0].IP)
 	require.Len(t, bindings[1].PassiveIPOverrides[0].Networks, 2)

+ 23 - 4
internal/ftpd/ftpd.go

@@ -16,12 +16,14 @@
 package ftpd
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net"
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 
 	ftpserver "github.com/fclairamb/ftpserverlib"
 
@@ -75,6 +77,10 @@ type Binding struct {
 	// PassiveIPOverrides allows to define different IP addresses for passive connections
 	// based on the client IP address
 	PassiveIPOverrides []PassiveIPOverride `json:"passive_ip_overrides" mapstructure:"passive_ip_overrides"`
+	// Hostname for passive connections. This hostname will be resolved each time a passive
+	// connection is requested and this can, depending on the DNS configuration, take a noticeable
+	// amount of time. Enable this setting only if you have a dynamic IP address
+	PassiveHost string `json:"passive_host" mapstructure:"passive_host"`
 	// Set to 1 to require client certificate authentication.
 	// Set to 2 to require a client certificate and verfify it if given. In this mode
 	// the client is allowed not to send a certificate.
@@ -168,11 +174,24 @@ func (b *Binding) checkPassiveIP() error {
 	return nil
 }
 
-func (b *Binding) getPassiveIP(cc ftpserver.ClientContext) string {
+func (b *Binding) getPassiveIP(cc ftpserver.ClientContext) (string, error) {
 	if b.ForcePassiveIP != "" {
-		return b.ForcePassiveIP
+		return b.ForcePassiveIP, nil
 	}
-	return strings.Split(cc.LocalAddr().String(), ":")[0]
+	if b.PassiveHost != "" {
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		addrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", b.PassiveHost)
+		if err != nil {
+			logger.Error(logSender, "", "unable to resolve hostname %q: %v", b.PassiveHost, err)
+			return "", fmt.Errorf("unable to resolve hostname %q: %w", b.PassiveHost, err)
+		}
+		if len(addrs) > 0 {
+			return addrs[0].String(), nil
+		}
+	}
+	return strings.Split(cc.LocalAddr().String(), ":")[0], nil
 }
 
 func (b *Binding) passiveIPResolver(cc ftpserver.ClientContext) (string, error) {
@@ -191,7 +210,7 @@ func (b *Binding) passiveIPResolver(cc ftpserver.ClientContext) (string, error)
 			}
 		}
 	}
-	return b.getPassiveIP(cc), nil
+	return b.getPassiveIP(cc)
 }
 
 // HasProxy returns true if the proxy protocol is active for this binding

+ 12 - 0
internal/ftpd/internal_test.go

@@ -1127,3 +1127,15 @@ func TestConfigsFromProvider(t *testing.T) {
 	err = dataprovider.UpdateConfigs(nil, "", "", "")
 	assert.NoError(t, err)
 }
+
+func TestPassiveHost(t *testing.T) {
+	b := Binding{
+		PassiveHost: "invalid hostname",
+	}
+	_, err := b.getPassiveIP(nil)
+	assert.Error(t, err)
+	b.PassiveHost = "localhost"
+	ip, err := b.getPassiveIP(nil)
+	assert.NoError(t, err, ip)
+	assert.Equal(t, "127.0.0.1", ip)
+}

+ 1 - 0
sftpgo.json

@@ -113,6 +113,7 @@
         "min_tls_version": 12,
         "force_passive_ip": "",
         "passive_ip_overrides": [],
+        "passive_host": "",
         "client_auth_type": 0,
         "tls_cipher_suites": [],
         "passive_connections_security": 0,