mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 21:33:14 +00:00
7a8514bdfa
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>
380 lines
19 KiB
Rust
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;
|
|
}
|