Quellcode durchsuchen

ftpd: add support for TLS session reuse

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino vor 1 Jahr
Ursprung
Commit
62b87083bb

+ 1 - 0
docs/full-configuration.md

@@ -164,6 +164,7 @@ The configuration file contains the following sections:
     - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
     - `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Please note that we expect the proxy header on control and data connections. Default `true`.
     - `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
+    - `tls_session_reuse`, integer. 0 means session reuse is not checked, clients may or may not resume TLS sessions. 1 means TLS session reuse is required for explicit FTPS. Not supported for implicit TLS. Default: `0`.
     - `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
     - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.

+ 22 - 22
go.mod

@@ -9,15 +9,15 @@ require (
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
 	github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
-	github.com/aws/aws-sdk-go-v2 v1.20.2
-	github.com/aws/aws-sdk-go-v2/config v1.18.34
-	github.com/aws/aws-sdk-go-v2/credentials v1.13.33
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.9
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.78
-	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.3
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.38.3
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.1
-	github.com/aws/aws-sdk-go-v2/service/sts v1.21.3
+	github.com/aws/aws-sdk-go-v2 v1.20.3
+	github.com/aws/aws-sdk-go-v2/config v1.18.35
+	github.com/aws/aws-sdk-go-v2/credentials v1.13.34
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.10
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.79
+	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.4
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.38.4
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.2
+	github.com/aws/aws-sdk-go-v2/service/sts v1.21.4
 	github.com/bmatcuk/doublestar/v4 v4.6.0
 	github.com/cockroachdb/cockroach-go/v2 v2.3.5
 	github.com/coreos/go-oidc/v3 v3.6.0
@@ -85,18 +85,18 @@ require (
 	cloud.google.com/go/iam v1.1.2 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
 	github.com/ajg/form v1.5.1 // indirect
-	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.39 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.33 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.40 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.34 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.33 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.13.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.3 // indirect
-	github.com/aws/smithy-go v1.14.1 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.41 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.35 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.13.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.4 // indirect
+	github.com/aws/smithy-go v1.14.2 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@@ -172,7 +172,7 @@ require (
 )
 
 replace (
-	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230818123055-c8426c7a1b8d
+	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230820134433-b2a9de7897b3
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
 	github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230804183749-f40d052136b8

+ 44 - 44
go.sum

@@ -72,48 +72,48 @@ github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTe
 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/aws/aws-sdk-go-v2 v1.20.2 h1:0Aok9u/HVTk7RtY6M1KDcthbaMKGhhS0eLPxIdSIzRI=
-github.com/aws/aws-sdk-go-v2 v1.20.2/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12 h1:lN6L3LrYHeZ6xCxaIYtoWCx4GMLk4nRknsh29OMSqHY=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.12/go.mod h1:TDCkEAkMTXxTs0oLBGBKpBZbk3NLh8EvAfF0Q3x8/0c=
-github.com/aws/aws-sdk-go-v2/config v1.18.34 h1:bFf7CtSgwz/vE4tl0cNbWbf6EDQ2TZR5VrsrO9ardoY=
-github.com/aws/aws-sdk-go-v2/config v1.18.34/go.mod h1:uJ/keVhwR8vsSaErMu2Vb3dArUZZKLVTcOsKXIFfvjs=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.33 h1:esA1X5Eti1xSGCF0W0LYpHH/r6p+MqT0DiKXsfDEPxs=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.33/go.mod h1:jNC10ZEYuLlt9IOowix60yNiO6vGA14RVK3oUfX5KgI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.9 h1:DnNHcClgyFV5suHJ4axqhmG3YeRGgIu6yv29IEWR9aE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.9/go.mod h1:kz0hzQXlc/5Y5mkbwTKX8A+aTRA45t8Aavly60bQzAQ=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.78 h1:yKlVjl84XK9IshIDplZCUaqwK6jvpQ/h1dQwrzMwZj0=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.78/go.mod h1:IIZC114y2/TZCDCczXrq2bL2nLDUtLqjEFT7zurX8kA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.39 h1:OBokd2jreL7ItwqRRcN5QiSt24/i2r742aRsd2qMyeg=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.39/go.mod h1:OLmjwglQh90dCcFJDGD+T44G0ToLH+696kRwRhS1KOU=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.33 h1:gcRN6PXAo8w3HYFp2wFyr+WYEP4n/a25/IOhzJl36Yw=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.33/go.mod h1:S/zgOphghZAIvrbtvsVycoOncfqh1Hc4uGDIHqDLwTU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.40 h1:glWaI8WyeYqQN4zh4zqogzSpNPj8rf11Nj+oE3ghQPw=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.40/go.mod h1:OCnFHzgaBY2PuGiHSzLlfqV4j5rJrky7YMfBXcx2Uk0=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.2 h1:9Np6KOCKYnjMwJd1/17ReLdN21gnloI80LNP3uCKk44=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.2/go.mod h1:0YZJZKZCSSbQYQrXpqv0DpIaOMcZ27+OHFaSJTmN+8o=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13 h1:iV/W5OMBys+66OeXJi/7xIRrKZNsu0ylsLGu+6nbmQE=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.13/go.mod h1:ReJb6xYmtGyu9KoFtRreWegbN9dZqvZIIv4vWnhcsyI=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.34 h1:gQE3p36iC+wwf/hDaCw+tNVXmNxDUehqv5nAvnoG+yc=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.34/go.mod h1:swEfojiNWdgJaOTNT65+XsMclEx4k/tyzBAVEi0Y6vM=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.33 h1:cr70Hw6Lq9cqRst1y4YOHLiaVWaWtBPiqdloinNkfis=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.33/go.mod h1:kcNtzCcEoflp+6e2CDTmm2h3xQGZOBZqYA/8DhYx/S8=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.2 h1:M5vGdcDO+jUGWu7d4BXwcLRXp3UikWXAiCfQI20rqFQ=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.2/go.mod h1:bC2B9AS4ygwMNrefck3XeD6YwXeplWhY6Z2UtlGjv1s=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.3 h1:NnDCBIRlYtreJIuK0PfqpvgPc3aYHpp2dFnxWxAfzUE=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.3/go.mod h1:h2345n5Sc82zNIm7XItORbqv44cBx+di6gS7gpZG9OU=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.38.3 h1:yWclTL4cyiqLBWSjxDJ1tjiIzP4x4Kp85aAUtKSbtwA=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.38.3/go.mod h1:yER+u7+gwH6dXy5xRTC2OfoHpYY1BFRiS0SF5iamO6M=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.1 h1:JBrOoTb1gfm4EhlwbMigvLRgOHgouSyQFRbOVQWn3wU=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.1/go.mod h1:fYrcZAwlCzBXN7+5RiJlokZbdIbEsEjwonLyPZQGVGg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.13.3 h1:nceOkYE0jmaG9CoyXHJJm00FAQ8JE+/LCKJJ06hH/Nc=
-github.com/aws/aws-sdk-go-v2/service/sso v1.13.3/go.mod h1:DApEBnZzexe+LDLaNrGOJA8xtRMCpikLW1gX7jZhHxc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.3 h1:90qW9puxI7LgmiYKSPhx6wz4XqgVauTxCyS3185+JpA=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.3/go.mod h1:kKpyLjToIS7E3z0672lBhxIPD+uoQ9V0MYRYCVGIkO0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.21.3 h1:s3wBkMxfA/u2EJJl6KRsPcWv858lDHkhinqXyN6fkZI=
-github.com/aws/aws-sdk-go-v2/service/sts v1.21.3/go.mod h1:b+y9zL57mwCRy6ftp9Nc7CONGHX3sZ50ZCLTrI5xpCc=
-github.com/aws/smithy-go v1.14.1 h1:EFKMUmH/iHMqLiwoEDx2rRjRQpI1YCn5jTysoaDujFs=
-github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/aws/aws-sdk-go-v2 v1.20.3 h1:lgeKmAZhlj1JqN43bogrM75spIvYnRxqTAh1iupu1yE=
+github.com/aws/aws-sdk-go-v2 v1.20.3/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 h1:OPLEkmhXf6xFPiz0bLeDArZIDx1NNS4oJyG4nv3Gct0=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM=
+github.com/aws/aws-sdk-go-v2/config v1.18.35 h1:uU9rgCzrW/pVRUUlRULiwKQe8RoEDst1NQu4Qo8kOtk=
+github.com/aws/aws-sdk-go-v2/config v1.18.35/go.mod h1:7xF1yr9GBMfYRQI4PLHO8iceqKLM6DpGVEvXI38HB/A=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.34 h1:/EYG4lzayDd5PY6HQQ2Qyj/cD6CR3kz96BjTZAO5tNo=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.34/go.mod h1:+wgdxCGNulHme6kTMZuDL9KOagLPloemoYkfjpQkSEU=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.10 h1:mgOrtwYfJZ4e3QJe1TrliC/xIkauafGMdLLuCExOqcs=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.10/go.mod h1:wMsSLVM2hRpDVhd+3dtLUzqwm7/fjuhNN+b1aOLDt6g=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.79 h1:Lc2K7rBQlWnY+HB3cNrz/zpEF+ncyn//iJ6gpi5vfR4=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.79/go.mod h1:NpiOzmnVgrRfF/ZbqShAsisq5/6DWYEG2QqHnsdUnbE=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40 h1:CXceCS9BrDInRc74GDCQ8Qyk/Gp9VLdK+Rlve+zELSE=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40/go.mod h1:5kKmFhLeOVy6pwPDpDNA6/hK/d6URC98pqDDqHgdBx4=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34 h1:B+nZtd22cbko5+793hg7LEaTeLMiZwlgCLUrN5Y0uzg=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34/go.mod h1:RZP0scceAyhMIQ9JvFp7HvkpcgqjL4l/4C+7RAeGbuM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.41 h1:EcSFdpLdkF3FWizimox0qYLuorn9e4PNMR27mvshGLs=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.41/go.mod h1:mKxUXW+TuwpCKKHVlmHGVVuBi9y9LKW8AiQodg23M5E=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.3 h1:uHhWcrNBgpm9gi3o8NSQcsAqha/U9OFYzi2k4+0UVz8=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.3/go.mod h1:jYLMm3Dh0wbeV3lxth5ryks/O2M/omVXWyYm3YcEVqQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 h1:m0QTSI6pZYJTk5WSKx3fm5cNW/DCicVzULBgU/6IyD0=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14/go.mod h1:dDilntgHy9WnHXsh7dDtUPgHKEfTJIBUTHM8OWm0f/0=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.35 h1:oCUrlTzh9GwhlYdyDGNAS6UgqJRzJp5rKoYCJWqLyZI=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.35/go.mod h1:YVHrksq36j0sbXCT6rSuQafpfYkMYqy0QTk7JTCTBIU=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34 h1:JwvXk+1ePAD9xkFHprhHYqwsxLDcbNFsPI1IAT2sPS0=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34/go.mod h1:ytsF+t+FApY2lFnN51fJKPhH6ICKOPXKEcwwgmJEdWI=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.3 h1:rPDAISw3FjEhrJoaxmQjuD+GgBfv2p3AVhmAcnyqq3k=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.3/go.mod h1:TXBww3ANB+QRj+/dUoYDvI8d/u4F4WzTxD4mxtDoxrg=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.4 h1:/AyMci1EGQIRzUPgXHkLmxUTDEmyBV+kSk/RWi94UKk=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.15.4/go.mod h1:qQqwZdFSIOyDh/4I16LMDA/c5N4xBxQnynpIU6qzxpE=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.38.4 h1:P4p346B+YMTTCH9D4I/FWYl+E7BjSLQxqk1e2KYDI5w=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.38.4/go.mod h1:uDxTlJiuPhbtRRPMHrPYRkn1Ck7Mtk3BEJiDut+gR5Y=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.2 h1:6N4VK/eLcMYonOqGgihkYlgjE2URxEMqjjS/1zErTKA=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.2/go.mod h1:aYWGu8cQcyRdfDi/V4agl6VDmDz2N42VhiHj0xMf77o=
+github.com/aws/aws-sdk-go-v2/service/sso v1.13.4 h1:WZPZ7Zf6Yo13lsfTetFrLU/7hZ9CXESDpdIHvmLxQFQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.13.4/go.mod h1:FP05hDXTLouXwAMQ1swqybHy7tHySblMkBMKSumaKg0=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.4 h1:pYFM2U/3/4RLrlMSYXwL1XPBCWvaePk2p+0+i/BgHOs=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.4/go.mod h1:4pdlNASc29u0j9bq2jIQcBghG5Lx2oQAIj91vo1u1t8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.21.4 h1:zj4jxK3L54tGyqKleKDMK4vHolENxlq11dF0v1oBkJo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.21.4/go.mod h1:CQRMCzYvl5eeAQW3AWkRLS+zGGXCucBnsiQlrs+tCeo=
+github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
+github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
@@ -160,8 +160,8 @@ github.com/drakkan/crypto v0.0.0-20230804183749-f40d052136b8 h1:TUieQf6mz4xlWJav
 github.com/drakkan/crypto v0.0.0-20230804183749-f40d052136b8/go.mod h1:jjOR8ZXZPvxgpYUhVmAtGUCuD1OFc5Hq984QRL686so=
 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/ftpserverlib v0.0.0-20230818123055-c8426c7a1b8d h1:WcXXDwXoVS85iFI6nCugkkMZqvU9Hb4GAa6MErVlaxY=
-github.com/drakkan/ftpserverlib v0.0.0-20230818123055-c8426c7a1b8d/go.mod h1:dI9/yw/KfJ0g4wmRK8ZukUfqakLr6ZTf9VDydKoLy90=
+github.com/drakkan/ftpserverlib v0.0.0-20230820134433-b2a9de7897b3 h1:z1XhTv2fAIkd0/T6NtYwW78iZWxwZclBPXcqiQnUELw=
+github.com/drakkan/ftpserverlib v0.0.0-20230820134433-b2a9de7897b3/go.mod h1:dI9/yw/KfJ0g4wmRK8ZukUfqakLr6ZTf9VDydKoLy90=
 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/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=

+ 6 - 0
internal/config/config.go

@@ -1163,6 +1163,12 @@ func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
 		isSet = true
 	}
 
+	tlsSessionReuse, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_SESSION_REUSE", idx), 0)
+	if ok {
+		binding.TLSSessionReuse = int(tlsSessionReuse)
+		isSet = true
+	}
+
 	tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
 	if ok {
 		binding.MinTLSVersion = int(tlsVer)

+ 4 - 0
internal/config/config_test.go

@@ -962,6 +962,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__PORT", "2200")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "f")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2")
+	os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE", "1")
 	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")
@@ -985,6 +986,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PORT")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE")
+		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE")
 		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")
@@ -1012,6 +1014,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "127.0.0.1", bindings[0].Address)
 	require.False(t, bindings[0].ApplyProxyConfig)
 	require.Equal(t, 2, bindings[0].TLSMode)
+	require.Equal(t, 1, bindings[0].TLSSessionReuse)
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
 	require.Len(t, bindings[0].PassiveIPOverrides, 0)
@@ -1027,6 +1030,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "127.0.1.1", bindings[1].Address)
 	require.True(t, bindings[1].ApplyProxyConfig) // default value
 	require.Equal(t, 1, bindings[1].TLSMode)
+	require.Equal(t, 0, bindings[1].TLSSessionReuse)
 	require.Equal(t, 13, bindings[1].MinTLSVersion)
 	require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
 	require.Empty(t, bindings[1].PassiveHost)

+ 10 - 0
internal/ftpd/ftpd.go

@@ -66,6 +66,8 @@ type Binding struct {
 	// Set to 1 to require TLS for both data and control connection.
 	// Set to 2 to enable implicit TLS
 	TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
+	// 0 disabled, 1 required
+	TLSSessionReuse int `json:"tls_session_reuse" mapstructure:"tls_session_reuse"`
 	// Certificate and matching private key for this specific binding, if empty the global
 	// ones will be used, if any
 	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
@@ -133,6 +135,14 @@ func (b *Binding) IsValid() bool {
 	return b.Port > 0
 }
 
+func (b *Binding) isTLSModeValid() bool {
+	return b.TLSMode >= 0 && b.TLSMode <= 2
+}
+
+func (b *Binding) isTLSSessionReuseValid() bool {
+	return b.TLSSessionReuse >= 0 && b.TLSSessionReuse <= 1
+}
+
 func (b *Binding) checkSecuritySettings() error {
 	if b.PassiveConnectionsSecurity < 0 || b.PassiveConnectionsSecurity > 1 {
 		return fmt.Errorf("invalid passive_connections_security: %v", b.PassiveConnectionsSecurity)

+ 135 - 10
internal/ftpd/ftpd_test.go

@@ -58,14 +58,15 @@ import (
 )
 
 const (
-	logSender       = "ftpdTesting"
-	ftpServerAddr   = "127.0.0.1:2121"
-	sftpServerAddr  = "127.0.0.1:2122"
-	ftpSrvAddrTLS   = "127.0.0.1:2124" // ftp server with implicit tls
-	defaultUsername = "test_user_ftp"
-	defaultPassword = "test_password"
-	osWindows       = "windows"
-	ftpsCert        = `-----BEGIN CERTIFICATE-----
+	logSender               = "ftpdTesting"
+	ftpServerAddr           = "127.0.0.1:2121"
+	sftpServerAddr          = "127.0.0.1:2122"
+	ftpSrvAddrTLS           = "127.0.0.1:2124" // ftp server with implicit tls
+	ftpSrvAddrTLSResumption = "127.0.0.1:2126" // ftp server with implicit tls
+	defaultUsername         = "test_user_ftp"
+	defaultPassword         = "test_password"
+	osWindows               = "windows"
+	ftpsCert                = `-----BEGIN CERTIFICATE-----
 MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw
 RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
 dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw
@@ -263,7 +264,7 @@ var (
 	caCRLPath       string
 )
 
-func TestMain(m *testing.M) {
+func TestMain(m *testing.M) { //nolint:gocyclo
 	logFilePath = filepath.Join(configDir, "sftpgo_ftpd_test.log")
 	bannerFileName := "banner_file"
 	bannerFile := filepath.Join(configDir, bannerFileName)
@@ -271,6 +272,7 @@ func TestMain(m *testing.M) {
 	err := os.WriteFile(bannerFile, []byte("SFTPGo test ready\nsimple banner line\n"), os.ModePerm)
 	if err != nil {
 		logger.ErrorToConsole("error creating banner file: %v", err)
+		os.Exit(1)
 	}
 	// we run the test cases with UploadMode atomic and resume support. The non atomic code path
 	// simply does not execute some code so if it works in atomic mode will
@@ -431,6 +433,31 @@ func TestMain(m *testing.M) {
 	}()
 
 	waitTCPListening(ftpdConf.Bindings[0].GetAddress())
+
+	ftpdConf = config.GetFTPDConfig()
+	ftpdConf.Bindings = []ftpd.Binding{
+		{
+			Port:               2126,
+			CertificateFile:    certPath,
+			CertificateKeyFile: keyPath,
+			TLSMode:            1,
+			TLSSessionReuse:    1,
+			ClientAuthType:     2,
+		},
+	}
+	ftpdConf.CACertificates = []string{caCrtPath}
+	ftpdConf.CARevocationLists = []string{caCRLPath}
+
+	go func() {
+		logger.Debug(logSender, "", "initializing FTP server with config %+v", ftpdConf)
+		if err := ftpdConf.Initialize(configDir); err != nil {
+			logger.ErrorToConsole("could not start FTP server: %v", err)
+			os.Exit(1)
+		}
+	}()
+
+	waitTCPListening(ftpdConf.Bindings[0].GetAddress())
+
 	waitNoConnections()
 	startHTTPFs()
 
@@ -501,6 +528,16 @@ func TestInitializationFailure(t *testing.T) {
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "the provided passive IP \"127001\" is not valid")
 	ftpdConf.Bindings[1].ForcePassiveIP = ""
+	ftpdConf.Bindings[1].TLSMode = 2
+	ftpdConf.Bindings[1].TLSSessionReuse = 1
+	err = ftpdConf.Initialize(configDir)
+	require.Error(t, err, "TLS session resumption should not be supported with implicit FTPS")
+	ftpdConf.Bindings[1].TLSMode = 0
+	ftpdConf.Bindings[1].TLSSessionReuse = 100
+	err = ftpdConf.Initialize(configDir)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "unsupported TLS reuse mode")
+	ftpdConf.Bindings[1].TLSSessionReuse = 0
 	err = ftpdConf.Initialize(configDir)
 	require.Error(t, err)
 
@@ -3359,6 +3396,61 @@ func TestCombine(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestTLSSessionReuse(t *testing.T) {
+	u := getTestUser()
+	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+
+	client, err := getFTPClientWithSessionReuse(user, nil)
+	if assert.NoError(t, err) {
+		err = checkBasicFTP(client)
+		assert.NoError(t, err)
+
+		testFilePath := filepath.Join(homeBasePath, testFileName)
+		testFileSize := int64(65535)
+		err = createTestFile(testFilePath, testFileSize)
+		assert.NoError(t, err)
+
+		err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
+		assert.NoError(t, err)
+
+		localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
+		err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
+		assert.NoError(t, err)
+
+		entries, err := client.List("/")
+		assert.NoError(t, err)
+		assert.Len(t, entries, 1)
+
+		err = client.Quit()
+		assert.NoError(t, err)
+		err = os.Remove(testFilePath)
+		assert.NoError(t, err)
+		err = os.Remove(localDownloadPath)
+		assert.NoError(t, err)
+	}
+
+	// this TLS config does not support session resumption
+	tlsConfig := &tls.Config{
+		ServerName:         "localhost",
+		InsecureSkipVerify: true, // use this for tests only
+		MinVersion:         tls.VersionTLS12,
+	}
+	client, err = getFTPClientWithSessionReuse(user, tlsConfig)
+	if assert.NoError(t, err) {
+		err = checkBasicFTP(client)
+		assert.Error(t, err)
+
+		err = client.Quit()
+		assert.NoError(t, err)
+	}
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(user.GetHomeDir())
+	assert.NoError(t, err)
+}
+
 func TestClientCertificateAuthRevokedCert(t *testing.T) {
 	u := getTestUser()
 	u.Username = tlsClient2Username
@@ -3369,11 +3461,12 @@ func TestClientCertificateAuthRevokedCert(t *testing.T) {
 		ServerName:         "localhost",
 		InsecureSkipVerify: true, // use this for tests only
 		MinVersion:         tls.VersionTLS12,
+		ClientSessionCache: tls.NewLRUClientSessionCache(0),
 	}
 	tlsCert, err := tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))
 	assert.NoError(t, err)
 	tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
-	_, err = getFTPClient(user, true, tlsConfig)
+	_, err = getFTPClientWithSessionReuse(user, tlsConfig)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "bad certificate")
 	}
@@ -3869,6 +3962,38 @@ func getFTPClientImplicitTLS(user dataprovider.User) (*ftp.ServerConn, error) {
 	return client, err
 }
 
+func getFTPClientWithSessionReuse(user dataprovider.User, tlsConfig *tls.Config, dialOptions ...ftp.DialOption,
+) (*ftp.ServerConn, error) {
+	ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}
+	ftpOptions = append(ftpOptions, dialOptions...)
+	if tlsConfig == nil {
+		tlsConfig = &tls.Config{
+			ServerName:         "localhost",
+			InsecureSkipVerify: true, // use this for tests only
+			MinVersion:         tls.VersionTLS12,
+			ClientSessionCache: tls.NewLRUClientSessionCache(0),
+		}
+	}
+	ftpOptions = append(ftpOptions, ftp.DialWithExplicitTLS(tlsConfig))
+	client, err := ftp.Dial(ftpSrvAddrTLSResumption, ftpOptions...)
+	if err != nil {
+		return nil, err
+	}
+	pwd := defaultPassword
+	if user.Password != "" {
+		if user.Password == emptyPwdPlaceholder {
+			pwd = ""
+		} else {
+			pwd = user.Password
+		}
+	}
+	err = client.Login(user.Username, pwd)
+	if err != nil {
+		return nil, err
+	}
+	return client, err
+}
+
 func getFTPClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config, dialOptions ...ftp.DialOption,
 ) (*ftp.ServerConn, error) {
 	ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}

+ 5 - 0
internal/ftpd/internal_test.go

@@ -945,6 +945,11 @@ func TestVerifyTLSConnection(t *testing.T) {
 
 	err = server.verifyTLSConnection(state)
 	assert.Error(t, err) // no verified certification chain
+	err = server.VerifyTLSConnectionState(nil, state)
+	assert.NoError(t, err)
+	server.binding.ClientAuthType = 1
+	err = server.VerifyTLSConnectionState(nil, state)
+	assert.Error(t, err)
 
 	crt, err = tls.X509KeyPair([]byte(caCRT), []byte(caKey))
 	assert.NoError(t, err)

+ 19 - 4
internal/ftpd/server.go

@@ -104,11 +104,15 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
 		}
 	}
 
-	if s.binding.TLSMode < 0 || s.binding.TLSMode > 2 {
-		return nil, errors.New("unsupported TLS mode")
+	if !s.binding.isTLSModeValid() {
+		return nil, fmt.Errorf("unsupported TLS mode: %d", s.binding.TLSMode)
 	}
 
-	if s.binding.TLSMode > 0 && certMgr == nil {
+	if !s.binding.isTLSSessionReuseValid() {
+		return nil, fmt.Errorf("unsupported TLS reuse mode %d", s.binding.TLSSessionReuse)
+	}
+
+	if (s.binding.TLSMode > 0 || s.binding.TLSSessionReuse > 0) && certMgr == nil {
 		return nil, errors.New("to enable TLS you need to provide a certificate")
 	}
 
@@ -122,6 +126,7 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
 		ConnectionTimeout:        20,
 		Banner:                   s.statusBanner,
 		TLSRequired:              ftpserver.TLSRequirement(s.binding.TLSMode),
+		TLSSessionReuse:          ftpserver.TLSSessionReuse(s.binding.TLSSessionReuse),
 		DisableSite:              !s.config.EnableSite,
 		DisableActiveMode:        s.config.DisableActiveMode,
 		EnableHASH:               s.config.HASHSupport > 0,
@@ -284,7 +289,9 @@ func (s *Server) buildTLSConfig() {
 			s.binding.GetAddress(), s.binding.ciphers, certID)
 		if s.binding.isMutualTLSEnabled() {
 			s.tlsConfig.ClientCAs = certMgr.GetRootCAs()
-			s.tlsConfig.VerifyConnection = s.verifyTLSConnection
+			if s.binding.TLSSessionReuse != int(ftpserver.TLSSessionReuseRequired) {
+				s.tlsConfig.VerifyConnection = s.verifyTLSConnection
+			}
 			switch s.binding.ClientAuthType {
 			case 1:
 				s.tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
@@ -303,6 +310,14 @@ func (s *Server) GetTLSConfig() (*tls.Config, error) {
 	return nil, errors.New("no TLS certificate configured")
 }
 
+// VerifyTLSConnectionState implements the MainDriverExtensionTLSConnectionStateVerifier extension
+func (s *Server) VerifyTLSConnectionState(_ ftpserver.ClientContext, cs tls.ConnectionState) error {
+	if !s.binding.isMutualTLSEnabled() {
+		return nil
+	}
+	return s.verifyTLSConnection(cs)
+}
+
 func (s *Server) verifyTLSConnection(state tls.ConnectionState) error {
 	if certMgr != nil {
 		var clientCrt *x509.Certificate

+ 1 - 0
sftpgo.json

@@ -113,6 +113,7 @@
         "address": "",
         "apply_proxy_config": true,
         "tls_mode": 0,
+        "tls_session_reuse": 0,
         "certificate_file": "",
         "certificate_key_file": "",
         "min_tls_version": 12,