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