Files
rustfs/crates/protocols/src/sftp/constants.rs
T
escapecode 7a8514bdfa feat(sftp): add macOS and Windows platform support (#3372)
The session watchdog now selects its detection method per
platform. It previously probed kernel TCP state through a
Linux-only procfs path, so on macOS every healthy idle session
was killed within a minute, and on Windows the watchdog never
spawned at all, leaving wedged sessions with no cleanup. Linux
keeps its fast-kill watchdog unchanged. Other platforms get a
silence-only backstop that kills a session only at the
documented 30-minute idle ceiling.

The host-key loader now has a Windows arm. It loads OpenSSH
format host keys from the configured directory and logs a
one-time warning to restrict NTFS ACLs on the key directory,
the same operator-managed approach FTPS, WebDAV, KMS, and IAM
already use on Windows. Startup previously aborted with
UnsupportedPlatform because the Unix mode-bit permission check
has no Windows equivalent. tokio's io-uring feature is now
enabled only in Linux builds. io-uring is a Linux kernel
interface and enabling it unconditionally broke the Windows
build.

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 18:08:23 +08:00

380 lines
19 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Named constants for the SFTP protocol implementation, grouped by purpose.
//!
//! s3_error_codes: AWS S3 error-code substrings the driver matches when
//! classifying backend errors into SFTP status codes.
//!
//! http_error_codes: HTTP status-code substrings the driver matches when
//! a backend reports an HTTP error by number rather than by S3 code.
//!
//! posix: POSIX mode bits (S_IFDIR, S_IFREG, permission triples) returned
//! in SFTP FileAttributes for S3 resources.
//!
//! protocol: SFTP protocol version supported by the driver and the SSH
//! subsystem name clients request.
//!
//! limits: caps, defaults, and AWS-imposed constants used across the SFTP
//! driver and server.
/// S3 error-code substrings matched by the driver when classifying backend
/// errors into SFTP status codes. The constants below are fragments of the
/// public AWS S3 error-code vocabulary, which backends include in their
/// error messages.
pub mod s3_error_codes {
/// AWS S3 error code returned by HeadObject / GetObject when the
/// key does not exist.
pub const NO_SUCH_KEY: &str = "NoSuchKey";
/// AWS S3 error code returned by HeadBucket when the bucket does
/// not exist.
pub const NO_SUCH_BUCKET: &str = "NoSuchBucket";
/// Generic "not found" string returned by S3-compatible backends
/// (MinIO, Wasabi, ecstore) that do not always use the AWS
/// NoSuchKey / NoSuchBucket vocabulary on every miss.
pub const NOT_FOUND: &str = "NotFound";
/// AWS error code returned when an IAM policy denies the requested
/// action on the resource.
pub const ACCESS_DENIED: &str = "AccessDenied";
/// Generic forbidden string returned by S3-compatible backends that
/// do not always use the AWS AccessDenied vocabulary.
pub const FORBIDDEN: &str = "Forbidden";
/// Returned by AbortMultipartUpload when the upload_id is no
/// longer live (already completed, already aborted, or reclaimed
/// by the bucket lifecycle rule). Drop's retry loop downgrades
/// this to a debug log to avoid noise when the tombstone-retry
/// path races a successful inline completion.
pub const NO_SUCH_UPLOAD: &str = "NoSuchUpload";
}
/// HTTP status-code substrings matched by the driver when a backend
/// reports an HTTP error by number rather than by S3 error code. These
/// are a different vocabulary from s3_error_codes (HTTP wire statuses
/// rather than S3 API error codes) and kept in a separate module.
pub mod http_error_codes {
pub const NOT_FOUND: &str = "404";
pub const FORBIDDEN: &str = "403";
}
/// POSIX mode bits returned in SFTP FileAttributes for S3 resources.
/// SFTPv3 draft section 5 defines the permissions field as a u32
/// carrying POSIX stat.h mode bits. S3 has no POSIX mode metadata, so
/// the server returns a fixed type bit (S_IFDIR for buckets and
/// prefixes, S_IFREG for objects) combined with a conventional
/// permission triple. Clients that inspect the type bit to distinguish
/// files from directories would otherwise treat every entry as a
/// regular file.
pub mod posix {
use crate::constants::paths::{DIR_MODE, DIR_PERMISSIONS, FILE_MODE, FILE_PERMISSIONS};
/// Directory mode returned for bucket and prefix entries.
/// S_IFDIR | 0o755 = 0o040755.
pub const POSIX_DIR_MODE: u32 = DIR_MODE | DIR_PERMISSIONS;
/// Regular-file mode returned for object entries.
/// S_IFREG | 0o644 = 0o100644.
pub const POSIX_FILE_MODE: u32 = FILE_MODE | FILE_PERMISSIONS;
/// POSIX file-type mask (S_IFMT). Isolates the four high bits of a
/// mode value so the file-type field can be compared against
/// S_IFDIR, S_IFREG, S_IFLNK, and the other POSIX type constants.
/// Compiled in test builds only. The runtime path reads the full
/// mode from POSIX_DIR_MODE / POSIX_FILE_MODE.
#[cfg(test)]
pub const POSIX_TYPE_MASK: u32 = 0o170000;
}
/// SFTP protocol identifiers and version numbers.
pub mod protocol {
/// SFTP protocol version supported by this server. The wire format and
/// packet semantics are defined by the SFTP Internet Draft
/// draft-ietf-secsh-filexfer-02. Later drafts (versions 4 to 6) change
/// the attribute and timestamp encodings. Supporting them would require
/// a separate driver type, not a parameter on the version-3 driver.
pub const SFTP_VERSION: u32 = 3;
/// SSH subsystem name that clients request to start SFTP.
pub const SFTP_SUBSYSTEM_NAME: &str = "sftp";
}
/// Limits, defaults, and AWS-defined constants used across the SFTP
/// driver and server. Three roles share this module.
///
/// AWS-imposed limits. S3_COPY_OBJECT_MAX_SIZE, S3_MIN_PART_SIZE,
/// S3_MAX_PART_SIZE, and S3_MAX_MULTIPART_PARTS reflect the S3 API
/// contract and do not change per deployment.
///
/// Operational bounds. DEFAULT_HANDLES_PER_SESSION, the
/// BACKEND_OP_TIMEOUT trio (DEFAULT, MIN, MAX), the READ_CACHE_*
/// values, and SHUTDOWN_DRAIN_TIMEOUT_SECS govern per-session and
/// process-wide resource use. Each has a paired RUSTFS_SFTP_* env var
/// for operator override.
///
/// SSH transport overrides. SSH_MAXIMUM_PACKET_SIZE,
/// SSH_CHANNEL_BUFFER_SIZE, and SSH_EVENT_BUFFER_SIZE override russh
/// defaults so the inbound mpsc absorbs client pipelining during
/// multi-MB transfers.
pub mod limits {
/// Maximum payload size accepted from a single READ request, in bytes.
/// Matches OpenSSH's default chunk size and bounds per-request memory.
pub const MAX_READ_LEN: u32 = 256 * 1024;
/// Default number of simultaneously-open SFTP handles per session.
/// Used when RUSTFS_SFTP_HANDLES_PER_SESSION is unset or out of
/// range. 64 covers the typical OpenSSH / rsync / WinSCP
/// pipelining ceiling.
pub const DEFAULT_HANDLES_PER_SESSION: usize = 64;
/// Lower validation bound on RUSTFS_SFTP_HANDLES_PER_SESSION.
/// Below this a single client opening one file plus a directory
/// listing already runs out of handles.
pub const HANDLES_PER_SESSION_MIN: usize = 8;
/// Upper validation bound on RUSTFS_SFTP_HANDLES_PER_SESSION.
/// Each handle can hold a part_size-sized buffer (write path), so
/// at default part_size = 16 MiB the worst-case session memory
/// is 16 GiB at this cap.
pub const HANDLES_PER_SESSION_MAX: usize = 1024;
/// Seconds between SSH keepalive probes. Passed into
/// russh::server::Config at server-build time. russh sends an
/// SSH-level keepalive request after this many seconds of silence.
/// If the client does not respond after KEEPALIVE_MAX consecutive
/// probes the connection is closed.
///
/// This detects dead TCP connections where the client disappeared
/// without sending FIN (network failure, killed process, etc).
/// Active but slow connections are unaffected because they still
/// respond to the small SSH keepalive packets even during large
/// transfers. OpenSSH's ServerAliveInterval defaults to 15 seconds
/// on the client side. 15 seconds on the server side is consistent
/// with that.
pub const KEEPALIVE_INTERVAL_SECS: u64 = 15;
/// Number of consecutive missed keepalive responses before russh
/// closes the connection. Passed into russh::server::Config at
/// server-build time. With KEEPALIVE_INTERVAL_SECS = 15, a truly
/// dead connection is closed within ~45 seconds.
pub const KEEPALIVE_MAX: usize = 3;
/// Wallclock deadline applied to russh::server::run_stream while
/// the SSH KEX and password auth handshake completes. A peer that
/// completes TCP and stalls before KEXINIT (or that drives KEX or
/// auth so slowly that no SSH-layer timer fires) is dropped after
/// this many seconds, freeing the spawn-task slot. Inactivity and
/// keepalive timers do not cover this window because they run
/// inside the post-handshake session loop.
pub const HANDSHAKE_DEADLINE_SECS: u64 = 30;
/// Tick interval for the per-session watchdog on every platform.
/// On Linux worst-case detection latency is
/// WEDGE_FAST_KILL_SILENCE_SECS plus one to two ticks. On non-Linux
/// targets the fallback watchdog uses the same tick to check silence
/// against WEDGE_FALLBACK_KILL_SILENCE_SECS.
pub const WEDGE_WATCHDOG_TICK_SECS: u64 = 15;
/// Silence threshold at which a session whose underlying TCP socket
/// is in CLOSE_WAIT is force-cancelled by the watchdog.
///
/// A healthy session is never simultaneously silent at the SFTP
/// handler AND in CLOSE_WAIT: peer FIN normally appears as Ok(0)
/// on the SSH library read poll within milliseconds. 30 s leaves
/// room for two keepalive intervals (15 s each) before the
/// watchdog overrides, so a transient scheduler stall does not
/// trip it.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub const WEDGE_FAST_KILL_SILENCE_SECS: u64 = 30;
/// Fallback silence threshold. On Linux it is the backstop for
/// cases where /proc/net/tcp is unreadable (filesystem permissions,
/// namespace tricks) or where the wedge is in a state other than
/// CLOSE_WAIT. On non-Linux targets it is the only kill path from
/// the fallback watchdog module, because /proc/net/tcp does not
/// exist and the CLOSE_WAIT probe is unavailable. 1800 s sits above
/// russh's default inactivity_timeout (600 s) so russh's own
/// inactivity close fires first on a healthy idle session.
pub const WEDGE_FALLBACK_KILL_SILENCE_SECS: u64 = 1800;
// The three constants below override russh defaults for the SSH
// transport the SFTP subsystem runs on. russh defaults
// (channel_buffer_size 100, event_buffer_size 10) are tight enough
// that the inbound mpsc fills under client pipelining, the
// session-loop reading arm blocks on chan.send(...).await, and
// inbound CHANNEL_WINDOW_ADJUST stops being drained. PuTTY-derived
// stacks (FileZilla, Cyberduck) reach the limit during multi-MB
// downloads.
/// Maximum SSH packet size advertised by the server, in bytes.
/// Matches russh's default. Set explicitly so behaviour does not
/// depend on russh's chosen default.
pub const SSH_MAXIMUM_PACKET_SIZE: u32 = 32 * 1024;
/// Capacity of the bounded mpsc that russh's session loop uses
/// for inbound CHANNEL_DATA. russh default is 100. Raised to
/// defer fill past typical client pipelining depths.
pub const SSH_CHANNEL_BUFFER_SIZE: usize = 1024;
/// Capacity of the bounded mpsc that russh's session loop uses
/// for channel-level events. russh default is 10. Raised to
/// defer fill past typical client pipelining depths.
pub const SSH_EVENT_BUFFER_SIZE: usize = 1024;
// The four constants below are S3 protocol limits defined by the AWS
// S3 API. They are not SFTP operational policy and do not change per
// deployment. The ecstore client crate defines the same four values
// under different names (ABS_MIN_PART_SIZE, MAX_PART_SIZE,
// MAX_PARTS_COUNT, MAX_SINGLE_PUT_OBJECT_SIZE). They live here as
// SFTP-scoped copies because the protocols crate must not depend on
// ecstore internals: the StorageBackend trait abstraction would leak.
/// S3 CopyObject single-shot size limit (5 GiB). Source objects
/// larger than this require UploadPartCopy. Mirrors the
/// MAX_SINGLE_PUT_OBJECT_SIZE constant in ecstore but cannot be
/// imported from there.
pub const S3_COPY_OBJECT_MAX_SIZE: u64 = 5 * 1024 * 1024 * 1024;
/// S3 minimum part size in bytes (5 MiB). Every part of a multipart
/// upload except the last must be at least this size, or
/// CompleteMultipartUpload returns EntityTooSmall. Mirrors ecstore's
/// ABS_MIN_PART_SIZE but cannot be imported from there.
pub const S3_MIN_PART_SIZE: u64 = 5 * 1024 * 1024;
/// S3 maximum part size in bytes (5 GiB). Any single UploadPart call
/// carrying a body larger than this is rejected with EntityTooLarge.
/// Mirrors the MAX_PART_SIZE constant in ecstore but cannot be
/// imported from there. AWS sets S3_COPY_OBJECT_MAX_SIZE and
/// S3_MAX_PART_SIZE independently to 5 GiB. The values are not
/// coupled. Future S3 versions could move them apart, so they
/// remain separate constants.
pub const S3_MAX_PART_SIZE: u64 = 5 * 1024 * 1024 * 1024;
/// Maximum number of parts in a single multipart upload (S3 limit).
/// Exceeding this causes UploadPart to fail. Mirrors ecstore's
/// MAX_PARTS_COUNT but cannot be imported from there.
pub const S3_MAX_MULTIPART_PARTS: i32 = 10_000;
/// Maximum seconds the SFTP server waits for session tasks to
/// finish after a shutdown signal before the runtime cancels them.
/// This is the cleanup-grace window for the Drop impl on each
/// SftpDriver (which issues AbortMultipartUpload for live
/// upload_ids), not a transfer-completion window. In-flight
/// transfers do not need to finish inside this timer. Cancellation
/// past this timeout leaves any remaining upload_ids to the bucket
/// AbortIncompleteMultipartUpload lifecycle rule.
pub const SHUTDOWN_DRAIN_TIMEOUT_SECS: u64 = 30;
/// Maximum number of buckets returned by the root READDIR. S3
/// ListBuckets is not paginated so the backend can hand back an
/// arbitrarily long response. Truncating here bounds the Vec
/// allocation and keeps the SSH channel window usage low for a
/// principal with many visible buckets. Overflow is logged as a
/// warn so operators know truncation happened.
pub const ROOT_LISTING_MAX_ENTRIES: usize = 10_000;
/// Maximum entries requested per ListObjectsV2 page for READDIR.
/// The S3 default is 1000. Asking for a specific value keeps the
/// per-page allocation and SSH channel window usage under operator
/// control. Each entry's longname is bounded by a filename plus a
/// fixed-width header, so 1000 entries stays under the 2 MiB
/// channel window.
pub const READDIR_PAGE_MAX_KEYS: i32 = 1_000;
/// Default per-call deadline applied to every StorageBackend
/// invocation issued by the SFTP driver. A backend that does not
/// respond within this many seconds returns Failure to the client
/// and logs a warning naming the backend method. Used when
/// RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS is unset or out of range.
/// The keepalive timer (KEEPALIVE_INTERVAL_SECS times KEEPALIVE_MAX,
/// approximately 45 s) closes a stuck SSH transport but cannot detect
/// a backend that accepted the request and never returned a body.
/// This deadline closes that gap.
pub const DEFAULT_BACKEND_OP_TIMEOUT_SECS: u64 = 60;
/// Lower validation bound on RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS.
/// Below 5 s a healthy backend under load (cold-cache HEAD on a
/// large bucket, multipart Complete on hundreds of parts) can
/// time out under normal operating conditions.
pub const BACKEND_OP_TIMEOUT_MIN_SECS: u64 = 5;
/// Upper validation bound on RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS.
/// 600 s is the longest single backend call expected in normal
/// use. Above that the SSH keepalive (about 45 s) takes over the
/// liveness role.
pub const BACKEND_OP_TIMEOUT_MAX_SECS: u64 = 600;
/// Maximum number of retries the small-file PutObject path in
/// commit_write attempts after a transient backend error
/// (SlowDown, RequestTimeout, Throttling, InternalError, etc).
/// Three retries covers the typical S3 retry-after window without
/// holding the SFTP CLOSE response open beyond the keepalive
/// timer. Total elapsed before giving up is the sum of
/// COMMIT_WRITE_BACKOFF_MS plus the cumulative call time.
pub const COMMIT_WRITE_MAX_RETRIES: usize = 3;
/// Backoff schedule between commit_write PutObject retries, in
/// milliseconds. Index zero is the wait between attempt 0 and
/// attempt 1, and so on. The exponential 250 / 500 / 1000 cadence
/// matches typical S3 SDK defaults and stays inside the worst-case
/// 2 s combined wait that a CLOSE response can absorb without the
/// client surfacing a hang.
pub const COMMIT_WRITE_BACKOFF_MS: [u64; COMMIT_WRITE_MAX_RETRIES] = [250, 500, 1000];
/// Per-handle read cache window size in bytes. On a cache miss
/// the driver fetches at most this many bytes from the backend,
/// then returns the requested portion to the client and stores
/// the rest in the per-handle buffer. With the 4 MiB default and
/// the 256 KiB MAX_READ_LEN, sixteen FXP_READs are returned from
/// one backend call. Overridable per installation via
/// RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES.
pub const READ_CACHE_WINDOW_DEFAULT: u64 = 4 * 1024 * 1024;
/// Lower validation bound on RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES
/// for non-zero values. The cache-window floor reflects MAX_READ_LEN.
/// Below it a single MAX_READ_LEN FXP_READ cannot be satisfied from
/// one cached chunk, so the per-handle allocation costs memory with
/// no benefit. To turn the cache off entirely, use the
/// READ_CACHE_DISABLED sentinel.
pub const READ_CACHE_WINDOW_MIN: u64 = MAX_READ_LEN as u64;
/// Sentinel value for RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES that
/// disables the per-handle read cache. The populate path is
/// short-circuited, no buffer is retained between FXP_READs, and
/// the process-wide accumulator is not touched. Each FXP_READ
/// takes one backend call.
pub const READ_CACHE_DISABLED: u64 = 0;
/// Upper validation bound on RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES.
/// Bounds single-handle memory at a value that fits inside
/// READ_CACHE_TOTAL_MEM_DEFAULT even with four concurrent
/// handles open.
pub const READ_CACHE_WINDOW_MAX: u64 = 64 * 1024 * 1024;
/// Process-wide ceiling on cumulative read cache memory across
/// every live SFTP handle. When the accumulator plus a new
/// window would exceed this value, the populate call is skipped.
/// The read still completes from the freshly-fetched bytes
/// without storing them in the cache. The next FXP_READ on the
/// same handle issues a fresh backend call instead of being
/// returned from the buffer. Overridable per installation via
/// RUSTFS_SFTP_READ_CACHE_TOTAL_MEM_BYTES.
pub const READ_CACHE_TOTAL_MEM_DEFAULT: u64 = 256 * 1024 * 1024;
/// Lower validation bound on
/// RUSTFS_SFTP_READ_CACHE_TOTAL_MEM_BYTES. Below this value, even
/// a single window at the default window size cannot be stored
/// without breaching the cap, leaving every read on the no-cache
/// path.
pub const READ_CACHE_TOTAL_MEM_MIN: u64 = 16 * 1024 * 1024;
}