// 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; }