mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 06:50:40 +02:00
server: skip default local unix sockets that cannot be bound weed server fatally exited during S3, filer, or SFTP startup when the default /tmp/seaweedfs-<svc>-<port>.sock path could not be removed or bound. On shared hosts (e.g. CI runners where another user's weed left a socket in the sticky /tmp directory) a stale socket file turns into 'operation not permitted' and kills the whole server, even though the TCP listeners are unaffected. Warn and skip the unix socket listener instead, matching how the gRPC local socket already degrades. An explicitly configured -localSocket path still fails fast, since a co-located S3 server may be configured to dial the filer through it.
232 lines
9.8 KiB
Go
232 lines
9.8 KiB
Go
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"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/grace"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/version"
|
|
)
|
|
|
|
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
|
|
trustedUserCAKeysFile *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", "", "ip address to bind to. If empty, default to 0.0.0.0.")
|
|
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, certificate")
|
|
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.trustedUserCAKeysFile = cmdSftp.Flag.String("trustedUserCAKeysFile", "", "path to a file with trusted user CA public keys (OpenSSH authorized_keys format); required when 'certificate' is in -authMethods. Analogous to OpenSSH TrustedUserCAKeys and MinIO --sftp=trusted-user-ca-key")
|
|
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 {
|
|
sftpOptionsStandalone.resolvePaths()
|
|
// 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()
|
|
}
|
|
|
|
// resolvePaths expands "~" in every user-supplied path flag.
|
|
// Idempotent — safe to call from any entry point.
|
|
func (sftpOpt *SftpOptions) resolvePaths() {
|
|
*sftpOpt.sshPrivateKey = util.ResolvePath(*sftpOpt.sshPrivateKey)
|
|
*sftpOpt.hostKeysFolder = util.ResolvePath(*sftpOpt.hostKeysFolder)
|
|
*sftpOpt.userStoreFile = util.ResolvePath(*sftpOpt.userStoreFile)
|
|
*sftpOpt.trustedUserCAKeysFile = util.ResolvePath(*sftpOpt.trustedUserCAKeysFile)
|
|
}
|
|
|
|
func (sftpOpt *SftpOptions) startSftpServer() bool {
|
|
if *sftpOpt.bindIp == "" {
|
|
*sftpOpt.bindIp = "0.0.0.0"
|
|
}
|
|
util.SetOutboundLocalIP(*sftpOpt.bindIp)
|
|
filerAddress := pb.ServerAddress(*sftpOpt.filer)
|
|
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
|
|
|
|
// Load JWT configuration for filer signing
|
|
v := util.GetViper()
|
|
filerSigningKey := v.GetString("jwt.filer_signing.key")
|
|
v.SetDefault("jwt.filer_signing.expires_after_seconds", 600)
|
|
filerSigningExpiresAfter := v.GetInt("jwt.filer_signing.expires_after_seconds")
|
|
|
|
// 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,
|
|
TrustedUserCAKeysFile: *sftpOpt.trustedUserCAKeysFile,
|
|
FilerSigningKey: []byte(filerSigningKey),
|
|
FilerSigningExpiresAfter: filerSigningExpiresAfter,
|
|
})
|
|
|
|
// Register reload hook for HUP signal
|
|
grace.OnReload(service.Reload)
|
|
|
|
// Set up Unix socket if on non-Windows platforms
|
|
if runtime.GOOS != "windows" {
|
|
localSocket := *sftpOpt.localSocket
|
|
explicit := localSocket != ""
|
|
if !explicit {
|
|
localSocket = fmt.Sprintf("/tmp/seaweedfs-sftp-%d.sock", *sftpOpt.port)
|
|
}
|
|
if err := os.Remove(localSocket); err != nil && !os.IsNotExist(err) {
|
|
if explicit {
|
|
glog.Fatalf("Failed to remove %s, error: %s", localSocket, err.Error())
|
|
}
|
|
glog.Warningf("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 {
|
|
if explicit {
|
|
glog.Fatalf("Failed to listen on %s: %v", localSocket, err)
|
|
}
|
|
glog.Errorf("Failed to listen on %s, skipping local socket server: %v", localSocket, err)
|
|
return
|
|
}
|
|
if err := service.Serve(sftpSocketListener); err != nil {
|
|
glog.Errorf("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", version.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
|
|
}
|