ssh_cli/constants.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-SECDEV-05: pure module — no `unsafe` permitted (crate root allows only OS FFI / test env).
3#![forbid(unsafe_code)]
4//! Named domain constants (Rules Rust — proibição de hardcode).
5//!
6//! Compile-time values with semantic names live here. Runtime host credentials,
7//! per-VPS timeouts, and primary-key material are **never** hardcoded: they are
8//! loaded once from CLI / OS keyring / XDG files under
9//! [`APP_NAME`] (`directories::ProjectDirs` + optional `--config-dir`).
10//!
11//! ## 12-Factor alignment (this product)
12//!
13//! | Factor | How ssh-cli applies it |
14//! |--------|------------------------|
15//! | III Config | Hosts in XDG `config.toml`; secrets via CLI flags / keyring / `secrets.key` (not `SSH_CLI_*` env stores) |
16//! | Build/Release/Run | Version from `CARGO_PKG_VERSION`; commit via `build.rs`; no prod URL in binary |
17//! | Backing services | SSH endpoints are user registry data, not compile-time constants |
18//!
19//! Modules keep *local* constants when the name is only meaningful in that
20//! module (e.g. concurrency RAM budget). Cross-cutting identity, storage file
21//! names, env var names, network defaults, and process timing are centralized.
22
23// ── Identity / XDG ──────────────────────────────────────────────────────────
24
25/// Binary and project name (`clap`, keyring service, XDG directory leaf).
26pub const APP_NAME: &str = "ssh-cli";
27
28/// `directories::ProjectDirs::from` qualifier (empty = no reverse-DNS prefix).
29pub const PROJECT_QUALIFIER: &str = "";
30
31/// `directories::ProjectDirs::from` organization (empty = app-only path).
32pub const PROJECT_ORGANIZATION: &str = "";
33
34/// Registry file name under the config directory.
35pub const CONFIG_FILE_NAME: &str = "config.toml";
36
37/// Primary-key file name (next to [`CONFIG_FILE_NAME`]; mode 0o600).
38pub const SECRETS_KEY_FILE_NAME: &str = "secrets.key";
39
40/// TOFU host-key store file name (sibling of [`CONFIG_FILE_NAME`]).
41pub const KNOWN_HOSTS_FILE_NAME: &str = "known_hosts";
42
43/// Active-VPS marker file name (sibling of [`CONFIG_FILE_NAME`]).
44pub const ACTIVE_VPS_FILE_NAME: &str = "active";
45
46/// Persisted UI language preference file name (sibling of [`CONFIG_FILE_NAME`]).
47pub const LANG_PREFERENCE_FILE_NAME: &str = "lang";
48
49/// XDG subdirectory for TLS material (mTLS client certs, ACME account/certs).
50///
51/// Layout (all under config dir, mode 0o600 for secrets):
52/// - `tls/mtls/<name>/cert.pem` + `key.pem`
53/// - `tls/acme/account.json`
54/// - `tls/acme/<domain>/cert.pem` + `key.pem` + `order.json` (pending)
55pub const TLS_DIR_NAME: &str = "tls";
56
57/// Subdirectory of [`TLS_DIR_NAME`] for imported mTLS client identities.
58pub const TLS_MTLS_DIR_NAME: &str = "mtls";
59
60/// Subdirectory of [`TLS_DIR_NAME`] for ACME account + issued certificates.
61pub const TLS_ACME_DIR_NAME: &str = "acme";
62
63/// ACME account credentials file name (JSON, 0o600).
64pub const TLS_ACME_ACCOUNT_FILE_NAME: &str = "account.json";
65
66/// Certificate chain PEM file name under an ACME/mTLS identity directory.
67pub const TLS_CERT_FILE_NAME: &str = "cert.pem";
68
69/// Private key PEM file name under an ACME/mTLS identity directory.
70pub const TLS_KEY_FILE_NAME: &str = "key.pem";
71
72/// Pending ACME order state file name (agent two-step DNS-01 flow).
73pub const TLS_ACME_ORDER_FILE_NAME: &str = "order.json";
74
75// ── Environment variable names (historical / fail-closed surface) ───────────
76// Product config is CLI + XDG only. These names are kept for fail-closed
77// rejection of secrets env stores and for test cleanup of legacy vars.
78// G-AUD-12: do not reintroduce SSH_CLI_HOME / LANG / FORCE_TEXT as stores.
79// G-UNSAFE-08: plaintext opt-out is CLI-only (`--allow-plaintext-secrets`).
80// G-UNSAFE-14: concurrency is CLI-only (`--max-concurrency` + auto formula).
81
82/// Historical name only — **not** read as product config store.
83/// Use CLI `--config-dir` for isolated config roots.
84pub const ENV_HOME: &str = "SSH_CLI_HOME";
85
86/// Hex primary-key material. **Fail-closed:** presence is rejected (not a store).
87/// Use XDG `secrets.key`, `--secrets-key-file`, or `--use-keyring`.
88pub const ENV_SECRETS_KEY: &str = "SSH_CLI_SECRETS_KEY";
89
90/// Path to a primary-key file (hex). **Fail-closed:** presence is rejected (not a store).
91/// Use CLI `--secrets-key-file` or XDG `secrets.key`.
92pub const ENV_SECRETS_KEY_FILE: &str = "SSH_CLI_SECRETS_KEY_FILE";
93
94/// Historical name only — prefer CLI `--use-keyring` (not an env store).
95pub const ENV_USE_KEYRING: &str = "SSH_CLI_USE_KEYRING";
96
97/// Historical name only — **not** read as product store.
98/// Use CLI `--lang` or `locale set` (XDG `lang`).
99pub const ENV_LANG: &str = "SSH_CLI_LANG";
100
101/// Historical name only — **not** read as product store.
102/// Use `--json` / `--output-format text`.
103pub const ENV_FORCE_TEXT: &str = "SSH_CLI_FORCE_TEXT";
104
105// ── OS keyring identity ─────────────────────────────────────────────────────
106
107/// Keyring service name (must match historical installs).
108pub const KEYRING_SERVICE: &str = APP_NAME;
109
110/// Canonical keyring user for the primary key.
111pub const KEYRING_USER_PRIMARY: &str = "secrets-primary-key";
112
113/// Legacy keyring user (read-only migration alias).
114pub const KEYRING_USER_LEGACY: &str = "secrets-master-key";
115
116// ── Network defaults (named; overridable via CLI / registry) ────────────────
117
118/// Default SSH port when the user omits `--port` on `vps add`.
119pub const DEFAULT_SSH_PORT: u16 = 22;
120
121/// Default local bind for `tunnel --bind` (loopback-only for safety).
122pub const DEFAULT_TUNNEL_BIND_ADDR: &str = "127.0.0.1";
123
124/// Origin address advertised on SSH direct-tcpip channels for local forwards.
125pub const TUNNEL_CHANNEL_ORIGIN_ADDR: &str = "127.0.0.1";
126
127/// Origin port advertised on SSH direct-tcpip channels (`0` = ephemeral/unspecified).
128pub const TUNNEL_CHANNEL_ORIGIN_PORT: u16 = 0;
129
130// ── Process / async timing (units in the name) ──────────────────────────────
131
132/// Tokio runtime graceful shutdown budget after the one-shot command returns.
133pub const RUNTIME_SHUTDOWN_TIMEOUT_SECS: u64 = 2;
134
135/// Tunnel accept-loop cooperative signal poll interval.
136pub const TUNNEL_SIGNAL_POLL_INTERVAL_MS: u64 = 200;
137
138/// Grace period to drain in-flight tunnel forwards after stop.
139pub const TUNNEL_FORWARD_DRAIN_TIMEOUT_SECS: u64 = 2;
140
141/// Multi-host fan-out signal poll interval inside `map_bounded`.
142pub const FAN_OUT_SIGNAL_POLL_INTERVAL_MS: u64 = 50;
143
144// ── SSH transport / TCP dial (Rules Rust — rede) ────────────────────────────
145
146/// Delay before starting the next Happy Eyeballs dial candidate (RFC 8305-inspired).
147pub const HAPPY_EYEBALLS_ATTEMPT_DELAY_MS: u64 = 50;
148
149/// SSH-level keepalive interval (seconds between keepalives when idle).
150pub const SSH_KEEPALIVE_INTERVAL_SECS: u64 = 15;
151
152/// Max unanswered SSH keepalives before russh closes the session.
153pub const SSH_KEEPALIVE_MAX: usize = 3;
154
155/// SSH identification string sent to the server (G-SSH-02).
156///
157/// Generic product id — must **not** embed the exact `russh` crate version
158/// (server fingerprinting / version targeting).
159pub const SSH_CLIENT_ID: &str = "SSH-2.0-ssh-cli";
160
161/// Initial SSH channel window size (2 MiB — OpenSSH-class throughput).
162pub const SSH_WINDOW_SIZE: u32 = 2 * 1024 * 1024;
163
164/// Maximum SSH packet size (32 KiB — RFC 4253 common default).
165pub const SSH_MAX_PACKET_SIZE: u32 = 32 * 1024;
166
167/// Rekey after this many seconds of session lifetime (RFC 4253 §9).
168pub const SSH_REKEY_TIME_SECS: u64 = 3600;
169
170/// Rekey after this many bytes read or written (1 GiB — RFC 4253 §9).
171pub const SSH_REKEY_BYTE_LIMIT: usize = 1 << 30;
172
173/// Enable TCP-level `SO_KEEPALIVE` on the SSH dial socket (complements SSH KA).
174pub const TCP_KEEPALIVE_ENABLED: bool = true;
175
176/// Minimum RSA private-key size accepted for publickey auth (bits).
177pub const SSH_RSA_MIN_BITS: usize = 2048;
178
179/// Preferred RSA size; smaller (but ≥ min) keys log a warning (bits).
180pub const SSH_RSA_PREFERRED_BITS: usize = 3072;
181
182/// Windows OpenSSH agent named pipe (platform default when `--use-agent` has no path).
183pub const WINDOWS_SSH_AGENT_PIPE: &str = r"\\.\pipe\openssh-ssh-agent";
184
185// ── SFTP subsystem (G-SFTP) ─────────────────────────────────────────────────
186
187/// SSH subsystem name for SFTP v3 (`request_subsystem`).
188pub const SFTP_SUBSYSTEM: &str = "sftp";
189
190/// Stream chunk size for SFTP file I/O (bytes). Never load whole files into RAM.
191pub const SFTP_IO_CHUNK: usize = 32 * 1024;
192
193/// Max recursion depth for `sftp upload|download --recursive`.
194pub const SFTP_MAX_RECURSION_DEPTH: u32 = 64;
195
196/// Fail-closed cap on directory listing entries (`sftp ls` / recursive walk).
197pub const SFTP_LIST_MAX_ENTRIES: usize = 100_000;
198
199/// Basename used only when a multi-file source has no file name component.
200/// Prefer rejecting empty names; this is a last-resort label (G-SFTP-R13).
201pub const SFTP_FALLBACK_BASENAME: &str = "ssh-cli-unnamed";
202
203// ── Agent retry policy (Rules Rust — retry/backoff; re-invoke, not in-process) ─
204
205/// Max retries **after** the first failure for agents re-invoking on exit 74.
206///
207/// Total attempts = `AGENT_RETRY_MAX_RETRIES + 1` (3). Matches `docs/AGENTS.md`.
208pub const AGENT_RETRY_MAX_RETRIES: u32 = 2;
209
210/// Base backoff delay (ms) for agent full-jitter formula.
211pub const AGENT_RETRY_BASE_MS: u64 = 200;
212
213/// Ceiling for a single agent backoff sleep (ms).
214pub const AGENT_RETRY_MAX_DELAY_MS: u64 = 5_000;
215
216/// Hard cap on configured retries (prevents accidental retry storms).
217pub const HARD_RETRY_MAX_RETRIES: u32 = 10;
218
219/// Hard ceiling for a single delay (ms).
220pub const HARD_RETRY_MAX_DELAY_MS: u64 = 60_000;
221
222// ── At-rest crypto sizes (ChaCha20-Poly1305) ─────────────────────────────────
223
224/// Primary-key length in bytes.
225pub const PRIMARY_KEY_LEN_BYTES: usize = 32;
226
227/// Primary-key hex encoding length (`2 * PRIMARY_KEY_LEN_BYTES`).
228pub const PRIMARY_KEY_HEX_LEN: usize = 64;
229
230/// ChaCha20-Poly1305 nonce length in bytes.
231pub const AEAD_NONCE_LEN_BYTES: usize = 12;
232
233/// Poly1305 authentication tag length in bytes.
234pub const AEAD_TAG_LEN_BYTES: usize = 16;
235
236/// Unix permission mode for secret files (`config.toml`, `secrets.key`, `lang`).
237pub const SECRET_FILE_MODE_UNIX: u32 = 0o600;
238
239/// Unix permission mode for secret directories (TLS identity dirs, etc.).
240pub const SECRET_DIR_MODE_UNIX: u32 = 0o700;
241
242// ── Concurrency caps (single source; clap + fan-out) ─────────────────────────
243
244/// Minimum concurrent multi-host / fan-out units (always ≥ 1).
245pub const MIN_CONCURRENCY: usize = 1;
246
247/// Hard upper bound on concurrent multi-host sessions and SCP file channels.
248///
249/// Clap `--max-concurrency` / `--scp-file-concurrency` ranges must use this value.
250pub const MAX_CONCURRENCY: usize = 64;
251
252/// Alias used by the concurrency budget module (same as [`MAX_CONCURRENCY`]).
253pub const HARD_CAP: usize = MAX_CONCURRENCY;
254
255/// Documented per multi-host session RAM budget (bytes) for auto concurrency formula.
256pub const RAM_PER_TASK_BYTES: u64 = 16 * 1024 * 1024;
257
258/// I/O oversubscribe factor vs CPU count for multi-host fan-out.
259pub const IO_OVERSUBSCRIBE: usize = 4;
260
261/// Conservative CPU×IO cap when free RAM cannot be read (non-Linux).
262pub const NON_LINUX_CPU_CAP: usize = 8;
263
264// Compile-time invariants (const/static rules).
265const _: () = assert!(!APP_NAME.is_empty());
266const _: () = assert!(!CONFIG_FILE_NAME.is_empty());
267const _: () = assert!(!SECRETS_KEY_FILE_NAME.is_empty());
268const _: () = assert!(!KNOWN_HOSTS_FILE_NAME.is_empty());
269const _: () = assert!(!ACTIVE_VPS_FILE_NAME.is_empty());
270const _: () = assert!(!ENV_HOME.is_empty());
271const _: () = assert!(DEFAULT_SSH_PORT > 0);
272const _: () = assert!(!DEFAULT_TUNNEL_BIND_ADDR.is_empty());
273const _: () = assert!(RUNTIME_SHUTDOWN_TIMEOUT_SECS > 0);
274const _: () = assert!(TUNNEL_SIGNAL_POLL_INTERVAL_MS > 0);
275const _: () = assert!(TUNNEL_FORWARD_DRAIN_TIMEOUT_SECS > 0);
276const _: () = assert!(FAN_OUT_SIGNAL_POLL_INTERVAL_MS > 0);
277const _: () = assert!(HAPPY_EYEBALLS_ATTEMPT_DELAY_MS > 0);
278const _: () = assert!(SSH_KEEPALIVE_INTERVAL_SECS > 0);
279const _: () = assert!(SSH_KEEPALIVE_MAX > 0);
280const _: () = assert!(!SSH_CLIENT_ID.is_empty());
281const _: () = assert!(SSH_WINDOW_SIZE > 0);
282const _: () = assert!(SSH_MAX_PACKET_SIZE > 0);
283const _: () = assert!(SSH_REKEY_TIME_SECS > 0);
284const _: () = assert!(SSH_REKEY_BYTE_LIMIT > 0);
285const _: () = assert!(SSH_RSA_MIN_BITS >= 2048);
286const _: () = assert!(SSH_RSA_PREFERRED_BITS >= SSH_RSA_MIN_BITS);
287const _: () = assert!(!WINDOWS_SSH_AGENT_PIPE.is_empty());
288const _: () = assert!(!SFTP_SUBSYSTEM.is_empty());
289const _: () = assert!(SFTP_IO_CHUNK > 0);
290const _: () = assert!(SFTP_MAX_RECURSION_DEPTH > 0);
291const _: () = assert!(SFTP_LIST_MAX_ENTRIES > 0);
292const _: () = assert!(!SFTP_FALLBACK_BASENAME.is_empty());
293const _: () = assert!(AGENT_RETRY_MAX_RETRIES > 0);
294const _: () = assert!(AGENT_RETRY_BASE_MS >= 50);
295const _: () = assert!(AGENT_RETRY_MAX_DELAY_MS >= AGENT_RETRY_BASE_MS);
296const _: () = assert!(HARD_RETRY_MAX_RETRIES >= AGENT_RETRY_MAX_RETRIES);
297const _: () = assert!(HARD_RETRY_MAX_DELAY_MS >= AGENT_RETRY_MAX_DELAY_MS);
298const _: () = assert!(PRIMARY_KEY_HEX_LEN == PRIMARY_KEY_LEN_BYTES * 2);
299const _: () = assert!(AEAD_NONCE_LEN_BYTES == 12);
300const _: () = assert!(AEAD_TAG_LEN_BYTES == 16);
301const _: () = assert!(SECRET_FILE_MODE_UNIX == 0o600);