diff --git a/docs/full-configuration.md b/docs/full-configuration.md index ffd68218..5401328e 100644 --- a/docs/full-configuration.md +++ b/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`. diff --git a/go.mod b/go.mod index 7cf79b92..3c711ac7 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 47ec20d0..cce951b3 100644 --- a/go.sum +++ b/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= diff --git a/internal/config/config.go b/internal/config/config.go index 36ee71de..f6c1b87f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -75,6 +75,7 @@ var ( MinTLSVersion: 12, ForcePassiveIP: "", PassiveIPOverrides: nil, + PassiveHost: "", ClientAuthType: 0, TLSCipherSuites: nil, PassiveConnectionsSecurity: 0, @@ -1116,28 +1117,9 @@ 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)) - if ok { - binding.Port = int(port) - isSet = true - } - - address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx)) - if ok { - binding.Address = address - isSet = true - } - - applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx)) - if ok { - binding.ApplyProxyConfig = applyProxyConfig - isSet = true - } - certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE", idx)) if ok { binding.CertificateFile = certificateFile @@ -1162,15 +1144,9 @@ func getFTPDBindingFromEnv(idx int) { isSet = true } - passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx)) + tlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_CIPHER_SUITES", idx)) if ok { - binding.ForcePassiveIP = passiveIP - isSet = true - } - - passiveIPOverrides := getFTPDPassiveIPOverridesFromEnv(idx) - if len(passiveIPOverrides) > 0 { - binding.PassiveIPOverrides = passiveIPOverrides + binding.TLSCipherSuites = tlsCiphers isSet = true } @@ -1180,12 +1156,6 @@ func getFTPDBindingFromEnv(idx int) { isSet = true } - tlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_CIPHER_SUITES", idx)) - if ok { - binding.TLSCipherSuites = tlsCiphers - isSet = true - } - pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx)) if ok { binding.PassiveConnectionsSecurity = int(pasvSecurity) @@ -1198,12 +1168,59 @@ func getFTPDBindingFromEnv(idx int) { isSet = true } + 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 + } + + address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx)) + if ok { + binding.Address = address + isSet = true + } + + applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx)) + if ok { + binding.ApplyProxyConfig = applyProxyConfig + isSet = true + } + + passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx)) + if ok { + binding.ForcePassiveIP = passiveIP + isSet = true + } + + 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.PassiveHost = passiveHost + isSet = true + } + debug, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__DEBUG", idx)) if ok { binding.Debug = debug isSet = true } + if getFTPDBindingSecurityFromEnv(idx, &binding) { + isSet = true + } + applyFTPDBindingFromEnv(idx, isSet, binding) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 43780f8f..939934b3 100644 --- a/internal/config/config_test.go +++ b/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) diff --git a/internal/ftpd/ftpd.go b/internal/ftpd/ftpd.go index 628b72c7..0b125e92 100644 --- a/internal/ftpd/ftpd.go +++ b/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 diff --git a/internal/ftpd/internal_test.go b/internal/ftpd/internal_test.go index d2526d42..769b4ca8 100644 --- a/internal/ftpd/internal_test.go +++ b/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) +} diff --git a/sftpgo.json b/sftpgo.json index ca00eea9..1be81480 100644 --- a/sftpgo.json +++ b/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,