aboutsummaryrefslogtreecommitdiff
path: root/weed/command/sftp.go
diff options
context:
space:
mode:
authorMohamed Sekour <mohamed.sekour@exfo.com>2025-05-05 20:43:49 +0200
committerGitHub <noreply@github.com>2025-05-05 11:43:49 -0700
commit93aed187e94dcaebf8e8f60cc5f180b49289649f (patch)
tree7cc3796e3fb47487717539caefdcf2e34968047c /weed/command/sftp.go
parenta2c5510ae1a17446c23d80eddb29d06df1fd0bbe (diff)
downloadseaweedfs-93aed187e94dcaebf8e8f60cc5f180b49289649f.tar.xz
seaweedfs-93aed187e94dcaebf8e8f60cc5f180b49289649f.zip
Add SFTP Server Support (#6753)
* Add SFTP Server Support Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> * fix s3 tests and helm lint Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> * increase helm chart version * adjust version --------- Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> Co-authored-by: chrislu <chris.lu@gmail.com>
Diffstat (limited to 'weed/command/sftp.go')
-rw-r--r--weed/command/sftp.go193
1 files changed, 193 insertions, 0 deletions
diff --git a/weed/command/sftp.go b/weed/command/sftp.go
new file mode 100644
index 000000000..117f01d6e
--- /dev/null
+++ b/weed/command/sftp.go
@@ -0,0 +1,193 @@
+package command
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "runtime"
+ "time"
+
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/pb"
+ filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
+ "github.com/seaweedfs/seaweedfs/weed/security"
+ "github.com/seaweedfs/seaweedfs/weed/sftpd"
+ stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
+ "github.com/seaweedfs/seaweedfs/weed/util"
+)
+
+var (
+ sftpOptionsStandalone SftpOptions
+)
+
+// SftpOptions holds configuration options for the SFTP server.
+type SftpOptions struct {
+ filer *string
+ bindIp *string
+ port *int
+ sshPrivateKey *string
+ hostKeysFolder *string
+ authMethods *string
+ maxAuthTries *int
+ bannerMessage *string
+ loginGraceTime *time.Duration
+ clientAliveInterval *time.Duration
+ clientAliveCountMax *int
+ userStoreFile *string
+ dataCenter *string
+ metricsHttpPort *int
+ metricsHttpIp *string
+ localSocket *string
+}
+
+// cmdSftp defines the SFTP command similar to the S3 command.
+var cmdSftp = &Command{
+ UsageLine: "sftp [-port=2022] [-filer=<ip:port>] [-sshPrivateKey=</path/to/private_key>]",
+ Short: "start an SFTP server that is backed by a SeaweedFS filer",
+ Long: `Start an SFTP server that leverages the SeaweedFS filer service to handle file operations.
+
+Instead of reading from or writing to a local filesystem, all file operations
+are routed through the filer (filer_pb) gRPC API. This allows you to centralize
+your file management in SeaweedFS.
+ `,
+}
+
+func init() {
+ // Register the command to avoid cyclic dependencies.
+ cmdSftp.Run = runSftp
+
+ sftpOptionsStandalone.filer = cmdSftp.Flag.String("filer", "localhost:8888", "filer server address (ip:port)")
+ sftpOptionsStandalone.bindIp = cmdSftp.Flag.String("ip.bind", "0.0.0.0", "ip address to bind SFTP server")
+ sftpOptionsStandalone.port = cmdSftp.Flag.Int("port", 2022, "SFTP server listen port")
+ sftpOptionsStandalone.sshPrivateKey = cmdSftp.Flag.String("sshPrivateKey", "", "path to the SSH private key file for host authentication")
+ sftpOptionsStandalone.hostKeysFolder = cmdSftp.Flag.String("hostKeysFolder", "", "path to folder containing SSH private key files for host authentication")
+ sftpOptionsStandalone.authMethods = cmdSftp.Flag.String("authMethods", "password,publickey", "comma-separated list of allowed auth methods: password, publickey, keyboard-interactive")
+ sftpOptionsStandalone.maxAuthTries = cmdSftp.Flag.Int("maxAuthTries", 6, "maximum number of authentication attempts per connection")
+ sftpOptionsStandalone.bannerMessage = cmdSftp.Flag.String("bannerMessage", "SeaweedFS SFTP Server - Unauthorized access is prohibited", "message displayed before authentication")
+ sftpOptionsStandalone.loginGraceTime = cmdSftp.Flag.Duration("loginGraceTime", 2*time.Minute, "timeout for authentication")
+ sftpOptionsStandalone.clientAliveInterval = cmdSftp.Flag.Duration("clientAliveInterval", 5*time.Second, "interval for sending keep-alive messages")
+ sftpOptionsStandalone.clientAliveCountMax = cmdSftp.Flag.Int("clientAliveCountMax", 3, "maximum number of missed keep-alive messages before disconnecting")
+ sftpOptionsStandalone.userStoreFile = cmdSftp.Flag.String("userStoreFile", "", "path to JSON file containing user credentials and permissions")
+ sftpOptionsStandalone.dataCenter = cmdSftp.Flag.String("dataCenter", "", "prefer to read and write to volumes in this data center")
+ sftpOptionsStandalone.metricsHttpPort = cmdSftp.Flag.Int("metricsPort", 0, "Prometheus metrics listen port")
+ sftpOptionsStandalone.metricsHttpIp = cmdSftp.Flag.String("metricsIp", "", "metrics listen ip. If empty, default to same as -ip.bind option.")
+ sftpOptionsStandalone.localSocket = cmdSftp.Flag.String("localSocket", "", "default to /tmp/seaweedfs-sftp-<port>.sock")
+}
+
+// runSftp is the command entry point.
+func runSftp(cmd *Command, args []string) bool {
+ // Load security configuration as done in other SeaweedFS services.
+ util.LoadSecurityConfiguration()
+
+ // Configure metrics
+ switch {
+ case *sftpOptionsStandalone.metricsHttpIp != "":
+ // nothing to do, use sftpOptionsStandalone.metricsHttpIp
+ case *sftpOptionsStandalone.bindIp != "":
+ *sftpOptionsStandalone.metricsHttpIp = *sftpOptionsStandalone.bindIp
+ }
+ go stats_collect.StartMetricsServer(*sftpOptionsStandalone.metricsHttpIp, *sftpOptionsStandalone.metricsHttpPort)
+
+ return sftpOptionsStandalone.startSftpServer()
+}
+
+func (sftpOpt *SftpOptions) startSftpServer() bool {
+ filerAddress := pb.ServerAddress(*sftpOpt.filer)
+ grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
+
+ // metrics read from the filer
+ var metricsAddress string
+ var metricsIntervalSec int
+ var filerGroup string
+
+ // Connect to the filer service and try to retrieve basic configuration.
+ for {
+ err := pb.WithGrpcFilerClient(false, 0, filerAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
+ resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{})
+ if err != nil {
+ return fmt.Errorf("get filer %s configuration: %v", filerAddress, err)
+ }
+ metricsAddress, metricsIntervalSec = resp.MetricsAddress, int(resp.MetricsIntervalSec)
+ filerGroup = resp.FilerGroup
+ glog.V(0).Infof("SFTP read filer configuration, using filer at: %s", filerAddress)
+ return nil
+ })
+ if err != nil {
+ glog.V(0).Infof("Waiting to connect to filer %s grpc address %s...", *sftpOpt.filer, filerAddress.ToGrpcAddress())
+ time.Sleep(time.Second)
+ } else {
+ glog.V(0).Infof("Connected to filer %s grpc address %s", *sftpOpt.filer, filerAddress.ToGrpcAddress())
+ break
+ }
+ }
+
+ go stats_collect.LoopPushingMetric("sftp", stats_collect.SourceName(uint32(*sftpOpt.port)), metricsAddress, metricsIntervalSec)
+
+ // Parse auth methods
+ var authMethods []string
+ if *sftpOpt.authMethods != "" {
+ authMethods = util.StringSplit(*sftpOpt.authMethods, ",")
+ }
+
+ // Create a new SFTP service instance with all options
+ service := sftpd.NewSFTPService(&sftpd.SFTPServiceOptions{
+ GrpcDialOption: grpcDialOption,
+ DataCenter: *sftpOpt.dataCenter,
+ FilerGroup: filerGroup,
+ Filer: filerAddress,
+ SshPrivateKey: *sftpOpt.sshPrivateKey,
+ HostKeysFolder: *sftpOpt.hostKeysFolder,
+ AuthMethods: authMethods,
+ MaxAuthTries: *sftpOpt.maxAuthTries,
+ BannerMessage: *sftpOpt.bannerMessage,
+ LoginGraceTime: *sftpOpt.loginGraceTime,
+ ClientAliveInterval: *sftpOpt.clientAliveInterval,
+ ClientAliveCountMax: *sftpOpt.clientAliveCountMax,
+ UserStoreFile: *sftpOpt.userStoreFile,
+ })
+
+ // Set up Unix socket if on non-Windows platforms
+ if runtime.GOOS != "windows" {
+ localSocket := *sftpOpt.localSocket
+ if localSocket == "" {
+ localSocket = fmt.Sprintf("/tmp/seaweedfs-sftp-%d.sock", *sftpOpt.port)
+ }
+ if err := os.Remove(localSocket); err != nil && !os.IsNotExist(err) {
+ glog.Fatalf("Failed to remove %s, error: %s", localSocket, err.Error())
+ }
+ go func() {
+ // start on local unix socket
+ sftpSocketListener, err := net.Listen("unix", localSocket)
+ if err != nil {
+ glog.Fatalf("Failed to listen on %s: %v", localSocket, err)
+ }
+ if err := service.Serve(sftpSocketListener); err != nil {
+ glog.Fatalf("Failed to serve SFTP on socket %s: %v", localSocket, err)
+ }
+ }()
+ }
+
+ // Start the SFTP service on TCP
+ listenAddress := fmt.Sprintf("%s:%d", *sftpOpt.bindIp, *sftpOpt.port)
+ sftpListener, sftpLocalListener, err := util.NewIpAndLocalListeners(*sftpOpt.bindIp, *sftpOpt.port, time.Duration(10)*time.Second)
+ if err != nil {
+ glog.Fatalf("SFTP server listener on %s error: %v", listenAddress, err)
+ }
+
+ glog.V(0).Infof("Start Seaweed SFTP Server %s at %s", util.Version(), listenAddress)
+
+ if sftpLocalListener != nil {
+ go func() {
+ if err := service.Serve(sftpLocalListener); err != nil {
+ glog.Fatalf("SFTP Server failed to serve on local listener: %v", err)
+ }
+ }()
+ }
+
+ if err := service.Serve(sftpListener); err != nil {
+ glog.Fatalf("SFTP Server failed to serve: %v", err)
+ }
+
+ return true
+}