mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
ftpd: allow hostnames as passive IP
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
561976bcd0
commit
a23fdea9e3
8 changed files with 95 additions and 41 deletions
|
@ -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`.
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue