ftpd: allow hostnames as passive IP

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-02-27 19:19:50 +01:00
parent 561976bcd0
commit a23fdea9e3
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
8 changed files with 95 additions and 41 deletions

View file

@ -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: - `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. - `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. - `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. - `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. - `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`. - `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`.

2
go.mod
View file

@ -20,7 +20,7 @@ require (
github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.2.20 github.com/cockroachdb/cockroach-go/v2 v2.2.20
github.com/coreos/go-oidc/v3 v3.5.0 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/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.21.0 github.com/fclairamb/ftpserverlib v0.21.0
github.com/fclairamb/go-log v0.4.1 github.com/fclairamb/go-log v0.4.1

4
go.sum
View file

@ -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/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 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU= 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-20230227175313-32996838bcd8 h1:tdkLkSKtYd3WSDsZXGJDKsakiNstLQJPN5HjnqCkf2c=
github.com/drakkan/webdav v0.0.0-20230124152008-9aaec6ea77c9/go.mod h1:8opebuqUyBXrvl7Vo/S1Zzl9U0G1X2Ceud440eVuhUE= 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 v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/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= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=

View file

@ -75,6 +75,7 @@ var (
MinTLSVersion: 12, MinTLSVersion: 12,
ForcePassiveIP: "", ForcePassiveIP: "",
PassiveIPOverrides: nil, PassiveIPOverrides: nil,
PassiveHost: "",
ClientAuthType: 0, ClientAuthType: 0,
TLSCipherSuites: nil, TLSCipherSuites: nil,
PassiveConnectionsSecurity: 0, PassiveConnectionsSecurity: 0,
@ -1116,28 +1117,9 @@ func getDefaultFTPDBinding(idx int) ftpd.Binding {
return binding return binding
} }
func getFTPDBindingFromEnv(idx int) { func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
binding := getDefaultFTPDBinding(idx)
isSet := false 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)) certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE", idx))
if ok { if ok {
binding.CertificateFile = certificateFile binding.CertificateFile = certificateFile
@ -1162,15 +1144,9 @@ func getFTPDBindingFromEnv(idx int) {
isSet = true 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 { if ok {
binding.ForcePassiveIP = passiveIP binding.TLSCipherSuites = tlsCiphers
isSet = true
}
passiveIPOverrides := getFTPDPassiveIPOverridesFromEnv(idx)
if len(passiveIPOverrides) > 0 {
binding.PassiveIPOverrides = passiveIPOverrides
isSet = true isSet = true
} }
@ -1180,12 +1156,6 @@ func getFTPDBindingFromEnv(idx int) {
isSet = true 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)) pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx))
if ok { if ok {
binding.PassiveConnectionsSecurity = int(pasvSecurity) binding.PassiveConnectionsSecurity = int(pasvSecurity)
@ -1198,12 +1168,59 @@ func getFTPDBindingFromEnv(idx int) {
isSet = true 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)) debug, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__DEBUG", idx))
if ok { if ok {
binding.Debug = debug binding.Debug = debug
isSet = true isSet = true
} }
if getFTPDBindingSecurityFromEnv(idx, &binding) {
isSet = true
}
applyFTPDBindingFromEnv(idx, isSet, binding) applyFTPDBindingFromEnv(idx, isSet, binding)
} }

View file

@ -947,6 +947,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2") 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__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_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__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__0__PASSIVE_CONNECTIONS_SECURITY", "1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS", "127.0.1.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__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP") 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_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__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY") os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS") 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, 12, bindings[0].MinTLSVersion)
require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP) require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
require.Len(t, bindings[0].PassiveIPOverrides, 0) 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.Equal(t, 0, bindings[0].ClientAuthType)
require.Len(t, bindings[0].TLSCipherSuites, 2) require.Len(t, bindings[0].TLSCipherSuites, 2)
require.Equal(t, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", bindings[0].TLSCipherSuites[0]) 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, 1, bindings[1].TLSMode)
require.Equal(t, 13, bindings[1].MinTLSVersion) require.Equal(t, 13, bindings[1].MinTLSVersion)
require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP) require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
require.Empty(t, bindings[1].PassiveHost)
require.Len(t, bindings[1].PassiveIPOverrides, 1) require.Len(t, bindings[1].PassiveIPOverrides, 1)
require.Equal(t, "192.168.1.1", bindings[1].PassiveIPOverrides[0].IP) require.Equal(t, "192.168.1.1", bindings[1].PassiveIPOverrides[0].IP)
require.Len(t, bindings[1].PassiveIPOverrides[0].Networks, 2) require.Len(t, bindings[1].PassiveIPOverrides[0].Networks, 2)

View file

@ -16,12 +16,14 @@
package ftpd package ftpd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
ftpserver "github.com/fclairamb/ftpserverlib" ftpserver "github.com/fclairamb/ftpserverlib"
@ -75,6 +77,10 @@ type Binding struct {
// PassiveIPOverrides allows to define different IP addresses for passive connections // PassiveIPOverrides allows to define different IP addresses for passive connections
// based on the client IP address // based on the client IP address
PassiveIPOverrides []PassiveIPOverride `json:"passive_ip_overrides" mapstructure:"passive_ip_overrides"` 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 1 to require client certificate authentication.
// Set to 2 to require a client certificate and verfify it if given. In this mode // 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. // the client is allowed not to send a certificate.
@ -168,11 +174,24 @@ func (b *Binding) checkPassiveIP() error {
return nil return nil
} }
func (b *Binding) getPassiveIP(cc ftpserver.ClientContext) string { func (b *Binding) getPassiveIP(cc ftpserver.ClientContext) (string, error) {
if b.ForcePassiveIP != "" { 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) { 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 // HasProxy returns true if the proxy protocol is active for this binding

View file

@ -1127,3 +1127,15 @@ func TestConfigsFromProvider(t *testing.T) {
err = dataprovider.UpdateConfigs(nil, "", "", "") err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err) 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)
}

View file

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