133 Commits

Author SHA1 Message Date
cd6a517fec chore: bump version to v1.14.1, soften banner-detection rationale
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 56s
Reframe the SSH client banner change as banner consistency with native
`ssh` rather than a VPN/Okta device-detection workaround. Okta-style
device posture relies on TCP/IP stack fingerprinting from the kernel,
which a userspace SSH banner cannot influence. The change is still
worth keeping for its original goal — making ssh-mux indistinguishable
from `ssh` at the protocol banner — but the docstring and CHANGELOG
should not promise an outcome it cannot deliver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.14.1
2026-04-29 13:39:27 +09:00
d4cf32dbed feat(pool): mimic local OpenSSH banner to fix VPN/Okta device fingerprinting
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 56s
VPN servers and Okta integrations parse the SSH client banner as a device
hint. Russh's default `SSH-2.0-russh_0.57` falls through to "Linux", which
is wrong for ssh-mux's Windows users. Detect the local `ssh -V` version
and reuse its exact banner so ssh-mux is indistinguishable from native ssh
on the same host. Falls back to a platform-appropriate default when ssh
isn't on PATH.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:14:36 +09:00
9d267cf7a8 feat!: always allow remote loopback any-port for direct-tcpip
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 56s
Remove the per-route ``allow_remote_loopback_any_port`` opt-in (added in
v1.13.4) and the top-level ``default_allow_remote_loopback_any_port``
fallback (added in v1.13.5). Both fields silently dropped out of the
config every time ``import-config`` rewrote ``ssh-mux-routes.toml``,
which is the path ``install`` takes — so VS Code / Cursor Remote-SSH
kept breaking on every reinstall and the user had to remember to
re-add the line.

Lift the restriction entirely instead: an authenticated route now
accepts ``direct-tcpip`` to its own host:port plus **any port** on
remote loopback. Non-loopback non-route targets stay denied so the
route still cannot be used to pivot into arbitrary internal hosts.

Threat model trade: the old gate guarded against a local actor with
code execution against the mux listener using the route as a
SOCKS-style proxy into the remote's localhost services. On a
single-user dev box that actor can already invoke ssh directly, so
the gate's defense-in-depth value is near zero — and its UX cost
(silent breakage on reinstall) was high.

Migration: nothing to do. ``RouteEntry`` and ``MuxConfig`` no longer
declare the fields, but serde ignores unknown keys, so existing
``allow_remote_loopback_any_port = true|false`` and
``default_allow_remote_loopback_any_port`` lines parse fine and are
simply no-ops now.

The ``feat!`` marker reflects the policy default change; behaviour
strictly relaxes (more channels allowed) so no caller-visible
breakage beyond the policy itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.14.0
2026-04-28 18:45:45 +09:00
c63474eb86 feat(forwarding): top-level default_allow_remote_loopback_any_port
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m17s
v1.13.4 added a per-route ``allow_remote_loopback_any_port`` flag so
VS Code / Cursor Remote-SSH could open ``direct-tcpip`` channels to
their random IDE-server localhost ports. For users whose routes are
predominantly IDE remote-dev targets, repeating the same opt-in line
under every ``[routes.X]`` section is just noise.

Add a top-level ``default_allow_remote_loopback_any_port`` (default
``false``, preserves the secure per-route opt-in) that acts as a
fallback for the per-route flag. The effective decision is the OR of
the two flags, so a per-route ``true`` keeps working even when the
top-level default is ``false`` — and a single top-level ``true``
covers every route at once.

The threat the opt-in guards against — a local process or other user
account using the route as a SOCKS-style proxy into the remote's
localhost services — is real on shared / multi-user machines but
mostly defense-in-depth on a single-user dev box, where any actor
with code execution against the local mux listener already has the
keys to ssh directly. Making it a single-line top-level switch keeps
the secure default the README recommends while making the IDE path
ergonomic for the common single-user case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.13.5
2026-04-28 17:48:27 +09:00
51210232a1 fix(forwarding): unblock VS Code/Cursor Remote-SSH with opt-in loopback flag
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m6s
Commit e7a527e ("Harden IPC and forwarding policy") restricted
direct-tcpip channels on remote loopback to the route's SSH port only.
This broke VS Code and Cursor Remote-SSH, which spawn their IDE server
on a random localhost port and forward to it via direct-tcpip — every
such channel was denied as "administratively prohibited" and the IDE
looped on "Setting up SSH host".

Add a per-route opt-in `allow_remote_loopback_any_port` (default false,
preserving the secure default) that accepts any port on the route's
remote loopback. Non-loopback non-route targets remain denied. Document
the new option in the README and CHANGELOG, and add a policy unit test
covering the opt-in path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:08:58 +09:00
e1e91a4741 chore: bump version to v1.13.3, document lockfile fix
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m18s
Release v1.13.3:
- Commit Cargo.lock so CI is reproducible (no code change)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.13.3
2026-04-28 11:56:19 +09:00
ef3507e688 build: commit Cargo.lock to fix CI
ssh-mux is a binary crate; the lockfile should be tracked so builds
are reproducible. The previous policy (lockfile in .gitignore) is
correct for library crates but wrong here.

Triggering failure: russh enables the `rsa` feature, which pulls
rsa-0.10.0-rc.12 transitively. rsa-rc.12 was written against
pkcs8-0.11.0-rc.11; pkcs8 has since cut a stable 0.11.0 with a
breaking change to `Error::KeyMalformed` (now a tuple variant).
Without a lockfile, CI's resolver picked the stable, and the build
failed with `match arms have incompatible types` deep in the rsa
crate. Local builds were unaffected because Cargo.lock pinned the
right rc.

Committing the existing Cargo.lock fixes CI. No code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:56:13 +09:00
a92b86a382 chore: bump version to v1.13.2, document re-import fix
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 56s
Release v1.13.2:
- import-config works as a re-import after ssh-mux is already installed
- warnings are now flushed before any bail (was: silent failure)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.13.2
2026-04-28 11:51:55 +09:00
a5c0f65b17 fix(import): support re-import after Include line is in place
Once setup-config has prepended `Include ssh-mux-hosts.conf` to
~/.ssh/config, every `ssh -G <alias>` resolves to 127.0.0.1 (because
the Include points at our generated 127.0.0.1:2222 routes), the
"looks like an existing ssh-mux entry" filter drops every alias, and
import-config bails with `no usable routes after resolution`. After
an upgrade — exactly when you'd want to re-import — the workflow was
broken.

Detect the exact line we generate (`Include ssh-mux-hosts.conf`) in
the source SSH config. If found, write a stripped temporary copy to
the same directory (so other relative Include directives in the
user's config keep resolving against the right base) and run
`ssh -F <stripped> -G <alias>`. An RAII guard removes the temp file
on exit. The user's original ~/.ssh/config is never written.

Also fix a UX bug where `no usable routes after resolution; see
warnings above` was emitted with no warnings actually printed —
warnings were accumulated for emission only after build_mux_config,
which the bail bypassed. A `flush_warnings` helper is now called
before every bail and on the normal exit path.

Three new unit tests cover both helpers; total import-config tests
go from 15 to 18, total bin tests from 100 to 103.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:51:48 +09:00
480ed466d5 chore: bump version to v1.13.1, document ProxyCommand exclusion
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 57s
Release v1.13.1:
- import-config skips hosts driven by ProxyCommand (e.g. AWS SSM)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.13.1
2026-04-27 17:53:05 +09:00
3ad03f5e9b fix: import-config skips ProxyCommand hosts and ProxyCommand jumps
ssh-mux multiplexes raw SSH over TCP and via a jump-host channel; it
cannot drive arbitrary transports such as the AWS SSM start-session
pipeline (Match host i-* / ProxyCommand "powershell aws ssm ...").
Previously such aliases were resolved via ssh -G, the EC2 instance
ID was captured as the route's hostname, and the alias was written
into routes.toml. ssh-mux would then try to open a direct TCP
connection to "i-0c5..." and fail at DNS / connect.

Capture proxycommand in ResolvedHost (treating "none" as unset, same
as proxyjump), then in build_mux_config:

- Skip any route whose resolved proxycommand is set, with a warning
  pointing the user at the original Host block (it stays in
  ~/.ssh/config and ssh.exe keeps using it via the ProxyCommand;
  the prepended Include does not contain the alias, so OpenSSH's
  first-match-wins falls through).
- Skip any route whose jump-host resolution has a proxycommand
  (an SSM-only bastion would be just as unreachable).
- Exclude ProxyCommand hosts from the jump-host majority vote so
  they cannot influence the picked [jump].

Three new unit tests lock the behavior in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:52:54 +09:00
31d5643ab6 chore: bump version to v1.13.0, document install bootstrap
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m21s
Release v1.13.0:
- ssh-mux install now chains import-config -> setup-config -> service
- install runs on non-Windows (skips Startup VBS, prints daemon cmd)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.13.0
2026-04-27 15:23:08 +09:00
e42a396daa feat: install runs the full bootstrap (import + setup-config + service)
Previously \`ssh-mux install\` only performed the Windows service
install step, leaving \`import-config\` and \`setup-config\` for the
user to chain manually. Now \`install\` is a one-shot bootstrap:

1. If \`~/.ssh/ssh-mux-routes.toml\` does not exist, derive it from
   \`~/.ssh/config\` via \`ssh -G <alias>\` (read-only — the source
   SSH config is never modified). If the file already exists it is
   reused as-is; \`ssh-mux import-config --write --force\` re-imports
   explicitly.
2. \`setup-config\` runs unchanged: generates \`ssh-mux-hosts.conf\`,
   prepends the \`Include\` line, pre-registers the host key.
3. (Windows) install service + start daemon, as before. (Other
   platforms) print the manual \`ssh-mux daemon …\` invocation. The
   command no longer errors out on non-Windows.

The standalone \`import-config\` and \`setup-config\` subcommands
remain available unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:23:02 +09:00
9527caca0e chore: bump version to v1.12.1, add CHANGELOG covering v1.11.0–v1.12.1
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m1s
Release v1.12.1:
- Build: satisfy clippy 1.95 collapsible_match lint in import.rs

Adds a CHANGELOG.md at the repository root following the
Keep-a-Changelog format. Versions before v1.11.0 remain tracked
only in the git log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.12.1
2026-04-27 14:44:44 +09:00
43c1c77a30 fix(lint): satisfy clippy 1.95 collapsible_match in import.rs
The `proxyjump` match arm in `parse_ssh_g_output` nested an `if !val
.eq_ignore_ascii_case("none")` filter inside the arm body. clippy
1.95 (used by CI) flags this as `collapsible_match`. Express it as
a guarded arm instead. No functional change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:44:36 +09:00
18f5972401 chore: bump version to v1.12.0
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 58s
Release v1.12.0:
- Fix oneshot panic on keyboard-interactive auth failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:35:08 +09:00
ed2521294b fix: clear upstream auth state after the KI driver terminates
When the upstream auth driver finished (success or failure),
auth_keyboard_interactive consumed `outcome_rx` via the select but
left the spent `UpstreamAuthState` parked in `self.upstream_auth`.
The next round of keyboard-interactive — which SSH retries after a
failed attempt — re-entered the same select and re-polled the
already-completed oneshot receiver, panicking with:

  panicked at tokio/src/sync/oneshot.rs:1289:
  called after complete

Symptom: one panic per keyboard-interactive failure.

Restructure the select to return `(Auth, driver_finished)`. When
the driver has terminated (outcome arm fires, or prompts channel
closes), clear `self.upstream_auth` after the borrow ends so the
next call spawns a fresh driver. Also clear state on the
responses_tx-send-failed path, where the driver has gone away and
keeping its state would leak the same trap into the next round.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:35:01 +09:00
65a3248619 chore: bump version to v1.11.0, document import-config and idle-timer fix
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 58s
Release v1.11.0:
- New `import-config` subcommand
- `setup-config` no longer mutates existing `Host` blocks
- Idle timer no longer reaps connections with open channels

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:09:06 +09:00
9cb95fac10 fix: freeze idle timer while a connection has open channels
The pool reaper used `last_used.elapsed() > timeout` as the staleness
signal, but `last_used` is only refreshed at channel open and channel
close — byte traffic on the relay never bumped it. As a result, a
connection with one or more open channels was reaped exactly
`--timeout` seconds after the channel opened, killing live tmux/SSH
sessions even while the user was actively typing. The reaper logged
this as `closing stale connection ... idle 607s with 1 zombie
channel(s)`.

`Pool::reap_decision` now keeps any connection whose russh handle is
alive and whose `active_channels > 0`; truly dead remotes are caught
by russh keepalive (~45s detection) which closes the handle and trips
the existing `is_closed()` branch, and `--max-lifetime` still enforces
an absolute cap. To prevent the new policy from being defeated by a
leaked counter, the relay task now holds an RAII guard that fires
`pool.channel_closed*` on Drop, so a panic in `relay_bidirectional`
can no longer pin a connection in the pool.

Six new regression tests in `pool::reap_decision_tests` lock the
policy in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:08:59 +09:00
1b68854d00 feat: add import-config and stop mutating user host blocks
`setup-config` no longer comments out conflicting `Host` blocks in
`~/.ssh/config`; it only prepends the `Include ssh-mux-hosts.conf`
line and emits `ProxyJump none` + `ProxyCommand none` on every
generated block to suppress option bleed-through. Overlapping aliases
are reported but not edited.

New `import-config` subcommand derives a `routes.toml` from an
existing OpenSSH client config. Literal `Host` aliases are enumerated
and then resolved with `ssh -G <alias>`, so `Match`, `Include`, and
`Host *` inheritance are applied by OpenSSH itself instead of a
hand-rolled parser. The source SSH config is never modified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:08:39 +09:00
e7a527e7e2 Harden IPC and forwarding policy
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m18s
v1.10.5
2026-04-25 21:01:39 +09:00
fe29db4b7e fix(lint): satisfy clippy 1.95 collapsible_match lints
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m19s
Rust 1.95's clippy flagged three collapsible_match cases. Fix daemon.rs
by pattern-matching `ext: 1` directly on ExtendedData. For local_server.rs,
the nested `if` calls consume the bound `data` (CryptoVec is non-Copy), so
they cannot be collapsed into pattern guards without cloning; silence those
two arms with #[allow(clippy::collapsible_match)] instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1.11.1
2026-04-23 12:56:06 +09:00
118163eace refactor: improve condition check readability in wait_for_daemon
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 1m1s
Refactored the condition check in the wait_for_daemon function within integration.rs to enhance readability by separating the result handling and success check into distinct lines. This change aims to clarify the logic flow and maintain consistency with recent code improvements.
2026-04-23 12:04:17 +09:00
2948d0e6e5 refactor: simplify hash encoding and improve condition check
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 45s
Updated the hash encoding in known_hosts.rs to directly pass the hash to the base64 encoder. Additionally, streamlined the condition check in integration.rs to combine the result handling and success check into a single line for better readability.
2026-04-23 11:42:41 +09:00
e745c2666e refactor: drop toast notification on session open
Some checks failed
CI / Check (fmt, clippy, test) (push) Failing after 1m15s
Toast fired a PowerShell subprocess per channel open, which is noisy
for clients like VS Code Remote-SSH that open dozens of channels per
window. The tracing::info!("SESSION OPENED: ...") log already records
the event for troubleshooting, and with this refactor it is emitted
inline in channel_open_session instead of through a dedicated helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1.11.0
2026-04-23 10:09:48 +09:00
47974483fb feat: forward upstream OTP to client's SSH session via keyboard-interactive
Replace the PowerShell OTP prompt window with in-band keyboard-interactive
prompts delivered through the local russh server, so the user types the
OTP in whatever app launched ssh (terminal, Cursor, VS Code Remote-SSH)
instead of a separate console window.

Flow on the local SSH server:
- auth_publickey verifies the client key and returns FAILURE with
  methods=[keyboard-interactive], gating entry (russh 0.57 clobbers
  partial_success on the publickey path, but the method list is honored
  by OpenSSH clients).
- auth_keyboard_interactive spawns an upstream-auth driver task that
  calls pool::ensure_{direct,jump}_connected. Prompts from the upstream
  (e.g. MFA OTP on the bastion) flow back via mpsc and are surfaced to
  the client as Auth::Partial; the client's responses are forwarded
  upstream. On success, Auth::Accept leaves the upstream warm in the
  pool for subsequent channel opens (which use dead auth channels since
  no re-auth is needed).

setup-config now emits PreferredAuthentications publickey,keyboard-interactive
and KbdInteractiveAuthentication yes. Existing users must re-run
setup-config after upgrading so ssh-mux-hosts.conf picks up the new
method allowlist.

Removes ~435 lines of PowerShell OTP named-pipe code (the Windows
handle_auth_prompts_gui/_blocking pair, the Unix variant, and the
create_console_auth_channels mpsc bridge that fed them). Adds
UpstreamAuthState, spawn_upstream_auth_driver, and dead_auth_channels
to take their place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:02:20 +09:00
6a37e8341c feat(pool): add ensure_{direct,jump}_connected warm-up methods
Expose two public entry points that authenticate and pool an upstream
SSH connection without opening a channel:

- ensure_direct_connected: direct route
- ensure_jump_connected_pub: jump-host (bastion), thin wrapper over
  the existing ensure_jump_connected

Used to relocate interactive authentication (OTP) from channel-open
time to the client's auth phase: the local SSH server can now drive
the upstream login during its own keyboard-interactive handshake and
hand the pooled connection off to subsequent channel opens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:50:58 +09:00
f5f5d8e61d feat: implement serialized jump-host connection setup to prevent duplicate OTP prompts
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m50s
Added a mechanism to serialize connections to jump-hosts, ensuring that concurrent sessions to different internal hosts do not initiate separate bastion logins. This change introduces a new `jump_connect_serializers` field in the `Pool` struct and a helper function `serialize_jump_host_setup` to manage the async mutex for jump-host connections. Updated the `ensure_jump_connected` method to utilize this serialization, enhancing the connection handling logic.

Includes tests to verify that only one connection attempt occurs for the same jump key, preventing race conditions that could lead to duplicate OTP prompts.

Made-with: Cursor
2026-04-15 09:27:57 +09:00
명선 최
6c51d2f4a8 fix: work around Win32-OpenSSH askpass failure on non-ASCII usernames
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m27s
Win32-OpenSSH posix_spawnp passes SSH_ASKPASS through narrow-string CreateProcessW, which cannot resolve Unicode characters in the home directory path (e.g. Korean/Japanese/Chinese usernames). This causes Cursor host key verification to silently fail with CreateProcessW error:2 while terminal SSH works fine.

setup-config now generates cursor-ssh.cmd and cursor-askpass.cmd wrapper scripts in the SSH dir (typically ASCII) when the home directory contains non-ASCII characters. The wrapper redirects SSH_ASKPASS to the ASCII-path proxy before calling ssh.exe.

Bump version to 1.10.4.

Made-with: Cursor
v1.10.4
2026-03-23 13:34:20 +09:00
명선 최
6a583abe42 chore: bump version to 1.10.3
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m23s
Made-with: Cursor
v1.10.3
2026-03-20 14:20:35 +09:00
명선 최
9894e9ee75 fix: move OTP hostname display after pipe init and embed in Read-Host prompt
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m27s
Windows Terminal clears the console buffer during initialization, so

Write-Host output before pipe connection was invisible. Move the

hostname header after pipe read and embed it in the Read-Host prompt

text itself ([hostname] prompt) for guaranteed visibility. Strip

trailing colon/whitespace from SSH prompt to avoid double-colon.

Made-with: Cursor
2026-03-20 14:18:33 +09:00
명선 최
029045d917 chore: bump version to 1.10.2 and update README security documentation
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m23s
Documents new security hardenings: token file ACL at creation (fail-closed), IPC bounded_read_line (8 KiB limit), Windows home via Known Folder API, centralized DACL checks, and authorized_keys fallback downgrade warning.

Made-with: Cursor
v1.10.2
2026-03-20 11:19:08 +09:00
명선 최
3696ae93d4 refactor: deduplicate Windows DACL checks via security::check_dacl_permissions
Remove ~530 lines of duplicate Win32 ACL enumeration from local_server.rs. check_windows_dacl and check_host_key_dacl now delegate to security::check_dacl_permissions with appropriate mask constants (FILE_DANGEROUS_MASK, HOST_KEY_DANGEROUS_MASK, DIR_DANGEROUS_MASK). Ensures consistent ACE handling including Object ACE fail-closed. Also upgrades authorized_keys fallback warning to note security downgrade risk.

Made-with: Cursor
2026-03-20 11:17:51 +09:00
명선 최
90fc99dc5f security: resolve Windows home directory via Known Folder API
Use SHGetKnownFolderPath(FOLDERID_Profile) to determine the user

profile directory instead of relying solely on the USERPROFILE

environment variable, which can be poisoned by a local attacker.

Falls back to USERPROFILE if the API call fails.

Made-with: Cursor
2026-03-20 11:17:06 +09:00
명선 최
fc6f3d81d6 security: add 8 KiB line-length limit to IPC protocol reads
Replace unbounded BufReader::read_line with bounded_read_line that

reads via fill_buf and enforces MAX_IPC_LINE_LEN (8192 bytes).

Prevents memory exhaustion DoS from a malicious local client

sending megabytes of data without a newline terminator.

Made-with: Cursor
2026-03-20 11:16:39 +09:00
명선 최
af6ef76743 security: enforce DACL on token file and directory at creation time
Previously only the existing token file was DACL-checked. Now the

token directory gets DIR_DANGEROUS_MASK validation after create_dir_all

and the token file gets TOKEN_DANGEROUS_MASK validation immediately

after writing. If the newly created file inherits unsafe permissions

from its parent, it is deleted and the operation fails closed.

Made-with: Cursor
2026-03-20 11:15:54 +09:00
명선 최
222bb217aa fix: embed OTP display name directly in PowerShell script
The hostname was not appearing in the OTP prompt window because the

pipe-based JSON transfer was unreliable. Embed the sanitized display

name directly in the PowerShell script string and show it in both

the window title and body before pipe I/O begins.

Made-with: Cursor
2026-03-20 10:59:31 +09:00
명선 최
fb15019431 security: harden install and SSH_MUX_SSH_DIR validation
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m14s
- Windows SSH_MUX_SSH_DIR: fail-closed when directory does not exist (prevents unsafe DACL inheritance from permissive parent directories)

- DIR_DANGEROUS_MASK: add FILE_DELETE_CHILD (0x0040) to prevent child-deletion attacks on the SSH directory

- Replace PowerShell Start-Process with Rust CreateProcess (CREATE_NO_WINDOW | DETACHED_PROCESS) for immediate daemon launch, eliminating single-quote command injection risk

- Resolve Startup folder via Known Folder API (FOLDERID_Startup), resistant to APPDATA env-var poisoning

- Bump version to 1.10.1

Made-with: Cursor
v1.10.1
2026-03-18 19:44:36 +09:00
명선 최
b0e3ee3b7e chore: bump version to v1.10.0, update README with security hardening docs
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m35s
Documents known_hosts wildcard patterns, Windows DACL checks for SSH_MUX_SSH_DIR and daemon token, Known Folder API for token path, and Unix /tmp graceful error handling.

Made-with: Cursor
v1.10.0
2026-03-18 13:48:05 +09:00
명선 최
76162d5357 security: replace panic with error return in Unix /tmp socket fallback
The /tmp/ssh-mux-{uid} fallback directory creation now returns errors instead of panicking, providing a graceful exit with clear error messages when another user pre-creates the directory (local DoS prevention).

Made-with: Cursor
2026-03-18 13:47:03 +09:00
명선 최
0dcc148f37 security: use Known Folder API for token path and add token file DACL check
Resolves LOCALAPPDATA via SHGetKnownFolderPath (resistant to env var poisoning) and validates token file DACL on both daemon and client side, preventing unauthorized read/write that could enable pipe squatting or phishing.

Made-with: Cursor
2026-03-18 13:45:46 +09:00
명선 최
22a94eb433 security: add Windows DACL check for SSH_MUX_SSH_DIR directory
Validates owner and DACL permissions on the SSH directory when set via SSH_MUX_SSH_DIR, preventing unauthorized write access that could lead to host key tampering or code execution via install.

Made-with: Cursor
2026-03-18 13:43:10 +09:00
명선 최
b7d8137c78 security: implement known_hosts wildcard pattern matching (*, ?, ! negation)
Prevents accept-new bypass when @cert-authority entries use wildcard hostnames like *.example.com. Previously treated as literals, these patterns now match correctly so CA detection works as expected.

Made-with: Cursor
2026-03-18 13:41:47 +09:00
명선 최
d0306b2a95 chore: bump version to v1.9.0, add file logging with panic hook and log rotation
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m34s
Daemon and serve modes now log to ssh-mux.log in the SSH directory (in addition to stderr) so crashes are diagnosable even when running in a hidden window. Includes automatic 5 MB rotation and a panic hook that persists panic messages before exit.

Made-with: Cursor
v1.9.0
2026-03-18 13:26:23 +09:00
명선 최
99350cee4f chore: bump version to v1.8.0, update README with connection lifecycle and OTP docs
Made-with: Cursor
v1.8.0
2026-03-17 21:25:50 +09:00
명선 최
563e584d4f fix: prevent jump host reaper from killing active via-jump connections, clean up OTP window title
Made-with: Cursor
2026-03-17 21:24:31 +09:00
명선 최
12f8b78b78 chore: bump version to v1.7.1, update README with security hardening docs
All checks were successful
CI / Check (fmt, clippy, test) (push) Successful in 1m14s
Made-with: Cursor
v1.7.1
2026-03-17 17:25:31 +09:00
명선 최
40a3f7e1ff test: add security regression tests for v1.7.1 hardening
known_hosts: CertAuthorityPresent for matching host, unmatched host
stays Unknown, Trusted takes priority over CA presence, wildcard
non-glob behavior verified.
security: powershell_path() returns absolute System32 path on Windows
and never returns bare 'powershell' name.

Made-with: Cursor
2026-03-17 17:23:15 +09:00
명선 최
e90a1f9d40 docs: fix stale DACL comment — SID failure is fail-closed, not OW fallback
The module-level comment in ipc.rs still described the old behavior of
falling back to OW DACL when SID resolution fails. Updated to reflect
the current fail-closed policy.

Made-with: Cursor
2026-03-17 17:20:48 +09:00
명선 최
374717a7fe security: block non-interactive accept-new when @cert-authority entries exist
If known_hosts contains @cert-authority entries for a host, the
environment uses CA-based trust. Since ssh-mux cannot verify CA-signed
host certificates, auto-accepting via accept-new would silently
downgrade the trust model. Non-interactive connections now refuse when
CA entries are present. Interactive mode warns and asks explicitly.

Made-with: Cursor
2026-03-17 17:18:03 +09:00